过去提到 Elasticsearch,我首先想到的是搜索引擎、倒排索引、BM25,以及大家比较熟悉的 ELK 日志分析体系。
而提到 Milvus,我首先想到的则是向量数据库、Embedding、HNSW 和相似度检索。
因此,我们很容易形成一个简单的认识:
Elasticsearch 负责关键词检索,Milvus 负责向量检索。
但随着我进一步了解两者的能力,我发现这个结论已经不够准确了。
现在的 Elasticsearch 不仅可以做 BM25,还支持稠密向量、稀疏向量、HNSW、元数据过滤和混合检索;而 Milvus 也不再只是单纯存储稠密向量,它同样开始支持稀疏向量、BM25、标量过滤和混合召回。
这就引出了一个更值得讨论的问题:
既然 Elasticsearch 和 Milvus 都能完成关键词检索、向量检索和元数据过滤,那么工业级 RAG 到底应该使用谁?什么时候只使用 Elasticsearch,什么时候只使用 Milvus,什么时候才需要 Elasticsearch 加 Milvus?
在回答这个问题之前,我们首先要把几个经常混在一起的概念彻底拆开:
- 索引结构;
- 搜索算法;
- 相似度度量;
- 元数据过滤;
- 排序与重排。
只有先理解一次检索到底发生了什么,后面的技术选型才不会停留在产品名称层面。
一、一次完整的检索,到底发生了什么
很多人会把向量检索简单理解为:
把问题转换成向量,再去数据库里找到最相似的几个向量。
这个描述不能说错,但它省略了很多关键步骤。
一次完整的检索,实际上包含下面几个环节:
这里的每一个环节,解决的问题都不一样。
| 概念 | 主要解决的问题 | 常见实现 |
|---|---|---|
| 索引结构 | 数据提前如何组织 | 倒排索引、HNSW、IVF |
| 搜索算法 | 查询时如何快速找到候选 | 图遍历、聚类探测、全量扫描 |
| 度量方式 | 两个向量如何判断远近 | Cosine、IP、L2 |
| 元数据过滤 | 哪些数据有资格参与检索 | 租户、权限、知识库、状态 |
| 召回排序 | 候选结果如何计算初始得分 | BM25、向量相似度 |
| 融合与精排 | 多路候选最终如何排序 | RRF、BGE-Reranker |
其中最容易混淆的,就是索引方式和相似度度量。
1.1 索引结构:数据提前怎么组织
索引的核心目的,不是直接判断哪一条数据最相似,而是:
提前组织数据,让查询时不需要检查全部数据。
假设数据库里有一百万条向量。
最简单的方法是,让查询向量分别和这一百万条向量计算一次相似度,然后再从中选出 Top 10。
这种方式当然能够找到准确结果,但是每次查询都需要完成一百万次向量比较。随着数据规模增加,查询延迟和计算成本都会线性上升。
所以,我们需要提前把向量组织起来。
例如 HNSW 会把相似的向量连接成一张多层近邻图;IVF 会把整个向量空间划分成多个聚类区域;倒排索引则会提前建立“词项到文档”的映射关系。
可以把索引理解成一张提前修建好的地图。
没有地图时,我们只能把整个城市的道路全部走一遍;有了地图之后,就可以先定位大致区域,再进入附近街道,最终找到目标位置。
因此,索引结构解决的是:
数据放在哪里,以及查询时应该先去哪里找。
1.2 搜索算法:沿着索引寻找候选
索引建立完成后,还需要配套的搜索方法。
以 HNSW 为例。
在建立索引时,HNSW 会把向量组织成多层图结构。上层节点少、跨度大,类似高速公路;下层节点多、连接密集,类似城市道路。
查询时,系统会:
- 从图的高层入口开始;
- 快速靠近查询向量所在的区域;
- 逐层向下;
- 在局部邻居中继续搜索;
- 返回一批可能相似的候选向量。
这意味着,HNSW 并不是单纯的距离公式。
更准确地说,它同时包含:
- 建索引阶段的图结构构建;
- 查询阶段的近似图搜索。
所以我们在工程实践中经常把 HNSW 称为“索引算法”,但它解决的核心问题仍然是:
怎样用更少的比较次数,找到尽可能接近真实答案的候选集合。
1.3 度量方式:到底怎样才算相似
当索引算法找到一批候选以后,系统还要判断这些候选和查询向量到底有多相似。
这就需要相似度或者距离函数。
最常见的三种方式是:
- Cosine:余弦相似度;
- IP:Inner Product,内积;
- L2:欧氏距离。
可以把三者理解为三种不同的“距离定义”。
假设我要在城市中寻找一家最近的医院,那么“最近”可以有不同含义:
- 直线距离最近;
- 驾车距离最近;
- 驾车时间最短。
它们面对的是同一批医院,但由于“距离”的定义不同,最后得到的结果可能不同。
向量检索也是如此。
Cosine:比较方向
余弦相似度更关注两个向量的方向是否一致,而不太关注向量本身的长度。
在文本 Embedding 中,我们通常更关心两段文本的语义方向是否接近,因此 Cosine 是非常常见的选择。
IP:计算内积
内积的计算方式是:
如果两个向量已经完成 L2 归一化,那么它们的模长都是 1,此时内积和余弦相似度的排序结果基本一致。
BGE-M3 的论文中,稠密检索使用归一化后的表示,并通过内积计算查询与文档的相关性。
L2:计算空间距离
L2 计算的是两个向量在空间中的欧氏距离:
L2 越小,代表两个向量越接近。
因此,我们可以用一句话总结索引和度量的区别:
HNSW、IVF 决定去哪里找;Cosine、IP、L2 决定找到的候选中谁更相似。
1.4 元数据过滤:谁有资格参加检索
工业级 RAG 中还有一个经常被忽略的环节,就是元数据过滤。
真实业务中的检索,通常不是:
从全部文档中找出与问题最相似的内容。
而是:
从当前租户、当前知识库、用户有权限访问、仍处于有效状态的文档中,找到与问题最相关的内容。
例如,一条知识库 Chunk 除了正文和向量,还可能包含:
tenant_id:属于哪个租户;kb_id:属于哪个知识库;department:属于哪个部门;doc_type:文档类型;status:文档是否有效;acl_roles:哪些角色可以访问;effective_date:生效时间;expiry_date:失效时间。
用户查询时,真正的检索条件可能是:
元数据过滤解决的是:
哪些数据有资格参与后面的相关性竞争。
它既不是索引算法,也不是相似度计算方式。
1.5 排序、融合和重排
完成初步召回后,还需要决定候选的最终顺序。
例如:
- BM25 返回一批关键词候选;
- Dense Vector 返回一批语义候选;
- Sparse Vector 返回一批学习型稀疏候选。
由于三路检索的分数范围不同,不能简单把它们的原始分数直接相加。
这时可以使用 RRF,也就是 Reciprocal Rank Fusion,根据不同召回通道中的排名进行融合。
Elasticsearch 当前的 Retriever API 可以组合关键词检索、稀疏检索和稠密向量检索,并通过 RRF 统一候选排名。
在 RRF 后面,还可以增加 BGE-Reranker,对查询和候选文本进行更精细的交叉计算。
因此,工业级 RAG 中常见的检索链路是:
二、从传统搜索重新理解 Elasticsearch
理解前面的基础概念以后,我们再回到 Elasticsearch。
Elasticsearch 最初的核心优势不是向量检索,而是全文搜索。
不过在讲全文搜索之前,还需要先解决一个术语问题:
Elasticsearch 中的“索引”到底指什么?
2.1 Elasticsearch 中至少有四种“索引”(重点、重点)
在 Elasticsearch 的语境里,“索引”这个词可能代表不同层次的概念。
第一种:Elasticsearch Index
例如:
rag_chunks_v1product_search_v1customer_service_logs_v1
这里的 Index 是一类 JSON 文档的逻辑集合,可以粗略类比关系型数据库中的表。
第二种:倒排索引
倒排索引是全文检索的底层数据结构,负责建立词项和文档之间的映射关系。
第三种:向量索引
例如 HNSW,负责组织 dense_vector 字段中的向量。
第四种:字段索引
例如 keyword、date、integer 字段,也会建立适合精确过滤、范围查询和聚合的数据结构。
所以:
创建一个 Elasticsearch Index,不等于选择了 HNSW 算法。
一个 Elasticsearch Index 内部,可以同时包含:
- 文本字段的倒排索引;
- Keyword 字段的精确索引;
- 日期和数值字段的结构化索引;
- Dense Vector 字段的 HNSW 索引;
- Sparse Vector 字段的稀疏索引。
这也是 Elasticsearch 能够把全文检索、向量检索和元数据过滤放在同一份文档中的基础。
2.2 倒排索引解决什么问题
假设有三个文档:
- 文档一:工业设备故障处理;
- 文档二:工业设备维护手册;
- 文档三:医院门诊预约流程。
倒排索引会形成类似下面的结构:
| 词项 | 对应文档 |
|---|---|
| 工业 | 文档一、文档二 |
| 设备 | 文档一、文档二 |
| 故障 | 文档一 |
| 维护 | 文档二 |
| 医院 | 文档三 |
| 门诊 | 文档三 |
当用户查询“工业设备故障”时,Elasticsearch 不需要逐个扫描所有文档,而是可以直接根据词项找到对应的文档列表。
Elasticsearch 会先通过 Analyzer 对文本进行分词和归一化,再建立由词项字典和 Posting List 组成的倒排索引。
因此,倒排索引解决的是:
如何快速找到包含相关词项的文档。
2.3 BM25 不是倒排索引
倒排索引找到候选以后,还需要判断哪些文档更加相关。
这就是 BM25 的工作。
BM25 会综合考虑:
- 查询词在当前文档中出现了多少次;
- 这个词在整个文档集合中是否稀有;
- 当前文档是否过长;
- 同一个词重复出现后的收益递减。
例如“故障”这个词在所有文档中都很少出现,那么包含“故障”的文档通常更值得关注。
而“设备”如果在大量文档中都出现,它对区分文档的作用就会下降。
所以:
倒排索引负责找到候选文档,BM25 负责给这些候选计算关键词相关性分数。
这和向量检索中的关系很相似:
| 文本检索 | 向量检索 |
|---|---|
| 倒排索引 | HNSW、IVF |
| BM25 | Cosine、IP、L2 |
| Posting List | 向量近邻候选 |
| Analyzer | Embedding 模型 |
当然,这只是为了帮助理解,两套体系的底层原理并不完全相同。
三、Elasticsearch 如何完成向量检索
现在的 Elasticsearch 已经能够作为向量数据库使用。
当我们把 Embedding 存入 dense_vector 或 sparse_vector 字段后,就可以基于向量相似度进行检索。
3.1 Dense Vector 是什么
用户输入一段文本:
设备出现故障以后应该怎样处理?
经过 BGE-M3 后,可以得到一个固定维度的稠密向量。
所谓稠密向量,是指向量的大部分维度都有数值。
例如:
[ [0.032,-0.126,0.781,\dots] ]
单个数字本身通常没有直观业务含义,但是整个向量共同表示文本的语义特征。
查询时,用户问题也会被转换成相同维度的向量,再与数据库中的文档向量比较。
3.2 FLAT:最直接的搜索方式
FLAT 可以理解为全量比较。
假设数据库中有十万条向量,那么查询时就分别计算查询向量与十万条向量之间的距离。
它的优点是:
- 实现简单;
- 不会因为近似索引而漏掉真实近邻;
- 适合作为召回率基准。
缺点也非常明显:
- 数据量越大,计算次数越多;
- 查询延迟随数据规模增长;
- 高并发下成本较高。
所以 FLAT 更适合:
- 小规模数据;
- 离线评测;
- 检查近似索引是否损失过多召回率。
3.3 HNSW:用图结构减少搜索范围
在大规模向量检索中,更常见的是 HNSW。
HNSW 会提前建立多层近邻图,查询时沿图搜索附近节点,而不是扫描全部向量。
Elasticsearch 的近似 kNN 搜索主要使用 HNSW;在当前版本中,普通浮点 dense_vector 默认使用量化后的 int8_hnsw,同时也允许根据场景配置其他 HNSW、Flat 和量化方案。
HNSW 中有三个非常重要的参数。
| 参数 | 所处阶段 | 参数增大后的主要影响 |
|---|---|---|
| m | 建索引 | 图连接更密,召回率可能提高,内存增加 |
| ef_construction | 建索引 | 建图质量提高,索引构建时间增加 |
| num_candidates | 查询 | 搜索候选增多,召回率提高,延迟增加 |
其中,m 和 ef_construction 主要决定索引图的质量;num_candidates 则决定一次查询愿意搜索多大的候选范围。
Elasticsearch 在每个分片中先获取一定数量的近似候选,再根据相似度选出 Top K,并合并各分片结果。
因此,HNSW 本质上是在做三者之间的平衡:
- 查询速度;
- 召回率;
- 内存与索引成本。
3.4 量化不是另一套检索逻辑
在 Elasticsearch 中,我们还会看到:
int8_hnswint4_hnswbbq_hnswint8_flatint4_flat
这些名称容易让人误以为它们是完全不同的检索算法。
实际上,它们更多是在回答另一个问题:
向量应该用多高的精度保存和参与检索?
原始浮点向量通常使用 32 位浮点数。量化可以把它压缩成 8 位、4 位甚至二进制表示,从而减少内存和磁盘消耗。
代价是:
- 向量精度下降;
- 相似度结果可能发生变化;
- 召回率可能出现损失。
因此:
HNSW 负责怎样搜索,量化负责向量怎样压缩。
二者可以组合,但不是同一个概念。
3.5 Elasticsearch 支持哪些度量方式
Elasticsearch 的 dense_vector 支持多种相似度方式,包括:
cosinedot_productl2_normmax_inner_product
普通浮点向量默认使用 Cosine;具体选择仍然应该以 Embedding 模型的训练方式和官方建议为依据。
对于 BGE-M3 这样的文本 Embedding,工程上不建议随意更换度量函数。
最稳妥的方式是:
- 确认模型输出是否已经归一化;
- 查看模型推荐的相关性计算方式;
- 使用评测集比较 Cosine 和 IP;
- 固定模型、向量版本和度量方式。
因为一旦更换 Embedding 模型或者度量方式,原有索引中的分数分布和排序结果都可能发生变化。
四、BGE-M3 为什么适合工业级混合检索
BGE-M3 的价值不只是生成一个稠密向量。
它可以在同一个模型中提供三种检索表示:
- Dense Retrieval;
- Sparse Retrieval;
- Multi-Vector Retrieval。
同时,它支持多语言和长文本输入。
4.1 Dense Vector:解决语义相似
Dense Vector 更擅长理解整体语义。
例如用户问:
孩子晚上一直咳嗽,应该去看哪个科?
文档中写的是:
夜间持续性咳嗽建议优先就诊呼吸内科。
两段文字表面上的词并不完全相同,但是语义高度一致。
Dense Vector 能够把这种语义关系编码到连续向量空间中。
因此,它比较适合:
- 自然语言问答;
- 同义表达;
- 问题改写;
- 跨语言检索;
- 用户问题和文档措辞不一致的场景。
4.2 Sparse Vector:保留词项,同时学习权重
BGE-M3 的 Sparse Vector 不是普通的固定长度稠密数组,而是一组非零 Token 与权重的对应关系。
例如:
{
"设备": 0.82,
"故障": 0.91,
"处理": 0.46
}
真实数据中可能使用 Token ID,而不是直接使用中文词语,但核心思想相同。
Sparse Vector 保留了词项维度,又通过模型学习词项的重要性。
因此,它兼具一些关键词检索和语义检索的特点:
- 保留较强的词项可解释性;
- 能强化重要术语;
- 能弱化无意义的常见词;
- 可以实现一定程度的词项扩展;
- 比单纯词频更能反映语义相关性。
Elasticsearch 的 sparse_vector 查询可以使用预计算的 Token—权重,并通过查询和文档稀疏向量的加权点积计算相关性。
4.3 Multi-Vector:更细粒度的 Token 交互
BGE-M3 还可以生成多向量表示。
Dense Retrieval 通常使用一个向量代表整段文本,而 Multi-Vector 会使用多个 Token 级向量表示文档。
查询时,可以让查询 Token 和文档 Token 进行更细粒度的匹配。
它的优势是相关性判断更加精细,但代价也更明显:
- 向量数量更多;
- 存储成本更高;
- 查询计算更加复杂;
- 不适合直接面对全库完成第一阶段召回。
因此,更常见的方式是:
BGE-M3 官方也推荐在 RAG 中采用混合召回加重排的检索链路。
五、为什么工业级 RAG 不能只依赖 Dense Vector
只使用 Dense Vector,看起来架构最简单:
但在真实业务中,它很容易遇到精确词召回问题。
例如:
- 设备错误码
E1107; - 交换机型号
H3C-S12500; - 合同编号
HT20260725001; - 国家标准
GB/T 19001-2016; - 药品名称;
- 人名;
- 机构名;
- 版本号;
- 接口字段名。
这些信息的共同特点是:
它们的价值主要来自精确匹配,而不是整体语义。
Dense Vector 可能会召回一批“设备故障处理”“错误代码说明”“设备维修手册”等语义相近的文档,但不一定能保证精确命中 E1107。
这正是 BM25 的优势。
另一方面,用户问题和文档可能使用完全不同的表达方式,这又是 Dense Vector 的优势。
因此,工业级 RAG 的重点不是争论 BM25 和 Dense 谁更强,而是让它们互相补足。
5.1 BM25、Dense 和 Sparse 的职责
| 检索方式 | 主要优势 | 典型场景 |
|---|---|---|
| BM25 | 精确词项、编号和专有名词 | 错误码、型号、合同号、法规 |
| Dense | 整体语义和同义表达 | 自然语言问题、语义改写 |
| Sparse | 学习型词项权重 | 术语强化、语义词项扩展 |
| Multi-Vector | Token 级细粒度交互 | 候选精排 |
一个比较合理的工业检索流程是:
这里每一层都有明确作用:
- BM25 保证精确术语;
- Dense 补充语义召回;
- Sparse 提供学习型词项匹配;
- RRF 合并不同分数体系;
- Reranker 做最终相关性判断。
六、元数据过滤和 Milvus Partition 是一回事吗
这也是一个非常容易混淆的问题。
答案是:
元数据过滤和 Partition 不是一回事,但 Partition 可以帮助进一步缩小搜索范围。
6.1 元数据过滤解决业务条件问题
假设 Milvus Collection 或 Elasticsearch Index 中存储了以下字段:
vectortenant_idkb_iddepartmentdoc_typestatus
用户查询时,可以限定:
只搜索
tenant_001中、属于equipment_manual知识库、状态为active的文档。
这就是元数据过滤。
它回答的是:
哪些数据符合业务条件?
Elasticsearch 可以在 kNN 查询中使用 Filter 进行预过滤,保证最终 Top K 候选同时满足元数据条件。
Milvus 同样支持基于标量字段的标准过滤和迭代过滤。
6.2 Milvus Partition 是数据组织范围
Milvus 中,一个 Collection 可以划分成多个 Partition。
例如,可以按业务或租户划分:
- 租户一;
- 租户二;
- 租户三。
查询时,如果明确指定只搜索某个 Partition,就可以避免访问其他 Partition。
Milvus 还支持 Partition Key,根据某个标量字段的值自动组织数据;查询条件中包含 Partition Key 后,可以缩小需要搜索的 Partition 范围。
6.3 Partition 不等于 Filter
二者的区别可以这样理解:
| 概念 | 作用 |
|---|---|
| Metadata Filter | 根据字段表达式筛选符合条件的数据 |
| Collection Partition | 把 Collection 划分为多个数据子集 |
| Partition Key | 根据指定字段自动完成分区路由 |
| IVF Cluster | 根据向量空间位置进行算法聚类 |
指定 Partition 后,Partition 内部仍然可以继续使用元数据 Filter。
例如:
此外,还要避免把 Milvus Partition 和 IVF 中的“向量聚类”混为一谈。
- Collection Partition 按业务组织数据;
- IVF Cluster 按向量空间位置组织数据;
- Metadata Filter 按字段条件筛选数据。
三者虽然都在缩小搜索范围,但层次完全不同。
6.4 Elasticsearch 中对应什么
Elasticsearch 的普通元数据过滤,主要通过 Query DSL 中的 filter 完成。
例如:
tenant_id = tenant_001status = activedepartment = maintenance
这些字段通常使用 keyword、date 或数值类型。
Elasticsearch 中没有和 Milvus Partition 完全相同的一对一概念。
比较接近的数据组织方式包括:
- 不同业务使用不同 Index;
- 通过 Shard 完成底层数据分布;
- 使用 Custom Routing 将相同租户的数据路由到指定 Shard;
- 查询时携带 Routing,减少需要访问的 Shard。
但对于大多数中小型企业 RAG,第一阶段通常不需要过早使用复杂 Routing。
更重要的是先保证:
tenant_id过滤正确;- ACL 权限过滤正确;
- 无效文档不会参与检索;
- 不同知识库的数据不会混召回。
七、Elasticsearch 是不是已经把 Milvus 的能力都做了
从工业 RAG 的常见需求来看,两者的能力确实已经高度重叠。
| 能力 | Elasticsearch | Milvus |
|---|---|---|
| 稠密向量 | 支持 | 支持 |
| 稀疏向量 | 支持 | 支持 |
| BM25 | 传统强项 | 已支持 |
| 元数据过滤 | 支持 | 支持 |
| ANN 检索 | 支持 | 支持 |
| 精确向量检索 | 支持 | 支持 |
| 混合检索 | 支持 | 支持 |
| 多向量 | 支持 | 支持 |
| 分布式部署 | 支持 | 支持 |
Milvus 当前支持多种稠密向量索引、Sparse Vector、BM25 Function 和混合检索。
所以今天已经不能再简单地说:
Elasticsearch 只能做关键词,Milvus 只能做向量。
更准确的说法应该是:
Elasticsearch 是从搜索引擎向混合检索平台演化;Milvus 是从向量数据库向混合向量检索平台演化。
二者的能力出现了交叉,但它们的能力中心仍然不同。
7.1 Elasticsearch 的核心仍然是搜索
Elasticsearch 的能力体系是从下面这条路线发展而来的:
它天然擅长:
- 全文检索;
- 精确词匹配;
- 短语查询;
- 多字段加权;
- 同义词;
- 模糊查询;
- 高亮;
- 时间与数值范围查询;
- 聚合统计;
- 文档型数据检索;
- 日志与可观测性数据分析。
因此,Elasticsearch 是:
以文档搜索和复杂查询为中心,逐步增加向量能力。
7.2 Milvus 的核心仍然是向量
Milvus 的能力体系是从另一条路线发展而来的:
Milvus 支持 FLAT、HNSW、IVF_FLAT、IVF_PQ、DiskANN、GPU 索引和多种量化变体,并提供面向不同规模与内存条件的索引选择。
它天然更关注:
- 大规模向量存储;
- ANN 性能;
- 多种向量索引;
- 内存和磁盘之间的权衡;
- GPU 加速;
- 多模态向量;
- 超大规模向量检索。
因此,Milvus 是:
以向量存储和 ANN 检索为中心,逐步增加文本和混合搜索能力。
八、什么时候只使用 Milvus
并不是所有 RAG 都需要 Elasticsearch。
如果业务本身就是向量优先,那么单独使用 Milvus 是合理的。
8.1 图片、视频和音频相似检索
例如:
- 以图搜图;
- 视频片段相似搜索;
- 人脸特征检索;
- 音频指纹检索;
- 商品图片相似度;
- 多模态推荐。
这些场景的核心不是关键词,而是高维特征向量之间的近邻关系。
即使增加关键词,它通常也只是辅助条件。
8.2 向量规模非常大
如果系统需要存储数亿、十亿甚至更大规模的向量,并且明确需要:
- DiskANN;
- IVF_PQ;
- GPU_CAGRA;
- GPU_IVF;
- 更细致的量化策略;
- 大于内存容量的数据搜索;
Milvus 的向量中心架构会更有优势。
DiskANN 等索引尤其适合数据规模超出内存容量的场景。
8.3 搜索条件相对简单
如果元数据主要是:
- 租户;
- 类别;
- 时间;
- 语言;
- 状态;
同时不需要复杂的:
- 同义词;
- 短语匹配;
- 搜索高亮;
- 多字段权重;
- 搜索运营规则;
- 大量聚合分析;
那么 Milvus 加标量过滤可能已经足够。
九、什么时候只使用 Elasticsearch
对于大多数企业文档型 RAG,我更倾向于优先考虑 Elasticsearch。
9.1 企业知识库和文档检索
例如:
- 医院知识库;
- 工业设备手册;
- 企业制度;
- 智能客服;
- 工单知识库;
- 电商规则;
- 产品说明书;
- 法律法规;
- 合同知识库。
这些场景通常同时需要:
- 精确词匹配;
- 语义搜索;
- 元数据过滤;
- 多租户隔离;
- 权限过滤;
- 文档溯源;
- 多字段查询。
它们的核心仍然是文档搜索,而不只是向量近邻。
9.2 BM25 的价值不可替代
工业知识中经常出现:
- 型号;
- 错误码;
- 接口名;
- 文件名;
- 合同号;
- 人名;
- 组织名;
- 法律条款编号;
- 药品名称。
这些内容通常不能只依赖 Dense Vector。
Elasticsearch 可以在同一个 Document 中保存:
- 原始 Chunk 文本;
- Dense Vector;
- Sparse Vector;
- 租户和权限字段;
- 文档位置和来源。
然后一次完成:
这可以显著降低第一版系统的架构复杂度。
9.3 希望一个系统完成检索闭环
假设第一版同时使用 Elasticsearch 和 Milvus,就要面对:
- 文档双写;
- 向量双写或拆分写入;
- 删除一致性;
- 更新一致性;
- ID 对齐;
- 权限条件对齐;
- 两套备份;
- 两套监控;
- 两套索引重建;
- 跨引擎结果融合。
如果单 Elasticsearch 已经能够满足延迟、召回率和数据规模要求,那么双引擎并不会让系统显得更工业级。
很多时候,它只是让系统更复杂。
十、什么时候需要 Elasticsearch 加 Milvus
Elasticsearch 加 Milvus 并不是错误架构。
问题在于,必须存在明确理由。
10.1 Elasticsearch 的向量性能经过压测后不够
这里的关键词是:
经过压测后。
不能因为 Milvus 是专业向量数据库,就默认 Elasticsearch 的向量能力一定不够。
应该通过真实数据比较:
- 向量数量;
- 向量维度;
- 查询并发;
- P95、P99 延迟;
- Recall@K;
- 内存使用;
- 索引构建时间;
- 数据写入速度;
- 过滤后的检索性能。
只有当测试证明 Elasticsearch 无法满足业务目标时,才需要拆分向量召回。
10.2 明确需要 Milvus 的专业索引能力
例如业务明确依赖:
- DiskANN;
- IVF_PQ;
- GPU_CAGRA;
- GPU_IVF;
- 超大规模多模态向量;
- 特殊的内存压缩方案。
这时可以让:
- Elasticsearch 负责 BM25、全文检索和部分 Sparse 召回;
- Milvus 负责大规模 Dense ANN;
- 应用层统一完成 RRF 或自定义融合。
10.3 企业已经有两套成熟平台
如果企业内部已经存在:
- 成熟的 Elasticsearch 搜索中台;
- 成熟的 Milvus 向量平台;
- 统一文档 ID;
- 统一写入和删除事件;
- 完整监控;
- 备份和容灾;
- 权限同步机制;
那么继续使用双引擎是合理的。
因为此时系统复杂度已经被平台能力消化。
但对于从零开发的项目,双引擎的建设成本通常不能被忽略。
10.4 双引擎不是免费的
一个文档更新事件,在双引擎架构中可能经历:
如果 Elasticsearch 成功、Milvus 失败,应该怎么办?
- 重试 Milvus;
- 回滚 Elasticsearch;
- 标记文档为部分可用;
- 暂停该知识库查询;
- 使用旧版本向量;
- 进入补偿任务队列。
删除文档时也会遇到同样问题。
所以:
双引擎不是“多部署一个数据库”那么简单,而是引入了分布式数据一致性问题。
十一、我的工业级 RAG 应该怎样设计
结合企业文档、BGE-M3、混合检索和 Agent 工作流,我认为第一阶段可以采用下面的架构。
11.1 MinIO 负责原始文件
MinIO 主要保存:
- PDF;
- Word;
- PPT;
- 图片;
- 音频;
- 原始上传文件;
- 解析后的 Markdown;
- 中间 JSON;
- 页面截图;
- 表格和图片产物。
不建议把完整 PDF 二进制直接塞进 Elasticsearch。
Elasticsearch 中只需要保存:
- Chunk 文本;
- 文档 ID;
- 页码;
- 段落位置;
- MinIO 对象地址;
- 检索字段;
- 元数据;
- 向量。
11.2 PostgreSQL 负责业务事实
PostgreSQL 或 MySQL 主要保存:
- 用户;
- 租户;
- 知识库;
- 文档任务;
- 解析状态;
- 文档版本;
- 权限关系;
- 上传记录;
- 删除状态;
- 评测任务;
- Agent 任务信息。
关系型数据库仍然是业务事实来源。
Elasticsearch 是检索副本,不应该成为所有业务状态的唯一来源。
11.3 Elasticsearch 负责可检索数据
Elasticsearch 中的一条 Chunk,可以包含:
| 字段类别 | 示例字段 |
|---|---|
| 标识 | chunk_id、doc_id、version_id |
| 租户 | tenant_id、organization_id |
| 知识库 | kb_id |
| 文本 | title、content |
| 向量 | content_dense、content_sparse |
| 位置 | page_number、section_title |
| 权限 | acl_users、acl_roles |
| 状态 | status、effective_date |
| 溯源 | source_uri、source_filename |
| 模型版本 | embedding_model、parser_name |
这样,BM25、Dense、Sparse 和权限过滤可以围绕同一条 Document 完成。
11.4 Redis 负责短期状态
Redis 可以承担:
- Query Cache;
- Embedding Cache;
- 会话上下文;
- Agent 中间状态;
- 分布式锁;
- 任务进度;
- 限流;
- 热点问题缓存。
但 Redis 不适合作为完整知识库的长期事实来源。
11.5 LangGraph 负责复杂流程编排
当 RAG 进入 Agent 阶段以后,检索不再一定是固定的一次 Query。
一个 Agent 可能需要:
- 判断用户意图;
- 选择知识库;
- 查询业务接口;
- 执行多轮检索;
- 判断召回结果是否充分;
- 改写问题;
- 扩展关键词;
- 二次检索;
- 触发人工确认;
- 生成最终回答。
LangGraph 管理的是这条工作流,而 Elasticsearch 负责其中的检索能力。
十二、第一阶段为什么不建议直接加入 Milvus
对于一个企业文档型 RAG,第一阶段最重要的不是组件数量,而是把检索闭环跑通。
我更建议优先完成:
- 文档解析;
- Chunk 策略;
- Metadata 设计;
- BM25 召回;
- Dense 召回;
- Sparse 召回;
- RRF 融合;
- Reranker 精排;
- 权限过滤;
- 召回评测。
这套流程单独使用 Elasticsearch 就可以完成。
如果第一阶段直接引入 Milvus,很容易把大量时间消耗在:
- 环境部署;
- 双写代码;
- 数据一致性;
- 跨引擎融合;
- 两套 Schema;
- 两套监控;
- 两套异常处理。
而真正决定 RAG 效果的内容可能还没有做好:
- 文档是否正确解析;
- Chunk 是否合理;
- 问题是否需要改写;
- Metadata 是否完整;
- 权限是否准确;
- 评测集是否真实;
- Reranker 是否有效。
所以第一阶段应该优先验证检索方法,而不是优先堆叠基础设施。
十三、什么时候再引入 Milvus
Milvus 不应该因为“感觉更专业”而加入,而应该因为出现了可测量的瓶颈。
可以建立一套评测体系。
| 评测维度 | 关注内容 |
|---|---|
| Recall@K | 正确文档是否进入前 K 条 |
| MRR | 正确文档排名是否足够靠前 |
| NDCG | 多条相关文档的整体排序质量 |
| P95 延迟 | 大多数查询的响应时间 |
| P99 延迟 | 极端查询延迟 |
| QPS | 单位时间可处理请求数 |
| 内存 | 向量索引占用 |
| 磁盘 | 原始向量与量化索引占用 |
| 构建时间 | 全量重建索引需要多久 |
| 更新延迟 | 文档更新多久后可检索 |
分别测试:
- 纯 BM25;
- 纯 Dense;
- 纯 Sparse;
- BM25 加 Dense;
- BM25 加 Dense 加 Sparse;
- RRF 后结果;
- Reranker 后结果;
- 不同
num_candidates; - 不同 Chunk 长度;
- 不同 Embedding 模型。
如果测试结果表明:
- Elasticsearch 的 Dense 延迟无法满足要求;
- 内存成本过高;
- 向量规模超出预期;
- 明确需要 DiskANN 或 GPU;
- 向量检索已经成为独立平台;
再将 Dense ANN 拆分到 Milvus。
这时,Milvus 的加入是由数据驱动的,而不是由技术名词驱动的。
十四、最后的判断
重新理解 Elasticsearch 以后,我认为最重要的并不是记住 ES 和 Milvus 各自支持多少种索引,而是先把检索过程中的层次分清楚。
第一,索引结构和度量方式不是一回事。
- 索引结构决定数据如何组织;
- 搜索算法决定如何快速找到候选;
- 度量方式决定候选之间谁更相似;
- 元数据过滤决定谁有资格参与检索;
- 排序和重排决定最终结果如何呈现。
第二,Elasticsearch 和 Milvus 的能力虽然越来越重叠,但能力中心不同。
- Elasticsearch 以全文搜索、文档查询和复杂过滤为中心;
- Milvus 以大规模向量存储和 ANN 检索为中心。
第三,不是所有工业级 RAG 都需要双引擎。
对于大多数企业文档知识库:
Elasticsearch 已经可以完成 BM25、Dense Vector、Sparse Vector、Metadata Filter、RRF 和结果重排前的候选召回。
第四,Milvus 应该在向量问题真正成为独立瓶颈时加入。
例如:
- 向量规模非常大;
- ES 向量检索经过压测后不够;
- 需要 DiskANN、IVF_PQ 或 GPU 索引;
- 企业已经具备成熟向量平台。
所以,我对工业级 RAG 架构选型的最终理解是:
不要从数据库名称出发设计架构,而要先从业务数据、检索方式、访问权限、性能目标和评测结果出发。
架构并不是组件堆得越多越专业。
真正合理的方式是:
先使用最小系统跑通完整检索闭环,再根据真实数据和压力测试决定是否拆分。
对于企业文档型 RAG,第一阶段从 Elasticsearch 开始,把 BM25、BGE-M3 Dense、BGE-M3 Sparse、元数据过滤、RRF 和 BGE-Reranker 跑通,通常是一条更稳、更容易评测,也更容易演进的路线。