AnswersHybrid Search & Ranking

Can You Do Hybrid Search Through the Elasticsearch API?

Can one Elasticsearch search request combine a keyword query with a kNN vector search, how are the two scores combined, and what do you do when you have no embeddings of your own?

5 min readAugust 2026

The short answer

Yes. The search body accepts a query and a knn clause in one request, returns the union of both result sets, and scores each hit as the keyword score plus the vector score, weighted by the boost on each clause. Reciprocal rank fusion replaces the sum when you ask for it.

If you have no embeddings of your own, map the field as semantic_text and use a semantic query, and the engine embeds at index and query time. TopK serves all three shapes through its Elasticsearch-compatible API, with managed embedding behind semantic_text.

How do you send both retrievers in one request?

You put the keyword query under query and the vector search under knn, in the same body. Elasticsearch runs both, combines the matches as a disjunction, and scores each hit as the sum of its keyword score and its vector score, with a boost on either clause weighting its share of the sum (Elastic kNN guide). A document that both retrievers return outranks a document that only one returns.

POST /books/_search
{
"query": { "match": { "body": { "query": "dragons", "boost": 0.9 } } },
"knn": {
"field": "embedding",
"query_vector": [0.12, -0.03, ...],
"k": 20,
"boost": 0.1
},
"size": 10
}

The k on the knn clause is how many vector matches enter the union, and size is how many hits you get back after the scores are combined. TopK's Elasticsearch API keeps the same arithmetic. A document that matches both retrievers scores above one that matches either alone, and raising the knn boost raises the vector share of the score.

Should you add scores or fuse by rank?

Adding scores is a weighted fusion, and it works when the two scores live on scales you understand. A BM25 score is unbounded and a normalized cosine sits between 0 and 1, so the boost values do the calibration, and you should expect to tune them per corpus. Reciprocal rank fusion ignores the score values and combines positions instead, which needs no calibration and is the reason it is the default choice when you have no way to compare the two scales (Elastic RRF reference). Elasticsearch expresses it today as an rrf retriever that wraps a standard and a knn retriever, with rank_window_size setting how many hits each contributes and rank_constant defaulting to 60. TopK's Elasticsearch API accepts the earlier form, a top-level rank block with rrf next to query and knn, which Elasticsearch documented from 8.8 to 8.13, and it does not accept the retriever syntax yet.

Two ways to combine the same two result lists in one request.

query + knn, scores added

Both retrievers run

score = boost × keyword + boost × vector

Boosts calibrate the scales

Right when you can tune the weights

query + knn, RRF

Both retrievers run

Positions fused, scores ignored

No calibration needed

Right when the scales are unknown

Both methods merge two lists that were ranked separately, so a document that sits just outside both top lists never gets ranked on the combined signal. RRF versus true hybrid covers that limit and TopK's measurement of it, where scoring once over the full candidate set improved nDCG@10 by 4.58% on average over RRF across five BEIR datasets (July 2025). Through the Elasticsearch API you get the two fusion methods above, and through TopK's native query you get the single ranking expression.

What if you have no embeddings of your own?

Map the text field as semantic_text and query it with a semantic clause. In Elasticsearch, a semantic_text field is backed by an inference endpoint that embeds the text at index time and the query at search time, so you never handle vectors in your application (Elastic semantic_text reference). A semantic query can stand alone, or it can share a request with a knn clause over a separate dense_vector field, and the two combine the same way a keyword query and knn do.

POST /books/_search
{
"query": { "semantic": { "field": "content", "query": "a quest to destroy a cursed ring" } },
"size": 10
}

On TopK, a semantic clause is a scoring clause, so it is valid under query, must, or should, and a must_not around it returns a 400. Adding a sort on another field returns hits in sort order with a null _score, as Elasticsearch does for any scored query.

On TopK, a semantic_text field is embedded by TopK's managed inference, so the same request works without an inference endpoint of your own. TopK accepts inference_id, search_inference_id, and chunking_settings on the mapping, so an existing mapping loads without edits, and TopK's managed model does the embedding whatever inference_id names.

What does this look like on TopK?

The request is one of the bodies above, sent by the Elasticsearch client you already use, with the endpoint and the API key pointed at TopK.

from elasticsearch import Elasticsearch
es = Elasticsearch("https://<your-topk-es-endpoint>", api_key="<topk-api-key>")
res = es.search(
index="books",
query={"semantic": {"field": "content", "query": "a quest to destroy a cursed ring"}},
knn={"field": "embedding", "query_vector": query_embedding, "k": 20},
size=10,
)

The compatibility overview covers what TopK accepts and rejects, and the kNN guide covers the vector clause on its own. If you want keyword, vector, and multi-vector signals in one ranking expression instead of a fusion of two lists, the native TopK query does that in one line.