diff --git a/frontend/base.html b/frontend/base.html deleted file mode 100644 index 596163a..0000000 --- a/frontend/base.html +++ /dev/null @@ -1,89 +0,0 @@ - - - - - - 店匠通用搜索 - Base Configuration - - - -
- -
-
- - 0 products found -
-
- -
-
- - - - - -
- -
-
Categories:
-
-
- - -
-
Vendor:
-
-
- - -
-
Tags:
-
-
- - -
-
Price Range:
-
-
- - -
- -
-
- - -
-
-
-

Welcome to Shoplazza Base Search

-

Enter keywords to search for products

-
-
-
- - - - - - -
- - - - - diff --git a/frontend/static/js/app.js b/frontend/static/js/app.js index 94f5f87..71b7cf2 100644 --- a/frontend/static/js/app.js +++ b/frontend/static/js/app.js @@ -608,7 +608,7 @@ function displayDebugInfo(data) { // ES Query if (debugInfo.es_query) { html += '
ES Query DSL:'; - html += `
${escapeHtml(JSON.stringify(debugInfo.es_query, null, 2))}
`; + html += `
${escapeHtml(customStringify(debugInfo.es_query))}
`; html += '
'; } @@ -616,6 +616,19 @@ function displayDebugInfo(data) { debugInfoDiv.innerHTML = html; } +// Custom JSON stringify that compresses numeric arrays (like embeddings) to single line +function customStringify(obj) { + return JSON.stringify(obj, (key, value) => { + if (Array.isArray(value)) { + // Only collapse arrays that contain numbers (like embeddings) + if (value.every(item => typeof item === 'number')) { + return JSON.stringify(value); + } + } + return value; + }, 2).replace(/"\[/g, '[').replace(/\]"/g, ']'); +} + // Helper functions function escapeHtml(text) { if (!text) return ''; diff --git a/frontend/static/js/app_base.js b/frontend/static/js/app_base.js deleted file mode 100644 index c72bbc5..0000000 --- a/frontend/static/js/app_base.js +++ /dev/null @@ -1,590 +0,0 @@ -// 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; -} -- libgit2 0.21.2