深色模式
向量检索与混合检索 Hybrid Search
摘要:本文面向 RAG 检索层工程师。纯向量检索擅长语义但弱于精确匹配(产品型号、错误码、专名)。混合检索(稠密向量 + 稀疏/BM25)兼顾两者,是生产 RAG 的事实标配。我们用 Milvus 2.5 内置 BM25 函数给出可复制配置,并解释 RRF 与加权融合。
纯向量检索的盲区
"订单号 ORD-20240901-8823 为什么失败?"这类查询依赖精确词匹配,语义向量往往把它和一堆"订单失败"的文档混在一起。BM25 等稀疏检索恰好擅长精确词频匹配。两者结合才能既懂语义又认专名。
核心概念
| 检索类型 | 代表 | 擅长 | 不擅长 |
|---|---|---|---|
| 稠密(Dense) | 嵌入向量 + ANN | 语义相似、 paraphrasing | 精确专名、罕见词 |
| 稀疏(Sparse) | BM25 / SPLADE / BGE-M3 sparse | 精确词匹配、ID、术语 | 语义泛化 |
| 混合(Hybrid) | Dense + Sparse 融合 | 两者兼有 | 实现与调参更复杂 |
架构与原理
融合常用两法:
- RRF(Reciprocal Rank Fusion):
score = Σ 1/(k+rank),只看排名不看原始分数,鲁棒、无需归一化。 - 加权(Weighted):
score = w1·s_dense + w2·s_sparse,需把两路分数归一化到同量纲。
生产实践:Milvus 内置 BM25 混合检索
Milvus 2.5+ 提供服务端 BM25BuiltInFunction,无需客户端维护语料词典(来源:Milvus 官方文档 full_text_search_with_langchain,访问日期 2026-10-09)。
建集合(dense + sparse 两字段)
python
# pip install pymilvus langchain-milvus
from pymilvus import MilvusClient, Function, FunctionType, DataType, FieldSchema, CollectionSchema
client = MilvusClient(uri="http://localhost:19530")
COLLECTION = "rag_hybrid"
schema = CollectionSchema([
FieldSchema("id", DataType.VARCHAR, is_primary=True, max_length=128),
FieldSchema("dense", DataType.FLOAT_VECTOR, dim=1024), # bge-m3 维度
FieldSchema("text", DataType.VARCHAR, max_length=4096),
])
# 在集合创建时声明 BM25 函数:text -> sparse 向量
bm25 = Function(name="bm25", input_field_names=["text"],
output_field_names=["sparse"], function_type=FunctionType.BM25)
schema.add_function(bm25)
index_params = client.prepare_index_params()
index_params.add_index(field_name="dense", index_type="AUTOINDEX", metric_type="COSINE")
index_params.add_index(field_name="sparse", index_type="SPARSE_INVERTED_INDEX", metric_type="BM25")
client.create_collection(COLLECTION, schema=schema, index_params=index_params)1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
混合检索(RRFRanker)
python
from pymilvus import AnnSearchRequest, RRFRanker
# 离线已用 bge-m3 编码 dense;BM25 由服务端按 text 自动计算
query_dense = bge_model.encode([query])[0] # 1024 维
req_dense = AnnSearchRequest([query_dense], "dense", {"metric_type": "COSINE"}, limit=10)
req_sparse = AnnSearchRequest([query], "sparse", {"metric_type": "BM25"}, limit=10)
results = client.hybrid_search(
COLLECTION, reqs=[req_dense, req_sparse],
ranker=RRFRanker(k=100), limit=5,
output_fields=["text", "source"],
)1
2
3
4
5
6
7
8
9
10
11
12
2
3
4
5
6
7
8
9
10
11
12
加权融合(需分数归一化)
python
from pymilvus import WeightedRanker
# 权重按评测调优;0.7 稠密 + 0.3 稀疏是中文通用起点[版本相关/需评测验证]
results = client.hybrid_search(
COLLECTION, reqs=[req_dense, req_sparse],
ranker=WeightedRanker(0.7, 0.3), limit=5,
)1
2
3
4
5
6
2
3
4
5
6
BM25 vs BGE-M3 sparse
不想引入独立 BM25 语料统计时,可直接用 BAAI/bge-m3 的 sparse 输出(见 embedding-selection.md)作为第二路稀疏检索,与 dense 同源、免维护词典。Milvus LangChain 集成支持 BM25SparseEmbedding 与 BM25BuiltInFunction 两种方式,官方推荐内置函数。
验证
bash
# 构造对照 query 验证两路互补
# 语义 query:"怎么申请退款" -> 应主要靠 dense 命中
# 专名 query:"ORD-20240901-8823" -> 应主要靠 sparse 命中
# 检查 hybrid 是否两路都覆盖;用 evaluation.md 的 context_recall 量化1
2
3
4
2
3
4
回滚与清理
schema 变更需重建
新增/修改向量字段(如从单 dense 改 dense+sparse)属于 schema 变更,必须新建集合并重建数据,无法直接 inplace 改。BM25 函数须在集合创建时声明。
故障排查
- 稀疏路无结果:未声明 BM25 函数或
text字段为空 → 检查add_function与写入的 text。 - 融合后变差:权重失衡或分数未归一化 → 改用 RRF 起步,再逐步试加权。
- RRF 全部平局:两路返回高度重叠且 k 过大 → 调小
k(默认 60)。 - 延迟翻倍:两路并行 ANN + 倒排,QPS 高时扩容或加候选裁剪(先各取 2×limit 再融合)。
安全与合规
检索即权限边界
混合检索的每路请求都必须携带租户/权限过滤表达式(Milvus 的 filter 参数)。仅做语义召回而不做行级过滤,会造成跨租户数据泄露。BM25 与 dense 两路都要加同一过滤条件,避免一路漏过滤。
成本与性能
量级参考,非实测报价。
- 存储:sparse 向量用
SPARSE_INVERTED_INDEX,仅存非零词,中文长尾词多时体积可控;dense 1024 维占主。 - 算力:两路检索比单路约翻倍检索开销,但常在减少重排候选数上回本(召回更准 → 重排更省力)。
- 延迟:单路 ANN 通常毫秒级;混合在局域网内多数仍 < 50ms(取决于数据量与索引),高 QPS 需压测。