01.

背景
谈及混合搜索(Hybrid Search),它作为RAG应用检索阶段的核心环节,本质上是将向量搜索与基于关键词的全文检索有机结合,再借助RRF算法对两种检索结果进行合并与重排,从而有效提升数据召回率。需要明确的是,全文检索与语义检索从来就不是非此即彼的单选题。在实际应用场景中,两者必须兼顾:既要理解语义内涵,又要精准匹配关键词。以学术论文写作为例,用户不仅希望搜索结果能返回与查询概念语义相关的内容,还期待那些包含原始查询中特定术语和名称的信息也能被精确呈现。正因如此,越来越多的搜索应用开始采用混合搜索策略,融合两种方法的优势,在灵活的语义相关性与可预测的精确关键词匹配之间找到最佳平衡点。
从Milvus 2.4版本开始,系统引入了多向量搜索及执行混合搜索(即多向量搜索)的能力。混合搜索允许用户在同一时间跨多个向量列进行检索。这项功能使得多模态搜索、稀疏与全文关键词搜索、密集向量搜索,以及密集与全文搜索的混合检索成为现实,极大地拓展了向量相似性搜索和数据分析的灵活性。
02.
Milvus BM25
在最新发布的Milvus 2.5中,带来了一个“全新”的全文检索能力。具体而言,有两项重要更新:
- 针对全文检索所依赖的BM25算法,这次采用了Sparse-BM25方案。基于稀疏向量(Sparse Vector)实现的BM25,在存储效率和检索性能上开辟了新的空间,并且完美融入了Milvus以向量为核心检索范式的产品理念。
- 同时引入了原始文本插入与查询能力,用户无需再手动将文本转换为稀疏向量。这一步,让Milvus在处理非结构化数据的方向上又迈出了坚实的一步。
关于Sparse-BM25的原理,它与Elasticsearch以及其他全文搜索系统中常用的BM25算法类似,但它是专门为稀疏向量设计的,能够实现同等效果的全文搜索功能。
- 具备数据剪枝功能的高效检索算法:通过剪枝丢弃搜索查询中的低值稀疏向量,向量数据库可以显著缩小索引大小,同时以最小的质量损失获得最优的性能。
- 进一步的性能优化:将词频表示为稀疏向量而非倒排索引,这使得其他基于向量的优化成为可能。例如,用图索引替代暴力扫描以实现更高效的搜索;或者利用乘积量化(PQ)和标量量化(SQ)进一步减少内存占用。
03.
Milvus BM25 Hybrid Search
接下来,我们通过一个具体的案例来实践一下。整个过程分为几个步骤:
1. 首先,准备数据和问题。数据来自Milvus 2.5的release notes,并通过llama-index的SentenceWindowNodeParser对数据进行分块处理。
!wget https://raw.githubusercontent.com/milvus-io/milvus-docs/v2.5.x/site/en/release_notes.md -O milvus_2_5.md
documents = SimpleDirectoryReader(
input_files=["./milvus_2_5.md"]
).load_data()
# Create the sentence window node parser
node_parser = SentenceWindowNodeParser.from_defaults(
window_size=3,
window_metadata_key="window",
original_text_metadata_key="original_text",
)
# Extract nodes from documents
nodes = node_parser.get_nodes_from_documents(documents)
# query question
query = "What are the key features in milvus 2.5?"
2. 其次,创建collection的schema以及索引。这里原始文本数据存储在text列,Sparse-BM25数据存储在sparse_bm25列,需要通过转换Function来实现。
bm25_function = Function(
name="bm25",
function_type=FunctionType.BM25,
input_field_names=["text"],
output_field_names="sparse_bm25",
)
schema = MilvusClient.create_schema(
auto_id=False,
enable_dynamic_field=True,
)
# Add fields to schema
schema.add_field(field_name="id", datatype=DataType.INT64, is_primary=True)
schema.add_field(field_name="text", datatype=DataType.VARCHAR, max_length=512, enable_analyzer=True)
schema.add_field(field_name="sparse_bm25", datatype=DataType.SPARSE_FLOAT_VECTOR)
schema.add_field(field_name="dense", datatype=DataType.FLOAT_VECTOR, dim=dense_dim)
bm25_function = Function(
name="bm25",
function_type=FunctionType.BM25,
input_field_names=["text"],
output_field_names="sparse_bm25",
)
schema.add_function(bm25_function)
index_params = client.prepare_index_params()
# Add indexes
index_params.add_index(
field_name="dense",
index_name="dense_index",
index_type="IVF_FLAT",
metric_type="IP",
params={"nlist": 128},
)
index_params.add_index(
field_name="sparse_bm25",
index_name="sparse_bm25_index",
index_type="SPARSE_WAND",
metric_type="BM25"
)
# Create collection
client.create_collection(
collection_name=collection_name,
schema=schema,
index_params=index_params
)
3. 然后,对数据进行Embedding后插入Collection。这里采用了OpenAI的text-embedding-3-large模型。
def gen_embedding(docs):
model_name = "text-embedding-3-large"
openai_ef = model.dense.OpenAIEmbeddingFunction(
model_name=model_name,
api_key=os.environ["OPENAI_API_KEY"]
)
return openai_ef.encode_documents(docs)
docs_embeddings = gen_embedding(docs)
query_embeddings = gen_embedding([query])
# Assemble data
data = [
{"id": idx, "dense": docs_embeddings[idx].data, "text": doc}
for idx, doc in enumerate(docs)
]
# Insert data
res = client.insert(
collection_name=collection_name,
data=data
)
4. 最后,进行查询测试。
4.1. 先测试一下普通查询的效果。
search_params = {
"metric_type": "IP",
"params": {"nprobe": 10}
}
res = client.search(
collection_name=collection_name,
data=[query_embeddings[0]],
anns_field="dense",
limit=5,
search_params=search_params,
output_fields=["text"]
)
查询结果如下:
TopK results:
0
0 Enhancements in cluster management, indexing, and data handling introduce new levels of flexibil...
1 With this release, Milvus integrates powerful new features like term-based search, clustering co...
2 Milvus 2.5 introduces a built-in Cluster Management WebUI, reducing system maintenance difficult...
3 nnv2.5.0-betannRelease date: November 26, 2024nn| Milvus version | Python SDK version | No...
4 nnRelease NotesnnFind out what’s new in Milvus!
从结果可以看出,最后一条召回的内容与查询问题的相关度明显不高。
4.2. 然后,执行混合搜索。先分别定义向量搜索和Sparse-BM25搜索的参数。
k=5 # get the top 5 docs related to the query
search_params_dense: { "metric_type": "IP", "params": {"nprobe": 10}}
request_dense = AnnSearchRequest([query_embeddings[0].data], "dense", search_params_dense, limit=k)
search_params_bm25 = {"metric_type": "BM25"}
request_bm25 = AnnSearchRequest([query], "sparse_bm25", search_params_bm25, limit=k)
reqs = [request_dense, request_bm25]
这里使用RRFRanker来执行混合搜索。
ranker = RRFRanker(100)
res = client.hybrid_search(
collection_name=collection_name,
reqs=reqs,
ranker=ranker,
limit=5,
output_fields=["text"]
)
for hits in res:
print("TopK results:")
for hit in hits:
print(hit)
查询结果如下:
TopK results:
0
0 nnv2.5.0-betannRelease date: November 26, 2024nn| Milvus version | Python SDK version | No...
1 Enhancements in cluster management, indexing, and data handling introduce new levels of flexibil...
2 This feature is disabled by default in Milvus 2.5 and will be officially a vailable in version 3....
3 With this release, Milvus integrates powerful new features like term-based search, clustering co...
4 Powered by Tantivy, Milvus 2.5 has built-in analyzers and sparse vector extraction, extending th...
对比之下,基于Sparse-BM25的混合搜索能够更精准地定位与查询相关的内容。与普通查询相比,召回内容的准确性明显更高。
04.
总结
本文深入探讨了Milvus 2.5中引入的Sparse-BM25基础原理,并详细演示了如何利用BM25算法在RAG开发中实现混合搜索。通过引入Sparse-BM25算法,Milvus能够在稀疏向量上执行高效的全文检索,并与密集向量搜索相结合,从而显著提升检索的召回率与精确度。
