ef_search bounds the dynamic candidate list maintained during the HNSW greedy walk at the base layer. Larger = wider exploration = higher recall + more work per query. Set per query at runtime; typical range 40-500.
Imagine HNSW like a hiker hopping between viewpoints, always heading to whichever nearby viewpoint looks best. When HNSW searches for the closest vectors, it walks a graph by always jumping to the most promising node it can see. **ef_search is the size of its 'shortlist' of promising candidates to keep around.** A bigger shortlist means it considers more possibilities before settling, finding better answers but doing more work. A smaller shortlist is fast but sometimes commits to a so-so answer too early. You can change this number for every query: tiny number for cheap browsing, big number for important retrievals.
Concept explanation~2 min read
Everything you need to truly understand this topic: intuition, mechanics, step by step explanation, code, formulas, and worked example. Click to expand.
Concept explanation~2 min read
Everything you need to truly understand this topic: intuition, mechanics, step by step explanation, code, formulas, and worked example. Click to expand.
ef_search is the most-touched HNSW knob in production because it is the only one set at query time. Understanding it well means understanding the HNSW search algorithm well; understanding it poorly means treating it as a magic number to be tweaked until recall numbers move. The interview signal is in the mechanism description.
The HNSW search and what ef_search bounds
HNSW search has two phases. First, an upper-layer descent finds an entry point near the query at the base layer. This phase is cheap (logarithmic in corpus size) and ef_search has no role here. Second, the base-layer search performs a bounded best-first walk.
The base-layer algorithm. Maintain a min-heap of unvisited candidates (ordered by distance to query) and a bounded max-heap of the top-K results found so far. At each step: pop the closest unvisited candidate, check whether it can still beat the current K-th best, examine its M graph neighbors, evaluate distances, update both heaps. Terminate when the closest unvisited candidate is farther than the K-th best.
Where ef_search bounds. ef_search is the maximum size of the candidate min-heap. When new candidates are added and the heap exceeds ef_search, the worst entries are dropped. This bounding is what trades recall for latency: a larger heap explores more of the graph; a smaller heap is faster but more prone to local minimum termination.
Situations where this technique stops working.
2–4 min · Everything important, quickly.
| Knob | Phase | Tunable per query | Affects recall | Affects build cost | Affects per query latency |
|---|---|---|---|---|---|
| M | Build | No (rebuild required) | Yes (ceiling) | Yes (significant) | Modest |
| ef_construction | Build | No (rebuild required) | Yes (ceiling) | Yes (significant) | No |
| ef_search | Query | Yes | Yes (within ceiling) | No | Yes (linear) |
Real products, models, and research that use this idea.
- pgvector exposes ef_search via SET hnsw.ef_search = N per session; production RAG deployments commonly use 40-160.
- Qdrant lets ef_search be overridden per query, supporting tier based adaptive retrieval.
What an interviewer would ask next. Try answering before peeking at the approach.
QWhat is the right ratio between ef_search and ef_construction?
ef_construction is usually 2-4x ef_search at production. ef_construction sets the build time recall ceiling; ef_search reaches toward that ceiling. Setting ef_construction too low caps the achievable recall regardless of ef_search.
Red flags & common mistakes
The phrases that signal junior thinking. Click to expand.
Red flags & common mistakes
The phrases that signal junior thinking. Click to expand.
Conflating ef_search with M or ef_construction. M and ef_construction are build time, baked into the index. ef_search is runtime tunable per query; changing it does not require a rebuild.
60 second bullets to scan on the way to the call.
ef_search bounds the dynamic candidate list at the base-layer greedy walk
Set per query at runtime, no rebuild required
Primary sources. Browse if you want the original framing.
Same topic, related formats. Practice these next.