Asked in: coding round at recsys, search, and embedding-team interviews.
Find the nearest reference vectors to each query and explain how the search should scale as the collection grows.
Problem
Context. Reference vectors X have shape (N, d); a query has shape (d,), or batched queries have shape (Q, d).
Distance is Euclidean distance over matching numeric features.
Task. Return k original reference-row indices per query with the smallest distances, then compare exact and approximate scaling options.
For reference points [[0, 0], [3, 0], [8, 0]], query [2, 0], and k = 1, return index 1.
Constraints. The provided partition-based sketches assume finite, representable arithmetic and 1 <= k < N.
The selected indices need not be distance-sorted; ties may select any valid neighbors.
Preserve inputs. Approximate search requires an explicit recall target and comparison against exact ground truth.
The naive solution
import numpy as np
def knn_brute(X, query, k):
"""X: (N, d) reference points. query: (d,). Returns top-k indices by L2 distance."""
dists = np.linalg.norm(X - query, axis=1)
return np.argpartition(dists, k)[:k]
Cost: O(N * d) per query, O(N) memory. Fine for N up to ~10K.
For batched queries, vectorize:
def knn_brute_batch(X, queries, k):
"""X: (N, d), queries: (Q, d). Returns (Q, k) of indices."""
dists = np.linalg.norm(X[None, :, :] - queries[:, None, :], axis=2) # (Q, N)
return np.argpartition(dists, k, axis=1)[:, :k]
Better: use the squared-distance trick to avoid the sqrt:
# ||x - q||^2 = ||x||^2 + ||q||^2 - 2 * x.q
def knn_squared(X, queries, k):
X_norm = (X**2).sum(axis=1) # (N,)
Q_norm = (queries**2).sum(axis=1) # (Q,)
dot = queries @ X.T # (Q, N)
sq_dists = X_norm[None, :] + Q_norm[:, None] - 2 * dot
return np.argpartition(sq_dists, k, axis=1)[:, :k]
This is the form used by FAISS’s flat index. Order-of-magnitude faster than the naive form for batched queries because of BLAS-accelerated matmul.
Learning objective: choose an index by separating three decisions: whether approximation is allowed, how to avoid visiting every vector, and whether stored vectors must be compressed.
Learning objective
IVF scans fewer vectors; PQ stores fewer bytes per vector
Derivation
Which change saves representation bytes without reducing scanned-vector count?
What an L5 answer should add
“Brute-force distance evaluation costs O(N * d) per query. Whether it meets the budget depends on hardware, batching, distance, precision, and data movement; a row-count threshold is not a latency guarantee.
Past that, use approximate nearest neighbor (ANN):
- Tree methods (KD-tree, ball tree): work well for low d (up to ~20). Useless for high d (curse of dimensionality).
- HNSW: hierarchical navigable small world graphs. State-of-the-art for moderate-d (~100-1000). Sublinear query time, high recall. Used in Qdrant, Weaviate, pgvector with HNSW.
- IVF (inverted file): cluster the index, search only nearest clusters. Lower recall but very memory efficient. Used in FAISS.
- PQ (product quantization): compress vectors to bytes, do approximate distance computation. Trades recall for memory dramatically.
- Graph search plus quantization: another candidate with build, recall, memory, and update trade-offs; no combination is universally best at a catalog size.
Trade-off knobs: recall vs query latency vs memory vs build time. Tune to your use case.”
What an L6 answer adds
“…practical things:
Preserve the intended score. Nonzero unit-normalized vectors have equivalent cosine, dot-product, and squared-L2 rankings. Normalizing arbitrary item vectors can change an inner-product task; inner-product search does not universally require it. Match query/index transforms and test against an exact top-k oracle with a declared tie policy.
GPU vs CPU. For very high-throughput query workloads, GPU brute-force (FAISS-GPU, ScaNN) is competitive with CPU ANN up to surprisingly large N (millions). For low-throughput or memory-constrained, CPU ANN wins.
Updates are an index contract. Insertion, deletion, compaction, and concurrent-query behavior vary by implementation. For a changing catalog, evaluate freshness and update cost alongside query recall instead of assuming every index is static.
Recall requirements are task-dependent. Set an acceptable miss cost and recall floor from the application; neither a recommendation nor search label determines a universal percentage. Tune index parameters against that floor and measured serving cost.
Don’t roll your own. FAISS, Qdrant, ScaNN, and Annoy cover the design space. Use them. The implementation details (cache-aware data layouts, SIMD-friendly distance computation) are decade-tuned.”
Tells that get you a strong-hire vote
- You name the squared-distance + matmul trick for batched queries.
- You distinguish brute-force regimes from ANN regimes by N.
- You name HNSW, IVF, PQ as the standard ANN families.
- You mention recall vs latency vs memory trade-offs.
- You don’t suggest implementing ANN from scratch.
Tells that get you down-leveled
- Loops over examples in Python (use vectorized ops).
- Computing
sqrtwhen squared distance gives the same ranking. - “Use a KD-tree” for high-dimensional data.
- No knowledge of HNSW.
- Confusing exact KNN with ANN.
Common follow-up
“What if you have a billion vectors?”
The L6 answer:
“Single-machine memory is your floor. A billion 768-dim float32 vectors is 3 TB; doesn’t fit on one machine. Options: (1) Quantize aggressively (INT8 or PQ) to fit in memory. (2) Distribute across machines, query in parallel, merge top-K. (3) Use a hierarchical index where the top-level shards by cluster and only relevant shards are queried (IVF-style). Vespa, ScaNN, and large-scale FAISS deployments handle this. Don’t build it from scratch; the engineering is more involved than the algorithm.”
Related: Two-tower vs cross-encoder: when to use which?, Designing a RAG system that actually works, System design case study: personalized search ranking.