关键词与混合检索
关键词与混合检索 · 阅读后完成下方实验与检查题,再用「标记完成」记录进度。
学习目标
读完本课你能读懂本站资料库检索的真实实现——FTS5 全文索引优先、中文退化为 LIKE 子串匹配——并能对两类问题(技术标识符、自然语言)分别分析关键词与向量检索谁更靠谱,最后把两路合起来做混合检索。
问题场景
同一个资料库,两个问题:
- 问「
doc_chunk表有哪些列」——关键词检索一击命中,因为片段里就写着doc_chunk;向量检索反而可能把讲「数据表设计」的泛化段落也排进来。 - 问「怎么让页面不再闪」——关键词检索全军覆没(片段里没有「闪」字);语义上它对应「组件重复渲染排查」,向量检索能捞回来。
没有一种检索对所有问题都强,这就是要对比、要组合的原因。
核心解释
先读本站的真实实现(lib/ai/tools.ts 的 search_documents 分支),它分三层:
// 第一层:FTS5 全文索引(fts_doc 虚表,tokenize='unicode61')
let sql = "SELECT document_id, collection_slug, heading, text FROM fts_doc WHERE fts_doc MATCH ?";
rows = db.prepare(sql).all(query); // 英文按词切分匹配,效率高
// 第二层:中文兜底。FTS5 的 unicode61 分词器不切分中文——
// 「删除接口」会被当成一个整体 token,长中文查询基本匹配不上。
// 于是退化为 LIKE 子串匹配(个人资料量级足够用):
if (rows.length === 0) {
rows = db.prepare(
`SELECT document_id, collection_slug, heading, text FROM doc_chunk WHERE text LIKE ? LIMIT 5`
).all(`%${query}%`);
}
// 第三层:无命中时返回明确提示,不让模型编造
// { total: 0, message: "资料库没有命中内容;如无必要不要编造资料来源。" }
两个关键认知:
- FTS5(Full-Text Search,全文检索)是 SQLite 内置的倒排索引。
unicode61分词器按空格和标点切词,对英文友好;中文没有空格,长查询整体当 token,命中率骤降——所以中文场景真正干活的是 LIKE 兜底。这是「个人资料量级」下的务实取舍,不是缺陷。 - 关键词检索的本质是「字面包含」:精确、零成本、可解释,但对同义改写无能为力。向量检索(A20)本质是「语义距离」:能泛化,但需要嵌入服务,且对精确标识符不可靠。
混合检索就是两路都跑,各取所长再合并。最简单的合并是加权求和:
function hybridSearch(query: string) {
const kw = keywordSearch(query); // 关键词路:命中 score=1,未命中 0
const vec = vectorSearch(query, 5); // 向量路:余弦分数(接入向量后)
const scores = new Map();
for (const h of kw) scores.set(h.id, (scores.get(h.id) ?? 0) + 0.5);
for (const v of vec) scores.set(v.id, (scores.get(v.id) ?? 0) + 0.5 * v.score);
return [...scores.entries()].sort((a, b) => b[1] - a[1]).slice(0, 5);
}
如何验证:拿固定的问题集(5 个技术标识符问题 + 5 个自然语言问题)分别跑三种检索,记录每题 top-1 是否命中期望片段。你会看到清晰分工:标识符题关键词赢,改写题向量赢,混合两头不丢。
示例拆解
本课实验区用预设的语义分数模拟向量检索,对照看两类问题的表现:
| 问题类型 | 例子 | 关键词(LIKE) | 向量 | 结论 |
|---|---|---|---|---|
| 技术标识符 | doc_chunk 有哪些列 |
精确命中 | 可能混入泛化段落 | 信关键词 |
| 自然语言改写 | 页面不再闪 | 零命中 | 命中「重复渲染」 | 需要向量 |
| 编号引用 | A15 讲什么 | 命中 | 不可靠 | 信关键词 |
关键点:
- 中文查询在本站真正走的是 LIKE 子串匹配,所以查询词要贴近原文用词——「幂等」能中,「防止重复提交生效两次」中不了。
- 混合合并时两路分数量纲要对齐(都归一化到 0~1),否则一路碾压另一路,合并形同虚设。
- 混合检索每多一路就多一份成本:向量路每次提问都调嵌入服务,关键词路几乎免费。
动手任务
- 跑本课实验区:对「组件重新渲染」和「useState」两个问题,分别记录关键词命中数和向量 top-2,验证「标识符信关键词、改写靠向量」。
- 给片段列表加一条「正文含 useState 但讲的是嵌套路由」的干扰片段,观察关键词检索误命中——理解精确匹配的副作用。
- 实现实验区任务里的 0.5/0.5 加权合并,观察两个问题的 top-2 变化;回答:哪一路分数权重应该更高,取决于什么?
常见错误
- 以为 FTS5 对中文友好:unicode61 不切分中文,长中文查询在 MATCH 层基本全空——看到「FTS5 支持全文检索」就以为中文查询没问题,是本站实现里最容易被误读的一处。
- 用自然语言长句当关键词查询:LIKE 是整串子串匹配,整句进去几乎必然零命中;关键词查询要抽成短词。
- 合并时不管量纲:余弦分数 0.7 和关键词命中的 1 直接相加,权重失衡,混合结果比单路还差。
- 两路结果不去重:同一片段两路都命中,合并后重复占位,挤掉别的候选。
检查与复盘
自查清单:
- 能说出本站检索三层结构(FTS5 MATCH → LIKE 兜底 → 无命中提示)及每层触发条件
- 能对任意一个新问题预判哪种检索更可能命中,并说理由
- 能写出加权合并的实现并指出量纲问题
参考资料
- SQLite 官方文档:FTS5 —— 全文索引与分词器的行为说明
- SQLite 官方文档:LIKE 操作符 —— 兜底匹配的表达式语义
- OpenAI 文档:Embeddings —— 向量一路的来源
- 下一课 A22 讲命中之后的事:回答要能落到可打开的片段上。
读完这一课了?
标记完成后,工作台会安排接下来的学习。