客户端搜索 vs 服务端检索:静态站点搜索方案选型指南

2026-06-11搜索静态站点Fuse.js架构选型

核心决策:不是所有搜索都需要向量数据库

看到一个常见误区:凡是说到"搜索",就往 RAG + 向量数据库上靠。实际上,文档量级才是决定架构的核心变量——小规模用重武器是过度设计,大规模用轻方案则不可用。

选型决策树

文档数量 < 500 篇?
  ├─ 是 → 客户端搜索(search-index.json + Fuse.js / Lunr.js)
  │       优势:零后端、零成本、毫秒级响应
  │       限制:首次加载索引(50-200KB),中文分词效果一般
  │
  └─ 否 → 文档数量 500 - 5000 篇?
         ├─ 是 → 服务端全文检索(Meilisearch / Elasticsearch)
         │       优势:精准、快速、支持中文分词
         │       成本:需要服务器或托管服务
         │
         └─ 否(> 5000 篇 + 语义需求)
              → 向量数据库 + 混合检索
                优势:语义理解、多模态
                成本高、运维复杂

方案一:Fuse.js 客户端搜索(推荐 < 500 篇)

索引生成

核心思路:构建时扫描所有内容文件,提取纯文本,生成一个 JSON 索引文件,随站点一起部署。

// generate-search-index.js
const fs = require('fs');
const path = require('path');

function stripHtml(html) {
    return html
        .replace(/<style[^>]*>[\s\S]*?<\/style>/gi, '')
        .replace(/<script[^>]*>[\s\S]*?<\/script>/gi, '')
        .replace(/<[^>]+>/g, ' ')
        .replace(/\s+/g, ' ')
        .trim();
}

const dirs = ['projects', 'posts', 'knowledge'];
const index = [];

for (const dir of dirs) {
    const files = fs.readdirSync(dir).filter(f => f.endsWith('.html'));
    for (const file of files) {
        const raw = fs.readFileSync(path.join(dir, file), 'utf8');
        const text = stripHtml(raw);
        const title = (raw.match(/<title>([^<]+)<\/title>/) || ['', file])[1];
        index.push({
            title, url: `${dir}/${file}`,
            snippet: text.slice(0, 300),
            body: text.slice(0, 4000)   // 全文索引
        });
    }
}
fs.writeFileSync('search-index.json', JSON.stringify(index));

Fuse.js 配置

const fuse = new Fuse(searchIndex, {
    keys: [
        { name: 'title', weight: 0.6 },
        { name: 'tags', weight: 0.2 },
        { name: 'body', weight: 0.2 }
    ],
    threshold: 0.4,           // 模糊匹配容错度(0=精确,1=全匹配)
    distance: 100,            // 匹配距离
    includeScore: true,       // 返回相关性分数
    minMatchCharLength: 2,    // 最短匹配字符数
    ignoreLocation: true      // 忽略匹配位置权重
});

threshold 调优:0.3-0.4 是中文搜索的甜点区。太低(0.1)会漏掉很多相关结果,太高(0.6)会返回大量噪音。建议结合 includeScore 排序,把得分最高的放在前面。

降级方案

索引 JSON 加载或解析失败时,不应整个搜索功能不可用:

async function loadSearchIndex() {
    try {
        const resp = await fetch('/search-index.json');
        if (!resp.ok) throw new Error('Index load failed');
        return await resp.json();
    } catch {
        // 降级:用页面中已有的 allPosts 数据
        console.warn('搜索索引加载失败,使用降级方案');
        return window.allPosts.map(p => ({
            title: p.title,
            url: p.url,
            snippet: p.description || '',
            body: p.tags?.join(' ') || ''
        }));
    }
}

方案二:Meilisearch 服务端搜索

当文档超过 500 篇时推荐。特点:Rust 编写、极快、内置中文分词(jieba)、RESTful API。

# Docker 一键部署
docker run -p 7700:7700 getmeili/meilisearch

# 索引文档
curl -X POST 'http://localhost:7700/indexes/posts/documents' \
  -H 'Content-Type: application/json' \
  --data-binary @search-documents.json

# 搜索
curl 'http://localhost:7700/indexes/posts/search?q=RAG+混合检索&limit=10'

成本:Meilisearch Cloud 有免费额度(100K documents),对小中型站点足够。

方案三:向量数据库 + 混合检索

当用户查询是自然语言且需要语义理解时(如"怎么让 AI 不瞎编"→匹配"幻觉问题"相关文档),纯关键词搜索不够。此时引入 Embedding + 向量检索 + RRF 融合。

但记住:500 篇以下不需要这个方案。过度设计不仅增加运维负担,还引入延迟(Embedding 生成 + 向量检索比倒排索引慢一个数量级)。

决策 Checklist

  • 文档 < 500 篇、不需要语义搜索 → Fuse.js 客户端搜索
  • 文档 500-5000、需要精准中文搜索 → Meilisearch
  • 需要语义理解和多模态检索 → 向量数据库 + RRF 混合检索
  • 不想管运维 → Algolia DocSearch(开源项目免费)
  • 搜索索引必须和内容同步更新(CI/CD 中自动重新生成)
未标记