Runbooks/RAG RunbookTrack B · Embeddings and indexLLM Inference Runbook →0%
  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
RAG Runbook · Document 06 of 16 · Track B — Embeddings and index

Track B · Document 06 · Embeddings and index

Dimensions, Metrics and Matryoshka

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.

Reads in about 35 minutes · 9 figures, 4 of them interactive · 9 interview questions · prints to clean A4

What is in this document

  1. Dimensionality: what it buys and costs
  2. Size, and normalisation
  3. The three metrics
  4. The collapse
  5. The operator, and the mismatch
  6. Where normalisation lives
  7. Matryoshka
  8. Truncation, and the forgotten step
  9. The quality curve
  10. Adaptive retrieval
  11. Metric choice meets quantisation
  12. Symptom → cause
  13. Interview questions
  14. FAQ
  15. Cheat sheet

1 · Dimensionality: what it buys, what it costs

Dimensionality is how many numbers are in each vector. More dimensions means more room to encode distinctions; fewer means the model compresses harder and some distinctions are lost. That is the whole idea, and everything interesting is in what it costs.

DIMENSION IS A STRAIGHT-LINE MULTIPLIER ON EVERYTHING DOWNSTREAM dimensions bytes / vector one copy across replicas What the extra dimensions actually buy Retrieval quality rises steeply to about 512, flattens badly past about 1024, and is close to indistinguishable above 2048 on most corpora. The memory bill does none of that. It rises in a perfectly straight line, forever, multiplied by every replica you run. No index parameter comes close to this lever. Moving 1536 to 768 saves more than tuning every graph setting you have.

Latency, by contrast, moves far less than people expect. A distance computation is linear in dimension, but search time is dominated by how many vectors you touch and by memory bandwidth, not by the arithmetic. Halving the dimension halves the bytes moved, which helps — but it is a memory decision first and a latency decision a distant second.

The memory maths — learn this cold

This is the single most quoted calculation in RAG interviews, and you should be able to do it out loud without a whiteboard. Bytes per vector = dimensions × 4 for float32. So 384 dimensions is 1.5 KB, 768 is 3 KB, 1024 is 4 KB, 1536 is 6 KB, 3072 is 12 KB. Multiply by the corpus, then by the replica count, and you have the base of the whole capacity conversation.

What the extra dimensions actually buy

Below ~512

Quality rises steeply. Every dimension you add is doing real work.

This is where the model is genuinely constrained.

512 to ~1024

Still improving, but the curve is bending.

Most production systems live here.

Above ~1024

Gains flatten badly. 1536 to 3072 doubles the memory bill and buys very little.

The bill does not flatten. It rises in a straight line, forever.

Latency

Moves far less than people expect.

Search time is dominated by how many vectors you touch, not by the arithmetic per comparison.

The curse of dimensionality, which is a bait question

In high dimensions, distances between random points concentrate: everything becomes roughly equidistant from everything else, and nearest-neighbour search becomes meaningless. 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 low-dimensional manifold inside the high-dimensional space, and it is trained specifically so that distances there are meaningful. The honest answer is: the phenomenon is real, it does not apply in the way the question implies, and the empirical evidence is that retrieval quality improves with dimension up to a point and then plateaus — which is not what the curse would predict. If the curse bit at 1536 dimensions, nothing in this runbook would work.

2 · Size, and normalisation

Before the metrics, one piece of arithmetic that everything else rests on: how long a vector is, and what happens when you make every vector the same length.

THE SIZE OF A VECTOR IS PYTHAGORAS, EXTENDED TO AS MANY NUMBERS AS YOU LIKE [3, 4] 3 4 length 5 [0.6, 0.8] — length 1 1 · square every number 3² = 9 4² = 16 2 · add the squares 9 + 16 = 25 3 · take the square root √25 = 5 the norm, written ‖v‖ 4 · normalise — divide every number by the norm [3, 4] ÷ 5 = [0.6, 0.8] new norm = √(0.36 + 0.64) = 1 Identical at 1024 dimensions: square all 1024, add all 1024 squares, take the square root. The formula does not change with dimension. The key property: a vector’s size is computed from its own numbers only. Ten million chunks means ten million separately computed sizes.
  1. Square every number. 3² = 9, 4² = 16.
  2. Add the squares. 9 + 16 = 25.
  3. Take the square root. √25 = 5. That is the norm, written ‖v‖.
  4. Normalise: divide every number by the norm. [3, 4] ÷ 5 = [0.6, 0.8], whose norm is exactly 1.

Computing the norm is measurement; normalising is rescaling. Four words that are constantly conflated in conversation: size is this number; dimension is how many numbers there are; storage size is bytes on disk; score is a comparison between two vectors. Say “norm” or “magnitude” when you mean size and the ambiguity disappears.

What normalising changes, and what it does not

Changes?
The direction the vector pointsNo — every number is divided by the same value, so the ratios between them are untouched
The length of the vectorYes — it becomes exactly 1
Which chunks are nearest to a queryNo, provided both sides are normalised
Whether a raw dot product is a valid similarityYes — this is the whole point

And one property worth stating explicitly because it catches people out: each vector gets its own divisor. Normalisation is not a global rescaling of the collection; it is ten million independent divisions, each by that vector’s own norm.

3 · The three metrics

Dot product, cosine similarity and Euclidean distance. Worked on one pair of vectors, where two of them will disagree with each other — and both will be right.

ONE PAIR: QUERY [3, 4] AND CHUNK [6, 8] — THE SAME DIRECTION, TWICE THE LENGTH dot product a · b = Σ (aᵢ × bᵢ) (3×6) + (4×8) = 18 + 32 = 50 not on a 0–1 scale; inflated because the chunk is long cosine similarity (a · b) ÷ (‖a‖ × ‖b‖) 50 ÷ 5 = 10 10 ÷ 10 = 1 = 1.00 identical direction — which is the truth Euclidean distance (L2) √Σ (aᵢ − bᵢ)² 6−3 = 3 → 9 8−4 = 4 → 16 √25 = 5.00 far apart — which is also the truth Cosine says 1.0, identical. Euclidean says 5.0, far apart. Both are correct: cosine looked only at direction; Euclidean also counted the length. Two routes to the same number, and only one of them is free You can divide the vectors once at write time, or divide the score on every comparison of every query, forever. The result is identical. Which is why production systems normalise at ingest and store size-1 vectors: the division has already happened and never happens again.

For text embeddings, magnitude is noise rather than signal — models emit arbitrary magnitudes that carry no meaning about the text. That is what makes cosine, or dot product on normalised vectors, the right default, and it is why Euclidean’s honest answer here is the less useful one.

DomainDoes magnitude mean anything?Metric
Text embeddingsNo — models emit arbitrary magnitudes Cosine, or dot product on normalised vectors
Recommendation vectors with a popularity term baked in Sometimes — length may encode confidence or frequency Dot product, deliberately
Physical measurements, coordinates, sensor readings Yes — the magnitude is the quantityEuclidean

4 · The collapse

Here is the finding that makes this whole subject smaller than it looks. Once the vectors are normalised, the three metrics stop being three metrics.

both vectors normalised to length 1
ON NORMALISED VECTORS, THE THREE METRICS COLLAPSE INTO ONE cosine and dot product become identical cos = (a·b) ÷ (1 × 1) = a·b not similar — identical, always, for every pair and Euclidean is a fixed function of cosine d² = 2 − 2 × cos strictly decreasing, so higher similarity always means smaller distance 1.41 0 cos −1 0 1 cosine similarity → Euclidean distance at this similarity cosine 0.90 distance 0.447 dot product on unit vectors = 0.90, the same number Because the relationship is monotonic, sorting by Euclidean distance produces exactly the same ordering as sorting by cosine. The scores differ; the ranking does not.

This is the headline finding, and it reframes the whole conversation: on normalised vectors the metric choice is an operational decision — index compatibility, safety, speed — and not a quality decision. Say that in an interview and the follow-up questions become much easier, because you have already framed them correctly.

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 between them is strictly monotonic.

So why pick one over another?

Three genuine reasons, in order of how often they actually matter:

1. index compatibilitythe metric is baked into the index at build time; query with a different operator and the index is bypassed entirely
2. safetycosine normalises internally on every comparison, so if a bug ships un-normalised vectors it still ranks correctly; dot product returns wrong rankings with no error
3. speeddot product skips two divisions and two square roots per comparison — roughly 5 to 10 percent of the metric itself, which is a few milliseconds in a budget dominated by the query embedding and the language model

The default is cosine, unless profiling proves the metric is on the critical path. Most teams pick cosine and never notice the cost. The first reason is the one that actually bites in production, and it has its own section below.

5 · The operator, the operator class, and the mismatch

The metric is not a runtime choice. It is compiled into the index when you build it, and the query has to agree — otherwise the index is quietly bypassed.

Metricpgvector operatorOperator classReturns
Cosine<=>vector_cosine_ops Cosine distance, 0 to 2
Inner product<#>vector_ip_ops Negative inner product
Euclidean<->vector_l2_ops L2 distance, 0 upwards
Manhattan<+>vector_l1_ops L1 distance — rarely used for text

Note the word distance in the third column for cosine. The operator returns 1 − similarity, so smaller is better and you sort ascending. Getting that backwards is its own silent failure, covered below.

THE FAILURE THAT HURTS SPEED RATHER THAN ACCURACY — AND STILL DOES NOT THROW index built with one operator class, queried with another CREATE INDEX … vector_cosine_ops SELECT … ORDER BY embedding <#> $1 the planner will not use a wrong-metric index what actually happens · falls back to a sequential scan · scores all ten million rows, one by one · and the results are correct, just slow The fix is a test, not a rule: assert in CI that the query plan contains an index scan and not a sequential scan. EXPLAIN ANALYZE SELECT id FROM chunks ORDER BY embedding <=> $1 LIMIT 10; want to see: Index Scan using chunks_embedding_idx alarm bell: Seq Scan on chunks

That last line is the trap: nothing looks broken. The answers are right, no error is raised, and you only catch it in the query plan or in a latency graph — which is why the assertion belongs in a build rather than in a runbook. The direction of the sort is the same family of bug: ORDER BY … DESC on a distance returns the least similar chunks, the pipeline runs perfectly, and it gets diagnosed as hallucination for weeks.

6 · Where normalisation lives, and how to prove it

Normalisation can happen in three places, and the only thing that actually matters is that both sides of the comparison agree.

WhereWhat it meansVerdict
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 on every 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

A system where neither side is normalised and the cosine operator is used is correct. A system where both sides are normalised and dot product is used is correct and fastest. A system where one side is normalised and the other is not, scored with a dot product, is silently wrong — and it is wrong in the same shape as every other failure in this runbook: no error, plausible scores, subtly bad ranking.

Proving it rather than assuming it

# at ingest, and again on the query path — the same assertion
assert abs(norm(vec) - 1.0) < 1e-6, f"vector not normalised: {norm(vec)}"

# and in CI, over a sample of live records
SELECT count(*) FROM chunks
WHERE abs(vector_norm(embedding) - 1) > 1e-6;   -- must be 0

Two assertions and one query. They cost nothing, they run in a build, and between them they eliminate an entire family of failure that is otherwise diagnosed as “the model feels worse lately”.

The three silent failures, side by side

FailureSymptomDetection
Normalisation asymmetry — one path normalises, the other does not Thresholds cut good chunks; the tail of the result list degrades Norm assertions on both paths
Prefix mismatchquery: and passage: applied inconsistently Recall falls from ~0.91 to ~0.63 Gold-set recall in CI — document 05
Metric mismatch — the query operator does not match the index operator class Latency goes from 20 ms to seconds; results still correct EXPLAIN assertion in CI, and latency alerting

Two of these hurt accuracy and one hurts speed. None of them throw. That sentence is worth saying in an interview, because it is the thesis of this entire track.

7 · Matryoshka: making the dimension decision reversible

Section 1 established that 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 → 512Matryoshka model: 1024 → 512
What you doRe-embed all 8,000,000 chunks Read the vectors, keep the first 512 numbers, renormalise
Model calls2.8 billion tokens through the modelZero
Wall clock~47 hours, competing with live ingest for quota A few hours of CPU
FootprintTwo full collections live during cutover A local data transformation
What it isA migrationA job
WHERE THE MEANING SITS, AND WHY THAT DECIDES WHETHER TRUNCATION WORKS ORDINARY MODEL — meaning spread evenly, in no particular order keep the first half → you kept an arbitrary half of the meaning; quality degrades badly and unpredictably dimension 3 is not more important than dimension 900 MATRYOSHKA MODEL — meaning deliberately front-loaded during training keep the first half → you kept the coarse, most important distinctions; quality holds up earliest dimensions carry the coarse distinctions The analogy: describing a book in a fixed number of sentences. One: “it’s a crime novel”. Three: that, plus 1930s Los Angeles, plus a detective investigating a disappearance. Ten: all of it. Each version is a prefix of the next, each is complete at its own level, and you never start over to get a shorter one — you stop early.

An ordinary model handed you ten sentences in random order. Stopping after three gives you three arbitrary facts rather than a summary. That is the whole difference, and it is a property of the training objective rather than of the architecture.

Where the property comes from

An ordinary model is trained with one loss, computed 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 up to the full width — and the losses are summed. The model is therefore forced to make every prefix independently useful, which is exactly the property you exploit later.

The cost is training-time only: a modest increase in training compute, and no change at all to inference. That asymmetry is why the technique has spread so quickly — it is free at the point of use.

8 · Truncation, and the step everybody forgets

Truncation is two steps, and the second one is not optional.

def truncate(vec, dim):
    short = vec[:dim]                          # 1. keep the prefix
    norm  = math.sqrt(sum(x * x for x in short))
    return [x / norm for x in short]           # 2. RENORMALISE
TWO UNIT-LENGTH DOCUMENT VECTORS, TRUNCATED TO THE SAME PREFIX Doc A prefix norm 0.840 Doc B prefix norm 0.938 Doc B gets a scoring bonus it has not earned Both prefixes are shorter than 1, and by different amounts. Scored with a raw dot product, every similarity is multiplied by that surviving norm. Doc B is scored about 12% higher than Doc A on identical relevance, purely because more of its magnitude happened to sit early. Why this bug is dangerous rather than merely wrong Nothing crashes. The dimensions are correct. Scores look plausible on a dashboard. Results are on-topic, just subtly mis-ranked. It surfaces weeks later as “answer quality feels worse” — the same family of silent failure as the missing query prefix in document 05. The rule: truncation and renormalisation belong in the same function, in a shared library. Never expose a truncate() that does not renormalise.

Documents whose meaning is concentrated early get systematically over-ranked; documents with meaning spread later get systematically buried. That has nothing to do with relevance — it is an artefact of where the model happened to put magnitude. Tick the box and watch it disappear.

And it must be identical on both sides

This is the shared-space rule from document 05 wearing a new costume. If documents are stored at 256 dimensions and the query is truncated to 512, or truncated without renormalising while the documents were renormalised, you are comparing vectors from two different spaces again — and it fails exactly as silently.

The defence is the same one: one function, in a shared library, that truncates and renormalises together, used by both paths. Never expose a truncate() that does not renormalise.

9 · The quality curve

How much quality does each halving actually cost? For a model trained at 1024 dimensions, published and community results cluster around the shape below.

THE FIRST HALVING IS NEARLY FREE. EACH ONE AFTER COSTS MORE. 100% 85% 70% dimensions kept typical quality retained at this dimension ~99% quality retained 16 GB of vectors down from 32 GB at full dimension the first halving: nearly free These are typical published figures, not promises. Your corpus decides, and some corpora decide badly. Corpora that lose more: many near-duplicate documents, where fine distinctions are exactly what the trailing dimensions encode; densely technical vocabularies; and very short chunks, where each vector carries less redundancy to spare.

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 is what lets you move inside that band without re-embedding anything.

Dimensions keptBytes per vector 8M chunksTypical quality retained
1024 (full)4,09632 GB100%
5122,04816 GB~99%
2561,0248 GB~97%
1285124 GB~92%
642562 GB~85%
321281 GB~70%

Put it together with section 1 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 trailing dimensions encode and near-duplicates differ only in fine distinctions. So do densely technical vocabularies, and very short chunks where each vector carries less redundancy. Measure retention on your own gold set before committing to a truncation.

10 · Adaptive retrieval, and the pattern behind it

This is the part that earns marks at architect level, because it uses Matryoshka as a system design rather than a storage trick.

The insight: retrieval quality has two separable jobs. Getting the right chunk into the candidate pool is the retriever’s job, and ordering the pool correctly is the reranker’s. Truncated vectors are very good at the first job and only slightly worse at the second.

SHORTLIST WITH THE CHEAP VECTOR. RESCORE WITH THE EXPENSIVE ONE. 1 · embed the query full model, truncate to 256, renormalise — same function 2 · pass one, the wide one graph search over 8M truncated vectors in RAM → ~200 ids, 5–10 ms 3 · pass two, the narrow one fetch 200 full vectors from SSD, 200 exact dot products — 3–8 ms 4 · reordered top 10 a quarter of the RAM, near full-dimension quality The expensive comparison is the one you do 200 times, not 8,000,000 times. That asymmetry is the whole trick. And it is the same pattern four times over Matryoshka adaptive retrieval 256-dim ANN over 8M exact 1024-dim over 200 quantisation with rescoring int8 ANN over 8M fp32 exact over 200 retrieve then rerank bi-encoder over 8M cross-encoder over 100 hybrid retrieval with fusion BM25 + dense over 8M fusion + rerank over 200 cheap approximate wide pass expensive exact narrow pass
  1. Embed the query with the full model, truncate to 256 and renormalise — the same function the ingest path used.
  2. Pass one, the shortlist. Graph search over the 8M truncated vectors in RAM returns about 200 candidate ids in 5 to 10 ms.
  3. Pass two, the rescore. Fetch those 200 full vectors from SSD and take 200 exact dot products against the full query vector. The fetch dominates: 3 to 8 ms on local NVMe.
  4. Return the reordered top 10. A quarter of the RAM, and close to full-dimension quality.

The interview line: “These are all the same architectural pattern — a cheap approximate wide pass feeding an expensive exact narrow pass. Matryoshka applies it to the dimension axis, quantisation to the precision axis, reranking to the model axis.” Saying that shows you understand the system rather than the feature.

Check the latency budget before committing

The rescoring pass adds an I/O hop, and whether that is acceptable depends on your budget. It does not fit when:

Mitigations, in order of how often they work: shrink the shortlist from 200 to 50; cache full vectors for hot chunks in RAM; co-locate the full-vector store on the same node as the shard; and skip the rescore entirely for queries where pass one already shows a clear score gap.

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. In that case, truncate aggressively and let the reranker do the precision work.

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.

11 · Metric choice meets quantisation

One connection to make before leaving this document, because it is the reason the normalisation discipline matters more than it first appears.

SetupBehaviour under quantisation
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 vectorsIdentical 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 rankings shuffle unpredictably

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.

The full treatment of quantisation is document 12. The connection to carry there is that the normalisation invariant is what makes quantisation behave predictably, which is also why the adaptive-retrieval pattern in section 10 and the rescoring pattern in document 12 are the same idea on two different axes.

12 · Symptom → cause

SymptomMost likely causeWhat to check first
Latency jumped from milliseconds to seconds; answers are still right Metric mismatch — the query operator does not match the index operator class The query plan. An index scan should appear; a sequential scan is the alarm
Similarity thresholds that used to work now cut good chunks Normalisation asymmetry, so scores are on a different scale than they were Assert the norm on both paths. Then question the threshold itself — absolute similarity thresholds are fragile
Results are confidently, completely irrelevant Sort direction reversed on a distance operator ORDER BY … ASC on a distance; DESC returns the least similar
Recall dropped after switching to a smaller dimension Truncation without renormalisation, or truncating one side only Whether truncation and renormalisation are in the same function, and whether both paths use it
Some documents are systematically over-ranked Un-renormalised prefixes: documents with magnitude concentrated early get a bonus The distribution of stored vector norms. It should be a spike at 1.0
Quantisation cost more recall than expected Quantising un-normalised vectors, so the damage is uneven Norms before the compression step
Truncation lost far more quality than the published curve suggests A corpus of near-duplicates, or very short chunks Measure retention on your own gold set rather than trusting the curve
Memory is dominated by vectors and nobody can explain why Dimension. It is a straight-line multiplier and nothing else comes close dimensions × 4 × chunks × replicas, before anything else

13 · Interview questions

ArchitectCosine, dot product or Euclidean — which do you use and why?

On normalised vectors it does not matter for quality, because all three produce the same ranking. Cosine on unit vectors is the dot product, and Euclidean distance is a strictly decreasing function of cosine, so sorting by any of them gives the same order.

So I choose on operational grounds. Cosine by default, because it normalises internally and therefore still ranks correctly if a bug ships un-normalised vectors, whereas dot product would be silently wrong. Dot product on pre-normalised vectors if profiling shows the metric is genuinely on the critical path, which it usually is not. And whichever I pick, the index operator class and the query operator have to agree, or the index is bypassed entirely.

ArchitectWhy normalise at all?

Because magnitude is noise for text embeddings — models emit arbitrary magnitudes that say nothing about the text — and because doing the division once at write time is free, whereas doing it inside every comparison of every query is not.

It also makes quantisation behave. Once every vector has the same magnitude, the compression error is uniform across records rather than proportional to length, so the damage is something you can bound. Normalise before you quantise.

ArchitectYour search suddenly takes two seconds instead of twenty milliseconds, and the results are still correct. What happened?

Almost certainly a metric mismatch. The index was built with one operator class and the query is using 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 — it is just doing ten million comparisons to get them.

I would confirm it in the query plan, and then put that assertion in CI: a test that fails if the plan contains a sequential scan. It is the kind of regression that otherwise recurs every time somebody writes a new query.

ArchitectHow many dimensions would you use, and how would you decide?

I would start from the memory arithmetic, because dimension is a straight-line multiplier on the largest line in the budget and no index parameter comes close to it. Ten million chunks at 1536 dimensions in float32 is 61 GB per copy, times the replica count.

Then I would measure. Quality rises steeply to about 512, bends by 1024 and is close to flat above 2048, so the useful band is narrow. I would pick the smallest dimension within noise of the best on my own gold set — and if the model supports Matryoshka truncation, I would treat the choice as reversible and revisit it once the corpus is real.

ArchitectWhat is Matryoshka and why does it matter architecturally?

It is a training objective that computes the loss at several prefix lengths at once, so the model is forced to front-load information: the first 256 dimensions of a 1024-dimension vector are themselves a complete, usable 256-dimension vector.

Architecturally it does two things. It makes the dimension decision reversible — going from 1024 to 512 becomes a local data transformation rather than a full re-embed — and it enables adaptive retrieval: shortlist over truncated vectors in RAM, then rescore the top 200 against full vectors on SSD. A quarter of the memory for close to full quality.

The step people forget is renormalising after truncation. 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.

ArchitectIs the curse of dimensionality a problem at 1536 dimensions?

Not in the way the question implies. The theorem is about random points in uniform high-dimensional space, where distances concentrate and nearest-neighbour search stops meaning anything. Embeddings are not random points — a trained model places related text on a much lower-dimensional manifold inside that space, and it is trained specifically so 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, and that is the real argument for keeping it down.

ArchitectWe are inheriting a system built on Euclidean distance. Should we migrate it to cosine?

Probably not, and I would want to answer it with a measurement rather than a preference. First verify that the vectors are normalised. Then run the gold set under both metrics and compare recall.

If the numbers are identical, do not migrate — zero benefit, full rebuild cost, and a rebuild on eight million vectors is hours of work plus a blue-green cutover. If the numbers differ, 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 managerThe team wants to halve the dimension to cut the infrastructure bill. What do you ask?

Three things. Does the model support Matryoshka truncation — because that changes this from a fortnight-long migration into an afternoon job, and it is the first thing to establish. What does the gold set say retention is on our corpus, rather than on the published curve, because near-duplicate-heavy corpora lose considerably more. And is the truncation and renormalisation in one shared function used by both paths, because if not we will ship a silent ranking bug alongside the saving.

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 of quality.

Eng managerHow do you stop this class of silent bug reaching production?

By turning 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 the ingest and query paths; the query plan uses an index rather than a sequential scan; and gold-set recall stays above a threshold.

The general principle I would push in the team is that retrieval fails quietly and almost everything else fails loudly, so retrieval needs assertions where other subsystems can rely on exceptions. That framing is what gets the tests written, because it explains why they are not optional.

14 · FAQ

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

Verify rather than assume, then assert. Many models do 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 costs nothing and converts a silent ranking bug into a loud ingest failure.

Can I use an absolute similarity threshold, like “only return results above 0.75”?

Cautiously, and never as the only control. Similarity scores are not calibrated: the distribution differs by model, by corpus, by query length and by chunk length, so 0.75 means different things in different systems and drifts when any of those change. Prefer a relative rule — take the top k, or cut where the score gap widens — and if you must use an absolute threshold, derive it from your gold set and re-derive it whenever the model changes.

Does the vector database store normalised vectors, or normalise on read?

It stores exactly what you gave it. The cosine operator normalises during the comparison, which is why it is safe against un-normalised input and slightly slower. What the database will not do is silently rewrite your vectors, so if you want stored unit vectors, your pipeline has to produce them.

Can I truncate a vector from a model that was not trained with Matryoshka?

You can, and you should expect it to degrade badly and unpredictably, because meaning is spread across all dimensions with no ordering. Truncating it is like deleting a random two-thirds of the words from a sentence. The technique depends entirely on the training objective, so check the model card — and if it is ambiguous, measure recall at a couple of prefix lengths before designing around it.

If I truncate to 256, do I still need the full vectors?

Only if you intend to rescore. Adaptive retrieval keeps the full vectors on SSD and reads 200 of them per query, which costs disk rather than RAM. If your gold set shows recall at 256 is already at parity and you have a cross-encoder reranker downstream, you can discard the full vectors entirely and let the reranker handle ordering — which is simpler and often better.

Should the truncated dimension be a power of two?

Not for any mathematical reason, but yes in practice: models are trained with losses at specific prefix lengths, and those are conventionally powers of two. Truncating to 300 on a model trained at 256 and 512 is untested territory that may or may not behave. Stay on the lengths the model card lists.

Does halving the dimension halve the latency?

No, and the reason is worth understanding. Distance computation is linear in dimension, so that part halves, but search time is dominated by how many vectors you touch and by memory bandwidth. Halving the dimension halves the bytes moved, which does help — typically noticeably, but nothing like a factor of two end to end. Treat it as a memory decision that has a pleasant latency side effect.

What is the relationship between cosine distance and cosine similarity?

Distance is 1 − similarity, so pgvector’s <=> returns a number from 0 to 2 where smaller is better and you sort ascending. Most confusion in this area comes from mixing the two in the same codebase — a scoring function that returns similarity, a database that returns distance, and a threshold applied to whichever one happened to be in scope. Pick one convention, name your variables for it, and convert at exactly one boundary.

Is int8 quantisation the same thing as reducing dimensions?

No, and they compose. Quantisation keeps the dimensions and shrinks each number; dimension reduction keeps the numbers and removes dimensions. 1024 dimensions at int8 and 256 dimensions at float32 both come to 1 KB per vector, and they fail differently — quantisation adds uniform noise to every comparison, truncation removes the fine distinctions entirely. Document 12 covers the first properly.

15 · Cheat sheet

The formulas

norm ‖v‖ = √(Σ vᵢ²) — Pythagoras, extended
normalise divide every element by ‖v‖; the new norm is 1
dot product Σ (aᵢ × bᵢ)
cosine (a · b) ÷ (‖a‖ × ‖b‖)
Euclidean √(Σ (aᵢ − bᵢ)²)
the collapse, on unit vectors cos = a·b, and d² = 2 − 2·cos

The numbers

bytes per vector dimensions × 4 for float32
384 / 768 / 1024 / 1536 / 3072 1.5 / 3 / 4 / 6 / 12 KB each
quality by dimension steep to 512, bending by 1024, flat above 2048
Matryoshka retention 512: ~99% · 256: ~97% · 128: ~92% · 64: ~85%
adaptive retrieval 256-dim shortlist 5–10 ms, 200-vector rescore 3–8 ms
cos 0.9 = distance 0.447 · cos 0.5 = 1.000 · cos 0 = 1.414

The one-liners

The ninety-second version

“Dimension is the highest-leverage number in the memory budget, because bytes per vector is just dimensions times four and that multiplies by the corpus and by every replica. Quality rises steeply to about 512 and is flat above 2048, so the useful band is narrow — and the memory bill does not flatten at all.

For metrics: normalise at ingest, and then cosine, dot product and Euclidean all produce the same ranking, because cosine on unit vectors is the dot product and distance is a monotonic function of cosine. So the metric is an operational choice — I use cosine because it is safe against an un-normalised vector slipping through, and I make sure the query operator matches the index operator class, because a mismatch silently falls back to a full scan that returns correct answers slowly.

If the model is Matryoshka-trained I treat dimension as reversible: truncate the prefix and renormalise — always in the same function, because prefixes have different surviving norms and skipping the renormalise gives some documents an unearned ranking bonus. And then adaptive retrieval: shortlist over 256-dimension vectors in RAM, rescore the top 200 against full vectors on disk. A quarter of the memory for close to full quality, and it is the same cheap-wide, expensive-narrow pattern as quantisation with rescoring and as reranking.”

Where this connects

Thread from this documentResolved in
Choosing the model that decides the dimension in the first place 05 · Choosing an embedding model
Changing the dimension when the model is not Matryoshka-trained 08 · Fine-tuning, versioning and migration
Why the metric is baked into the index at build time 09 · Flat, IVF and HNSW
Quantisation, rescoring, and the memory stack in full 12 · Quantisation and capacity
The reranker that makes an aggressive truncation safe 15 · Hybrid retrieval and reranking
Measuring retention on your own corpus 16 · Evaluation and observability

Questions to ask them