# Hybrid Search (Keyword + Vector) by [kirupa](https://www.kirupa.com/me/index.htm) | filed under [AI Fundamentals](https://www.kirupa.com/ai_fundamentals/index.htm) Jun owns a Trailhead tent. He doesn't remember what it's called, but the tag on the bag says `TR-2041`, so he asks the help assistant: *Does the TR-2041 come with a footprint?* (A footprint is a sheet you put under a tent to protect its floor. Some people call it a ground sheet or a ground cloth, which is going to matter in a second.) The answer lives on the product page for the **Ridgeline 2** tent, model `TR-2041`. In its list of what's in the box, it says ***ground sheet included***. So to find it, a search needs to do two different things at once. It needs to match the code `TR-2041` exactly, character for character. And it needs to understand that ***footprint*** and ***ground sheet*** mean the same thing. Here is the answer Jun is hoping to see: ![Trailhead's help assistant chat window. Jun asks, Does the TR-2041 come with a footprint? The assistant replies, Yes! The Ridgeline 2 (TR-2041) comes with a ground sheet included.](https://www.kirupa.com/ai_fundamentals/images/hybrid_search_chat_footprint.png) As it turns out, the two most common kinds of search are each good at one of those and bad at the other. Hybrid search is what we get when we stop choosing between them. Onwards! ## Two Ways to Search **Keyword search** is the classic kind, the one that has powered search boxes for decades. It looks for documents that contain the same words as the query. The most widely used scoring method is called BM25, and its intuition is pretty friendly: - A document that contains a query word scores higher than one that doesn't. - Rare words count for more than common words. Matching `2041` tells us a lot. Matching ***the*** tells us nothing. - Saying a word more times helps, but with diminishing returns, and long documents don't win just by being long. **Vector search** is the kind we built in [Embeddings](https://www.kirupa.com/ai_fundamentals/embeddings.htm). It turns the query and each document into lists of numbers that capture meaning, then finds documents whose meaning is close to the query's. Here are the two side by side, looking at the same question: ![Jun's question, Does the TR-2041 come with a footprint?, branches to two panels. Keyword search matches the exact words TR-2041 and footprint. Vector search places the question among documents on a map of meaning.](https://www.kirupa.com/ai_fundamentals/images/hybrid_search_two_searches.png) ## Where Each One Stumbles Let's look at what each search returns for Jun's question. These rankings are illustrative, but they're the kind of thing you'd actually see. **Keyword search** returns: 1. TR-2041 pole replacement guide 2. Ridgeline 2 product page (TR-2041) 3. Ridgeline 1 product page (TR-2014) 4. ***What is a tent footprint?*** help article Keyword search loved the code. The pole replacement guide mentions `TR-2041` eight times, so it jumped to the top even though it has nothing to do with footprints. Meanwhile, the Ridgeline 2 page says ***ground sheet***, not ***footprint***, so the only word helping it is the code. **Vector search** returns: 1. ***What is a tent footprint?*** help article 2. Ridgeline 2 product page (TR-2041) 3. Ridgeline 1 product page (TR-2014) 4. Caring for your ground sheet Vector search understood the topic perfectly. Everything it found is about footprints and ground sheets. What it doesn't understand is codes. To an embedding model, `TR-2041` and `TR-2014` are almost the same blob of letters and digits, so the right tent and the wrong tent sit right next to each other in its list. Its top result is a general article that explains footprints but says nothing about Jun's tent. Here are the two lists next to each other, with the names shortened a bit: ![Keyword results in order: Pole guide, Ridgeline 2, Ridgeline 1, Footprint FAQ, with the pole guide marked off topic. Vector results: Footprint FAQ, Ridgeline 2, Ridgeline 1, Sheet care, with Ridgeline 1 marked wrong tent. Ridgeline 2 is highlighted at number 2 in both lists.](https://www.kirupa.com/ai_fundamentals/images/hybrid_search_lists.png) Neither list has the right answer at #1. Look closer, though. The Ridgeline 2 page is #2 in **both** lists. Each search had a different blind spot, and the right answer was the one document that both of them liked. If we could combine the two lists in a way that rewards that agreement, it would float to the top. ## Combining the Lists The obvious idea is to add up the scores. The trouble is that the scores are in completely different units. A BM25 score might be 14.7, and it has no fixed top end. A cosine similarity might be 0.83, and it never goes above 1. Here are the two scores for the Ridgeline 2 page next to each other: ![Two score cards for the Ridgeline 2 page. The keyword score is 14.7, with no upper limit. The vector score is 0.83, never above 1. A plus sign with a red question mark sits between them.](https://www.kirupa.com/ai_fundamentals/images/hybrid_search_scales.png) Adding those is like adding a temperature to a shoe size. We could try to rescale both into the same range, and some systems do, but the rescaling gets fiddly and shifts from one query to the next. A simpler trick is to ignore the scores entirely and use only the **ranks**. This is called **Reciprocal Rank Fusion** (aka RRF), and it comes from a 2009 paper by Gordon Cormack, Charles Clarke, and Stefan Büttcher. For every document, in every list it appears in, we add: ```text 1 / (k + rank) ``` `rank` is the document's position in that list (1, 2, 3, ...), and `k` is a constant that softens the difference between neighboring ranks. The original paper used `k = 60`, and that's still the default in a lot of tools. Let's work through it for Jun's question. The Ridgeline 2 page is #2 in keyword and #2 in vector: ```text 1/(60 + 2) + 1/(60 + 2) = 0.0161 + 0.0161 = 0.0323 ``` The footprint article is #4 in keyword and #1 in vector: ```text 1/(60 + 4) + 1/(60 + 1) = 0.0156 + 0.0164 = 0.0320 ``` The pole guide is #1 in keyword, but it doesn't appear in the vector list at all, so it gets only one term: ```text 1/(60 + 1) = 0.0164 ``` Doing that for every document gives us one merged list: | Rank | Document | RRF score | |---|---|---| | 1 | Ridgeline 2 product page (TR-2041) | 0.0323 | | 2 | ***What is a tent footprint?*** | 0.0320 | | 3 | Ridgeline 1 product page (TR-2014) | 0.0317 | | 4 | TR-2041 pole replacement guide | 0.0164 | | 5 | Caring for your ground sheet | 0.0156 | Here is the same merge as a picture, with the two input lists on the left: ![The keyword and vector lists lead into a fused list. Ridgeline 2 is first with 0.0323, then Footprint FAQ with 0.0320, Ridgeline 1 with 0.0317, Pole guide with 0.0164, and Sheet care with 0.0156.](https://www.kirupa.com/ai_fundamentals/images/hybrid_search_fused.png) The right page wins. The general footprint article comes second, which is actually nice context for the model to have. And the pole guide, which only one search liked, sinks to #4. What's noteworthy is how close the top scores are. RRF with `k = 60` is deliberately gentle. It rewards documents that show up consistently across lists rather than documents that win one list by a mile. That's exactly the behavior we want when each search has a different blind spot. ## When Hybrid Search Earns Its Keep Hybrid search is worth the extra work when your users type things that are exact: - product codes, SKUs, and part numbers - error codes and log messages, like `ERR_CONNECTION_RESET` - names of people, places, and features - jargon or acronyms your embedding model may never have seen It's also worth it when they type things that are fuzzy: synonyms, paraphrases, and questions that describe a problem instead of naming it. Most real search boxes get both kinds, often in the same sentence, like Jun's did. The costs are real but modest. We maintain two indexes instead of one (many vector databases now include keyword search, which helps), and every query runs two searches plus a merge. In return, we stop having to guess which kind of question is coming. ## Conclusion Jun's question needed an exact match on `TR-2041` and a fuzzy match between ***footprint*** and ***ground sheet***. Keyword search handled the first and fumbled the second. Vector search did the reverse. By running both and merging their ranked lists with Reciprocal Rank Fusion, the one page that both searches liked rose to the top. RRF isn't the only way to merge, and a lot of teams eventually tune a weighted blend for their own data. But it needs no training, no score rescaling, and only one constant, which makes it a great place to start. There's one thing hybrid search still can't do. If Trailhead sells the Ridgeline 2 in both the US and Canada, with different accessories in each box, both searches will happily return both pages. Picking the right one is a job for [Metadata Filtering](https://www.kirupa.com/ai_fundamentals/metadata_filtering.htm). And once we have a decent shortlist, we can make the order even sharper with [Reranking](https://www.kirupa.com/ai_fundamentals/reranking.htm).