Files
zhuangang/topics/backend/elasticsearch/scenarios/kb-search-sync.md
T

86 lines
4.3 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
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,比基线差不上线。”