NEWVectors or files. Pick a path.Start →
    Vector Search
    8 min read
    Updated 2026-11-09

    Why Does My Search Slow Down When I Combine Keyword and Vector Search?

    A hybrid query runs a keyword search and a vector search and then merges them, so it costs more than either one. When it costs far more, the searches are usually running one after another, each fetching a deep candidate list, with a filter checked hit by hit. How to find which of the three it is, the fixes in Elasticsearch, OpenSearch and vector stores, and our own before and after numbers at 2.65 million rows.

    Ethan Steininger
    By , Founder & CEO, Mixpeek
    Hybrid Search
    BM25
    Vector Search
    Filtered Search
    Performance
    MVS

    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?



    StepSlow versionFast version
    Running the searchesOne after another: total time is the sum of every searchAt the same time: total time is close to the slowest search
    Candidate depthEach search returns thousands of candidates "to be safe"Each returns what the merge can use, often a few hundred
    Filter on the keyword sideRank first, then read each hit's fields to test the filter, and search again deeper when too few passRestrict the search to the rows the filter allows, then rank them once
    Filter on the vector sideRebuild the set of allowed rows on every queryRead the allowed rows from an index the store already keeps
    MergeNormalize and sort every candidate from every searchRank 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?



  1. Run the searches concurrently. In application code, issue both requests together and merge when
  2. both return. Inside one engine, use its native hybrid query: Elasticsearch's RRF retriever runs its sub-searches independently and merges them, and OpenSearch's [hybrid query](https://docs.opensearch.org/latest/query-dsl/compound/hybrid/) combines them in one request.
  3. Size the candidate depth from measurement. Keep rank_window_size and num_candidates at the
  4. smallest values where recall on your test queries stops improving.
  5. Filter before ranking. Put filters in filter context, or use a store that restricts both the vector
  6. and the keyword search to the filter's rows.
  7. Check the answers. A faster path should return the same documents in the same order. Compare a
  8. sample of real queries before and after; tied keyword scores can reorder at the cut, and that is the only difference you should accept.

    What 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)BeforeAfter
    Vector search, top 5,0001,324 ms75 ms
    Keyword search, top 200about 650 msabout 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.
    Managed Mixpeek

    Put multimodal search to work

    Connect a bucket and Mixpeek runs the whole multimodal search pipeline for you: extraction, indexing, and search over your own objects. No models to wire up, nothing to host.

    Start with Managed
    MVS · bring your own

    Already have vectors?

    Keep your embeddings on your own cloud and run dense, sparse, and BM25 search directly on object storage. From $25/mo.

    Start with MVS

    Hybrid search whose legs run at the same time

    MVS, Mixpeek's vector store on object storage, runs a hybrid search's vector and keyword legs concurrently and applies the filter before ranking, so a filtered hybrid query costs about its slowest leg.

    See MVSRead the changelog