Lexical search (BM25-style text matching) and vector search (nearest neighbours over embeddings) fail differently. Lexical search is precise on exact terms — a SKU, a brand name, a model number — but blind to synonyms and paraphrase. Vector search understands "warm winter jacket" is close to "insulated parka," but it can rank a document with none of the query's actual words above one that mentions all of them.
Hybrid search runs both and merges the results. The interesting engineering problem is how you merge two ranked lists whose scores live on completely different, incomparable scales.
Why you can't just average the scores
A lexical text-match score and a cosine similarity score are not the same unit. One is roughly [0, very large] and depends on term frequency and document length; the other is bounded [-1, 1] and depends on embedding geometry. Averaging them, or even min-max normalizing each list separately and averaging, tends to let whichever retriever happens to produce a wider score spread dominate the merged ranking — regardless of which retriever actually found the better result.
Reciprocal rank fusion
Reciprocal rank fusion (RRF) sidesteps the scale problem entirely by ignoring the scores and using only the rank of each document within its own list:
For each retriever i that returned document d, take 1 / (k + rank) — a document at rank 1 contributes far more than one at rank 50 — and sum across retrievers. k (a constant, typically 60) softens how much rank-1 dominates over rank-2; without it, a document at rank 1 in one list would completely swamp a document that ranked well in every list. A document that ranks well in both the lexical and vector lists — even at rank 5 in each, not rank 1 — often ends up outranking a document that's rank 1 in only one list.
The result: no score normalization, no per-retriever calibration, and a merge that rewards agreement between retrievers, which correlates well with actual relevance in practice.
Weighted variants
Plain RRF treats every retriever equally. AACSearch's fusion layer also supports weighted RRF, where each retriever's contribution is scaled before summing — useful when you know, for a given query shape, that vector search is more reliable (long natural-language queries) or lexical is more reliable (short, code/SKU-like queries). A small classifier looks at query features — token count, presence of digits, script — and picks retrieval weights accordingly, rather than using one fixed blend for every query.
Where this runs
The fusion layer merges query rewrite variants during zero-hit rescue, and the same reciprocal-rank-fusion code powers multi-query retrieval in the AI assistant, where several reformulated queries are searched in parallel and their results merged into one candidate set before an answer is generated. One fusion implementation, several call sites — one bug fix or improvement benefits every retrieval path that uses it.
The takeaway
If you're building hybrid search yourself: don't average scores across retrievers with different scales. Rank-based fusion is simpler to implement, has no calibration to maintain, and degrades gracefully when one retriever returns nothing at all.
Related Posts
Faceted search that scales to millions of SKUs
Why facet counts get slow as a catalogue grows, the difference between an exact and an estimated count, and how to keep faceted navigation fast without hiding filters shoppers need.
Webhook delivery: signatures, retries and safe replays
How AACSearch signs outbound webhooks, what happens when your endpoint is briefly down, and how an Idempotency-Key keeps a retried write from landing twice.
Multi-tenant search architecture: how tenant isolation works in AACSearch
Collection namespacing on the engine proxy, full versus search key scopes, short-lived browser keys, per-organization quotas in a Durable Object and organization-scoped events — what a platform operator gets when embedding search for many customers.