Runbooks/ RAG Runbook Track B · Embeddings and index Deep readQuick learn
  1. 00 Start
  2. /
  3. 01 Chunking
  4. 02 Parsing
  5. 03 Identity
  6. 04 Access
  7. /
  8. 05 Models
  9. 06 Vectors
  10. 07 Limits
  11. 08 Model ops
  12. 09 Index I
  13. 10 Index II
  14. 11 Tuning
  15. 12 Capacity
  16. /
  17. 13 Sharding
  18. 14 Filtering
  19. 15 Hybrid
  20. /
  21. 16 Proof
Document 09 · Quick learn Bandwidth, not maths · Regions or routes · Recall or the number means nothing

Three waysto not look at everything

The baseline that is not an index, the one that divides the space, and the one that walks a graph — with the arithmetic that decides which of them you should be running.

“You can always be fast if you are allowed to be wrong.”
61 ms
flat, 1M × 768-d — and cores do not help
1.7%
of the corpus scanned at recall 0.94
25×
HNSW’s index overhead over IVF’s
efSearch > k
the one rule
Flat searchthe baseline that is not an index

Compare the query against every vector, return the exact top k. No structure, no build step, no parameters, no approximation. It is the thing every other index is trying to avoid doing — and understanding precisely why it is slow is what makes the rest make sense.

Worked1M × 768-d fp32 = 3.07 GB scanned · at ~50 GB/s that is ~61 ms · ~16 QPS per nodeit is a bandwidth problem, not a maths problem
Why adding cores does not fix it

Bandwidth is a socket-level shared resource: a second concurrent query does not get its own bus, it competes for the same one. A 64-core machine gives roughly the same flat throughput as an 8-core one.

The operational tell is distinctive: CPU graphs look healthy while latency collapses, because a stalled core still reports as busy
And it is where ground truth comes from

Every approximate index has a recall figure, and a recall figure requires knowing the true answer. Flat search over a sample of your own corpus is where that truth comes from.

If a vendor quotes latency without recall, the number is meaningless — you can always be fast if you are allowed to be wrong
When flat is genuinely right

Under about 100,000 vectors. For a small entitled set — exact search on 500 vectors is sub-millisecond and it is perfect recall. And as ground truth, offline, where slowness does not matter.

IVFpartition the space once, read a fraction of it
Before

Flat search would compare the query against every vector.

Build

k-means finds nlist centroids; every vector is assigned to its nearest; the vectors are physically reordered so each bucket is contiguous.

Route

Score the query against the centroids — nlist comparisons, the routing scan.

Probe

Open the nearest nprobe buckets. Here, two of five.

Scan

Every vector inside them, exactly. The other three were never read.

k-means, from zero

1. Scatter nlist points at random — the starting centroids. 2. Assign every vector to its nearest. 3. Move each centroid to the average position of what it holds. 4. Repeat until they stop moving — usually a few dozen passes.

A centroid is not a real vector — it is a made-up average, and nothing is stored there
The buckets are semantic

They group by meaning, because meaning is all the clustering could see.

Which is exactly why metadata filtering interacts badly with IVF — your filter cuts across a partition that knows nothing about it

The suffix: what IVF-Flat, IVF-SQ and IVF-PQ mean

IVF-Flat

Uncompressed, exactly as the model produced them.

3.07 GB at 1M × 768-d
Highest recall — the only approximation is routing
IVF-SQ

Each value compressed from four bytes to one.

768 MB — 4×
One to two points lower, and most of that is recoverable by rescoring
IVF-PQ

The vector is split into sub-vectors, each replaced by a codebook id.

~96 MB — 32×
Materially lower, and effectively requires rescoring · doc 12
nlist and nprobederive one, measure the other
1
nprobe = 1~2,000 vectors scanned — 0.2% of the corpus
recall ~0.65
4
nprobe = 4~5,000 — 0.5%
~0.82
8
nprobe = 8~9,000 — 0.9%
~0.88
16
nprobe = 16~17,000 — 1.7%
~0.94
32
nprobe = 32~33,000 — 3.3%
~0.97
64
nprobe = 64~65,000 — 6.5%
~0.99
128
nprobe = 128~129,000 — 12.9%
~0.995
How to derive nlist, rather than recite √N

Four steps. How many vectors can I scan in the latency budget, at my bandwidth? Keep about forty per cent headroom. Divide by the nprobe I intend to use — that gives vectors per bucket. Divide the corpus by that to get nlist.

On 50M vectors with a 10 ms budget that lands around 8,000, which happens to be near √N — and that agreement is a coincidence of the example. Deriving it is what lets you respond when the interviewer halves the budget mid-answer
The asymmetry, and what it enables

nlist is build-time: changing it means new k-means, new assignment, new memory layout, a full rebuild. nprobe goes in the request — per query, no rebuild, no restart, no deploy.

Which enables a genuinely useful production pattern: different nprobe values for different traffic classes against one index, so an interactive path runs cheap and an analytical path runs thorough

How IVF fails — and the first one is not a bug

Boundary loss

A query lands near a boundary, so its true top ten is split across two regions.

Nothing is wrong with the clustering — this is what partitioning is
At nprobe = 1

The neighbours on the other side are unreachable.

Never read, never scored, no error raised
The mitigation

nprobe above 1. Hedging across the nearest few regions is exactly what the knob is for.

Centroid drift

A bulk load into a clustering fitted to older data skews bucket sizes and moves the corpus away from its centroids.

p99 rises before p50 does, and recall decays without anything alerting
HNSWnavigate a graph instead of dividing the space

Connect every vector to its nearby neighbours and walk towards the query — on a stack of graphs that gets coarser as you go up.

Layer 0 holds everything

1,000,000 nodes.

Each layer above

A shrinking random sample — roughly one in M.

~31,000 · ~1,000 · ~30 · ~1
Enter at the top

Long coarse hops to get roughly right.

Descend to layer zero

Short precise steps to get exactly right.

What M is

The width of every step. When the walk arrives at a node, the engine reads its M entries and scores them. One list per layer — a vector present in three layers holds three separate address books.

Layer zero is usually allowed a larger cap, commonly 2×M, because it does the precision work
What M is not

Not the number of layers. The layer count is not a setting at all — it falls out of the random draws. And not a guarantee: it is a cap. A node in a sparse region still gets filled to M, but with whatever is least far away.

M has two effects at once, because the level multiplier that decides how fast the pyramid narrows is itself derived from M. Raise M and you get richer neighbour lists and a shallower hierarchy

The search walk, and efSearch

1

Pick the best unexplored candidate on the shortlist.

2

Read its M neighbours and score them.

3

Merge in, keep the best efSearch, drop the rest.

4

Stop when the best unexplored is already worse than the worst held.

Because anything it leads to is further still
The one rule: efSearch must exceed k

Or every slot in the shortlist is an answer and there is no room to explore — recall collapses.

efSearch is how many good candidates you remember, not how many you read
What the graph costsroughly twenty-five times IVF’s overhead
Workedevery node stores up to M ids per layer at 4 bytes, and 2×M at layer zero · 1M vectors at M = 32 is ~260 MB of edges against 3.07 GB of vectorsabout 8.5% overhead
HNSW

~260 MB of edge lists at 1M × M=32.

~8.5% overhead
IVF

Centroids plus a directory.

~11 MB — ~0.1% overhead
So HNSW carries

Roughly 25× the index overhead of IVF.

Academic at a million vectors. Another machine at a hundred million

HNSW — the default for interactive search

Scores ~2,000 vectors where IVF scores ~17,000.

  • Wins on latency at equal recall — it reads a route rather than a region
  • Takes incremental inserts gracefully
  • Loses on index overhead, build time, delete handling
  • And gains less from quantisation, because random access is latency-bound
How much data?
How much RAM?
What is the
write pattern?

IVF — when memory overhead dominates

0.1% overhead against 8.5%.

  • Wins when the graph overhead becomes a machine
  • Rebuilds cleanly and fast — right when the corpus is rebuilt in bulk anyway
  • Benefits more from quantisation, because sequential scanning is bandwidth-bound
  • Loses on latency at equal recall, and on incremental inserts
And if the whole thing does not fit in RAM, both answers are wrong

The graph walk is random access across the whole structure, so once pages come from disk you are paying a disk seek per hop — and there are dozens of hops per query.

That is precisely the gap DiskANN was built for — document 10
Answer it out loudthe ninety-second version, then six questions
“Flat compares against everything and is exact — it is a bandwidth problem, 3 GB at 1M×768 and about 61 ms, and cores do not help because bandwidth is shared at the socket. IVF partitions once with k-means into nlist buckets and at query time opens the nearest nprobe of them — nlist is build-time and expensive, nprobe is per-query and free. HNSW builds a stack of graphs, enters at the top for coarse hops and walks layer zero with a beam of width efSearch — M is build-time, efSearch is per-query. HNSW wins on latency at equal recall and takes inserts gracefully; IVF wins on index overhead by twenty-five times and rebuilds cleanly. And if it does not fit in RAM, both are wrong.”

Six questions, and the spine of each answer

Architect

Why is flat search slow, and why do cores not fix it?

  • It is a bandwidth problem rather than a maths problem
  • Every query reads every vector: 1M × 768-d fp32 is 3.07 GB, at ~50 GB/s that is ~61 ms
  • Bandwidth is a socket-level shared resource — a second query competes for the same bus
So a 64-core machine gives roughly the same flat throughput as an 8-core one. The tell: CPU graphs look healthy while latency collapses, because a stalled core still reports as busy.
Architect

How would you choose nlist?

  • Derive it from the latency budget rather than recite the square-root rule
  • How many vectors can I scan in the budget, at my bandwidth? Keep ~40% headroom
  • Divide by the nprobe I intend to use → vectors per bucket. Divide the corpus by that → nlist
On 50M with a 10 ms budget that lands near 8,000 — close to √N, and that agreement is a coincidence of the example. Deriving it is what lets you respond when the interviewer halves the budget mid-answer.
Architect

Explain HNSW, and what M and efSearch control.

  • A stack of graphs over the same vectors. Layer zero holds everything; each layer above is a shrinking random sample, roughly one in M
  • M = how many neighbours each node keeps — the width of every step, and build-time
  • efSearch = the size of the candidate shortlist during the bottom-layer walk — per query
The one rule: efSearch must exceed k, or every slot is an answer and there is no room to explore. And efSearch is how many candidates you remember, not how many you read.
Architect

IVF or HNSW?

  • Three things first: how much data, how much RAM, what the write pattern is
  • HNSW wins on latency at equal recall — ~2,000 vectors scored against IVF’s ~17,000 — and takes inserts gracefully
  • IVF wins on index overhead by more than an order of magnitude: 0.1% against 8.5%
  • IVF also rebuilds fast and benefits more from quantisation
HNSW by default for interactive search; IVF when overhead dominates at scale, when the corpus is rebuilt in bulk anyway, or when I intend to quantise heavily. And if it does not fit in RAM, both are wrong.
Architect

Why does an approximate index lose recall at all?

  • Because both mechanisms deliberately decline to look at most of the corpus, and sometimes the answer is in what they declined to look at
  • IVF: boundary loss — a query near a boundary has neighbours split across regions, and the unprobed ones are never read, not ranked low
  • HNSW: the walk terminating in a local minimum, or the beam being too narrow to hold a promising detour
In both cases the loss is silent: you get k results with plausible scores. Which is why every ANN index needs a recall figure measured against flat search on your own corpus.
Eng manager

The team says search got slower after a data load.

  • First: which index.
  • IVF → bucket imbalance and centroid drift. p99 rises before p50, and recall decays without anything alerting. Check the bucket size distribution and gold-set recall over time
  • HNSW → whether the working set still fits in memory, because the walk is random access and it falls off a cliff the moment it touches disk
Both are capacity questions wearing a performance costume, which is usually the right first hypothesis after a data load.
Fast answersthe eight that come up most

QIs HNSW always better than IVF?

For latency at equal recall on a corpus that fits in memory, usually — which is why it is the default nearly everywhere. It loses on index overhead by ~25×, on build time, on delete handling, and on how much it gains from quantisation. At a hundred million vectors those stop being footnotes.

QCan I change M without rebuilding?

No. M determines the size of every neighbour list, so changing it means building the structure again — hours of CPU, though the vectors themselves are untouched and there is no re-embedding. Which is why efSearch is the knob you reach for first.

QWhat is a sensible starting point for M and efSearch?

M 16–32, efSearch 64–128, efConstruction around 200. But treat those as a starting point for a sweep rather than an answer — deriving all three from a budget is what an interviewer is actually asking for.

QDoes IVF have anything equivalent to efSearch?

nprobe plays the same role — a per-query dial trading latency for recall without touching the structure. The difference is what the dial buys: nprobe reads more regions, sequentially; efSearch keeps more candidates alive during a walk, randomly. Same tradeoff, completely different cost profile on the hardware.

QWhy is the HNSW layer count not a parameter?

Because each vector draws its own top layer at random, from a distribution whose shape is set by M. The pyramid narrows by roughly a factor of M per level, so 1M at M=32 lands at four or five layers — nobody chose that. Good to volunteer, because the follow-up is “so how do you tune the depth?” and the answer is you tune M.

QIs IVF-PQ the same thing as product quantisation?

IVF-PQ is IVF with PQ applied to the vectors inside the buckets. Two separable ideas that compose: IVF decides which vectors you look at, PQ decides how compactly each is stored. And the confusing part — both use k-means, for completely different purposes.

QWhat happens to HNSW if the index does not fit in RAM?

It falls apart, faster than intuition suggests. The walk is random access across the whole graph, so once pages come from disk you pay a disk seek per hop — and there are dozens of hops per query.

QHow do I measure recall if I do not know the true answer?

Compute it with flat search over a sample. A few hundred real queries, an exact scan to get the true top k, then measure what your approximate index returned against that. It is slow and it does not matter, because you run it offline. Without this step you have latency numbers and no idea what they cost you.

Every approximate index is a decision to not look at most of your corpus — and the recall you lose is the part it declined to look at, returned silently with plausible scores. IVF declines by region and fails at boundaries; HNSW declines by route and fails in local minima. So the number that makes any latency figure meaningful is recall measured against flat search on your own corpus — because you can always be fast if you are allowed to be wrong. And the choice between them is three questions: how much data, how much RAM, and what the write pattern is.

IVF at nprobe 16 · 1.7% scanned
never read
Where this goes next

Questions to ask them

Which index type, and why that one?

What is your measured recall, and against what ground truth?

Is nprobe or efSearch tuned per traffic class, or global?

Does the whole index fit in RAM on one node?

For IVF: when was the clustering last refitted, and has recall drifted?

What triggers a rebuild — a calendar, or a signal?

← 08 · Model ops Read the full chapter → 10 · Index II →