// SearchEngine Frontend - Modern UI (Multi-Tenant) const API_BASE_URL = 'http://localhost:6002'; document.getElementById('apiUrl').textContent = API_BASE_URL; // Get tenant ID from input function getTenantId() { const tenantInput = document.getElementById('tenantInput'); if (tenantInput) { return tenantInput.value.trim(); } return '1'; // Default fallback } // State Management let state = { query: '', currentPage: 1, pageSize: 20, totalResults: 0, filters: {}, rangeFilters: {}, sortBy: '', sortOrder: 'desc', facets: null, lastSearchData: null, debug: true // Always enable debug mode for test frontend }; // Initialize document.addEventListener('DOMContentLoaded', function() { console.log('SearchEngine loaded'); console.log('Debug mode: always enabled (test frontend)'); document.getElementById('searchInput').focus(); }); // 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(); const tenantId = getTenantId(); if (!query) { alert('Please enter search keywords'); return; } if (!tenantId) { alert('Please enter tenant ID'); return; } state.query = query; state.currentPage = page; state.pageSize = parseInt(document.getElementById('resultSize').value); const from = (page - 1) * state.pageSize; // Define facets (简化配置) const facets = [ { "field": "category.keyword", "size": 15, "type": "terms" }, { "field": "vendor.keyword", "size": 15, "type": "terms" }, { "field": "tags.keyword", "size": 10, "type": "terms" }, { "field": "min_price", "type": "range", "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', 'X-Tenant-ID': tenantId, }, body: JSON.stringify({ query: query, size: state.pageSize, from: from, filters: Object.keys(state.filters).length > 0 ? state.filters : null, range_filters: Object.keys(state.rangeFilters).length > 0 ? state.rangeFilters : null, facets: facets, sort_by: state.sortBy || null, sort_order: state.sortOrder, debug: state.debug }) }); if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`); } const data = await response.json(); state.lastSearchData = data; state.totalResults = data.total; state.facets = data.facets; displayResults(data); displayFacets(data.facets); displayPagination(); displayDebugInfo(data); 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.results || data.results.length === 0) { grid.innerHTML = `

No Results Found

Try different keywords or filters

`; return; } let html = ''; data.results.forEach((spu) => { const score = spu.relevance_score; html += `
${spu.image_url ? ` ${escapeHtml(spu.title)} ` : `
No Image
`}
${spu.price ? `$${spu.price.toFixed(2)}` : 'N/A'}${spu.compare_at_price && spu.compare_at_price > spu.price ? `$${spu.compare_at_price.toFixed(2)}` : ''}
${spu.in_stock ? 'In Stock' : 'Out of Stock'} ${spu.skus && spu.skus.length > 0 ? `(${spu.skus.length} skus)` : ''}
${escapeHtml(spu.title || 'N/A')}
${spu.vendor ? escapeHtml(spu.vendor) : ''}${spu.category ? ' | ' + escapeHtml(spu.category) : ''} ${spu.tags ? `
Tags: ${escapeHtml(spu.tags)}
` : ''}
`; }); grid.innerHTML = html; } // Display facets as filter tags (重构版 - 标准化格式) function displayFacets(facets) { if (!facets) return; facets.forEach(facet => { // 根据字段名找到对应的容器 let containerId = null; let maxDisplay = 10; if (facet.field === 'category.keyword') { containerId = 'categoryTags'; maxDisplay = 10; } else if (facet.field === 'vendor.keyword') { containerId = 'brandTags'; maxDisplay = 10; } else if (facet.field === 'tags.keyword') { containerId = 'supplierTags'; maxDisplay = 8; } if (!containerId) return; const container = document.getElementById(containerId); if (!container) return; let html = ''; // 渲染分面值 facet.values.slice(0, maxDisplay).forEach(facetValue => { const value = facetValue.value; const count = facetValue.count; const selected = facetValue.selected; html += ` ${escapeHtml(value)} (${count}) `; }); container.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 } // Handle price filter (重构版 - 使用 rangeFilters) function handlePriceFilter(value) { if (!value) { delete state.rangeFilters.price; } else { const priceRanges = { '0-50': { lt: 50 }, '50-100': { gte: 50, lt: 100 }, '100-200': { gte: 100, lt: 200 }, '200+': { gte: 200 } }; if (priceRanges[value]) { state.rangeFilters.price = priceRanges[value]; } } performSearch(1); } // Handle time filter (重构版 - 使用 rangeFilters) function handleTimeFilter(value) { if (!value) { delete state.rangeFilters.create_time; } else { const now = new Date(); let fromDate; switch(value) { case 'today': fromDate = new Date(now.setHours(0, 0, 0, 0)); break; case 'week': fromDate = new Date(now.setDate(now.getDate() - 7)); break; case 'month': fromDate = new Date(now.setMonth(now.getMonth() - 1)); break; case '3months': fromDate = new Date(now.setMonth(now.getMonth() - 3)); break; case '6months': fromDate = new Date(now.setMonth(now.getMonth() - 6)); break; } if (fromDate) { state.rangeFilters.create_time = { gte: fromDate.toISOString() }; } } performSearch(1); } // Clear all filters function clearAllFilters() { state.filters = {}; state.rangeFilters = {}; document.getElementById('priceFilter').value = ''; document.getElementById('timeFilter').value = ''; performSearch(1); } // Update clear filters button visibility function updateClearFiltersButton() { const btn = document.getElementById('clearFiltersBtn'); if (Object.keys(state.filters).length > 0 || Object.keys(state.rangeFilters).length > 0) { btn.style.display = 'inline-block'; } else { btn.style.display = 'none'; } } // Update product count function updateProductCount(total) { document.getElementById('productCount').textContent = `${total.toLocaleString()} SPUs found`; } // Sort functions function setSortByDefault() { // Remove active from all buttons and arrows document.querySelectorAll('.sort-btn').forEach(b => b.classList.remove('active')); document.querySelectorAll('.arrow-up, .arrow-down').forEach(a => a.classList.remove('active')); // Set default button active const defaultBtn = document.querySelector('.sort-btn[data-sort=""]'); if (defaultBtn) defaultBtn.classList.add('active'); state.sortBy = ''; state.sortOrder = 'desc'; performSearch(1); } function sortByField(field, order) { state.sortBy = field; state.sortOrder = order; // Remove active from all buttons (but keep "By default" if no sort) document.querySelectorAll('.sort-btn').forEach(b => b.classList.remove('active')); // Remove active from all arrows document.querySelectorAll('.arrow-up, .arrow-down').forEach(a => a.classList.remove('active')); // Add active to clicked arrow const activeArrow = document.querySelector(`.arrow-up[data-field="${field}"][data-order="${order}"], .arrow-down[data-field="${field}"][data-order="${order}"]`); if (activeArrow) { activeArrow.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 debug info function displayDebugInfo(data) { const debugInfoDiv = document.getElementById('debugInfo'); if (!state.debug || !data.debug_info) { // If debug mode is off or no debug info, show basic query info if (data.query_info) { let html = '
'; html += `
original_query: ${escapeHtml(data.query_info.original_query || 'N/A')}
`; html += `
detected_language: ${getLanguageName(data.query_info.detected_language)}
`; html += '
'; debugInfoDiv.innerHTML = html; } else { debugInfoDiv.innerHTML = ''; } return; } // Display comprehensive debug info when debug mode is on const debugInfo = data.debug_info; let html = '
'; // Query Analysis if (debugInfo.query_analysis) { html += '
Query Analysis:'; html += `
original_query: ${escapeHtml(debugInfo.query_analysis.original_query || 'N/A')}
`; html += `
normalized_query: ${escapeHtml(debugInfo.query_analysis.normalized_query || 'N/A')}
`; html += `
rewritten_query: ${escapeHtml(debugInfo.query_analysis.rewritten_query || 'N/A')}
`; html += `
detected_language: ${getLanguageName(debugInfo.query_analysis.detected_language)}
`; html += `
domain: ${escapeHtml(debugInfo.query_analysis.domain || 'default')}
`; html += `
is_simple_query: ${debugInfo.query_analysis.is_simple_query ? 'yes' : 'no'}
`; if (debugInfo.query_analysis.translations && Object.keys(debugInfo.query_analysis.translations).length > 0) { html += '
translations: '; for (const [lang, translation] of Object.entries(debugInfo.query_analysis.translations)) { if (translation) { html += `${getLanguageName(lang)}: ${escapeHtml(translation)}; `; } } html += '
'; } if (debugInfo.query_analysis.boolean_ast) { html += `
boolean_ast: ${escapeHtml(debugInfo.query_analysis.boolean_ast)}
`; } html += `
has_vector: ${debugInfo.query_analysis.has_vector ? 'enabled' : 'disabled'}
`; html += '
'; } // Feature Flags if (debugInfo.feature_flags) { html += '
Feature Flags:'; html += `
translation_enabled: ${debugInfo.feature_flags.translation_enabled ? 'enabled' : 'disabled'}
`; html += `
embedding_enabled: ${debugInfo.feature_flags.embedding_enabled ? 'enabled' : 'disabled'}
`; html += `
rerank_enabled: ${debugInfo.feature_flags.rerank_enabled ? 'enabled' : 'disabled'}
`; html += '
'; } // ES Response if (debugInfo.es_response) { html += '
ES Response:'; html += `
took_ms: ${debugInfo.es_response.took_ms}ms
`; html += `
total_hits: ${debugInfo.es_response.total_hits}
`; html += `
max_score: ${debugInfo.es_response.max_score?.toFixed(3) || 0}
`; html += '
'; } // Stage Timings if (debugInfo.stage_timings) { html += '
Stage Timings:'; for (const [stage, duration] of Object.entries(debugInfo.stage_timings)) { html += `
${stage}: ${duration.toFixed(2)}ms
`; } html += '
'; } // ES Query if (debugInfo.es_query) { html += '
ES Query DSL:'; html += `
${escapeHtml(JSON.stringify(debugInfo.es_query, null, 2))}
`; html += '
'; } html += '
'; debugInfoDiv.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 formatDate(dateStr) { if (!dateStr) return ''; try { const date = new Date(dateStr); return date.toLocaleDateString('zh-CN'); } catch { return dateStr; } } function getLanguageName(code) { const names = { 'zh': '中文', 'en': 'English', 'ru': 'Русский', 'ar': 'العربية', 'ja': '日本語', 'unknown': 'Unknown' }; return names[code] || code; }