搜索建议 #

搜索建议概述 #

搜索建议(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