The idea: two ways to find a document, and a way to merge them

A search box, or the retrieval step of a RAG system, has to turn a short query into the few documents that answer it. There are two classic ways. Keyword search with BM25 looks for the query's words in the documents. Dense vector search turns the query and the documents into embedding vectors and looks for the closest ones, so it matches meaning instead of spelling. Hybrid search runs both and merges the two ranked lists, most often with reciprocal rank fusion (RRF).

The query goes two ways. Top: tokenize, look up the inverted index, score with BM25, ranked list 1. Bottom: the embedding model makes a query vector, cosine similarity against the document vectors, ranked list 2. Both lists go into reciprocal rank fusion, 1 over 60 plus rank, which outputs the hybrid top 3
One query, two retrievers, and RRF merges their ranked lists by rank alone.

The page has a help centre of 8 one-line articles (d1 to d8) and five queries. Pick a query and press Search: the query runs through BM25 (blue), then vector search (purple), then RRF (yellow). The article that really answers the query turns green, and each result box says whether its list found it at #1, lower down, or not at all.

BM25: words, rare words, and length

BM25 is the default ranking of Lucene, Elasticsearch, OpenSearch and Solr. The query is tokenized the same way as the documents: lowercase, punctuation removed, stop words such as "my", "how", "the" dropped. An inverted index maps each term to the documents that contain it, so only those documents are scored at all (the page "Inverted Index" shows how that index is built). Each matching term adds:

idf(t) · tf · (k1 + 1) / (tf + k1 · (1 − b + b · |d| / avgdl))
idf(t) = ln(1 + (N − df + 0.5) / (df + 0.5))
  • idf: a term that few documents contain is worth more. With N = 8, a term in 1 article has idf 1.79; a term in no article has 2.89 but matches nothing.
  • tf saturation (k1 = 1.2): the second and third time a term appears count less than the first, so repeating a word does not win.
  • length normalisation (b = 0.75): a match in a short document counts more than the same match in a long one. In Demo 4 "phone" in d5 (7 terms) beats "battery" in d6 (10 terms), 1.87 to 1.60, though both terms have the same idf.

A document with none of the query's terms scores exactly 0 and is not returned. That is BM25's strength (it never returns something unrelated) and its weakness (it cannot find "end your subscription" for "cancel my plan").

Vector search: embeddings and cosine

An embedding model maps a text to a vector so that texts with similar meaning point in similar directions. Search computes the cosine similarity q·d / (|q| |d|) between the query vector and every document vector and keeps the k nearest. Real models have hundreds of unlabelled dimensions; the page uses 8 named ones (account, billing, cancel, problem, device, power, startup, shipping) and counts the words of each topic, so you can read the vectors. In Demo 3 "how do I cancel my plan" becomes billing 1, cancel 1, and d2 "To end your subscription … Stop membership" has billing 3, cancel 2: cosine 0.98.

A real vector database does not compare against every document: it uses an approximate nearest-neighbour index such as HNSW (see HNSW Vector Search). The ranking idea is the same.

Where each one fails

query typeBM25vector searchdemo
same words as the answer✓✓Demo 1
exact code, ID, product name, rare term✓ high idf✗ the model has no meaning for "E1042"Demo 2
synonyms, paraphrase, other wording✗ no shared word, score 0✓Demo 3
half right eachanswer at #2answer at #2Demo 4: RRF puts it first
nothing in the corpus answers it✓ returns nothing✗ always returns k results, even at cosine 1.00Demo 5
Two query examples side by side. Left: 'E1042 error at startup'. BM25 matches the rare term E1042 in d4 and ranks it first. The embedding ignores E1042 and ranks d5 'app crashes at boot' first. Right: 'how do I cancel my plan'. BM25 only finds 'plan' in the pricing page d3 and never sees d2. The embedding maps cancel to end and stop, and ranks d2 first
BM25 needs the same token; vectors need a token they have meaning for. Each one's blind spot is the other's strength.

Reciprocal rank fusion

The two scores cannot be added: BM25 is unbounded (4.40 for d1 in Demo 1) and cosine is between −1 and 1. RRF (Cormack, Clarke and Büttcher, 2009) throws the scores away and keeps only the ranks:

RRF(d) = Σ over lists  1 / (k + rank of d in that list),   k = 60

A document missing from a list adds nothing for it. The constant k = 60 flattens the curve, so #1 is worth only a little more than #2 (1/61 = 0.0164 vs 1/62 = 0.0161) and being in both lists matters most. In Demo 4 the answer d6 is #2 in both lists: 1/62 + 1/62 = 0.0323. BM25's #1, d5, is #4 for vectors: 1/61 + 1/64 = 0.0320. Vectors' #1, d7, is not in the BM25 list: 0.0164. The document both retrievers agree on wins.

The same rule works against you in Demo 3: the pricing page d3 is BM25's only match and vector #2, so it beats the real answer d2, which only vectors found. Hybrid keeps d2 in the top 3, but someone still has to pick it. Production systems add a reranker (a cross-encoder that reads the query and each candidate together) over the top 20–50 fused results, or let the LLM read all of them.

Hybrid search in practice

systemhow hybrid is done
Elasticsearch / OpenSearcha bool or hybrid query with a match and a knn clause, fused by the rrf retriever or a normalisation processor
PostgreSQLfull-text search (tsvector, ts_rank) plus pgvector, fused in SQL with 1/(60 + rank)
vector databases (Weaviate, Qdrant, Milvus, Pinecone)sparse BM25-style vectors next to dense ones, fused by RRF or a weighted sum

Hybrid search is the usual first fix when a RAG system misses facts: see Retrieval-Augmented Generation, where retrieval is vectors only and the warranty question can lose to an off-topic marketing chunk.

See also Word2Vec: where embedding vectors come from (in the vector-search section).

What the page leaves out

A learned embedding model (8 hand-made dimensions here), subword tokenizers and stemming ("refunds" vs "refund"), BM25F field weights, approximate nearest-neighbour indexes, weighted-sum fusion with score normalisation, learned sparse models such as SPLADE, rerankers, query rewriting, and how to measure retrieval quality over many queries (recall@k, MRR, nDCG).