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 06 · Quick learn The memory multiplier · The collapse · Reversible, not permanent

How bigshould a vector be

How big a vector should be, how two of them get compared, and the training trick that turns the dimension decision from a migration into an afternoon.

“Sorting by any of the three gives the same order. The metric is an operational choice, not a quality one.”
× 4 bytes
dimensions × 4 — know it cold
3 metrics
that collapse into one on unit vectors
~97%
quality retained at a quarter of the RAM
47 hrs → 1
a migration, or an afternoon
Dimensionalitya straight-line multiplier on the largest line in the budget
bytes per vector = dimensions × 4  ·  then × chunks × replicas
the single most quoted calculation in RAG interviews — you should be able to do it out loud without a whiteboard
Learn this row cold
384

1.5 KB

MiniLM class
768

3 KB

Base-size open models
1024

4 KB

Large open models
1536

6 KB

The common API default
3072

12 KB

Large API tier
The curse of dimensionality, which is a bait question

In high dimensions, distances between random points concentrate: everything becomes roughly equidistant from everything else. That is a real theorem, and it is about random points in uniform high-dimensional space. Embeddings are not random points — a trained model puts semantically related text on a much lower-dimensional manifold inside that space, and it is trained specifically so that distances there are meaningful.

The empirical evidence settles it: retrieval quality improves with dimension up to a point and then plateaus. If the curse bit at 1536, none of this would work at all. What does bite at high dimension is the memory bill
Size, and normalisationthe arithmetic everything else rests on
1 · Square every number

3² = 9, 4² = 16.

2 · Add the squares

9 + 16 = 25.

3 · Square root

√25 = 5. That is the norm, ‖v‖.

4 · Divide through

[3, 4] ÷ 5 = [0.6, 0.8], whose norm is exactly 1.

What normalising changes, and what it does not

The direction

No change. Every number is divided by the same value, so the ratios between them are untouched.

The length

Changes. It becomes exactly 1.

Which chunks are nearest

No change — provided both sides are normalised.

Whether a raw dot product is valid

Changes. This is the whole point.

Where normalisation lives

The model emits unit vectors

Many modern models do this by default.

Convenient, but do not assume it — verify
Your ingest pipeline normalises

One division per vector, once, at write time.

The right place. Cost amortises to zero
The database normalises per comparison

Using the cosine operator on un-normalised vectors.

Correct — and you pay for it on every query, forever
Symmetry is the invariant, not normalisation itself

Neither side normalised, cosine operator: correct. Both sides normalised, dot product: correct and fastest. One side normalised and the other not, scored with a dot product: silently wrong.

And wrong in the same shape as every other failure in this runbook: no error, plausible scores, subtly bad ranking
The collapsethe finding that makes this subject smaller than it looks

Once the vectors are normalised, the three metrics stop being three metrics.

Cosine similarity

The angle between two vectors.

On unit vectors this is the dot product
Dot product

Sum of element-wise products.

Identical to cosine, once both sides are unit length
Euclidean distance

Straight-line distance.

A strictly decreasing function of cosine on unit vectors
The consequence, stated as a claim you can defend

On normalised vectors, all three metrics produce the same ranking. The metric choice is therefore an operational decision — speed, safety, tooling — and not a quality decision.

The scores differ. A cosine of 0.90 and a distance of 0.447 are different numbers about the same pair. But sorting by either produces the identical order, because the relationship is strictly monotonic

So why pick one over another?

Cosine · <=>

vector_cosine_ops

Default. It normalises internally, so it still ranks correctly if a bug ships un-normalised vectors
Inner product · <#>

vector_ip_ops

Fastest — on pre-normalised vectors. Silently wrong otherwise
Euclidean · <->

vector_l2_ops

Right when magnitude is a real quantity — coordinates, sensor readings
Manhattan · <+>

vector_l1_ops

Rarely used for text
And whichever you pick, the operator class and the query operator must agree

Or the planner will not use the index at all and falls back to scanning every row. The answers are still right — a full scan is exact — it is just doing ten million comparisons to get them.

Put it in CI: a test that fails if the query plan contains a sequential scan
Matryoshkaturning a migration into an afternoon

Dimension is the highest-leverage memory lever you have. The problem is that it has always felt permanent, because changing it means re-embedding every chunk. Matryoshka removes that.

Ordinary model · 1024 → 512

Re-embed all 8,000,000 chunks.

  • Model calls: 2.8 billion tokens through the model
  • Wall clock: ~47 hours, competing with live ingest for quota
  • Footprint: two full collections live during cutover
  • What it is: a migration
Same result.
Different project.

Matryoshka model · 1024 → 512

Read the vectors, keep the first 512 numbers, renormalise.

  • Model calls: zero
  • Wall clock: a few hours of CPU
  • Footprint: a local data transformation
  • What it is: a job
Where the property comes from

An ordinary model is trained with one loss on the full vector. A Matryoshka model is trained with several losses at once — the same objective evaluated on the first 64 dimensions, the first 128, the first 256, and so on — and the losses are summed. The model is forced to make every prefix independently useful.

The cost is training-time only: a modest increase in training compute, and no change at all to inference. That asymmetry is why it spread so fast
Truncation is two steps, and the second is not optional

Keep the first N numbers — then renormalise. Prefixes have different surviving norms, so without it, documents whose magnitude sits early get a systematic scoring bonus that has nothing to do with relevance.

And it must be identical on both sides. Never expose a truncate() that does not renormalise — one function, in a shared library, used by both paths
The quality curvethe real decision lives in a narrow band
1024
full32 GB at 8M chunks
100%
512
half16 GB
~99%
256
quarter8 GB
~97%
128
eighth4 GB
~92%
64
sixteenth2 GB
~85%
32
thirty-second1 GB
~70%
Put it together and you have the whole picture

Going up from 1024 to 3072 buys almost nothing. Coming down to 256 costs about three points. Everything below about 128 dimensions is quality you cannot afford to lose; everything above about 1024 is money you cannot justify spending.

The real decision lives in a narrow band — and Matryoshka lets you move inside that band without re-embedding
One caution worth stating unprompted

Those percentages are typical published figures, not promises. Corpora with many near-duplicate documents lose more, because fine distinctions are exactly what the dropped dimensions were carrying.

Measure retention on your corpus, not on the published curve

Adaptive retrieval: a quarter of the RAM, close to full quality

Embed and truncate

Full model, truncate to 256, renormalise — the same function ingest used.

Pass one · shortlist

Graph search over 8M truncated vectors in RAM → ~200 candidate ids.

5–10 ms
Pass two · rescore

Fetch those 200 full vectors from SSD, 200 exact dot products.

3–8 ms — the fetch dominates
Return the reordered top 10

A quarter of the RAM.

When to skip the rescore altogether

If you already have a cross-encoder reranker downstream, a vector rescore in front of it is largely redundant — the reranker reads the chunk text and will fix the ordering anyway.

The decision rule is precise: rescoring recovers precision, not recall. If recall@100 at 256 dimensions is already at parity with 1024, the shortlist is fine and only the ordering is at risk — and a reranker fixes ordering more effectively than a vector rescore does
Metric choice meets quantisationwhy the normalisation discipline matters more than it looks
Cosine on normalised vectors

Distortion is bounded and uniform — every vector has the same magnitude, so the error is comparable across chunks.

Dot product on normalised vectors

Identical to cosine.

Dot product on un-normalised vectors

Distortion scales with magnitude. Long vectors lose more in absolute terms, short ones less.

The damage is uneven, and therefore unboundable
The rule

Normalise before you quantise. Once every vector has the same magnitude, the metric choice stops affecting compression damage — and compression damage becomes something you can bound and measure rather than something that varies per record.

Which is also why adaptive retrieval and the rescoring pattern in document 12 are the same idea on two different axes
Answer it out loudthe ninety-second version, then six questions
“Dimension is a straight-line multiplier on the largest line in the memory budget — ten million chunks at 1536 in float32 is 61 GB per copy, times replicas — and no index parameter comes close to it. Quality rises steeply to about 512, bends by 1024 and is flat above 2048, so the useful band is narrow. On the metric: once vectors are normalised, cosine, dot product and Euclidean all produce the same ranking, so the choice is operational rather than a quality decision — cosine by default because it still ranks correctly if a bug ships un-normalised vectors. And if the model is Matryoshka, the dimension decision stops being permanent: truncating 1024 to 512 is reading the vectors and keeping the first half, not a forty-seven-hour re-embed. The step people forget is renormalising after truncation.”

Six questions, and the spine of each answer

Architect

Cosine, dot product or Euclidean?

  • On normalised vectors it does not matter for quality — all three produce the same ranking
  • Cosine on unit vectors is the dot product; Euclidean is a strictly decreasing function of cosine
  • So choose on operational grounds: cosine by default, because it normalises internally
  • Dot product on pre-normalised vectors only if profiling shows the metric is on the critical path
And whichever you pick, the index operator class and the query operator must agree, or the index is bypassed entirely.
Architect

Search takes two seconds instead of twenty ms, and results are still correct.

  • Almost certainly a metric mismatch
  • The index was built with one operator class and the query uses a different operator
  • So the planner will not use the index and falls back to scanning every row
  • The answers are right because a full scan is exact
Confirm it in the query plan, then put that assertion in CI: a test that fails if the plan contains a sequential scan. Otherwise it recurs every time somebody writes a new query.
Architect

How many dimensions, and how would you decide?

  • Start from the memory arithmetic — 10M chunks at 1536 in fp32 is 61 GB per copy, times replicas
  • Then measure: quality rises steeply to ~512, bends by 1024, flat above 2048
  • Pick the smallest dimension within noise of the best on my own gold set
And if the model supports Matryoshka truncation, treat the choice as reversible and revisit it once the corpus is real.
Architect

What is Matryoshka and why does it matter architecturally?

  • A training objective that computes the loss at several prefix lengths at once
  • So the model front-loads information: the first 256 dimensions of a 1024 vector are a complete 256-dimension vector
  • It makes the dimension decision reversible — a local data transformation, not a re-embed
  • And it enables adaptive retrieval: shortlist on truncated vectors in RAM, rescore the top 200 against full vectors on SSD
The step people forget is renormalising after truncation. Prefixes have different surviving norms, so without it some documents get a scoring bonus unrelated to relevance.
Architect

We inherited a system on Euclidean distance. Migrate to cosine?

  • Probably not — and I would answer with a measurement, not a preference
  • First verify the vectors are normalised. Then run the gold set under both metrics
  • Identical numbers → do not migrate. Zero benefit, full rebuild cost
  • Different numbers → the vectors are not normalised, and that is the actual bug
Fix that instead. It is cheaper, and it is the thing causing the problem.
Eng manager

The team wants to halve the dimension to cut the bill.

  • Three things. Does the model support Matryoshka truncation? That changes a fortnight into an afternoon
  • What does the gold set say retention is on our corpus, rather than on the published curve?
  • Is truncation-and-renormalisation one shared function used by both paths?
If those come back well, it is one of the best returns available: dimension is the largest single line in the memory budget, and the first halving typically costs about a point.
Fast answersand how to make each silent bug a build failure

QDo I need to normalise if the model already returns unit vectors?

Verify rather than assume, then assert. Many models emit unit vectors, and some emit them usually — a truncated input, an empty string or an unusual character sequence can produce something slightly off. A single assertion at write time converts a silent ranking bug into a loud ingest failure.

QCan I use an absolute similarity threshold, like “above 0.75”?

Cautiously, and never as the only control. Similarity scores are not calibrated: the distribution differs by model, corpus, query length and chunk length, and drifts when any of those change. Prefer a relative rule — top k, or cut where the score gap widens.

QHow do I stop this class of silent bug reaching production?

Turn each one into a build failure, because none of them announce themselves at runtime. Three assertions cover most of it: norms are 1.0 on both paths; the query plan uses an index; gold-set recall stays above a threshold.

QWhy does that framing get the tests written?

Because it explains why they are not optional: retrieval fails quietly and almost everything else fails loudly, so retrieval needs assertions where other subsystems can rely on exceptions.

QIs a cosine of 0.90 “good”?

Unanswerable without context. It is a different number in every system. What is meaningful is the gap between the top result and the tenth, and whether your gold set says the right chunk is in the top k.

QDoes the rescore pass need the full vectors in RAM?

No — that is the point. The 200 full vectors are fetched from SSD, and on local NVMe the fetch is 3–8 ms. Only the truncated vectors need to be resident.

Once the vectors are normalised, the metric stops being a quality decision. Cosine, dot product and Euclidean all produce the same ranking, so choose on operational grounds — cosine by default, because it is the one that still ranks correctly when a bug ships un-normalised vectors. The decision that does matter is dimension, because it is a straight-line multiplier on the largest line in the memory budget — and if the model is Matryoshka, that decision is reversible in an afternoon rather than permanent. Just never expose a truncate that does not renormalise.

1024 · 32 GB
512 · 16 GB
256 · 8 GB
128
32
Where this goes next

Questions to ask them

What dimension are you at, and was it chosen or inherited?

Does the model support Matryoshka truncation?

Are vectors normalised, and is there an assertion that says so?

Does the query operator match the index operator class?

Has anyone measured the quality curve on your corpus rather than the published one?

← 05 · Models Read the full chapter → 07 · Limits →