The trilemma — pick two
Recall, latency, and memory form a trilemma: Flat maxes recall, HNSW maxes speed-at-recall but eats RAM, IVFPQ maxes memory savings but sacrifices recall — no index wins all three, so every production decision is just choosing a point on this Pareto frontier.
Four indexes, three axes, one chart — and the picture is unambiguous: no index is best at recall AND latency AND memory at once. Choosing an index is choosing which axis to sacrifice. With the index-choice arc complete, we now leave the clean geometry behind and hit where production actually gets hard: real queries aren't just 'similar to X'.
Scene 10
The trilemma — pick two
- Watch
- Try it
- Predict
- Capture
Every dot the arc dropped, finally on one chart. Flat sits top-left: recall 1.0 — but it's slow and its bubble is huge (it keeps every full vector). HNSW is fast and high-recall, but its bubble is just as big — it stores full vectors PLUS a graph of edges. IVFPQ has a tiny bubble (compressed) and is fast, but it sits lower — it gave up recall. IVF lands in the middle. Look across all three axes at once: no dot is high AND far-left AND small. The curve through the best dots is the frontier — the boundary of what any index can reach. Nothing lives above-and-left-of it.
Highlighted lines are the ones running in the diagram right now.
def rank_exact(query, candidates):scored = []for v in candidates: # full float32 vectorsd = l2(query, v.full) # no approximationscored.append((d, v.id))return topk(scored) # recall ceiling = Flat# re-rank: rescue recall an approx index concededapprox = index.search(query, k * 10)return rank_exact(query, approx) # exact, but slower + holds vectors
def search_fast(query, nprobe, ef_search):cells = nearest_centroids(query, nprobe) # IVF: scan few cellscand = scan(cells)# OR HNSW greedy walk: enter top layer, hop downcand = greedy_walk(query, ef_search)return topk(cand)# tighter nprobe / ef_search = faster, lossier:# a true neighbor across an unprobed boundary is missed
def encode(v, m, codebooks): # split into m subvectorsreturn bytes(nearest_centroid_id(sub, codebooks[i]) # 1 byte eachfor i, sub in enumerate(split(v, m))) # 6144 bytes -> m bytesdef adc(query, code): # query stays full-precisionreturn sum(dist_table[i][code[i]] for i in range(m))# lossy: estimated distance, so recall sags vs Flat
Where this sits in Build a vector database (Pinecone / Weaviate / pgvector style)
Scene 10 of 15, in the Compress & pick act — PQ shrinks 6 KB to 96 bytes, IVFPQ combines both, and the trilemma forces a choice.. Recall, latency, memory: Flat maxes recall, HNSW speed-at-recall, IVFPQ memory — no index wins all three. Every deployment is a chosen point on the Pareto frontier.
Up next. Every query so far was pure similarity. But real users ask 'similar to this song AND released after 2020 AND under 3 minutes'. Bolting a metadata filter onto vector search sounds trivial — and it quietly breaks both obvious ways of doing it. Let's see how.
All 15 scenes in Build a vector database (Pinecone / Weaviate / pgvector style) · Every curriculum