In an Elasticsearch knn search, how do k and num_candidates differ?
answer
- One is a result size, one is effort
- Think 'how many' versus 'how hard'
- The expensive one multiplies by shard count
- Approximate search can miss true neighbours
- Tune it against an exact brute-force baseline
basics
~20 sk is how many nearest neighbours the search returns; num_candidates is how many candidates each shard explores in the HNSW graph before picking its best k. Raising num_candidates buys recall at the cost of latency.
solid answer
~50 sThey are the output size and the search effort. **k** is the number of neighbours you want back. **num_candidates** is the size of the candidate queue each shard keeps while walking the HNSW graph — the shard explores up to that many vectors, keeps its top `k`, and the coordinating node merges the per-shard lists into a final `k`. Because HNSW is approximate, a small `num_candidates` can miss true neighbours entirely; enlarging it makes the walk explore more of the graph and pushes recall toward exhaustive search, at proportionally more CPU and latency. `num_candidates` must be at least `k` and is capped (10,000), and the cost is per shard, so a 10-shard index does ten times the work. The tuning method is to measure recall against an exact brute-force baseline and raise `num_candidates` only until recall plateaus.
code
json · 10 linesPOST /products/_search
{
"size": 10,
"knn": {
"field": "desc_embedding",
"query_vector": [0.31, 0.02, -0.77],
"k": 10,
"num_candidates": 200
}
}go deeper
Remember which one you get back: k is the result count, num_candidates is the effort. Know that num_candidates must be at least as large as k.
Explain the bounded-queue graph walk, why the search is approximate, and that num_candidates is spent per shard while the returned hits are the merged global k.
Demonstrate the tuning loop: measure recall@k against an exact baseline, plot it against p99 latency, and pick the elbow. Connect over-sharding to multiplied vector-search cost.
Own the recall service level: what recall the product actually needs, how it is monitored as the corpus grows, and when the answer is a different index configuration rather than a bigger candidate queue.
## The two numbers do different jobs A `knn` search in Elasticsearch carries two integers that beginners routinely conflate. `k` is a result-set size: how many nearest neighbours you want. `num_candidates` is a search-effort dial: how hard each shard works to find them. Confusing them produces either a slow search that returns ten documents at the cost of an exhaustive scan, or a fast search that quietly misses the best answers. ## How approximate search actually runs Elasticsearch stores vectors in an HNSW graph — a layered proximity graph where each vector is linked to its near neighbours. Searching means entering the graph at an upper layer, greedily walking toward the query vector, and descending. The walk keeps a bounded priority queue of the best candidates seen so far; `num_candidates` is that bound. When the queue stops improving, the walk stops and the top `k` entries are returned. Because the walk is greedy over a bounded queue, it is *approximate*: a true nearest neighbour sitting in a poorly connected part of the graph can be missed. A larger queue means the walk keeps more plausible directions alive and backtracks more, so it finds more of the true top-k. In the limit, a queue as large as the segment is exhaustive search. ## The per-shard multiplier This is the detail interviewers probe. The graph walk happens **per shard** (in fact per segment, with results combined within the shard). Each shard explores up to `num_candidates` vectors and returns its own top `k`. The coordinating node then merges those lists and keeps the global top `k`. So: - Total work scales with `num_candidates × number of shards`, not with `k`. - Returned hits are `k`, not `k × shards`. - A heavily over-sharded index makes vector search expensive without making it better, because each small shard pays the full traversal cost. A useful corollary: adding shards tends to *raise* recall slightly, since each shard independently searches its own smaller graph, but it raises cost faster. Vector-heavy indices generally want fewer, larger shards than the reflexes learned from log indices suggest. ## Constraints and defaults `num_candidates` must be greater than or equal to `k`; asking for 100 neighbours out of a 50-candidate queue is rejected. It is also capped at 10,000 per shard, which is the ceiling on how much recall you can buy by turning this one dial. In recent versions `k` may be omitted, in which case it follows the request's `size`. Do not rely on a default for `num_candidates` — set it explicitly so that recall is a decision rather than an accident. ## Tuning method The honest way to pick `num_candidates` is measurement, not folklore: 1. Build a query set that resembles production traffic. 2. Compute exact ground truth once — brute-force the same vectors with `script_score` and vector functions, or an offline exact search. 3. For each `num_candidates` value, compute recall@k: the fraction of the true top-k that the approximate search returned. 4. Plot recall against p99 latency. Recall climbs steeply and then flattens; the elbow is your setting. If recall is still poor at the ceiling, `num_candidates` is the wrong dial. Raise `m` in `index_options` so the graph is better connected, raise `ef_construction` so it is better built, or reconsider quantization if a lossy vector representation is blurring near neighbours. ## Interactions worth knowing With a `filter` on the kNN search, candidates that fail the filter do not count toward your top-k, so filtered searches typically need a larger `num_candidates` to reach the same recall. When kNN is one leg of a hybrid retrieval, the kNN leg's `k` sets how many documents that leg can contribute to fusion — too small a `k` and a good lexical match never gets a vector-side rank to fuse with. And when kNN is expressed as a `knn` query inside the query DSL rather than as the top-level search option, the number of returned documents follows the surrounding request's `size` rather than a `k` you set on the clause, which is a common source of confusion when porting a request between the two forms. ## The short version for an interview `k` is what you ask for; `num_candidates` is what you pay for it; the payment is per shard; and the only defensible way to set it is to measure recall against exact search.
- Recall is still too low at the maximum num_candidates. What do you change next?The graph itself, not the query. Raise `m` in `index_options` so each node has more connections and the walk has more routes to the true neighbours, and raise `ef_construction` so the graph is built with better links in the first place. Both make indexing slower and the graph larger. If you are on aggressive quantization, also test a less lossy option — the loss may be in the vector representation rather than the traversal.
- Does adding shards to a vector index make kNN faster or slower?Each shard searches its own smaller graph in parallel, so wall-clock latency can improve, but total cluster work rises because every shard pays a full `num_candidates` traversal and the coordinating node merges more lists. Vector indices generally want fewer, larger shards than log-style indices; over-sharding multiplies the cost of every kNN request for no relevance gain.
- Why can a filtered kNN search need a larger num_candidates than an unfiltered one?Candidates that fail the filter cannot fill your top-k, so the traversal must find enough *matching* neighbours within its budget. When the filter is selective, a queue sized for the unfiltered case exhausts itself on non-matching regions of the graph and recall drops. Raise `num_candidates` for filtered traffic and re-measure recall separately from the unfiltered case.
k is how many candidates you plan to hire; num_candidates is how many CVs you agree to read first. Read more and you are likelier to find the genuinely best ten, but the screening takes proportionally longer.
saying these in an interview costs you the question
- Thinks num_candidates is the number of results returned
- Believes k results come back from every shard
- Assumes kNN is exact and cannot miss neighbours
- Sets num_candidates by guesswork instead of a recall measurement
- Says raising k improves relevance quality