我用 Elasticsearch 做 RAG 检索的一些“土经验”

我用 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
}
  • 要点knum_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,多个用 termsbool+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.2rescore_query_weight=1.0 是个稳妥起点。
  • 关键词操作符:召回阶段用默认(近似 OR),精排阶段用 AND;既有覆盖又有精准。
  • 查询节流:热词查询会导致 cache 抖动,建议配合节流/限频。

我踩过的一些坑(以及规避)

  • 向量维度对不上:embedding 模型换了,索引没跟着改。上线前把"维度校验"做成健康检查。
  • 权限写进 must :会参与打分、分数乱、缓存差。权限一定放在 filter
  • 只做向量:模型看不到关键词,答案玄学。一定要有关键词保底。
  • 召回窗口太小:精排阶段"没得选"。首屏准确率会直接下滑。
  • 文本字段乱映射 :把长文本设成 keyword,检索几乎废掉。记得用合适分词器。

评估与监控:别只盯生成,检索也很关键

  • 质量:首屏命中率、NDCG、用户纠错率。
  • 性能:P50/P95 延迟、节点资源、cache 命中率。
  • 稳定:向量降级率、降级后的满意度。
  • 安全:越权命中拦截率、审计日志完整度。

一套可以"先用起来"的默认组合

  • topK=10k=num_candidates=300
  • mustmatch(textContent=query)
  • filter:本人 / 公开 / 组织 三通道 OR
  • rescorequery_weight=0.2rescore_query_weight=1.0operator=AND
  • 失败降级 :纯文本 + 权限,min_score=0.3

如果把检索看成给 LLM 上菜:向量是"广撒网"关键词是"厨师挑菜"BM25 是"出菜顺序"权限是"门禁保安"降级是"备用炉火"。ES 把这套"后厨机制"都给你准备好了,关键是你得把火候调对。

相关推荐
JaguarJack2 小时前
PHP 8.2 vs PHP 8.3 对比:新功能、性能提升和迁移技巧
后端·php
学历真的很重要2 小时前
Claude Code 万字斜杠命令指南
后端·语言模型·面试·职场和发展·golang·ai编程
稻草猫.3 小时前
Java线程安全:volatile与wait/notify详解
java·后端·idea
IT_陈寒3 小时前
Vite 5年迭代揭秘:3个核心优化让你的项目构建速度提升200%
前端·人工智能·后端
拾贰_C4 小时前
【SpringBoot】前后端联动实现条件查询操作
java·spring boot·后端
catchadmin6 小时前
PHP 快速集成 ChatGPT 用 AI 让你的应用更聪明
人工智能·后端·chatgpt·php
callJJ10 小时前
从 0 开始理解 Spring 的核心思想 —— IoC 和 DI(2)
java·开发语言·后端·spring·ioc·di
mCell10 小时前
长期以来我对 LLM 的误解
深度学习·llm·ollama
你的人类朋友12 小时前
JWT的组成
后端