advanced-search-library
Version:
Intelligent search library with typo correction, autocomplete, and flexible data structure support
365 lines (307 loc) • 12.7 kB
JavaScript
/**
* Advanced Search Demo Application
*
* @author Rıdvan Sevindik <sevindikbusiness@gmail.com>
* @github https://github.com/Ridvan0
* @linkedin https://www.linkedin.com/in/ridvansevindik/
*/
class AdvancedSearchDemo {
constructor() {
this.search = new AdvancedSearch({
maxResults: 50,
minQueryLength: 1,
typoThreshold: 2,
historyLimit: 50
});
this.initializeElements();
this.bindEvents();
this.loadSampleData();
this.displayAllProducts();
}
initializeElements() {
this.searchInput = document.getElementById('searchInput');
this.searchButton = document.getElementById('searchButton');
this.resultsContainer = document.getElementById('resultsContainer');
this.autocompleteContainer = document.getElementById('autocompleteContainer');
this.historyContainer = document.getElementById('historyContainer');
this.statsContainer = document.getElementById('statsContainer');
this.resultsCount = document.getElementById('resultsCount');
this.resultsTitle = document.getElementById('resultsTitle');
// Filter elements
this.categoryFilter = document.getElementById('categoryFilter');
this.priceMinInput = document.getElementById('priceMin');
this.priceMaxInput = document.getElementById('priceMax');
this.sortSelect = document.getElementById('sortSelect');
this.clearFiltersButton = document.getElementById('clearFilters');
this.currentQuery = '';
}
bindEvents() {
this.searchInput.addEventListener('input', (e) => {
this.handleSearchInput(e.target.value);
});
this.searchInput.addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
this.performSearch();
}
});
this.searchButton.addEventListener('click', () => {
this.performSearch();
});
// Filter events
this.categoryFilter.addEventListener('change', () => {
this.performSearch();
});
this.priceMinInput.addEventListener('input', () => {
this.performSearch();
});
this.priceMaxInput.addEventListener('input', () => {
this.performSearch();
});
this.sortSelect.addEventListener('change', () => {
this.performSearch();
});
this.clearFiltersButton.addEventListener('click', () => {
this.clearFilters();
});
// Click outside to hide autocomplete
document.addEventListener('click', (e) => {
if (!this.searchInput.contains(e.target) && !this.autocompleteContainer.contains(e.target)) {
this.autocompleteContainer.style.display = 'none';
}
});
}
loadSampleData() {
this.search.addData(sampleData);
this.populateFilters();
this.updateStats();
}
populateFilters() {
const categories = [...new Set(this.search.data.map(item => item.category))];
this.categoryFilter.innerHTML = '<option value="">All Categories</option>';
categories.forEach(category => {
const option = document.createElement('option');
option.value = category;
option.textContent = category;
this.categoryFilter.appendChild(option);
});
}
handleSearchInput(query) {
this.currentQuery = query;
if (query.length >= 3) {
this.showAutocomplete(query);
} else {
this.autocompleteContainer.style.display = 'none';
}
if (query.length >= 1) {
this.performSearch();
} else {
this.displayAllProducts();
}
}
showAutocomplete(query) {
const suggestions = this.search.autocomplete(query);
if (suggestions.length > 0) {
this.autocompleteContainer.innerHTML = '';
suggestions.forEach(suggestion => {
const item = document.createElement('div');
item.className = 'autocomplete-item';
item.textContent = suggestion;
item.addEventListener('click', () => {
this.searchInput.value = suggestion;
this.currentQuery = suggestion;
this.performSearch();
this.autocompleteContainer.style.display = 'none';
});
this.autocompleteContainer.appendChild(item);
});
this.autocompleteContainer.style.display = 'block';
} else {
this.autocompleteContainer.style.display = 'none';
}
}
performSearch() {
const query = this.currentQuery;
const filters = this.getFilters();
let results;
if (query.trim() === '') {
results = this.getAllProductsFiltered(filters);
} else {
results = this.search.search(query, filters);
}
this.displayResults(results);
this.updateResultsCount(results.length);
this.updateStats();
this.updateSearchHistory();
this.autocompleteContainer.style.display = 'none';
}
getFilters() {
const filters = {};
if (this.categoryFilter.value) {
filters.category = this.categoryFilter.value;
}
const minPrice = parseFloat(this.priceMinInput.value);
const maxPrice = parseFloat(this.priceMaxInput.value);
if (!isNaN(minPrice) || !isNaN(maxPrice)) {
filters.priceRange = [
isNaN(minPrice) ? 0 : minPrice,
isNaN(maxPrice) ? 999999 : maxPrice
];
}
if (this.sortSelect.value) {
filters.sortBy = this.sortSelect.value;
}
return filters;
}
getAllProductsFiltered(filters) {
// Get all products and apply filters manually for non-search results
let results = this.search.data.map(item => ({...item}));
// Apply filters
if (filters.category) {
results = results.filter(item =>
item.category && item.category.toLowerCase().includes(filters.category.toLowerCase())
);
}
if (filters.priceRange) {
const [min, max] = filters.priceRange;
results = results.filter(item =>
item.price >= min && item.price <= max
);
}
// Sort results
const sortFunctions = {
'relevance': (a, b) => b.viewCount - a.viewCount, // Use viewCount as relevance for all products
'price_asc': (a, b) => (a.price || 0) - (b.price || 0),
'price_desc': (a, b) => (b.price || 0) - (a.price || 0),
'name_asc': (a, b) => (a.name || '').localeCompare(b.name || ''),
'rating_desc': (a, b) => (b.rating || 0) - (a.rating || 0)
};
const sortFn = sortFunctions[filters.sortBy] || sortFunctions['relevance'];
return results.sort(sortFn);
}
displayAllProducts() {
const filters = this.getFilters();
const results = this.getAllProductsFiltered(filters);
this.displayResults(results);
this.updateResultsCount(results.length);
this.currentQuery = '';
this.updateResultsTitle();
}
displayResults(results) {
this.resultsContainer.innerHTML = '';
if (results.length === 0) {
const noResults = document.createElement('div');
noResults.className = 'no-results';
noResults.innerHTML = `
<h3>No results found</h3>
<p>Try different search terms or check your filters.</p>
`;
this.resultsContainer.appendChild(noResults);
return;
}
results.forEach(item => {
const productCard = this.createProductCard(item);
this.resultsContainer.appendChild(productCard);
});
}
createProductCard(item) {
const card = document.createElement('div');
card.className = 'product-card';
const relevanceScore = item.relevanceScore ? ` (${item.relevanceScore.toFixed(1)})` : '';
card.innerHTML = `
<div class="product-header">
<h3 class="product-name">${item.name}${relevanceScore}</h3>
<div class="product-category">${item.category}</div>
</div>
<div class="product-details">
<div class="product-brand">Brand: ${item.brand}</div>
<div class="product-price">$${item.price}</div>
<div class="product-rating">
<span class="stars">${this.generateStars(item.rating)}</span>
<span class="rating-value">(${item.rating})</span>
</div>
</div>
<div class="product-description">${item.description}</div>
<div class="product-tags">
${item.tags.map(tag => `<span class="tag">${tag}</span>`).join('')}
</div>
<div class="product-stats">
<small>Views: ${item.viewCount || 0}</small>
</div>
`;
return card;
}
generateStars(rating) {
const fullStars = Math.floor(rating);
const hasHalfStar = rating % 1 >= 0.5;
let stars = '';
for (let i = 0; i < fullStars; i++) {
stars += '★';
}
if (hasHalfStar) {
stars += '☆';
}
return stars;
}
clearFilters() {
this.categoryFilter.value = '';
this.priceMinInput.value = '';
this.priceMaxInput.value = '';
this.sortSelect.value = 'relevance';
this.performSearch();
}
updateResultsCount(count) {
this.resultsCount.textContent = `${count} results`;
this.updateResultsTitle();
}
updateResultsTitle() {
if (this.currentQuery) {
this.resultsTitle.textContent = `Results for "${this.currentQuery}"`;
} else {
this.resultsTitle.textContent = 'All Products';
}
}
updateStats() {
const stats = this.search.getStats();
this.statsContainer.innerHTML = `
<div class="stat-item">
<span class="stat-label">Total Items:</span>
<span class="stat-value">${stats.totalItems}</span>
</div>
<div class="stat-item">
<span class="stat-label">Total Searches:</span>
<span class="stat-value">${stats.totalSearches}</span>
</div>
<div class="stat-item">
<span class="stat-label">Unique Searches:</span>
<span class="stat-value">${stats.uniqueSearches}</span>
</div>
`;
}
updateSearchHistory() {
const history = this.search.getSearchHistory().slice(0, 10);
if (history.length === 0) {
this.historyContainer.innerHTML = '<p>No search history yet</p>';
return;
}
this.historyContainer.innerHTML = '';
history.forEach(entry => {
const historyItem = document.createElement('div');
historyItem.className = 'history-item';
historyItem.innerHTML = `
<span class="history-query">${entry.query}</span>
<span class="history-time">${new Date(entry.timestamp).toLocaleTimeString()}</span>
`;
historyItem.addEventListener('click', () => {
this.searchInput.value = entry.query;
this.currentQuery = entry.query;
this.performSearch();
});
this.historyContainer.appendChild(historyItem);
});
}
}
// Initialize the demo when the page loads
document.addEventListener('DOMContentLoaded', () => {
window.demo = new AdvancedSearchDemo();
console.log('✅ Advanced Search Demo loaded successfully');
});