86 lines
4.3 KiB
Markdown
86 lines
4.3 KiB
Markdown
---
|
||
id: backend.elasticsearch.scenario.kb-search-sync
|
||
title: 知识库混合检索:数据库到 ES 的同步、权限过滤、分词词典和检索效果评估
|
||
type: scenario
|
||
domain: backend
|
||
stack: [elasticsearch, postgresql, mq, rag]
|
||
tags: [混合检索, 数据同步, CDC, outbox, 权限过滤, IK 词典, 召回率]
|
||
level: intermediate
|
||
prerequisites: [backend.elasticsearch.search-hybrid, ai.rag.hybrid-rerank]
|
||
est_hours: 2.5
|
||
related: [backend.elasticsearch.interview, ai.rag.scenario.permission-aware-kb, backend.mq.reliability]
|
||
week: [6, 14]
|
||
---
|
||
|
||
# 知识库混合检索:数据库到 ES 的同步、权限过滤、分词词典和检索效果评估
|
||
|
||
## 业务背景与问题
|
||
|
||
企业知识库 RAG 一开始只用了 pgvector 向量检索,问题:
|
||
|
||
1. 用户搜产品型号、错误码、合同编号,向量检索经常找不到;
|
||
2. 文档在后台修改后,问答仍引用旧内容;
|
||
3. 偶尔检索到用户无权查看的部门文档;
|
||
4. 每次调整分词或检索参数,不知道效果是变好还是变差。
|
||
|
||
## 约束
|
||
|
||
- 文档的主数据在 PostgreSQL(含权限)。
|
||
- 修改后 1 分钟内可检索到。
|
||
- 不能引入复杂的大数据组件。
|
||
|
||
## 方案设计
|
||
|
||
1. **检索层**:ES 索引同时保存分块的文本(IK 分词 + 业务词典:产品型号、内部术语)和向量;查询时 BM25 和 kNN 两路检索,应用层 RRF 融合,再用重排模型取前 5。
|
||
2. **权限**:每个分块冗余 `tenant_id`、`dept_ids`、`visibility` 等 `keyword` 字段;查询时把当前用户的权限转换成 `filter`,两路检索都带上;返回前再按数据库做一次最终校验(防止同步延迟导致的权限变更未生效)。
|
||
3. **同步**:
|
||
- 文档修改时在同一个数据库事务中写入 outbox 表;
|
||
- 后台任务读取 outbox 发到消息队列;
|
||
- 消费者重新分块、计算向量、按“文档 id + 分块序号”写入 ES(幂等),删除多余的旧分块;
|
||
- 失败重试,超过次数进入死信并告警;
|
||
- 每天对账:数据库与 ES 的文档版本号比对,修复差异。
|
||
4. **变更映射**:通过别名 + 新索引重建,不停机。
|
||
5. **评估**:维护一份标注集(问题 → 应该命中的文档),每次调整分词词典、权重、`num_candidates` 都计算召回率@K 和 MRR,比基线差就不上线。
|
||
|
||
## 关键代码
|
||
|
||
权限条件同时作用于两路检索:
|
||
|
||
```python
|
||
def acl_filter(user) -> list[dict]:
|
||
return [
|
||
{"term": {"tenant_id": user.tenant_id}},
|
||
{"bool": {"should": [
|
||
{"term": {"visibility": "public"}},
|
||
{"terms": {"dept_ids": user.dept_ids}},
|
||
], "minimum_should_match": 1}},
|
||
]
|
||
|
||
kw = es.search(index="kb", size=20, query={"bool": {"must": {"match": {"text": q}}, "filter": acl_filter(user)}})
|
||
vec = es.search(index="kb", size=20, knn={"field": "vec", "query_vector": embed(q), "k": 20,
|
||
"num_candidates": 200, "filter": acl_filter(user)})
|
||
candidates = rrf(ids(kw), ids(vec))[:20]
|
||
top5 = rerank(q, candidates)[:5]
|
||
```
|
||
|
||
完整可运行的基础示例:[examples/hybrid-search](../examples/hybrid-search/);权限设计详见[权限感知知识库](../../../ai/rag/scenarios/permission-aware-kb.md)。
|
||
|
||
## 取舍
|
||
|
||
| 选择 | 优点 | 缺点 |
|
||
| --- | --- | --- |
|
||
| **ES 做混合检索(本方案)** | 关键词能力强、过滤和聚合成熟、向量也支持 | 多一套集群和同步链路 |
|
||
| 只用 pgvector + PG 全文检索 | 组件少、强一致 | 中文分词和相关性调优能力弱一些 |
|
||
| 专用向量库 + ES | 各自最强 | 两套检索系统,复杂度最高 |
|
||
|
||
## 易错点
|
||
|
||
- 只同步新增和修改,忘记处理删除和权限变更。
|
||
- 消费者不是幂等的,重试后出现重复分块。
|
||
- 业务词典更新后没有重建索引(已写入的数据仍按旧分词)。
|
||
- 只凭几个例子判断效果,没有标注集和指标。
|
||
|
||
## 面试怎么讲
|
||
|
||
> “纯向量检索对型号、错误码这种精确词召回很差,我们引入 ES 做混合检索:IK 分词加业务词典,BM25 和 kNN 两路召回,应用层 RRF 融合后再重排。权限字段冗余到每个分块,作为 filter 同时作用于两路检索,返回前再校验一次。同步用 outbox + 消息队列,消费者按文档 id+分块号幂等写入,每天对账。每次调参都用标注集跑召回率@K 和 MRR,比基线差不上线。”
|