KnowFlow ②:ES 混合检索(KNN 向量 + BM25 Rescore)面试深挖

源码口径更新(2026-09-29):本文保留学习笔记与原理示例。项目实际实现、个人增量和测试边界请先看项目总览;示例数字不代表可复核的项目实测。

KnowFlow ②:ES 混合检索(KNN 向量 + BM25 Rescore)

项目:KnowFlow 企业级 RAG 知识库系统 | 整理时间:2026-06-07 | 适用于后端实习/校招面试


一、先搞懂:为什么单纯的关键词搜索不够用?

1.1 关键词搜索的死穴

假设你公司有个技术文档库,里面有一句话:

“MySQL 主从复制延迟过高的排查思路”

用户搜索:“MySQL 同步慢怎么查?”

BM25(关键词搜索)的结果:

1
2
3
4
5
6
7
8
搜 "MySQL 同步慢怎么查?"
↓
分词:["MySQL", "同步", "慢", "怎么", "查"]
↓
去文档里找包含这些词的地方
↓
"主从复制延迟过高" 里没有"同步慢"这个词 → 匹配不上 ❌
"排查思路" 里没有"怎么查" → 匹配不上 ❌

这就是语义鸿沟——用户说的词和文档里的词不一样,但意思是一样的,传统搜索找不到。

1.2 向量搜索怎么解决这个问题?

把每个文本块都变成一个 2048 维的向量(可以理解为一组 2048 个数字),意思相近的文本,它们的向量在空间里距离就很近。

1
2
3
4
"MySQL 同步慢怎么查?"  → 向量 A: [0.12, -0.34, ..., 0.78]  (2048维)
"MySQL 主从复制延迟过高的排查思路" → 向量 B: [0.11, -0.33, ..., 0.76] (2048维)

计算余弦相似度:A·B ≈ 0.92(非常接近!)→ 匹配上了 ✅

1.3 那为什么还要 BM25?(面试必问)

向量搜索也有死角:

场景 向量搜索 BM25 关键词搜索
“MySQL 8.0 新特性”(精确关键词) 可能把”PostgreSQL 14 新特性”也召回了,不够精确 ✅ 精准匹配”MySQL 8.0”,准确率极高
“怎么配置主从复制”(语义相似) ✅ 能理解语义,召回全面 ❌ 必须出现”配置””主从复制”这些词才行
企业专有名词(”XX 系统””YY 平台”) 训练数据里没有这些词,向量质量差 ✅ 精确匹配,不依赖训练数据

结论:两个都要,取长补短。


二、KnowFlow 的混合检索架构(核心设计)

2.1 整体流程图

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
用户输入:"MySQL 同步慢怎么查?"
│
├─ 第一步:生成查询向量(调用 Embedding API)
│ ↓
│ [0.12, -0.34, ..., 0.78] (2048维)
│
├─ 第二步:KNN 向量召回(ES 的 knn 查询)
│ 从 ES 里找向量最相似的 Top-K 个文本块
│ (K = topK × 30,放大召回窗口,比如要 5 个结果就先召回 150 个)
│
├─ 第三步:BM25 关键词重排序(ES 的 rescore)
│ 对 KNN 召回的 150 个结果,用 BM25 算关键词相关度
│ 把向量相似但关键词不匹配的往后排
│
└─ 第四步:返回 Top-K
权限条件同时作用于 KNN 和文本查询分支;ES 会把两路按 OR 合并

2.2 ES 索引结构(面试可能问)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// knowledge_base 索引的 mapping(简化版)
{
"mappings": {
"properties": {
"textContent": { "type": "text", "analyzer": "ik_max_word" }, // 原文内容,用于 BM25
"vector": { "type": "dense_vector", "dims": 2048 }, // 向量,用于 KNN
"fileMd5": { "type": "keyword" }, // 所属文件
"chunkId": { "type": "keyword" }, // 文本块 ID
"userId": { "type": "keyword" }, // 上传者 ID
"orgTag": { "type": "keyword" }, // 组织标签
"public": { "type": "boolean" } // 是否公开
}
}
}

💡 为什么 textContent 和 vector 存在同一个索引里?
答:这样 KNN 召回和 BM25 重排序可以在一次查询里完成,不用先向量搜一次、再关键词搜一次、再合并结果,省一次网络往返,延迟更低。


三、源码深度解析:HybridSearchService.java

3.1 KNN 向量召回 + 权限过滤

1
2
3
4
5
Query permission = permissionFilter(userDbId, userEffectiveTags);
SearchRequest request = permissionSearchRequest(query, queryVector, topK, permission);
// request.knn().get(0).filter() 与 request.query().bool().filter()
// 都包含同一权限条件:本人、公开、所属组织三者至少满足一个。
SearchResponse<EsDocument> response = esClient.search(request, EsDocument.class);

3.2```

3.2 当前实现是 BM25 加权 Rescore

HybridSearchService 在一个 Elasticsearch 请求中设置顶层 knn 和文本 query,Elasticsearch 将两路候选按 OR 合并。随后 rescore 在窗口内按 queryWeight=0.2 保留原始分数,再以 rescoreQueryWeight=1.0 加入 BM25 match 分数。这里没有独立的两份排名,也没有按 1/(k+rank) 计算,所以不是 RRF。

顶层文本查询中的权限过滤不会自动约束顶层 KNN。当前源码给两路分别使用同一权限过滤条件;3 项请求结构回归测试已通过;真实 Elasticsearch 端到端隔离测试仍待补充。topK × 30 是候选窗口设置,不代表有准确率或召回率提升的实测结论。


四、权限过滤的多租户设计(KnowFlow 的核心业务)

4.1 三种权限级别

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
用户能搜到哪些文档?

┌─ 自己上传的文档 ──────────────────────┐
│ WHERE userId = 当前用户数据库 ID │
└────────────────────────────────────────┘
OR
┌─ 标记为公开的文档 ────────────────────┐
│ WHERE isPublic = true │
└────────────────────────────────────────┘
OR
┌─ 同组织的文档(含父级组织!)─────────┐
│ WHERE orgTag IN (用户所有有效标签) │
│ 比如用户在 "技术部-后端组" │
│ 他能看到:"技术部" 和 "后端组" 的文档│
└────────────────────────────────────────┘

4.2 组织标签的层级关系(源码解析)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// OrgTagCacheService.getUserEffectiveOrgTags()
// 递归收集用户所有有效标签(含父级)

Set<String> allEffectiveTags = new HashSet<>();

// 用户直接拥有的标签
allEffectiveTags.add("技术部-后端组");

// 递归找父标签
collectParentTags("技术部-后端组", allEffectiveTags);
// → 找到父标签 "技术部",加入
// → 再找 "技术部" 的父标签,如果有的话继续加入

// 最后 allEffectiveTags = ["技术部-后端组", "技术部", "DEFAULT"]
// DEFAULT 是内置的默认标签,所有人都有

ES 查询里怎么用:

1
2
3
4
5
6
7
8
// 用户的有效标签列表:["技术部-后端组", "技术部", "DEFAULT"]
// ES 查询:orgTag 命中任意一个就行
.should(s3 -> {
userEffectiveTags.forEach(tag ->
bf.should(sh -> sh.term(t -> t.field("orgTag").value(tag)))
);
return bf;
})

4.3 为什么权限过滤要放在 filter 里而不是 must 里?

答:filter 上下文不计算相关度分数,只做 true/false 判断,可以利用 ES 的缓存机制加速。权限过滤和条件过滤不需要参与分数计算,所以用 filter 而不是 must,性能更好。


五、Embedding 失败时的降级策略(系统的鲁棒性)

5.1 为什么向量生成会失败?

1
2
3
4
5
6
7
8
9
调用大模型 Embedding API
↓
可能的原因:
- 网络超时(大模型服务不稳定)
- API Key 配额用完了
- 文本太长超过 Token 限制
- 大模型服务正在发版/维护
↓
向量生成失败 → queryVector = null

5.2 降级方案:纯 BM25 搜索

1
2
3
4
5
6
7
8
// HybridSearchService.searchWithPermission()
List<Float> queryVector = embedToVectorList(query);

if (queryVector == null) {
// 向量生成失败!降级到纯关键词搜索
log.warn("向量生成失败,仅使用文本匹配进行搜索");
return textOnlySearchWithPermission(query, userDbId, userEffectiveTags, topK);
}

💡 面试亮点:这是一个很实际的工程设计。大模型依赖的服务不一定 100% 可靠,系统要有降级能力。纯 BM25 虽然召回质量低一些,但至少能用,总比给用户报 500 错误好。


六、ES 复合索引设计(面试八股文)

6.1 为什么要建复合索引?

1
2
3
4
5
6
7
-- 权限过滤是最常见的查询条件,不加索引会全表扫描!
WHERE userId = 'user123' OR isPublic = true OR orgTag IN ('tag1','tag2')

-- 复合索引设计
CREATE INDEX idx_permission ON knowledge_base (userId, orgTag, isPublic);
-- ES 里不需要手动建,keyword 类型默认可索引
-- 但要注意:text 类型不能用于 filter,必须用 keyword 类型

ES 里的注意事项:

字段 类型 能不能用于 term 过滤 原因
userId keyword ✅ keyword 类型是精确值,可用于 term 查询
orgTag keyword ✅ 同上
public boolean ✅ boolean 类型可精确匹配
textContent text ❌ text 类型会被分词,不能用 term 精确匹配

💡 面试追问:”如果要支持 textContent 的关键词精确过滤怎么办?”
答:用 multi-field,给 textContent 同时建 text 类型和 keyword 类型:"fields": {"keyword": {"type": "keyword"}},然后过滤用 textContent.keyword。


七、面试八股文高频题

Q1:ES 的 KNN 搜索底层是什么算法?

答:ES 8.x 的 dense_vector 字段使用 HNSW(Hierarchical Navigable Small World) 算法,是一种基于图的近似最近邻搜索算法。查询复杂度约 O(log N),比暴力搜索 O(N) 快很多。牺牲少量精度(recall 约 95~99%)换取大幅速度提升。

Q2:为什么 KNN 的 k 要设成 topK × 30?

答:KNN 是近似搜索,会有一些误召回或漏召回。把召回窗口放大(比如要 5 个最终结果,先召回 150 个),然后在这 150 个里面用 BM25 精确重排序,这样最终Top-K 的质量更高。这叫 “召回+重排”两阶段检索,是搜索系统的标准做法。

Q3:当前 Rescore 与 RRF 有什么区别?

答:当前实现把原始查询分数乘以 0.2,再加上 BM25 重排分数;RRF 只看两份结果各自的名次。项目没有实现 RRF,也没有成套检索评测,不能声称 RRF 带来效果提升。

Q4:如果文档库里有 1 亿条数据,KNN 搜索会变慢吗?

答:HNSW 的查询复杂度是 O(log N),1 亿条也只会访问图中很少的节点,速度影响不大。但索引会占用较多内存(向量要全部加载到内存里才能快)。解决思路:如果向量维度高(2048 维),可以考虑用 PQ(Product Quantization)压缩,或者升级到 ES 的 int8_hnsw 量化索引,内存占用降到 1/4。

Q5:多租户场景下,怎么防止 A 租户通过精心构造的查询”撞库”到 B 租户的数据?

答:三层防护:

  1. 应用层:所有搜索接口先从 JWT Token 里取出 userId 和 orgTags,作为参数传入 ES 查询的 filter 里,用户无法伪造;
  2. ES 层:用 filter 做权限过滤,不匹配的文档直接不返回,不是靠分数压低,而是根本不出现在结果集里;
  3. 组织标签层级:即使 A 用户知道 B 用户的文档 ID,只要 orgTag 不匹配,就搜不到。

八、简历上怎么讲这句话(可直接用)

可核实表述:使用 Elasticsearch 的 KNN 与文本查询召回候选,再以 BM25 Rescore 调整排序;权限过滤同时加在两路查询上。尚无可靠的离线评测数据可量化准确率提升。

面试时被问到,按这个顺序讲:

  1. 先说痛点:纯向量检索在企业场景不行,专有名词向量模型没见过,召回不准;
  2. 说方案:KNN 向量召回(负责语义相似)+ BM25 关键词匹配(负责精确匹配),ES 合并两路候选,再用 BM25 Rescore 加权重排;
  3. 说细节:KNN 的 k 放大 30 倍再重排,防止近似搜索的误差;权限过滤放 filter 上下文利用缓存;
  4. 说降级:Embedding 服务挂了自动降级到纯 BM25,系统不挂。

© 2026 KnowFlow 面试手册 · 转载请注明出处