// 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();
});
// 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 ? `
` : `
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) : ''}
${source.create_time ? `
Listed: ${formatDate(source.create_time)}
` : ''}
`;
});
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
}
// Handle price filter
function handlePriceFilter(value) {
if (!value) {
delete state.filters.price;
} else {
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);
}
// Handle time filter
function handleTimeFilter(value) {
if (!value) {
delete state.filters.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.filters.create_time = {
from: fromDate.toISOString()
};
}
}
performSearch(1);
}
// Clear all filters
function clearAllFilters() {
state.filters = {};
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) {
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 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 = `
← Previous
`;
// 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 += `1 `;
if (startPage > 2) {
html += `... `;
}
}
for (let i = startPage; i <= endPage; i++) {
html += `
${i}
`;
}
if (endPage < totalPages) {
if (endPage < totalPages - 1) {
html += `... `;
}
html += `${totalPages} `;
}
html += `
Next →
`;
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 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;
}