Hybrid search: BM25 full-text and vector search in one database

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.

  • Apache-2.0 open source
  • 40K+ GitHub stars
  • Built-in BM25 full-text search
  • RRF and weighted rankers
Every retrieval path runs inside one Milvus hybrid_search call.
Every retrieval path runs inside one Milvus hybrid_search call.
Milvus and Zilliz Cloud adopters
  • Salesforce
  • Exa.ai
  • Walmart
  • Reddit
  • NVIDIA
  • eBay
  • Roblox
  • Airtable

Why Milvus as a hybrid search vector database

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.

Sparse and dense vectors, one list

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.

Fusion and metadata filtering

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.

Rerank after fusion

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.

Dense vector search (QPS, higher is better)Cohere 1M, 768 dimensions
  • Zilliz CloudZillizCloud-8cu-perf13,316.23 QPSP99 2 ms · recall 0.9383
  • MilvusMilvus-16c64g-sq4u-fp16-force_merge9,575.69 QPSP99 2.3 ms · recall 0.9189

Source: VectorDBBench leaderboard, labels as published. Results depend on configuration; run VectorDBBench on your own data.

BM25 full-text search on MS MARCO (QPS)Zilliz Cloud (managed Milvus), 8 CU performance, built-in BM25, AUTOINDEX, top 100
  • 100K documentstask15919-perf-8cu-oneseg-26-retest / MS MARCO Small12,134.21 QPSP99 3.5 ms
  • 1M documentstask15919-perf-8cu-oneseg-26-retest / MS MARCO Medium9,734.75 QPSP99 5.2 ms
  • 8.8M documentstask15919-perf-8cu-oneseg-26-retest / MS MARCO Large4,252.64 QPSP99 17.1 ms

Source: VectorDBBench FullTextSearch results (GitHub). Serial P99 latency. Results depend on configuration.

Zilliz Cloud runs the same Milvus engine, BM25 included, fully managed.

Try Zilliz Cloud free

Hybrid search vs vector search: the evidence

Microsoft: hybrid beats keyword and vector alone

Azure 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.

  • Customer data, NDCG@3: keyword 40.6, vector 43.8, hybrid 48.4
  • BEIR, NDCG@10: 40.6, 45.0, 48.4
  • MIRACL (18 languages), NDCG@10: 49.6, 58.3, 58.8
  • Hybrid + semantic ranker on customer data: 60.1

Anthropic: 49% fewer failed retrievals

Across codebases, fiction, ArXiv and science papers, combining contextual embeddings with contextual BM25 cut the top-20 failure rate (1 minus recall@20).

  • Embeddings only: 5.7% failed
  • Contextual embeddings + contextual BM25: 2.9%
  • Anthropic reports a 49% reduction

Research: complementary signals

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.

  • Weighted (convex) fusion beat RRF in and out of domain
  • It needs only a small labeled set to tune

Lexical search vs semantic search vs hybrid: when does BM25 beat vector search?

Keyword vs vector vs hybrid search by query type, NDCG@3; Microsoft Azure AI Search (BM25, Ada-002 vectors, RRF hybrid)
Query typeKeyword (BM25)Vector (Ada-002)Hybrid (keyword + vector)
Keyword queries79.211.761.0
Exact snippet search51.141.551.0
Low query/doc term overlap23.036.135.9
Queries with misspellings28.839.140.6
Long queries42.741.648.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).

How do I build hybrid search for RAG?

Hybrid search with BM25 and embeddings: one BM25 Function, then a dense request and a raw-text request fused with 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"],
)

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.

Frequently asked questions

What is hybrid search in a vector database, and how does it work?

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.

Do I still need Elasticsearch for keyword search?

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.

How does reciprocal rank fusion (RRF) work?

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.

How do I tune the balance between keyword and semantic search?

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.

What is the difference between hybrid and vector search, and does hybrid search always outperform vector-only search?

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.

Can I use hybrid search from LangChain or LlamaIndex?

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.

Ship keyword and vector search on one database

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.