联合搜索:向量 + 关键词 + 知识图谱怎么并排跑
一、三种通道,三种盲区
给湖北理工计算机学院做官网助手,上了三套检索。很快发现各管各的,谁也覆盖不全。
学生问:"王教授带的研究生发了哪些深度学习论文?"
向量检索:搜到"深度学习课程大纲""王教授个人简介"——语义相关,但不精准
BM25: 搜到"深度学习""论文"精确命中——但不知道跟王教授什么关系
图谱检索:沿王教授→指导→研究生→发表→论文路径,捞出完整链——但可能漏掉没入图的论文
任何一个单独用都丢信息。方案:三个并排跑,结果合并去重,再交给 Rerank 统一排一次。
二、三种通道各负责什么
查询
│
├── 通道1: 向量检索
│ 强项: 语义模糊的查询
│ 例: "有哪些偏理论性的课程"
│ 输出: top-20
│
├── 通道2: BM25 关键词检索
│ 强项: 精确术语、编号、人名
│ 例: "CS201是什么课"
│ 输出: top-20
│
└── 通道3: 知识图谱
强项: 实体关系查询
例: "王教授带哪些课"
输出: top-20
│
└── [合并去重] → ~40条
│
└── [Rerank重排] → top-5
三、为什么三通道中 BM25 最容易被忽视
向量检索有时候太聪明了,聪明到绕路。
学生搜"数据结构",向量可能返回"算法设计与分析""高级数据结构"——语义对,但不是学生要的那门课。BM25 直接按词匹配,精确命中"数据结构"这门课名。
向量在"长尾语义"上无敌,BM25 在"精确术语"上无敌。谁也别代替谁。
BM25 配置思路(伪代码):
索引:
字段: 标题(权重×3), 正文(权重×1)
中文分词: 用你的分词器
精确匹配: 标题字段额外开一个不拆词的子字段
查询:
if 查询看起来像个精确术语(短、有数字、含编号):
优先匹配精确子字段
else:
常规 match + phrase
不用纠结具体 ES API——所有全文检索引擎都支持这个思路。
四、三通道并行跑
三个通道之间没有依赖,并行执行,总延迟 = 最慢那个通道的时长。
function 并行检索(查询):
并行执行:
future1 = 线程池.submit(向量检索, 查询, top_k=20)
future2 = 线程池.submit(BM25检索, 查询, top_k=20)
future3 = 线程池.submit(图谱检索, 查询, top_k=20)
等待全部完成
return {
向量结果: future1.result(),
BM25结果: future2.result(),
图谱结果: future3.result(),
}
三个通道之间没有依赖,就是三个独立检索,扔进线程池同时跑就行。
五、合并去重
三个通道可能返回同一条文档——同一段话可能同时被向量和 BM25 命中。需要去重。
function 去重(三个通道的结果):
已见指纹 = {}
合并列表 = []
for each 结果 in 所有结果:
指纹 = MD5(结果.内容.前200字) // 前200字相同 ≈ 同一条
if 指纹 not in 已见指纹:
已见指纹.add(指纹)
合并列表.append(结果)
return 合并列表 // 大约30~50条
六、Rerank 收尾
合并后还有 30-50 条候选。全塞给 LLM token 太多,而且顺序不对——向量给的 0.85 分和 BM25 给的 0.85 分不可比。
Rerank 用 Cross-Encoder 把 query 和每条 doc 做一次真正的"对比打分"——不是看 embedding 距离,是看 query 和 doc 拼在一起后模型的判断。
function Rerank(查询, 候选列表, 最终保留数=5):
送Rerank模型(query=查询, documents=候选列表.内容)
模型返回每条候选的重排分数
按分数从高到低取前N条
return top-N
Rerank 延迟约 200ms,跟 LLM 生成的 1.5 秒比起来可以忽略。
七、容错——某个通道挂了不炸全局
生产环境不是怕某个通道效果差,是怕它挂了把整个检索拖死。
function 安全检索(通道名, 检索函数, 超时秒数=5):
try:
return 线程池.submit(检索函数).result(timeout=超时秒数)
catch 超时 or 异常:
记录日志("{通道名}挂了,返回空")
return [] // 安静降级,不抛异常
图谱挂了 → 向量和 BM25 继续跑 → 效果略降但服务不中断。
八、什么时候该开哪个通道
不是所有查询都需要三通道齐开。判断逻辑:
function 选通道(查询):
通道 = {"向量"} // 向量是标配,始终开
if 查询有精确术语(短、编号、专有名词):
通道.add("BM25")
if 查询有实体关系(人名+动作、先修课、谁教、属于):
通道.add("图谱")
return 只跑通道里选中的那些
消融实验数据(在学院官网 100 条查询上):
| 通道组合 | 语义型查询(40条) | 关键词型(25条) | 关系型(35条) |
|---|---|---|---|
| 纯向量 | 0.78 | 0.52 | 0.33 |
| 向量+BM25 | 0.78 | 0.88 | 0.33 |
| 向量+BM25+图谱 | 0.78 | 0.88 | 0.86 |
| +Rerank | 0.83 | 0.92 | 0.92 |
一眼结论:BM25 救命于关键词,图谱救命于关系,Rerank 每类都提 3-5 分。
九、成本
检索环节 每查一次
─────────────────────────
向量检索 免费(本地跑)
BM25检索 免费(本地跑)
图谱查询 免费(本地跑)
Rerank API ¥0.002
LLM 生成回答 ¥0.01
─────────────────────────
合计 ¥0.012
检索环节除了 Rerank 调一次 API,全是本地执行。
十、总结
联合检索的思路就三步:
- 并行跑:三种通道各搜各的,谁不依赖谁
- 合并去重:前 200 字指纹去重,不丢信息
- Rerank 收尾:Cross-Encoder 统一打分,取 top-5
不是为了炫技——是被真实查询逼出来的。学生问"做 NLP 的老师有哪些",纯向量返回的全是"NLP 课程介绍",没有一个教师名字。向量+图谱并排跑,图谱捞出教师列表,向量补课程细节,Rerank 排一出最终答案。从 0.33 到 0.92,这就是并排跑的价值。