Why does my search slow down when I combine keyword and vector search?
A combined search, usually called hybrid search, runs a keyword search (BM25) and a vector search over the same data and merges the two ranked lists. It will always cost a little more than either search alone. When it costs several times more, three causes cover most cases: the searches run one after another, so you pay the sum of their times; each one fetches a deep list of candidates for the merge; and a filter is checked on each hit after ranking, which makes the keyword side search deeper and deeper to fill its quota. Run the searches at the same time, size the candidate lists to what the merge needs, and apply the filter before ranking.
Where does the time go in a hybrid query?
| Step | Slow version | Fast version |
|---|---|---|
| Running the searches | One after another: total time is the sum of every search | At the same time: total time is close to the slowest search |
| Candidate depth | Each search returns thousands of candidates "to be safe" | Each returns what the merge can use, often a few hundred |
| Filter on the keyword side | Rank first, then read each hit's fields to test the filter, and search again deeper when too few pass | Restrict the search to the rows the filter allows, then rank them once |
| Filter on the vector side | Rebuild the set of allowed rows on every query | Read the allowed rows from an index the store already keeps |
| Merge | Normalize and sort every candidate from every search | Rank fusion over the top of each list |
The merge itself is rarely the problem. Reciprocal Rank Fusion only reads ranks, so combining a few hundred candidates from each side takes microseconds. The time goes into the searches that feed it.
Why are the searches running one after another?
Many hybrid implementations are a loop: for each search in the query, run it and wait. Three searches of 1 second each then take 3 seconds even though the machine could have run them together. This is common in application code that calls a keyword engine and a vector database in turn, and it can also sit inside a database's own hybrid path. Two searches that do not depend on each other can always run at the same time, and the merge waits for both.
Why does the candidate list need to be so deep?
Fusion can only rank documents that some search returned, so each search has to return more than the final page. Elasticsearch's RRF retriever calls this [
rank_window_size](https://www.elastic.co/docs/reference/elasticsearch/rest-apis/reciprocal-rank-fusion),
the size of each search's result set, and its documentation says a higher value "will improve result
relevance at the cost of performance". A kNN search adds its own depth, num_candidates, the number
of candidates it considers per shard. Setting both to thousands for a page of 10 results multiplies the
work. Measure recall at a few depths on your own queries and keep the smallest depth where it stops
improving.Why does a filter make the keyword side so slow?
A keyword index ranks every document that contains the words. If the filter is tested afterwards, on each hit, the search has to read hits until enough of them pass. With a narrow filter most hits fail, so the engine asks for more hits and starts again, often doubling the depth each round. The cost then grows with how many documents contain the words across the whole collection, and the filter does little to reduce it.
Search engines avoid this by applying the filter as a clause that restricts the candidates without scoring them. In Elasticsearch that is the filter context of a bool query. A vector store can do the same: resolve the filter's rows from its payload index, then rank only those rows.
How do I find which one it is?
1. Time each search alone, with the same filter. If the hybrid query takes about the sum of the parts, the searches are running one after another. 2. Lower the candidate depth by half and time it again. If the time drops by about half, depth is the cost. 3. Run the keyword search with and without the filter. If the filtered one is much slower, the filter is being checked per hit. 4. Read the profile:
"profile": true in Elasticsearch
(search profile API)
or OpenSearch shows the time per query clause.How do I fix it?
hybrid query](https://docs.opensearch.org/latest/query-dsl/compound/hybrid/) combines them in
one request.
rank_window_size and num_candidates at theWhat did this look like in our own vector store?
MVS, the vector store behind Mixpeek, had all three causes. A hybrid search awaited each leg in turn, the keyword legs read each hit's payload to test the filter and reran as they searched deeper, and the vector leg rebuilt its set of allowed rows on every call. Since 8 November 2026 the legs run at the same time, up to eight at once, and both kinds of leg take their allowed rows from the filter plan the store already builds for counts and facets. A keyword search whose filter matches up to 25,000 rows ranks them in one pass.
| Benchmark at 2.65 million rows, eight filters (median) | Before | After |
|---|---|---|
| Vector search, top 5,000 | 1,324 ms | 75 ms |
| Keyword search, top 200 | about 650 ms | about 230 ms |
The benchmark ran on a shared machine under load, 20 queries per row. The results were the same hits in the same order, except that keyword hits tied on score at the cut can come back in a different order, as they already did between two builds of the same index. The change is in the changelog for 8 November 2026. MVS keeps vectors and payloads on object storage, S3 or GCS. In a Mixpeek retriever, a feature_search stage lists one search per feature and a fusion method: rrf, dbsf, weighted, max or learned.
Related: what hybrid search is and how to fuse the rankings, why search gets slower as the collection grows, even with a narrow filter and the best hybrid search engines.
Frequently Asked Questions
Is hybrid search always slower than vector search alone?
It does more work, so it costs at least as much as its slowest search plus a merge that takes microseconds. With the searches running at the same time and filters applied before ranking, that difference is usually small. Much larger differences point to one of the three causes above.
What should rank_window_size be in Elasticsearch RRF?
It defaults to the request's size and must be at least that. Elastic's documentation says a higher value improves relevance at the cost of performance, so measure recall on your own queries at a few values, for example 50, 100 and 200, and keep the smallest one where recall stops rising.
Why is my filtered keyword search slower than my unfiltered one?
The filter is probably tested on each hit after ranking, so the engine reads hits until enough pass and searches again deeper when they do not. Apply the filter as a non-scoring clause, such as filter context in Elasticsearch, so the search only ranks the rows it allows.
Does running the searches concurrently change the results?
It should not. The merge reads the same lists in the same order whichever search finished first. Check it on a sample of real queries before you switch, as we did on 260 random mixes of searches before shipping ours.