BM25 built in
Declare a BM25 Function on a text field. Milvus builds sparse vectors on insert and scores raw-text queries; built-in analyzers tokenize English, Chinese (jieba), Arabic and Thai per field. No dual writes.
Milvus is a hybrid search vector database with BM25 built in: raw text, dense and sparse vectors are searched, filtered and fused in one hybrid_search call, no second search engine to sync. When should you use hybrid search? Whenever queries mix exact terms with meaning.
Declare a BM25 Function on a text field. Milvus builds sparse vectors on insert and scores raw-text queries; built-in analyzers tokenize English, Chinese (jieba), Arabic and Thai per field. No dual writes.
Error codes and SKUs match through BM25; paraphrased questions match through embeddings. Learned sparse embeddings such as SPLADE fit the same SPARSE_FLOAT_VECTOR field. One call returns one ranked list.
Fuse with RRF or a weighted ranker. Each request carries its own filter, applied before the ANN search, so tenant rules and metadata filtering run inside hybrid search rather than after it.
Hybrid search with reranking in the same call: a model ranker (vLLM, TEI, Cohere, Voyage AI) or a Function Chain rescores the fused list before it returns.
Zilliz Cloud runs the same Milvus engine, BM25 included, fully managed.
Try Zilliz Cloud freeAzure AI Search tested BM25, Ada-002 vectors and RRF hybrid on four customer datasets and public benchmarks. Hybrid scored highest of the three modes on each.
Across codebases, fiction, ArXiv and science papers, combining contextual embeddings with contextual BM25 cut the top-20 failure rate (1 minus recall@20).
BEIR, an 18-dataset zero-shot benchmark, found BM25 a robust baseline and that dense retrievers often generalize poorly. Bruch et al. show lexical and semantic scores are complementary.
| Query type | Keyword (BM25) | Vector (Ada-002) | Hybrid (keyword + vector) |
|---|---|---|---|
| Keyword queries | 79.2 | 11.7 | 61.0 |
| Exact snippet search | 51.1 | 41.5 | 51.0 |
| Low query/doc term overlap | 23.0 | 36.1 | 35.9 |
| Queries with misspellings | 28.8 | 39.1 | 40.6 |
| Long queries | 42.7 | 41.6 | 48.1 |
Across all ten query types in the study, hybrid beat or came within 0.2 points of the better single path on nine; the exception is keyword-only queries. Adding a semantic reranker lifted every row further (keyword queries 66.9, long queries 59.4).
Hybrid search with BM25 and embeddings: one BM25 Function, then a dense request and a raw-text request fused with 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"],
)Replace embed() with your 768-dim embedding model. Add expr="..." to either request to filter. Code requires a running server and your own embedding model; it is an example, not a benchmark.
Three steps: BM25 and vector requests run in parallel on one collection, a ranker fuses the two lists, and the top-k returns with its fields. In Milvus that is one hybrid_search call, filters and reranking included.
Not for BM25 retrieval. Milvus computes BM25 natively and searches with raw text. Keep a separate engine only for needs like log analytics or aggregations.
Each hit scores 1/(k + rank) in every list it appears in, and the scores are summed. With the default k=60 a rank-1 hit scores 1/61, so no score calibration is needed. Milvus 3.0.1+ also accepts per-path weights on RRF.
Use the weighted ranker, the equivalent of a hybrid search alpha parameter: scores are normalized to [0, 1] with arctan and each path gets a weight in [0, 1]. Bruch et al. found weighted fusion beats RRF with a small labeled set.
Not on every query: Microsoft measured BM25 alone at 79.2 vs hybrid 61.0 on keyword-only queries. Compare dense-only, BM25-only and hybrid on your own queries with recall@k or NDCG before you choose.
Yes. langchain-milvus adds BM25BuiltInFunction to the Milvus vector store with weighted or RRF ranking, and LlamaIndex has a full-text search guide for Milvus. Both call the same hybrid_search underneath.
Add a BM25 Function to your schema, send two requests, fuse with RRF or weights, rerank if you need to. Start from the hybrid search guide, or run it on managed Milvus.