Everyone spent the past year swapping traditional search indexes for vector databases and embeddings. It demos beautifully. Then you ship a RAG (Retrieval-Augmented Generation) pipeline to production and hit the wall that nobody puts in the demo, which is where hybrid search stops being optional.
I have watched this play out with several clients. They move to pure semantic search, and their customers stop being able to find a part number, a SKU or a technical brand name. The vector search decides a similar description is close enough. For the person searching, close enough is a failure. Hybrid search closes that gap between meaning and exact matches.
The keyword gap in semantic search
Semantic search reads intent well. Search for “waterproof running gear” and an embedding-based index will surface “rainproof jogging jackets” because the meanings sit close together. What it loses is precision, or what I call precision drift: turning text into high-dimensional vectors flattens the signal of rare, specific keywords, and those are usually the ones that decide whether a result is right.
That is a real problem in vector search optimization. Lean on similarity alone and a unique technical term gets buried under a pile of semantically similar noise. The fix is to bring old-school keyword matching back into the pipeline, with a better scoring function behind it: BM25.
Why BM25 beats TF-IDF
Most developers know TF-IDF (Term Frequency-Inverse Document Frequency). The idea is simple: a word that shows up often in this document and rarely across the whole collection matters. The weakness is that it scores linearly. In a long document, a keyword appearing 100 times scores far higher than one appearing 10 times, even when the relevance did not grow anything like that much.
BM25 (Best Matching 25) handles that with a saturation curve. Two parameters do most of the work:
- k1 sets how fast the reward for a repeated word tapers off, so the 50th occurrence of a term does not weigh as much as the first few.
- b penalizes long documents that simply happen to contain more words, so a 5,000-word blog post does not automatically outrank a 100-word product description.
For the math itself, I point colleagues at the official Okapi BM25 documentation or the Vespa implementation guides. Dense reading, and it is the foundation of most modern relevance tuning.
Putting hybrid search together
In a real application you do not pick between vector search and keyword search. You run both. The vector store answers for meaning, the keyword index (Elasticsearch, Meilisearch, whatever you already run) answers for exact terms, and rank fusion merges the two result sets into one ranking.
Here is roughly how I structure that retrieval logic on the backend:
<?php
/**
* Conceptual Hybrid Search Retrieval
* Prefixed with bbioon_ for safety
*/
function bbioon_get_hybrid_search_results($query) {
// 1. Fetch semantic results (Embeddings)
$vector_results = bbioon_vector_store_query($query, ['limit' => 5]);
// 2. Fetch keyword results (BM25 / Keyword)
$keyword_results = bbioon_bm25_index_query($query, ['limit' => 5]);
// 3. Merge and deduplicate
$combined = array_merge($vector_results, $keyword_results);
// 4. Perform Rank Fusion (simplified)
return bbioon_apply_reciprocal_rank_fusion($combined);
}
Tuning the weights
The weights are where this gets interesting. Technical documentation usually wants keyword matching to carry more of the score, while customer support queries do better leaning on semantic similarity. Finding those numbers for your own data is the actual dev work.
If this kind of retrieval work is eating your dev hours, hand it to me. I have been wrestling with WordPress since the 4.x days.
Fix retrieval, not the model
Stop trying to make embeddings do everything. A RAG pipeline that cannot find a specific term does not need a bigger model, it needs a better retrieval strategy. Pair the semantic reach of vectors with the arithmetic precision of BM25 and you get search that people are willing to trust. If yours is still pure vector, that is the refactor I would put next on the list.