我用 Elasticsearch 做 RAG 检索的一些"土经验"
大家好,我是程序员云喜,向大家分享我的编程成长之路!
最近在学习RAG的过程中,我越来越相信:检索不是"把文档翻出来"这么简单,而是"把模型最该看的那几段,稳稳地托到它面前"。Elasticsearch(ES)在这件事上非常趁手:它既能做语义向量检索,又能把传统 BM25 的可解释性发挥到极致,还能优雅地承载权限和降级策略。下面是我在项目里总结出来的一套"混合检索配方",不涉及任何业务源码,直接可落地。
我为什么不再迷信"只用向量"或"只用关键词"
- 只用向量:语义感知很强,但有时"懂你意思,却不说人话",返回的段落可能不包含任何关键词,给 LLM 喂过去就容易跑偏。
- 只用关键词:可解释、可控,但对表达差异、同义词、多语言就容易漏掉。
- 混合:先用向量把"可能有用的"都捞上来,再用关键词"定性",最后 BM25 精排"定序",兼顾召回与精准。
一套好用的混合检索配方
1) 语义召回:KNN 放大候选
想返回 topK=10,我会先把候选放大到 200~500(根据数据量和延迟定),比如 300。
json
POST /kb/_search
{
"knn": {
"field": "vector",
"query_vector": [0.12, -0.05, 0.33, 0.08],
"k": 300,
"num_candidates": 300
},
"size": 10
}
- 要点 :
k
与num_candidates
不要太小,否则后面再怎么精排也"巧妇难为无米之炊"。向量维度与度量方式(cosine/L2)要与索引一致。
2) 关键词"保底":must 保证文本相关
召回只是"捞",必须再加一道"必须命中关键词"的门槛,避免纯语义漂移。
json
"query": {
"bool": {
"must": [ { "match": { "textContent": "你的查询文本" } } ]
}
}
- 要点 :面向 QA 的查询,
match
通常足够好用;跨字段时可用multi_match
。
3) 权限不参与打分:filter 更香
权限、租户、组织等条件一律放到 filter
,不参与相关性打分,还能被 ES 缓存。
json
"filter": [
{
"bool": {
"should": [
{ "term": { "userId": "123" }},
{ "term": { "public": true }},
{ "terms": { "orgTag": ["deptA", "deptB"] }}
]
}
}
]
- 要点:
- 多标签 :只有 1 个时用
term
,多个用terms
或bool+should
;默认"至少命中一个"。 - 三通道 OR:本人 / 公开 / 组织,覆盖 80% 企业权限诉求。
4) 二阶段精排:让 BM25 说了算
在大候选集上用 BM25 重新打分排序,强调关键词完整覆盖(AND),最后只取前 N 条。
json
"rescore": {
"window_size": 300,
"query": {
"query_weight": 0.2,
"rescore_query_weight": 1.0,
"rescore_query": {
"match": {
"textContent": {
"query": "你的查询文本",
"operator": "AND"
}
}
}
}
}
- 怎么理解权重 :最终分数 ≈
0.2 × 初始分(语义召回) + 1.0 × BM25 分(文本精排)
。 - AND 的价值:多个关键词都出现,模型读起来"更像答案",幻觉风险更低。
5) 兜底策略:向量失败就"纯文本 + 权限"
嵌入服务不稳定、延迟抖动在所难免。线上要"宁可少点智能,也不能中断服务"。
json
POST /kb/_search
{
"query": {
"bool": {
"must": { "match": { "textContent": "你的查询文本" }},
"filter": {
"bool": { "should": [
{ "term": { "userId": "123" }},
{ "term": { "public": true }},
{ "terms": { "orgTag": ["deptA", "deptB"] }}
]}
}
}
},
"min_score": 0.3,
"size": 10
}
- 要点 :设一个
min_score
,把明显低质的匹配挡掉;对调用方"自动降级",体验更稳。
调参就像调味:几个我常用的刻度
- 召回窗口(k / num_candidates) :起步值可用
topK×30
(如 10→300);数据越大/过滤越重越要放大;延迟紧就收紧。 - 精排权重 :
query_weight=0.2
,rescore_query_weight=1.0
是个稳妥起点。 - 关键词操作符:召回阶段用默认(近似 OR),精排阶段用 AND;既有覆盖又有精准。
- 查询节流:热词查询会导致 cache 抖动,建议配合节流/限频。
我踩过的一些坑(以及规避)
- 向量维度对不上:embedding 模型换了,索引没跟着改。上线前把"维度校验"做成健康检查。
- 权限写进 must :会参与打分、分数乱、缓存差。权限一定放在
filter
。 - 只做向量:模型看不到关键词,答案玄学。一定要有关键词保底。
- 召回窗口太小:精排阶段"没得选"。首屏准确率会直接下滑。
- 文本字段乱映射 :把长文本设成
keyword
,检索几乎废掉。记得用合适分词器。
评估与监控:别只盯生成,检索也很关键
- 质量:首屏命中率、NDCG、用户纠错率。
- 性能:P50/P95 延迟、节点资源、cache 命中率。
- 稳定:向量降级率、降级后的满意度。
- 安全:越权命中拦截率、审计日志完整度。
一套可以"先用起来"的默认组合
- topK=10 ,k=num_candidates=300
- must :
match(textContent=query)
- filter:本人 / 公开 / 组织 三通道 OR
- rescore :
query_weight=0.2
,rescore_query_weight=1.0
,operator=AND
- 失败降级 :纯文本 + 权限,
min_score=0.3
如果把检索看成给 LLM 上菜:向量是"广撒网" ,关键词是"厨师挑菜" ,BM25 是"出菜顺序" ,权限是"门禁保安" ,降级是"备用炉火"。ES 把这套"后厨机制"都给你准备好了,关键是你得把火候调对。