KnowFlow 项目学习笔记(二):混合检索深度解析
写在前面:本学习笔记基于 KnowFlow 项目源码逐行解析,深度讲解混合检索的实现细节(向量检索 + 文本匹配、KNN 召回、BM25 Rescore、权限过滤)。并指出源码里的 3 处严重问题(❗❗ 标记),给出修复方案。适合面试前深度学习,确保”傻子都能懂”。
一、什么是混合检索?
傻子都能懂的解释:
想象你要在知识库里搜索”如何学习 Java”:
- 传统搜索(只用文本匹配):搜索”如何学习 Java”,只能找到包含这 6 个字的结果(比如”如何学习 Java 编程”)
- 向量搜索(只用语义相似):搜索”如何学习 Java”,可以找到”Java 入门指南”(虽然字不一样,但意思相似)
- 混合检索:结合两者优点,先用向量搜索找到”意思相似”的结果,再用文本匹配过滤掉不相关的结果
KnowFlow 的混合检索:
- 向量检索:把用户的查询转换成向量(一串数字),在 Elasticsearch 里找”向量相似度最高”的结果
- 文本匹配:用 BM25 算法(Elasticsearch 的默认评分算法),计算查询和文档的”文本相似度”
- 混合排序:把向量相似度和文本相似度结合起来,重新排序(Rescore)
二、源码解析:HybridSearchService.java
文件位置:PaiSmart-zuzhi/src/main/java/com/yizhaoqi/smartpai/service/HybridSearchService.java
2.1 混合检索主流程:searchWithPermission() 方法
源码(第 67-178 行):
1 | |
逐行解析(傻子都能懂版):
第 73 行:
List<String> userEffectiveTags = getUserEffectiveOrgTags(userId);- 获取用户的有效组织标签(用来做权限过滤)
- 比如用户属于”技术部”,他能看到”技术部”的文档 + 自己的文档 + 公开文档
第 77 行:
String userDbId = getUserDbId(userId);- 获取用户的数据库 ID(用来做权限过滤)
- 比如用户 ID 是
1001,他能看到userId = 1001的文档(自己的文档)
第 81 行:
final List<Float> queryVector = embedToVectorList(query);- 把用户的查询转换成向量(调用 embedding 服务)
- 比如查询是”如何学习 Java”,转换成向量:
[0.1, 0.2, 0.3, ...](768 维)
第 91-140 行:Elasticsearch 查询
- KNN 召回(第 95-100 行):在向量空间里找”距离最近”的文档(类似”找邻居”)
- 文本匹配过滤(第 102-103 行):必须包含查询关键词(比如搜索”Java”,结果里必须包含”Java”这个词)
- 权限过滤(第 104-123 行):只能看到自己有权限的文档
第 125-137 行:BM25 Rescore
- Rescore:重新评分(第二阶段)
- windowSize:只对前
recallK个结果重新评分(提高性能) - queryWeight:KNN 分数的权重(0.2 = 保留 20% 的 KNN 分数)
- rescoreQueryWeight:BM25 分数的权重(1.0 = BM25 分数占 100%)
第 145-161 行:处理结果
- 把 Elasticsearch 的返回结果转换成
SearchResult对象 hit.score():文档的评分(分数越高,排名越靠前)
- 把 Elasticsearch 的返回结果转换成
2.2 向量生成:embedToVectorList() 方法
源码(第 380-397 行):
1 | |
傻子都能懂的解释:
什么是向量?
- 向量就是”一串数字”(比如
[0.1, 0.2, 0.3, ...]) - 每个数字代表一个”特征”(类似”纬度”)
- 比如”苹果”这个词,转换成向量可能是
[0.1, 0.2, 0.3](表示”水果”特征) - “香蕉”这个词,转换成向量可能是
[0.1, 0.2, 0.4](也表示”水果”特征) - 因为”苹果”和”香蕉”的向量很接近,所以它们是”语义相似”的
什么是 Embedding?
- Embedding 就是把”文字”转换成”向量”的过程
- 比如用 OpenAI 的
text-embedding-ada-002模型,把”如何学习 Java”转换成 1536 维的向量
代码解析:
第 382 行:
List<float[]> vecs = embeddingClient.embed(List.of(text));- 调用 embedding 服务(可能是 OpenAI API,也可能是本地模型)
- 返回值是
List<float[]>(可能包含多个向量的列表)
第 387-392 行:把
float[]转换成List<Float>- Elasticsearch Java SDK 需要
List<Float>格式(不是float[])
- Elasticsearch Java SDK 需要
2.3 权限过滤:getUserEffectiveOrgTags() 方法
源码(第 402-429 行):
1 | |
傻子都能懂的解释:
什么是组织标签?
- 组织标签就是”部门”(比如”技术部”、”产品部”、”运营部”)
- 每个文档都有一个组织标签(比如”技术部”的文档,只有”技术部”的人能看)
什么是层级关系?
- 比如”技术部”下面有”前端组”、”后端组”
- 如果用户属于”技术部”,他能看到”技术部”、”前端组”、”后端组”的所有文档
代码解析:
第 408-419 行:获取用户
- 先尝试把
userId转换成Long(数据库 ID) - 如果转换失败,假设
userId是用户名(字符串)
- 先尝试把
第 422 行:
orgTagCacheService.getUserEffectiveOrgTags(user.getUsername())- 获取用户的有效组织标签(包含层级关系)
- 比如用户属于”技术部”,返回
["技术部", "前端组", "后端组"]
三、❗❗ 源码里的问题
问题 1:KNN 召回窗口太大,影响性能!
看代码(第 94-99 行):
1 | |
问题:
recallK = topK * 30(比如topK = 10,recallK = 300)- 如果
topK = 100,recallK = 3000(召回 3000 个结果) - Elasticsearch 需要对 3000 个结果重新评分(Rescore),性能很差
为什么要用 30 倍?
- 作者想提高召回率(担心漏掉相关结果)
- 但这样会严重影响性能(QPS 下降)
修复方案:动态调整召回窗口
1 | |
问题 2:权限过滤放在 KNN 查询里,影响性能!
看代码(第 102-123 行):
1 | |
问题:
- 权限过滤(filter)会过滤掉很多结果
- 如果 KNN 召回了 300 个结果,但权限过滤后只剩 10 个,浪费了很多计算资源
修复方案:先权限过滤,再 KNN 召回
1 | |
问题 3:没有分页,返回结果太多!
看代码(第 138 行):
1 | |
问题:
- 只有
size(返回结果数量),没有from(偏移量) - 如果用户输入
topK = 10000,Elasticsearch 会返回 10000 个结果(占用大量内存)
修复方案:限制最大返回数量
1 | |
更好的方案:支持分页
1 | |
四、面试八股文
4.1 什么是混合检索?为什么要用混合检索?
答:
- 混合检索是结合”向量检索”和”文本匹配”的搜索方式
- 好处:
- 语义理解:向量检索能理解”意思相似”的查询(比如”如何学习 Java”和”Java 入门指南”)
- 精确匹配:文本匹配能过滤掉不相关的结果(比如搜索”Java”,结果里必须包含”Java”这个词)
- 提高准确率:两者结合,比单独用一种方法更准确
4.2 什么是 KNN 召回?什么是 BM25 Rescore?
答:
- KNN 召回:在向量空间里找”距离最近”的文档(类似”找邻居”)
- BM25 Rescore:对 KNN 召回的结果重新评分(用 BM25 算法),提高准确率
4.3 什么是 Embedding?为什么要用 Embedding?
答:
- Embedding 是把”文字”转换成”向量”的过程
- 好处:
- 语义理解:向量相似的文本,意思也相似
- 跨语言:不同语言的文本,可以转换成同一个向量空间(比如中文”苹果”和英文”apple”的向量很接近)
4.4 什么是权限过滤?为什么要用权限过滤?
答:
- 权限过滤是确保用户只能看到自己有权限的文档
- 好处:
- 安全性:防止用户看到敏感文档(比如”技术部”的文档,不能让”产品部”的人看到)
- 合规性:符合数据保护法规(比如 GDPR)
五、总结
KnowFlow 的混合检索:
- 向量检索:用 KNN 算法召回”语义相似”的结果
- 文本匹配:用 BM25 算法过滤不相关的结果
- 权限过滤:确保用户只能看到自己有权限的文档
- Rescore:结合向量相似度和文本相似度,重新排序
源码里的问题(❗❗):
- KNN 召回窗口太大,影响性能
- 权限过滤放在 KNN 查询里,影响性能
- 没有分页,返回结果太多
修复方案:
- 动态调整召回窗口(
topK * 10,最多 500 个) - 先权限过滤,再 KNN 召回
- 支持分页(
from+size)
下一篇预告:《KnowFlow 项目学习笔记(三):安全 + 部署深度解析》
参考资料:
- Elasticsearch 官方文档:https://www.elastic.co/guide/en/elasticsearch/reference/current/index.html
- OpenAI Embedding API:https://platform.openai.com/docs/guides/embeddings
最后更新:2026-06-08 22:00:00