// SearchEngine Frontend - Modern UI const API_BASE_URL = 'http://120.76.41.98:6002'; document.getElementById('apiUrl').textContent = API_BASE_URL; // State Management let state = { query: '', currentPage: 1, pageSize: 20, totalResults: 0, filters: {}, sortBy: '', sortOrder: 'desc', aggregations: null, lastSearchData: null }; // Initialize document.addEventListener('DOMContentLoaded', function() { console.log('SearchEngine loaded'); document.getElementById('searchInput').focus(); // Setup price filter document.getElementById('priceFilter').addEventListener('change', function(e) { if (e.target.value) { togglePriceFilter(e.target.value); } }); }); // Keyboard handler function handleKeyPress(event) { if (event.key === 'Enter') { performSearch(); } } // Toggle filters visibility function toggleFilters() { const filterSection = document.getElementById('filterSection'); filterSection.classList.toggle('hidden'); } // Perform search async function performSearch(page = 1) { const query = document.getElementById('searchInput').value.trim(); if (!query) { alert('Please enter search keywords'); return; } state.query = query; state.currentPage = page; state.pageSize = parseInt(document.getElementById('resultSize').value); const from = (page - 1) * state.pageSize; // Define aggregations const aggregations = { "category_stats": { "terms": { "field": "categoryName_keyword", "size": 15 } }, "brand_stats": { "terms": { "field": "brandName_keyword", "size": 15 } }, "supplier_stats": { "terms": { "field": "supplierName_keyword", "size": 10 } }, "price_ranges": { "range": { "field": "price", "ranges": [ {"key": "0-50", "to": 50}, {"key": "50-100", "from": 50, "to": 100}, {"key": "100-200", "from": 100, "to": 200}, {"key": "200+", "from": 200} ] } } }; // Show loading document.getElementById('loading').style.display = 'block'; document.getElementById('productGrid').innerHTML = ''; try { const response = await fetch(`${API_BASE_URL}/search/`, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ query: query, size: state.pageSize, from: from, filters: Object.keys(state.filters).length > 0 ? state.filters : null, aggregations: aggregations, sort_by: state.sortBy || null, sort_order: state.sortOrder }) }); if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`); } const data = await response.json(); state.lastSearchData = data; state.totalResults = data.total; state.aggregations = data.aggregations; displayResults(data); displayAggregations(data.aggregations); displayPagination(); displayQueryInfo(data.query_info); updateProductCount(data.total); updateClearFiltersButton(); } catch (error) { console.error('Search error:', error); document.getElementById('productGrid').innerHTML = `
Search Error: ${error.message}

Please ensure backend service is running (${API_BASE_URL})
`; } finally { document.getElementById('loading').style.display = 'none'; } } // Display results in grid function displayResults(data) { const grid = document.getElementById('productGrid'); if (!data.hits || data.hits.length === 0) { grid.innerHTML = `

No Results Found

Try different keywords or filters

`; return; } let html = ''; data.hits.forEach((hit) => { const source = hit._source; const score = hit._custom_score || hit._score; html += `
${source.imageUrl ? ` ${escapeHtml(source.name)} ` : `
No Image
`}
${source.price ? `${source.price} ₽` : 'N/A'}
MOQ ${source.moq || 1} Box
${source.quantity || 'N/A'} pcs / Box
${escapeHtml(source.name || source.enSpuName || 'N/A')}
${source.categoryName ? escapeHtml(source.categoryName) : ''} ${source.brandName ? ' | ' + escapeHtml(source.brandName) : ''}
`; }); grid.innerHTML = html; } // Display aggregations as filter tags function displayAggregations(aggregations) { if (!aggregations) return; // Category tags if (aggregations.category_stats && aggregations.category_stats.buckets) { const categoryTags = document.getElementById('categoryTags'); let html = ''; aggregations.category_stats.buckets.slice(0, 10).forEach(bucket => { const key = bucket.key; const count = bucket.doc_count; const isActive = state.filters.categoryName_keyword && state.filters.categoryName_keyword.includes(key); html += ` ${escapeHtml(key)} (${count}) `; }); categoryTags.innerHTML = html; } // Brand tags if (aggregations.brand_stats && aggregations.brand_stats.buckets) { const brandTags = document.getElementById('brandTags'); let html = ''; aggregations.brand_stats.buckets.slice(0, 10).forEach(bucket => { const key = bucket.key; const count = bucket.doc_count; const isActive = state.filters.brandName_keyword && state.filters.brandName_keyword.includes(key); html += ` ${escapeHtml(key)} (${count}) `; }); brandTags.innerHTML = html; } // Supplier tags if (aggregations.supplier_stats && aggregations.supplier_stats.buckets) { const supplierTags = document.getElementById('supplierTags'); let html = ''; aggregations.supplier_stats.buckets.slice(0, 8).forEach(bucket => { const key = bucket.key; const count = bucket.doc_count; const isActive = state.filters.supplierName_keyword && state.filters.supplierName_keyword.includes(key); html += ` ${escapeHtml(key)} (${count}) `; }); supplierTags.innerHTML = html; } } // Toggle filter function toggleFilter(field, value) { if (!state.filters[field]) { state.filters[field] = []; } const index = state.filters[field].indexOf(value); if (index > -1) { state.filters[field].splice(index, 1); if (state.filters[field].length === 0) { delete state.filters[field]; } } else { state.filters[field].push(value); } performSearch(1); // Reset to page 1 } // Toggle price filter function togglePriceFilter(value) { const priceRanges = { '0-50': { to: 50 }, '50-100': { from: 50, to: 100 }, '100-200': { from: 100, to: 200 }, '200+': { from: 200 } }; if (priceRanges[value]) { state.filters.price = priceRanges[value]; } performSearch(1); } // Clear all filters function clearAllFilters() { state.filters = {}; document.getElementById('priceFilter').value = ''; performSearch(1); } // Update clear filters button visibility function updateClearFiltersButton() { const btn = document.getElementById('clearFiltersBtn'); if (Object.keys(state.filters).length > 0) { btn.style.display = 'inline-block'; } else { btn.style.display = 'none'; } } // Update product count function updateProductCount(total) { document.getElementById('productCount').textContent = `${total.toLocaleString()} products found`; } // Sort functions function setSortBy(btn, field) { // Remove active from all document.querySelectorAll('.sort-btn').forEach(b => b.classList.remove('active')); // Set active btn.classList.add('active'); if (field === '') { state.sortBy = ''; state.sortOrder = 'desc'; } else { state.sortBy = field; } performSearch(1); } function sortByField(field, order) { state.sortBy = field; state.sortOrder = order; // Update active state document.querySelectorAll('.sort-btn').forEach(b => b.classList.remove('active')); const btn = document.querySelector(`.sort-btn[data-sort="${field}"]`); if (btn) btn.classList.add('active'); performSearch(state.currentPage); } // Pagination function displayPagination() { const paginationDiv = document.getElementById('pagination'); if (state.totalResults <= state.pageSize) { paginationDiv.style.display = 'none'; return; } paginationDiv.style.display = 'flex'; const totalPages = Math.ceil(state.totalResults / state.pageSize); const currentPage = state.currentPage; let html = ` `; // Page numbers const maxVisible = 5; let startPage = Math.max(1, currentPage - Math.floor(maxVisible / 2)); let endPage = Math.min(totalPages, startPage + maxVisible - 1); if (endPage - startPage < maxVisible - 1) { startPage = Math.max(1, endPage - maxVisible + 1); } if (startPage > 1) { html += ``; if (startPage > 2) { html += `...`; } } for (let i = startPage; i <= endPage; i++) { html += ` `; } if (endPage < totalPages) { if (endPage < totalPages - 1) { html += `...`; } html += ``; } html += ` `; html += ` Page ${currentPage} of ${totalPages} (${state.totalResults.toLocaleString()} results) `; paginationDiv.innerHTML = html; } function goToPage(page) { const totalPages = Math.ceil(state.totalResults / state.pageSize); if (page < 1 || page > totalPages) return; performSearch(page); // Scroll to top window.scrollTo({ top: 0, behavior: 'smooth' }); } // Display query info function displayQueryInfo(queryInfo) { if (!queryInfo) return; const queryInfoDiv = document.getElementById('queryInfo'); let html = '
'; html += `
Original Query ${escapeHtml(queryInfo.original_query || 'N/A')}
Rewritten Query ${escapeHtml(queryInfo.rewritten_query || 'N/A')}
Detected Language ${getLanguageName(queryInfo.detected_language)}
Domain ${escapeHtml(queryInfo.domain || 'default')}
`; if (queryInfo.translations && Object.keys(queryInfo.translations).length > 0) { for (const [lang, translation] of Object.entries(queryInfo.translations)) { if (translation) { html += `
Translation (${getLanguageName(lang)}) ${escapeHtml(translation)}
`; } } } if (queryInfo.has_vector) { html += `
Semantic Search ✓ Enabled (Vector)
`; } html += '
'; queryInfoDiv.innerHTML = html; } // Helper functions function escapeHtml(text) { if (!text) return ''; const div = document.createElement('div'); div.textContent = text; return div.innerHTML; } function escapeAttr(text) { if (!text) return ''; return text.replace(/'/g, "\\'").replace(/"/g, '"'); } function getLanguageName(code) { const names = { 'zh': '中文', 'en': 'English', 'ru': 'Русский', 'ar': 'العربية', 'ja': '日本語', 'unknown': 'Unknown' }; return names[code] || code; }