内置 BM25
在文本字段上声明 BM25 Function,Milvus 写入时生成稀疏向量(倒排索引),查询直接用原始文本打分,全文检索与向量检索同库;内置分析器按字段分词,中文走 jieba 分词,另有英文、阿拉伯文、泰文。无需双写。
Milvus 是内置 BM25 的混合检索向量数据库:原始文本、稠密和稀疏向量在一次 hybrid_search 调用里检索、过滤并融合,不用再同步第二套搜索引擎。为什么要用混合检索?因为真实查询一半是精确词、一半是语义。
在文本字段上声明 BM25 Function,Milvus 写入时生成稀疏向量(倒排索引),查询直接用原始文本打分,全文检索与向量检索同库;内置分析器按字段分词,中文走 jieba 分词,另有英文、阿拉伯文、泰文。无需双写。
错误码、SKU 靠 BM25 命中,换了说法的问题靠 embedding 命中;SPLADE 这类学习型稀疏向量也放在同一个 SPARSE_FLOAT_VECTOR 字段。一次调用返回一份排序结果。
双路召回后用 RRF 或加权 ranker 融合。每路请求都带自己的过滤表达式,并在 ANN 之前生效,租户约束和元数据过滤在混合检索内部完成,而不是查完再筛。
混合检索重排序在同一次调用里完成:模型 ranker(vLLM、TEI、Cohere、Voyage AI)或 Function Chain 对融合后的列表重新打分再返回。
来源:VectorDBBench leaderboard,配置标签照原样引用。结果取决于配置,建议用 VectorDBBench 在自己的数据上跑一遍。
来源:VectorDBBench FullTextSearch 结果(GitHub)。延迟为串行 P99。结果取决于配置。
Zilliz Cloud 跑的是同一个 Milvus 引擎(含 BM25),全托管。
免费试用 Zilliz CloudAzure AI Search 在 4 个客户数据集和公开基准上对比了 BM25、Ada-002 向量和 RRF 混合检索,每组数据里混合检索都最高。
在代码库、小说、ArXiv 和科学论文上,上下文 embedding 加上下文 BM25 让 top-20 召回失败率(1 − recall@20)明显下降。
BEIR 在 18 个数据集上做零样本评测:BM25 是稳健基线,稠密模型跨领域泛化常常不足。Bruch 等人表明词法和语义打分互补。
| 查询类型 | 关键词(BM25) | 向量(Ada-002) | 混合(关键词 + 向量) |
|---|---|---|---|
| 纯关键词查询 | 79.2 | 11.7 | 61.0 |
| 原文片段检索 | 51.1 | 41.5 | 51.0 |
| 问题与文档用词重合度低 | 23.0 | 36.1 | 35.9 |
| 带错别字的查询 | 28.8 | 39.1 | 40.6 |
| 长查询 | 42.7 | 41.6 | 48.1 |
该基准共 10 类查询,混合检索在 9 类上超过较好的单路或与之相差不到 0.2 分,例外是纯关键词查询。再加语义重排后每一行都继续提升(纯关键词 66.9,长查询 59.4)。
BM25 混合检索两步:定义 BM25 Function,Milvus 自动生成稀疏向量;再把稠密请求和原始文本请求用 RRF 融合。
from pymilvus import (MilvusClient, DataType, Function, FunctionType,
AnnSearchRequest, RRFRanker)
client = MilvusClient(uri="http://localhost:19530")
schema = client.create_schema()
schema.add_field("id", DataType.INT64, is_primary=True, auto_id=True)
schema.add_field("text", DataType.VARCHAR, max_length=1000, enable_analyzer=True)
schema.add_field("dense", DataType.FLOAT_VECTOR, dim=768)
schema.add_field("sparse", DataType.SPARSE_FLOAT_VECTOR)
schema.add_function(Function(name="bm25", input_field_names=["text"],
output_field_names=["sparse"], function_type=FunctionType.BM25))
index_params = client.prepare_index_params()
index_params.add_index("dense", index_type="AUTOINDEX", metric_type="IP")
index_params.add_index("sparse", index_type="SPARSE_INVERTED_INDEX", metric_type="BM25")
client.create_collection("docs", schema=schema, index_params=index_params)
# Insert a document before searching. embed() is your 768-dimensional model.
client.insert(collection_name="docs", data=[{
"text": "Quiet white headphones with comfortable ear pads.",
"dense": embed("Quiet white headphones with comfortable ear pads."),
}])
query = "white headphones, quiet and comfortable"
res = client.hybrid_search(
collection_name="docs",
reqs=[AnnSearchRequest([embed(query)], "dense", {}, limit=10), # semantic
AnnSearchRequest([query], "sparse", {}, limit=10)], # BM25 on raw text
ranker=RRFRanker(60),
limit=5,
output_fields=["text"],
)embed() 换成你的 768 维 embedding 模型。给任一请求加 expr="..." 即可过滤。
三步:BM25 和向量两路请求在同一个 collection 上并行召回,ranker 把两份列表融合成一份,再带字段返回 top-k。Milvus 里就是一次 hybrid_search 调用,过滤和重排都包含在内。
做 BM25 检索不需要。Milvus 原生计算 BM25,直接用原始文本查询;只有日志分析、聚合这类需求才要另一套引擎。
每条结果在每份列表里按 1/(k + 名次) 打分再求和,k 默认 60,第一名得 1/61,不需要分数校准。Milvus 3.0.1 起 RRF 也支持按路加权。
用加权 ranker,相当于其他系统的 alpha 参数:加权融合前先做分数归一化(arctan 映射到 [0, 1]),再给每一路一个 [0, 1] 的权重。Bruch 等人发现少量标注数据就能把加权调得比 RRF 好。
不是每条都更好:微软测得纯关键词查询上单用 BM25 为 79.2,混合为 61.0。建议用自己的查询,以 recall@k 或 NDCG 对比纯稠密、纯 BM25 和混合三种策略。
两者都在 Milvus 里:BM25 全文索引零训练,SPLADE 等学习型稀疏向量要模型但更懂语义,可以同时作为两路。langchain-milvus 用 BM25BuiltInFunction 加加权或 RRF,LlamaIndex 有全文检索指南,底层都是同一个 hybrid_search。
schema 里加 BM25 Function,发两路请求,用 RRF 或加权融合,需要就再重排。从混合检索文档开始,或用托管 Milvus。