混合检索:BM25 与向量检索一库搞定

Milvus 是内置 BM25 的混合检索向量数据库:原始文本、稠密和稀疏向量在一次 hybrid_search 调用里检索、过滤并融合,不用再同步第二套搜索引擎。为什么要用混合检索?因为真实查询一半是精确词、一半是语义。

  • Apache-2.0 开源
  • GitHub 40K+ star
  • 内置 BM25 全文检索
  • RRF 与加权 ranker
所有召回路径都在一次 Milvus hybrid_search 调用中完成。
所有召回路径都在一次 Milvus hybrid_search 调用中完成。
Milvus 与 Zilliz Cloud 用户
  • Salesforce
  • Exa.ai
  • Walmart
  • Reddit
  • NVIDIA
  • eBay
  • Roblox
  • Airtable

为什么用 Milvus 做混合检索向量数据库

内置 BM25

在文本字段上声明 BM25 Function,Milvus 写入时生成稀疏向量(倒排索引),查询直接用原始文本打分,全文检索与向量检索同库;内置分析器按字段分词,中文走 jieba 分词,另有英文、阿拉伯文、泰文。无需双写。

稀疏向量与稠密向量,一份结果

错误码、SKU 靠 BM25 命中,换了说法的问题靠 embedding 命中;SPLADE 这类学习型稀疏向量也放在同一个 SPARSE_FLOAT_VECTOR 字段。一次调用返回一份排序结果。

融合与元数据过滤

双路召回后用 RRF 或加权 ranker 融合。每路请求都带自己的过滤表达式,并在 ANN 之前生效,租户约束和元数据过滤在混合检索内部完成,而不是查完再筛。

融合之后再重排

混合检索重排序在同一次调用里完成:模型 ranker(vLLM、TEI、Cohere、Voyage AI)或 Function Chain 对融合后的列表重新打分再返回。

稠密向量检索(QPS,越高越好)Cohere 1M,768 维
  • Zilliz CloudZillizCloud-8cu-perf13,316.23 QPSP99 2 ms · 召回率 0.9383
  • MilvusMilvus-16c64g-sq4u-fp16-force_merge9,575.69 QPSP99 2.3 ms · 召回率 0.9189

来源:VectorDBBench leaderboard,配置标签照原样引用。结果取决于配置,建议用 VectorDBBench 在自己的数据上跑一遍。

MS MARCO 上的 BM25 全文检索(QPS)Zilliz Cloud(托管 Milvus),8 CU 性能型,内置 BM25,AUTOINDEX,top 100
  • 100K 文档task15919-perf-8cu-oneseg-26-retest / MS MARCO Small12,134.21 QPSP99 3.5 ms
  • 1M 文档task15919-perf-8cu-oneseg-26-retest / MS MARCO Medium9,734.75 QPSP99 5.2 ms
  • 8.8M 文档task15919-perf-8cu-oneseg-26-retest / MS MARCO Large4,252.64 QPSP99 17.1 ms

来源:VectorDBBench FullTextSearch 结果(GitHub)。延迟为串行 P99。结果取决于配置。

Zilliz Cloud 跑的是同一个 Milvus 引擎(含 BM25),全托管。

免费试用 Zilliz Cloud

混合检索策略为什么有效:证据

微软:混合胜过关键词和纯向量

Azure AI Search 在 4 个客户数据集和公开基准上对比了 BM25、Ada-002 向量和 RRF 混合检索,每组数据里混合检索都最高。

  • 客户数据 NDCG@3:关键词 40.6,向量 43.8,混合 48.4
  • BEIR NDCG@10:40.6、45.0、48.4
  • MIRACL(18 种语言)NDCG@10:49.6、58.3、58.8
  • 客户数据上再加语义重排:60.1

Anthropic:召回失败减少 49%

在代码库、小说、ArXiv 和科学论文上,上下文 embedding 加上下文 BM25 让 top-20 召回失败率(1 − recall@20)明显下降。

  • 只用 embedding:失败率 5.7%
  • 上下文 embedding + 上下文 BM25:2.9%
  • Anthropic 给出的降幅是 49%

学术研究:两路信号互补

BEIR 在 18 个数据集上做零样本评测:BM25 是稳健基线,稠密模型跨领域泛化常常不足。Bruch 等人表明词法和语义打分互补。

  • 加权(凸组合)融合在域内、域外都优于 RRF
  • 只需少量标注数据即可调好权重

BM25 和向量检索的区别:关键词检索和语义检索各自什么时候更好

按查询类型对比关键词、向量、混合检索的 NDCG@3,来自微软 Azure AI Search 基准(BM25、Ada-002 向量、RRF 混合)
查询类型关键词(BM25)向量(Ada-002)混合(关键词 + 向量)
纯关键词查询79.211.761.0
原文片段检索51.141.551.0
问题与文档用词重合度低23.036.135.9
带错别字的查询28.839.140.6
长查询42.741.648.1

该基准共 10 类查询,混合检索在 9 类上超过较好的单路或与之相差不到 0.2 分,例外是纯关键词查询。再加语义重排后每一行都继续提升(纯关键词 66.9,长查询 59.4)。

RAG 怎么做混合检索?BM25 + 稠密向量 + RRF,一次调用

BM25 混合检索两步:定义 BM25 Function,Milvus 自动生成稀疏向量;再把稠密请求和原始文本请求用 RRF 融合。

python
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 调用,过滤和重排都包含在内。

关键词检索还需要 Elasticsearch 吗?

做 BM25 检索不需要。Milvus 原生计算 BM25,直接用原始文本查询;只有日志分析、聚合这类需求才要另一套引擎。

RRF 融合怎么算,k 怎么取?

每条结果在每份列表里按 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 和混合三种策略。

稀疏向量和全文索引选哪个?LangChain 或 LlamaIndex 里能用混合检索吗?

两者都在 Milvus 里:BM25 全文索引零训练,SPLADE 等学习型稀疏向量要模型但更懂语义,可以同时作为两路。langchain-milvus 用 BM25BuiltInFunction 加加权或 RRF,LlamaIndex 有全文检索指南,底层都是同一个 hybrid_search。

一个数据库上线关键词检索 + 语义检索

schema 里加 BM25 Function,发两路请求,用 RRF 或加权融合,需要就再重排。从混合检索文档开始,或用托管 Milvus。