第一个搜索应用 #

项目概述 #

我们将创建一个简单的商品搜索应用,实现以下功能:

  • 添加商品数据到Algolia
  • 实现即时搜索
  • 显示搜索结果
  • 添加筛选功能

准备工作 #

安装依赖 #

bash
# 创建项目目录
mkdir algolia-search-demo
cd algolia-search-demo

# 初始化项目
npm init -y

# 安装Algolia客户端
npm install algoliasearch

# 安装环境变量管理
npm install dotenv

项目结构 #

text
algolia-search-demo/
├── .env
├── package.json
├── data/
│   └── products.json
├── scripts/
│   └── import-data.js
└── public/
    └── index.html

步骤一:准备数据 #

创建示例数据 #

创建 data/products.json

json
[
  {
    "objectID": "1",
    "name": "iPhone 15 Pro",
    "description": "Apple iPhone 15 Pro with A17 Pro chip",
    "brand": "Apple",
    "category": "Smartphones",
    "price": 999,
    "stock": 50,
    "rating": 4.8,
    "image": "https://example.com/iphone15.jpg"
  },
  {
    "objectID": "2",
    "name": "Samsung Galaxy S24",
    "description": "Samsung Galaxy S24 with AI features",
    "brand": "Samsung",
    "category": "Smartphones",
    "price": 799,
    "stock": 100,
    "rating": 4.6,
    "image": "https://example.com/s24.jpg"
  },
  {
    "objectID": "3",
    "name": "MacBook Pro 14",
    "description": "Apple MacBook Pro with M3 Pro chip",
    "brand": "Apple",
    "category": "Laptops",
    "price": 1999,
    "stock": 30,
    "rating": 4.9,
    "image": "https://example.com/macbook.jpg"
  },
  {
    "objectID": "4",
    "name": "Dell XPS 15",
    "description": "Dell XPS 15 with Intel Core i9",
    "brand": "Dell",
    "category": "Laptops",
    "price": 1499,
    "stock": 45,
    "rating": 4.5,
    "image": "https://example.com/xps15.jpg"
  },
  {
    "objectID": "5",
    "name": "Sony WH-1000XM5",
    "description": "Sony noise canceling headphones",
    "brand": "Sony",
    "category": "Audio",
    "price": 349,
    "stock": 200,
    "rating": 4.7,
    "image": "https://example.com/sony.jpg"
  },
  {
    "objectID": "6",
    "name": "AirPods Pro",
    "description": "Apple AirPods Pro with active noise cancellation",
    "brand": "Apple",
    "category": "Audio",
    "price": 249,
    "stock": 150,
    "rating": 4.6,
    "image": "https://example.com/airpods.jpg"
  }
]

数据字段说明 #

字段 类型 说明
objectID string 唯一标识符(必需)
name string 商品名称
description string 商品描述
brand string 品牌
category string 分类
price number 价格
stock number 库存
rating number 评分

步骤二:导入数据 #

创建导入脚本 #

创建 scripts/import-data.js

javascript
require('dotenv').config();
const algoliasearch = require('algoliasearch');
const products = require('../data/products.json');

const client = algoliasearch(
  process.env.ALGOLIA_APP_ID,
  process.env.ALGOLIA_ADMIN_KEY
);

const index = client.initIndex(process.env.ALGOLIA_INDEX_NAME);

async function importData() {
  try {
    // 配置索引设置
    await index.setSettings({
      searchableAttributes: ['name', 'description', 'brand', 'category'],
      attributesForFaceting: ['brand', 'category', 'price'],
      customRanking: ['desc(rating)', 'desc(popularity)'],
      attributesToHighlight: ['name', 'description'],
      hitsPerPage: 10
    });

    console.log('索引设置已更新');

    // 导入数据
    const { objectIDs } = await index.saveObjects(products);
    console.log(`成功导入 ${objectIDs.length} 条数据`);

  } catch (error) {
    console.error('导入失败:', error);
  }
}

importData();

配置环境变量 #

创建 .env

bash
ALGOLIA_APP_ID=your_app_id
ALGOLIA_ADMIN_KEY=your_admin_key
ALGOLIA_INDEX_NAME=products

运行导入 #

bash
node scripts/import-data.js

输出:

text
索引设置已更新
成功导入 6 条数据

步骤三:创建搜索界面 #

HTML结构 #

创建 public/index.html

html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>商品搜索</title>
  <style>
    * {
      margin: 0;
      padding: 0;
      box-sizing: border-box;
    }
    
    body {
      font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
      background: #f5f5f5;
      padding: 20px;
    }
    
    .container {
      max-width: 1200px;
      margin: 0 auto;
    }
    
    .search-header {
      text-align: center;
      margin-bottom: 30px;
    }
    
    .search-header h1 {
      color: #333;
      margin-bottom: 20px;
    }
    
    .search-box {
      position: relative;
      max-width: 600px;
      margin: 0 auto;
    }
    
    .search-box input {
      width: 100%;
      padding: 15px 20px;
      font-size: 16px;
      border: 2px solid #ddd;
      border-radius: 8px;
      outline: none;
      transition: border-color 0.3s;
    }
    
    .search-box input:focus {
      border-color: #5468ff;
    }
    
    .filters {
      display: flex;
      gap: 10px;
      justify-content: center;
      margin: 20px 0;
      flex-wrap: wrap;
    }
    
    .filter-btn {
      padding: 8px 16px;
      border: 1px solid #ddd;
      background: white;
      border-radius: 20px;
      cursor: pointer;
      transition: all 0.3s;
    }
    
    .filter-btn:hover,
    .filter-btn.active {
      background: #5468ff;
      color: white;
      border-color: #5468ff;
    }
    
    .stats {
      text-align: center;
      color: #666;
      margin-bottom: 20px;
    }
    
    .results {
      display: grid;
      grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
      gap: 20px;
    }
    
    .product-card {
      background: white;
      border-radius: 12px;
      padding: 20px;
      box-shadow: 0 2px 8px rgba(0,0,0,0.1);
      transition: transform 0.3s, box-shadow 0.3s;
    }
    
    .product-card:hover {
      transform: translateY(-5px);
      box-shadow: 0 4px 16px rgba(0,0,0,0.15);
    }
    
    .product-image {
      width: 100%;
      height: 200px;
      background: #f0f0f0;
      border-radius: 8px;
      margin-bottom: 15px;
      display: flex;
      align-items: center;
      justify-content: center;
      color: #999;
    }
    
    .product-brand {
      color: #888;
      font-size: 12px;
      text-transform: uppercase;
      letter-spacing: 1px;
    }
    
    .product-name {
      font-size: 18px;
      font-weight: 600;
      color: #333;
      margin: 8px 0;
    }
    
    .product-description {
      color: #666;
      font-size: 14px;
      line-height: 1.5;
      margin-bottom: 15px;
    }
    
    .product-footer {
      display: flex;
      justify-content: space-between;
      align-items: center;
    }
    
    .product-price {
      font-size: 20px;
      font-weight: 700;
      color: #5468ff;
    }
    
    .product-rating {
      color: #ffc107;
    }
    
    .no-results {
      text-align: center;
      padding: 40px;
      color: #666;
    }
    
    .highlight {
      background: #fff3cd;
      padding: 0 2px;
      border-radius: 2px;
    }
  </style>
</head>
<body>
  <div class="container">
    <div class="search-header">
      <h1>商品搜索</h1>
      <div class="search-box">
        <input type="text" id="searchInput" placeholder="搜索商品名称、品牌或描述...">
      </div>
    </div>
    
    <div class="filters" id="filters">
      <button class="filter-btn active" data-filter="all">全部</button>
      <button class="filter-btn" data-filter="Smartphones">手机</button>
      <button class="filter-btn" data-filter="Laptops">笔记本</button>
      <button class="filter-btn" data-filter="Audio">音频</button>
    </div>
    
    <div class="stats" id="stats"></div>
    
    <div class="results" id="results"></div>
  </div>

  <script src="https://cdn.jsdelivr.net/npm/algoliasearch@4/dist/algoliasearch-lite.umd.js"></script>
  <script>
    // 初始化客户端
    const client = algoliasearch('YOUR_APP_ID', 'YOUR_SEARCH_KEY');
    const index = client.initIndex('products');
    
    const searchInput = document.getElementById('searchInput');
    const resultsContainer = document.getElementById('results');
    const statsContainer = document.getElementById('stats');
    const filterButtons = document.querySelectorAll('.filter-btn');
    
    let currentCategory = 'all';
    
    // 搜索函数
    async function search(query) {
      const filters = currentCategory === 'all' 
        ? '' 
        : `category:${currentCategory}`;
      
      try {
        const { hits, nbHits, processingTimeMS } = await index.search(query, {
          filters,
          hitsPerPage: 20
        });
        
        displayResults(hits);
        displayStats(nbHits, processingTimeMS);
      } catch (error) {
        console.error('搜索失败:', error);
      }
    }
    
    // 显示结果
    function displayResults(hits) {
      if (hits.length === 0) {
        resultsContainer.innerHTML = `
          <div class="no-results">
            <p>没有找到相关商品</p>
          </div>
        `;
        return;
      }
      
      resultsContainer.innerHTML = hits.map(hit => `
        <div class="product-card">
          <div class="product-image">商品图片</div>
          <div class="product-brand">${hit.brand}</div>
          <div class="product-name">${hit._highlightResult.name.value}</div>
          <div class="product-description">${hit._highlightResult.description.value}</div>
          <div class="product-footer">
            <span class="product-price">$${hit.price}</span>
            <span class="product-rating">★ ${hit.rating}</span>
          </div>
        </div>
      `).join('');
    }
    
    // 显示统计
    function displayStats(count, time) {
      statsContainer.textContent = `找到 ${count} 个结果,耗时 ${time}ms`;
    }
    
    // 搜索输入事件
    let debounceTimer;
    searchInput.addEventListener('input', (e) => {
      clearTimeout(debounceTimer);
      debounceTimer = setTimeout(() => {
        search(e.target.value);
      }, 300);
    });
    
    // 分类筛选事件
    filterButtons.forEach(btn => {
      btn.addEventListener('click', () => {
        filterButtons.forEach(b => b.classList.remove('active'));
        btn.classList.add('active');
        currentCategory = btn.dataset.filter;
        search(searchInput.value);
      });
    });
    
    // 初始加载
    search('');
  </script>
</body>
</html>

步骤四:运行应用 #

本地预览 #

bash
# 使用任意HTTP服务器
npx serve public

# 或使用Python
python -m http.server 8000 -d public

访问 http://localhost:3000http://localhost:8000

功能验证 #

测试搜索 #

  1. 输入"iPhone",应该显示iPhone相关产品
  2. 输入"Apple",应该显示所有Apple产品
  3. 输入"Sony",应该显示Sony耳机

测试筛选 #

  1. 点击"手机"按钮,只显示手机类别
  2. 点击"笔记本"按钮,只显示笔记本类别
  3. 点击"全部"按钮,显示所有产品

测试高亮 #

搜索结果中的匹配文字应该被高亮显示。

代码解析 #

搜索配置 #

javascript
const { hits, nbHits, processingTimeMS } = await index.search(query, {
  filters,        // 过滤条件
  hitsPerPage: 20 // 每页结果数
});

防抖处理 #

javascript
// 300ms防抖,避免频繁请求
let debounceTimer;
searchInput.addEventListener('input', (e) => {
  clearTimeout(debounceTimer);
  debounceTimer = setTimeout(() => {
    search(e.target.value);
  }, 300);
});

高亮显示 #

javascript
// 使用_highlightResult获取高亮内容
${hit._highlightResult.name.value}

总结 #

本章我们完成了一个完整的搜索应用:

  1. 准备商品数据
  2. 导入数据到Algolia
  3. 创建搜索界面
  4. 实现即时搜索和筛选

接下来,让我们深入了解 Algolia控制台

最后更新:2026-03-28