上个月团队做 RAG 检索方案选型还没等我把需求场景讲完就有同学拍板定调既然要做 RAG肯定得配一个向量数据库这不是标准答案吗。我当时没有急着反驳但心里很清楚这个判断大概率拍早了。后来我用一套已有的 Elasticsearch 集群跑通了整个流程效果不输专用向量库还少维护了一套组件。今天这篇就聊聊为什么 RAG 技术栈里始终被忽略的 Elasticsearch反而像扫地僧一样稳。先说结论如果你的场景需要“语义相似度召回 关键词精确匹配 结构化过滤”混着来Elasticsearch 几乎是唯一能用一个集群全部搞定还不出戏的方案。而这也正是大多数 RAG 落地项目的真实形态不是学术 demo 里那种“给一段 query 就返回最像的三段文本”那么单纯。1. 为什么 RAG 一上来就想到向量数据库1.1 惯性思维从哪来这两年大模型应用铺天盖地技术社区里讲 RAG 的教程十篇有八篇默认要引入一个向量数据库。截图里永远是一个文档切片工具、一个 embedding 接口、一个向量库、一个大模型。看多了之后很多人形成了一种路径依赖做 RAG等于要上向量数据库。甚至有些技术评审会上你不提向量数据库别人会认为你的方案不够“前沿”。这种惯性思维的根源其实是教程把流程简化了。为了把原理讲清楚教程通常只保留“文本转向量、向量检索、交给大模型”这三个环节至于业务里常见的过滤条件、精确匹配、权限隔离、热更新统统被抽象掉了。于是新手学到的不是一套检索系统而是一条单线流水线。1.2 你对 ES 的了解可能还停留在全文检索我问过身边不少人为什么不用 Elasticsearch。得到的回答高度一致ES 是搞全文搜索的做不了语义检索。这个说法放在 ES 6.x 时代基本成立但在 8.x 之后已经站不住脚了。ES 从 7.x 开始引入向量字段到 8.x 已经具备比较成熟的原生 kNN 检索能力支持 HNSW 图索引能够按余弦相似度、点积等方式直接查。更关键的是ES 从未丢掉自己的老本行BM25 全文检索、分词器、过滤器、聚合、权限控制这些能力在纯向量数据库里要么没有要么做得非常弱。你花大力气把文本切成向量存进向量库然后遇到一个精确编码需求比如用户输入“iPhone 15 256G 蓝色”向量库只能给你返回一堆语义相近的废话而 ES 可以在同一个索引里把 token 级别的匹配玩得明明白白。这让我想起一个很常见的工程误区为了“语义”两个字把基础但极其可靠的关键词检索给丢了。真实业务里用户可能就用产品型号、工单编号、错误码来提问这种场景下关键词精确匹配的命中率几乎百分之百语义向量反而会因为训练数据里没见过而出现偏差。2. ES 凭什么配得上“扫地僧”被忽略的原生向量检索2.1 原生向量检索能力dense_vector kNNES 8.x 的向量能力核心是新增了 dense_vector 字段类型并围绕它实现了图索引和近邻检索。你可以在 mapping 里声明一个字段专门存浮点数组维度通常和 embedding 模型的输出维度一致常见的有 384 维、768 维、1024 维。底层索引可以选择 HNSW 算法查询的时候直接发一个 knn 请求ES 会返回与 query_vector 最相近的 top K 文档。这跟专用向量数据库的核心原理是一回事只是在 ES 里它和文本检索、filter 聚合共存在同一个 Lucene 索引中。对研发团队来说这意味着不再需要单独搭一套 Milvus、Qdrant 或者 FAISS 服务也不用处理向量库和业务库之间的数据同步问题。你只需要一个 ES 集群数据写一份什么检索模式都能覆盖。有些人会说ES 的向量检索性能比不上专用 vector db。这话得分场景。实测在小规模到中等规模数据量下比如几百万条以内HNSW 参数调一调ES 的召回延迟完全可以控制在几十毫秒级别根本不会成为 RAG 链路的瓶颈。RAG 的耗时大头向来是 embedding 生成和 LLM 推理一个常用 embedding 模型调 API 可能就要几百毫秒相比之下检索那十几毫秒完全无感。2.2 从生产角度算三笔账第一笔是数据一致性账。很多团队做 RAG业务数据在 MySQL然后写定时任务同步到向量库同步过程中经常出现字段对不上、删除不同步的问题。如果直接用 ES项目里原本就可能有日志搜索、商品搜索已经在用 ES数据链路是现成的只需往 mapping 里加一个向量字段再补一个 embedding 写入逻辑同步成本大幅降低。第二笔是运维账。每增加一个中间件就要多考虑一台机器、监控面板、告警规则、备份方案、版本升级。向量数据库不像 Kafka 或 Redis 那么简单索引策略、分片设计、内存规划都得重新学一遍。而 ES 的运维经验在行业内已经非常成熟随便一个后端工程师都能处理常见问题。对于十几个人的小团队这个优势尤其明显。第三笔是业务灵活度账。RAG 很少只做“查相似文本”这一件事。通常用户会带条件只看某个知识库分类下的内容、只要 30 天内的公告、不要某个状态的数据。纯向量库做这类过滤有的能力很弱有的需要在召回后二次筛选还有的可能就根本不好实现。ES 的 term、range、wildcard、bool filter 可以把这些条件直接推到检索阶段效率完全不在一个量级。2.3 能力和定位速览对比能力Elasticsearch 8.x常见专用向量数据库向量检索支持 dense_vector HNSW kNN原生支持性能激进关键词全文检索BM25分词、同义词、模糊匹配支持完善弱通常不支持或很简陋结构化过滤强filter 与评分过程解耦部分支持能力参差数据同步与业务共用一套同步链路需要额外维护管道聚合分析完整聚合框架弱运维生态成熟稳定社区资料多较新问题排查资料少这张表不需要看得太细关键是最后两行运维生态和延伸能力。对一个要长期维护的 RAG 系统来说这两点的价值比某些 benchmark 里的 recall 多 0.5% 要实在得多。3. 纯向量检索不够用混合检索才是 RAG 的真实形态3.1 一个案例看清语义检索的短板我之前接手过一个客服知识库项目里面存了大量设备故障的处理文档。每篇文档都带有设备型号和错误码。用户提问很典型比如“TS-300 连不上 WiFi 怎么办”。我第一版方案只用了向量检索返回结果惨不忍睹语义上最接近的文档确实出来了但有的完全没提到“TS-300”这个型号。因为 embedding 模型对长尾的型号字符串不敏感很容易被“连不上”“WiFi”“怎么办”这些高频词带着走。后来我用了一个土办法把型号单独提取出来用 keyword 字段做 term 匹配再和向量检索结果做融合召回质量立刻上去了。这件事给我印象很深真实场景里用户的问题往往同时包含实体信息和自然语言描述。实体信息需要精确匹配自然语言描述需要语义匹配两者缺一不可。3.2 BM25 和向量检索是互补的BM25 召回的是“字面命中”的文档它对拼写、专有名词、代码片段有极高的精确度。向量召回处理的是“语义相似”对同义改写、口语化表达很友好。两种方式各有强项也各有盲区。BM25 搜不到同义表述比如“这个设备开不了机”和“设备无法启动”字面上差异很大。向量检索搜不到精确数字和型号因为 embedding 模型在数字维度的表现普遍不理想。大多数人做 RAG 的时候检索链路只用了其中一种。要么上向量数据库做纯语义检索要么沿用老的 ES 只做关键词搜索。前一种丢了精确后一种丢了泛化。混合检索就是一种取长补短的策略把 BM25 和向量召回的候选集合并起来用 RRF 或者加权求和做重排让两种信号互相补充。3.3 RRF 融合策略RRF 是一种简单有效的融合算法核心思想就是不看分数看排名。每个检索方式各自返回一份文档排名列表RRF 给每篇文档打分时用的公式是多个排名倒数的累加。如果一个文档在两种检索里都排在前面它的 RRF 分数就会显著高于只被一种方式命中的文档。这样做的好处是不用纠结 BM25 分数和向量相似度分数之间的尺度差异因为这两个分数一个取决于词频一个取决于余弦距离数值范围完全不对齐强行加权求和往往需要调一堆系数。ES 8.8 之后原生支持了 retriever 和 RRF可以在一个请求里同时执行文本查询和 knn 查询再由 ES 内部融合打分排序。这意味着混合检索不再是某种实验室特性而是可以用 DSL 直接在线上用的能力。如果你还在用 ES 8.8 以下的版本也可以自己取两路结果在业务侧做融合但整体体验会差一些有条件建议升级。4. 实操落地一套 ES 索引完成语义关键词混合召回4.1 前置准备embedding 模型与维度确定要跑通这套方案需要一个 embedding 模型把文本转化成向量。可以用开源的 BGE 系列、text2vec 系列也可以调用云厂商的 embedding 接口。维度方面常见的 384 维、768 维、1024 维都支持但维度越高内存占用越大写入越慢所以不必盲目追求最高维度按模型输出为准即可。有一点要注意切分文档做向量化时一个知识条目的文本块不能太大。向量检索本质是拿整个文本块去算相似度如果每个块 5000 字内容主题不聚焦query 的向量会被“平均”掉召回效果就模糊。实践中我倾向于将知识文档按段落或者按语义边界切成 300 到 800 字的小块再为每个块生成向量。元信息比如标题、分类、来源、ID用一个 text 或 keyword 字段单独保存不参与向量化但可以用来做过滤。4.2 索引 mapping注意向量字段参数下面是一份常见 mapping 配置。索引名称随意字段设计按业务需要调整。核心部分是 content_vector 字段的类型声明和 HNSW 参数。PUT /rag_docs { mappings: { properties: { doc_id: { type: keyword }, title: { type: text, fields: { keyword: { type: keyword } } }, content: { type: text }, category: { type: keyword }, status: { type: keyword }, publish_date: { type: date }, content_vector: { type: dense_vector, dims: 768, index: true, similarity: cosine, index_options: { type: hnsw, m: 16, ef_construction: 100 } } } } }几个关键参数解释一下。similarity选 cosine 适合绝大多数文本匹配场景如果 embedding 模型本身已经对向量做了归一化选 dot_product 也可以结果等价。m和ef_construction是 HNSW 图索引的核心参数。m控制每个节点的最大连接数值越大图越密召回精度越高但内存占用也越高。ef_construction控制建索引时的搜索范围值越大建图越慢索引质量越好。默认值 16 和 100 在大多数场景够用如果数据量到了一千万以上建议先用小规模数据压测。还有一点doc_id我建议单独用 keyword 存一份原始 ID。ES 的_id虽然也能用但如果有业务侧更新和删除维护起来不如显式字段方便。4.3 写入与查询侧代码示例写入文档的流程分两步。第一步切好文本块调用 embedding 模型生成向量第二步连同文本和向量一起写进 ES。用 Python 客户端示例的话大概是下面这样注意把向量转成 list 格式。from elasticsearch import Elasticsearch es Elasticsearch(http://localhost:9200) def add_doc(doc_id, title, content, category, publish_date, vector): doc { doc_id: doc_id, title: title, content: content, category: category, publish_date: publish_date, content_vector: vector } resp es.index(indexrag_docs, iddoc_id, documentdoc) return resp查询侧的核心是一个 knn 检索请求。下面这段 DSL 表示在 content_vector 字段上做近邻搜索返回最相似的 10 条候选集取 100 条。{ knn: { field: content_vector, query_vector: [0.01, 0.02, 0.03], k: 10, num_candidates: 100 } }num_candidates值得多说一句。它不是最终的返回条数而是 HNSW 图搜索时访问的候选节点数量。这个值越大越有可能找到真正的近邻但延迟会升高。当数据量大、精度要求高的时候num_candidates建议至少是k的 10 倍。如果你的k是 10num_candidates用 100 起步比较合理极限场景可以拉到 200 到 500但一定要压测延迟。4.4 业务查询如何组合 filter、BM25 和 kNN单一 knn 查询只能解决“给我相似的文档”但业务需求往往是“给我这个分类下、最近一个月发布的、状态为启用的相似文档”。这时候需要把 filter 加上去。ES 的 filter 不会参与评分只会缩小候选范围性能比 must 子句里的条件好很多。下面这段 DSL 同时做了三件事用 term 过滤分类、用 range 过滤时间、用 knn 召回相似文本。{ knn: { field: content_vector, query_vector: [0.01, 0.02, 0.03], k: 10, num_candidates: 100, filter: { bool: { filter: [ { term: { category: troubleshooting } }, { range: { publish_date: { gte: now-30d/d } } } ] } } } }如果还要叠加关键词匹配比如标题里必须包含“WiFi”字样就需要混合检索。ES 8.8 及以上版本可以直接用 retriever 语法做 RRF 融合示例如下。{ retriever: { rrf: { retrievers: [ { standard: { query: { bool: { must: [ { match: { title: WiFi } } ], filter: [ { term: { category: troubleshooting } } ] } } } }, { knn: { field: content_vector, query_vector: [0.01, 0.02, 0.03], k: 10, num_candidates: 100, filter: { term: { category: troubleshooting } } } } ], rank_window_size: 50, rank_constant: 60 } } }rank_window_size是每个检索器各自返回的候选数量也就是进入 RRF 融合的文档总数上限默认值是 50如果两个检索器要融合更多文档可以调大。rank_constant对应 RRF 公式里的常数项默认 60用于平滑排名倒数避免排位过分极端。这两项一般不需要频繁调保持默认即可。如果你的 ES 版本不支持 retriever退路是分别在业务侧调两个查询拿到两份文档 ID 列表后再做 RRF。顺序先执行 BM25 查询取前 N 个 ID再执行 knn 查询取前 N 个 ID最后按 RRF 公式对两路 ID 重排。效果上和原生 RRF 几乎一致只是多一次网络往返略显繁琐。5. 我在真实项目里踩过的坑5.1 常见报错和异常做向量检索时遇到最多的问题是维度不一致。embedding 模型换过一版输出维度从 768 变成 1024但新建索引时没改 mapping写入文档直接报错。ES 的 dense_vector 字段维度在创建索引时就固定了后期无法修改只能新建索引再 reindex。所以规划索引的时候先把 embedding 模型锁定不要在生产环境里随意切换。如果确实要换模型提前设计好双索引迁移方案。第二个容易踩的坑是向量字段声明了index: false。这种情况下字段内容可以存但不能做 kNN 查询只能通过 script_score 暴力计算相似度。小数据量还好数据量大了之后速度会非常慢。检查 mapping 时注意确保index为true且index_options中配置了 HNSW 参数。第三个问题比较隐蔽ES 的 kNN 查询结果不会像普通 query 那样稳定复现。因为 HNSW 是近似近邻算法每次搜索找到的可能不是全局绝对最优只是大概率最优。在离线评测的时候如果你拿同一批 query 反复跑召回结果可能有一小部分浮动这属于正常现象不代表系统故障。和团队对齐预期时要说明这一点避免被当成 bug 来查。5.2 性能相关经验向量索引是纯内存开销大户。每一个 dense_vector 字段都要为 HNSW 图预留空间维度越高、m 值越大内存占用越明显。一条 768 维的向量占用的内存大约在几 KB 到几十 KB 区间一千万条数据就能吃掉几个 GB 的内存。这还没算 ES 的其他开销。所以数据量大的时候不要做全量向量化存到一个索引按业务域拆成多个索引或者用 filter 把向量搜索限制到必要的子集都能显著降低内存压力。写入性能方面HNSW 索引的构建需要时间批量灌数据时建议把 refresh_interval 调大比如设置成 30 秒或者 -1等数据全部写完之后再恢复。否则每次 refresh 都会触发向量索引的更新大批量写入会非常慢。这个技巧对常规文档索引同样适用对向量字段效果尤其明显。还有一个容易被忽视的点分片数不要拍脑袋定。向量索引属于重内存索引分片数过多会导致每个分片的 HNSW 图都被复制多份内存被白白浪费。按单分片容量预估一条向量按 4 字节乘以维度再加图结构开销估算单分片控制在 50GB 以内比较稳妥。当然最终还是要靠压测决定。5.3 召回质量验证的一种土办法之前我收到过不少“效果不好”的反馈但问具体哪里不好对方只能说出“感觉不对”。为了避免凭感觉调参我养成了一个习惯维护一个固定的评测 query 集每一条 query 都手工标出应该召回哪几篇文档。模型和参数改版之后拿这几十条 query 跑一遍统计命中率。这个做法很土但非常有效。很多所谓的效果差最后发现是文档切分太粗或者是 embedding 模型选得不对压根还没到调参阶段。另一个更轻量的办法是在检索引擎前面加一个 debug 开关把用户真实 query 的检索候选打印出来。这样当大模型回答质量差时可以先看召回结果对不对再判断是检索问题还是生成问题。这个思路能帮你快速定位 RAG 链路的故障分层不至于一有问题就无头苍蝇一样重调 prompt。6. 选型判断什么情况下继续用向量库什么情况下用 ES6.1 什么时候别硬上 ESES 不是万能的。如果你的数据量已经到了几十亿条对向量检索延迟有极高要求同时有专职的中间件团队在维护那么专用向量数据库的极致性能可能更合适。ES 的 kNN 能力虽然强但在超大规模型数据集上的表现确实未必拼得过专门为向量检索优化的存储引擎。再一个场景是你的业务已经完全建立在一套向量库之上检索逻辑、数据管道、运维工具都围绕它转这时候仅仅因为“ES 也能做”就迁移ROI 不高。还要考虑版本因素。如果你的 ES 还停留在 7.x 早期向量能力需要通过插件或者 script_score 迂回实现体验会差不少。这时候要么规划升级要么先继续用已有的向量库。总之选型看的是当前团队实际条件和约束不是看谁的名气大。6.2 什么时候别多此一举上向量库反过来看如果你的团队已经有 ES 集群数据量在千万级以内业务涉及文本搜索和向量召回并存我认为新增一个向量数据库完全是多此一举。把这个结论放在评审会上最直接的论据就是ES 一个请求能完成混合检索向量库加 ES 要两跳才能完成类似逻辑还要处理两份数据的同步和一致性复杂度成倍增加。用一句行业里的老话不是组件越多系统越先进而是能够用最少组件满足需求的系统才最容易维护。还有一个很容易被忽略的场景知识库类 RAG 应用的文档数量其实没有想象中那么大。一个中型企业的内部知识库几万到几十万篇文档已经是常态膨胀到千万级的都不多见。这种量级对 ES 来说连热身都算不上上专用向量库是杀鸡用牛刀除了向上汇报时多几个关键词对线上实际体验没有本质提升。6.3 最终拍板清单做决策时我建议你按下面这套问题过一遍。现有技术栈里是不是已经有 ES如果有先试用向量检索能力跑一个 POC用真实 query 验证效果和延迟再决定要不要引入新组件。业务是否需要过滤、聚合、权限隔离如果答案是肯定的ES 的综合成本远低于向量库加 ES 的“双子星”架构。数据量级是否到了千万/亿级并且对召回精度和延迟有极端要求如果是再考虑专用向量数据库。团队是否愿意把 embedding 模型、切分策略、参数调优这些真正决定 RAG 效果的问题花时间打磨选型只是开始真正拉开差距的是后面这些细节。从这个角度回看标题Elasticsearch 所谓的“扫地僧”其实不是说它的向量检索性能碾压专用库而是说它在一个已经到处都在用、看起来平平无奇的组件里悄悄补齐了向量能力并且把混合检索这条路走通了。真正懂 RAG 的人早就开始算这笔工程账了。我个人的体会是做技术选型最忌讳被名词带着跑。先看清楚自己的数据长什么样、用户怎么提问、系统里已经有什么组件再决定要不要加新东西。很多时候你缺的不是一个向量数据库而是把已有工具的潜力用足。下一次别人再跟你提“RAG 标配向量库”你不妨先回一句那 ES 呢先把混合检索跑出来再说。