客户端搜索 vs 服务端检索:静态站点搜索方案选型指南
核心决策:不是所有搜索都需要向量数据库
看到一个常见误区:凡是说到"搜索",就往 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 中自动重新生成)