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.
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 Elasticsearches = 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.