搜索建议 #
搜索建议概述 #
搜索建议(Search Suggestions)包括:
- 自动补全:输入时自动补全搜索词
- 即时搜索:实时显示搜索结果
- 热门搜索:推荐热门搜索词
- 最近搜索:显示用户最近搜索
自动补全库 #
安装 #
bash
npm install @algolia/autocomplete-js
基本使用 #
javascript
import { autocomplete } from '@algolia/autocomplete-js';
import algoliasearch from 'algoliasearch';
const client = algoliasearch('APP_ID', 'SEARCH_KEY');
autocomplete({
container: '#autocomplete',
placeholder: '搜索商品...',
getSources({ query }) {
return [
{
sourceId: 'products',
getItems() {
return client.initIndex('products').search(query, {
hitsPerPage: 5
}).then(({ hits }) => hits);
},
templates: {
item({ item }) {
return `
<div class="suggestion-item">
<span class="name">${item.name}</span>
<span class="price">$${item.price}</span>
</div>
`;
}
}
}
];
}
});
多来源建议 #
商品+分类建议 #
javascript
autocomplete({
container: '#autocomplete',
placeholder: '搜索...',
getSources({ query }) {
return [
{
sourceId: 'products',
getItems() {
return client.initIndex('products').search(query, {
hitsPerPage: 3
}).then(({ hits }) => hits);
},
templates: {
header() {
return '<span class="header">商品</span>';
},
item({ item }) {
return `
<div class="product-item">
<img src="${item.image}" alt="${item.name}">
<div class="info">
<span class="name">${item.name}</span>
<span class="price">$${item.price}</span>
</div>
</div>
`;
}
}
},
{
sourceId: 'categories',
getItems() {
return client.initIndex('categories').search(query, {
hitsPerPage: 3
}).then(({ hits }) => hits);
},
templates: {
header() {
return '<span class="header">分类</span>';
},
item({ item }) {
return `
<div class="category-item">
<span class="icon">📁</span>
<span class="name">${item.name}</span>
</div>
`;
}
}
}
];
}
});
热门搜索 #
创建热门搜索索引 #
javascript
// 存储热门搜索词
const popularSearches = [
{ objectID: '1', query: 'iPhone', count: 15000 },
{ objectID: '2', query: 'Samsung', count: 12000 },
{ objectID: '3', query: 'laptop', count: 8000 }
];
await client.initIndex('popular_searches').saveObjects(popularSearches);
显示热门搜索 #
javascript
autocomplete({
container: '#autocomplete',
placeholder: '搜索...',
getSources({ query }) {
if (!query) {
// 空查询时显示热门搜索
return [
{
sourceId: 'popular',
getItems() {
return client.initIndex('popular_searches').search('', {
hitsPerPage: 5
}).then(({ hits }) => hits);
},
templates: {
header() {
return '<span class="header">热门搜索</span>';
},
item({ item }) {
return `
<div class="popular-item">
<span class="query">${item.query}</span>
<span class="count">${item.count}次</span>
</div>
`;
}
}
}
];
}
// 有查询时显示搜索结果
return [
{
sourceId: 'products',
getItems() {
return client.initIndex('products').search(query).then(({ hits }) => hits);
},
templates: {
item({ item }) {
return `<div>${item.name}</div>`;
}
}
}
];
}
});
最近搜索 #
存储最近搜索 #
javascript
class RecentSearches {
constructor(maxItems = 5) {
this.maxItems = maxItems;
this.storageKey = 'recent_searches';
}
get() {
const stored = localStorage.getItem(this.storageKey);
return stored ? JSON.parse(stored) : [];
}
add(query) {
let items = this.get();
// 移除重复项
items = items.filter(item => item !== query);
// 添加到开头
items.unshift(query);
// 限制数量
items = items.slice(0, this.maxItems);
localStorage.setItem(this.storageKey, JSON.stringify(items));
}
clear() {
localStorage.removeItem(this.storageKey);
}
}
显示最近搜索 #
javascript
const recentSearches = new RecentSearches();
autocomplete({
container: '#autocomplete',
placeholder: '搜索...',
getSources({ query }) {
if (!query) {
const recent = recentSearches.get();
if (recent.length > 0) {
return [
{
sourceId: 'recent',
getItems() {
return recent.map(q => ({ query: q }));
},
templates: {
header() {
return `
<span class="header">
最近搜索
<button onclick="recentSearches.clear()">清除</button>
</span>
`;
},
item({ item }) {
return `
<div class="recent-item">
<span class="icon">🕐</span>
<span class="query">${item.query}</span>
</div>
`;
}
}
}
];
}
}
// 搜索结果...
},
onSelect({ item }) {
recentSearches.add(item.query || item.name);
}
});
即时搜索 #
实现即时搜索 #
javascript
const searchInput = document.getElementById('searchInput');
const resultsContainer = document.getElementById('results');
let debounceTimer;
searchInput.addEventListener('input', (e) => {
clearTimeout(debounceTimer);
debounceTimer = setTimeout(async () => {
const query = e.target.value;
if (query.length < 2) {
resultsContainer.innerHTML = '';
return;
}
const results = await index.search(query, {
hitsPerPage: 20
});
renderResults(results.hits);
}, 300);
});
function renderResults(hits) {
resultsContainer.innerHTML = hits.map(hit => `
<div class="result-item">
<h3>${hit._highlightResult.name.value}</h3>
<p>${hit._snippetResult?.description?.value || ''}</p>
</div>
`).join('');
}
使用InstantSearch #
javascript
import instantsearch from 'instantsearch.js';
import { searchBox, hits } from 'instantsearch.js/es/widgets';
const search = instantsearch({
indexName: 'products',
searchClient: client
});
search.addWidgets([
searchBox({
container: '#searchbox',
placeholder: '搜索商品...',
showReset: true,
showSubmit: true,
showLoadingIndicator: true
}),
hits({
container: '#hits',
templates: {
item(hit) {
return `
<div class="hit">
<h3>${hit._highlightResult.name.value}</h3>
<p>$${hit.price}</p>
</div>
`;
},
empty(results) {
return `没有找到"${results.query}"的结果`;
}
}
})
]);
search.start();
搜索建议优化 #
防抖处理 #
javascript
function debounce(func, wait) {
let timeout;
return function(...args) {
clearTimeout(timeout);
timeout = setTimeout(() => func.apply(this, args), wait);
};
}
const debouncedSearch = debounce(async (query) => {
const results = await index.search(query);
renderSuggestions(results.hits);
}, 200);
最小字符数 #
javascript
searchInput.addEventListener('input', (e) => {
const query = e.target.value;
if (query.length < 2) {
hideSuggestions();
return;
}
showSuggestions(query);
});
缓存结果 #
javascript
const suggestionCache = new Map();
async function getCachedSuggestions(query) {
if (suggestionCache.has(query)) {
return suggestionCache.get(query);
}
const results = await index.search(query, {
hitsPerPage: 5
});
suggestionCache.set(query, results.hits);
// 限制缓存大小
if (suggestionCache.size > 100) {
const firstKey = suggestionCache.keys().next().value;
suggestionCache.delete(firstKey);
}
return results.hits;
}
完整示例 #
搜索建议组件 #
javascript
class SearchSuggestions {
constructor(options) {
this.container = document.querySelector(options.container);
this.index = options.index;
this.minChars = options.minChars || 2;
this.debounceTime = options.debounceTime || 200;
this.recentSearches = new RecentSearches();
this.cache = new Map();
this.init();
}
init() {
this.render();
this.bindEvents();
}
render() {
this.container.innerHTML = `
<div class="search-suggestions">
<input type="text" class="search-input" placeholder="搜索...">
<div class="suggestions-dropdown"></div>
</div>
`;
this.input = this.container.querySelector('.search-input');
this.dropdown = this.container.querySelector('.suggestions-dropdown');
}
bindEvents() {
let debounceTimer;
this.input.addEventListener('input', (e) => {
clearTimeout(debounceTimer);
debounceTimer = setTimeout(() => {
this.handleInput(e.target.value);
}, this.debounceTime);
});
this.input.addEventListener('focus', () => {
if (!this.input.value) {
this.showInitialSuggestions();
}
});
document.addEventListener('click', (e) => {
if (!this.container.contains(e.target)) {
this.hideDropdown();
}
});
}
async handleInput(query) {
if (query.length < this.minChars) {
this.showInitialSuggestions();
return;
}
const suggestions = await this.getSuggestions(query);
this.renderSuggestions(suggestions, query);
}
async getSuggestions(query) {
if (this.cache.has(query)) {
return this.cache.get(query);
}
const results = await this.index.search(query, {
hitsPerPage: 5
});
this.cache.set(query, results.hits);
return results.hits;
}
showInitialSuggestions() {
const recent = this.recentSearches.get();
if (recent.length > 0) {
this.renderRecentSearches(recent);
} else {
this.hideDropdown();
}
}
renderSuggestions(hits, query) {
this.dropdown.innerHTML = `
<div class="suggestions-list">
${hits.map(hit => `
<div class="suggestion-item" data-query="${hit.name}">
<span class="name">${hit._highlightResult.name.value}</span>
<span class="category">${hit.category}</span>
</div>
`).join('')}
</div>
`;
this.showDropdown();
this.bindSuggestionEvents();
}
renderRecentSearches(searches) {
this.dropdown.innerHTML = `
<div class="recent-searches">
<div class="header">
<span>最近搜索</span>
<button class="clear-btn">清除</button>
</div>
${searches.map(query => `
<div class="recent-item" data-query="${query}">
<span class="icon">🕐</span>
<span class="query">${query}</span>
</div>
`).join('')}
</div>
`;
this.showDropdown();
this.bindRecentEvents();
}
bindSuggestionEvents() {
this.dropdown.querySelectorAll('.suggestion-item').forEach(item => {
item.addEventListener('click', () => {
const query = item.dataset.query;
this.selectSuggestion(query);
});
});
}
bindRecentEvents() {
this.dropdown.querySelectorAll('.recent-item').forEach(item => {
item.addEventListener('click', () => {
const query = item.dataset.query;
this.input.value = query;
this.handleInput(query);
});
});
this.dropdown.querySelector('.clear-btn')?.addEventListener('click', () => {
this.recentSearches.clear();
this.hideDropdown();
});
}
selectSuggestion(query) {
this.recentSearches.add(query);
this.input.value = query;
this.hideDropdown();
// 触发搜索
this.onSelect(query);
}
onSelect(query) {
// 由外部实现
}
showDropdown() {
this.dropdown.style.display = 'block';
}
hideDropdown() {
this.dropdown.style.display = 'none';
}
}
// 使用
const suggestions = new SearchSuggestions({
container: '#search-container',
index: client.initIndex('products')
});
suggestions.onSelect = (query) => {
// 执行搜索
performSearch(query);
};
总结 #
搜索建议要点:
| 要点 | 说明 |
|---|---|
| 自动补全 | @algolia/autocomplete-js |
| 多来源 | 多个source配置 |
| 热门搜索 | 独立索引存储 |
| 最近搜索 | localStorage存储 |
| 即时搜索 | 防抖+实时查询 |
| 优化 | 缓存、最小字符数 |
接下来,让我们学习 多索引搜索。
最后更新:2026-03-28