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 08 of 16 · Track B — Embeddings and index

Track B · Document 08 · Embeddings and index

Fine-Tuning, Versioning and Migration

Teaching a model what “similar” means in your domain, deciding whether it should be the embedder or the reranker, and changing either one without an outage.

Reads in about 35 minutes · 11 figures, 1 interactive · 10 interview questions · prints to clean A4

What is in this document

  1. What fine-tuning actually means
  2. This is not LoRA territory
  3. The training loop
  4. Where the pairs come from
  5. Hard negatives
  6. Why fine-tune, and what first
  7. Embedder or reranker?
  8. What it costs afterwards
  9. Why there is no partial migration
  10. The parallel table
  11. The ordering rule
  12. Validation, cutover, rollback
  13. The model stamp
  14. Symptom → cause
  15. Interview questions
  16. FAQ
  17. Cheat sheet

1 · What fine-tuning actually means

You take a model already trained on a very large amount of general text, and you continue training it — on your own examples, for a short while, with a small learning rate. The examples are pairs: a real question, and the chunk that correctly answers it.

PAIR
  question:  "why is my widget bricked after the firmware push"
  chunk:     "Device fails to initialise following an OTA update.
              Recovery procedure: hold the reset pin for..."

The model’s job after training is to place those two things near each other in vector space, and to place wrong chunks further away.

SAME ARCHITECTURE, SAME DIMENSION, SAME TOKEN LIMIT — DIFFERENT NUMBERS INSIDE the base model trained on a very large amount of general text continue training, briefly your pairs, small learning rate, 1 to 3 epochs a different set of weights and therefore a different geometry — a different answer to the question “what is similar to what?” You are NOT teaching it facts It will not “know” your products, prices or policies. Facts live in your chunks. That is the premise of RAG, and fine-tuning does not change it. You ARE teaching it what “similar” means here That “bricked” and “fails to initialise” are the same event. That ERR_5521 and ERR_5522 are not the same event. The consequence that dominates everything after this: every vector you have already computed is now wrong. They were produced by the old geometry. The corpus must be re-embedded in full — which is why the second half of this document exists.

The model is a similarity function, and fine-tuning reshapes that function. Interviewers listen specifically for the distinction on the left: candidates who think fine-tuning teaches the model facts have misunderstood what RAG is for.

2 · This is not LoRA territory

A common confusion, worth clearing early because it changes what the work looks like. LoRA and its relatives were built for large language models — seven billion parameters and up — which are too large to fully fine-tune on ordinary hardware.

WHY LORA IS AN ANSWER TO A PROBLEM YOU DO NOT HAVE all-MiniLM-L6-v2 ~22 million parameters bge-base / e5-base class ~110 million bge-large / e5-large class ~335 million a small language model, for contrast ~7,000 million At a hundred million parameters you can fine-tune the whole model on one GPU, often in well under an hour for a few thousand pairs. Embedding models are small deliberately: one runs over every chunk in the corpus at ingest and over every query at serve time, forever. Nobody can afford a seven-billion-parameter one in that position.

Interview-safe phrasing: “Embedding models are small enough for full fine-tuning, so LoRA is usually unnecessary — it is an answer to a memory constraint that does not apply at this scale.” The honest caveat is that parameter-efficient methods can be applied, and there are cases with very large embedders or many tenant-specific variants where that is sensible.

3 · The training loop, concretely

“Training” is a large word. Here is exactly what happens, one batch at a time.

ONE GRADIENT STEP. NOTHING MORE EXOTIC THAN THIS. anchor — a real question positive — the chunk that answers it negative 1 negative 2 … and so on similarity of the anchor against each 0.61 the positive 0.74 a negative — outranking it 0.33 a negative is ahead of the positive, so this step will produce a large adjustment Adjust: the weights move in the direction that widens the gap — positive pulled in, negatives pushed out. Then repeat. Each nudge is tiny; the accumulation over hundreds of batches is what shifts the geometry. The only unusual part is the question the loss asks. Ordinary supervised learning asks “was the output correct?” and needs a correct answer. This asks “was the right chunk ranked above the wrong ones?” and needs only an order — which is exactly why it works for retrieval, where no target vector exists.
  1. Step input. An anchor (a real question), its positive (the chunk that answers it) and several negatives, all embedded by the current model.
  2. Score. Similarity of the anchor against each. Here a negative outranks the positive, so the adjustment will be large; a comfortable win would produce a tiny nudge.
  3. Adjust. Weights move in the direction that widens the gap: positive pulled in, negatives pushed out.
  4. Repeat. Hundreds of batches accumulate into a shifted geometry. The loss asks a ranking question rather than a correctness question, which is why it suits retrieval.

You are not training the model to produce a particular vector — no target vector exists. You are training it to produce vectors whose relative distances come out in the right order.

The loss, and why batch size is not a speed knob

The standard choice for retrieval fine-tuning supplies only positive pairs and takes the negatives free, from the rest of the batch.

5 negatives per anchor
SUPPLY ONLY POSITIVE PAIRS. THE NEGATIVES COME FREE, FROM THE REST OF THE BATCH. Every row is one anchor scored against every chunk in the batch. The diagonal cell is its own positive — the chunk that answers it. Every off-diagonal cell is a negative, and it cost nothing to obtain. So batch size is not a speed knob here. It is the number of negatives per example, which makes each step a harder and more informative comparison. batch 16 → 15 negatives per anchor batch 64 → 63 batch 256 → 255 This is the opposite of the usual advice about batch size, and it is a good detail to know. Where memory limits it, a gradient-caching variant gets large effective batches on modest hardware.

The workhorse loss for retrieval fine-tuning needs only positive pairs — a question and the chunk that answers it. That is what makes the data collection tractable: you never have to label what is wrong, only what is right.

LossWhen you use it
In-batch negatives (MultipleNegativesRanking, InfoNCE-family) The default for retrieval. Needs only question-and-correct-chunk pairs
The same, with explicit hard negatives When you can supply a known-wrong-but-plausible chunk per pair. Usually the single biggest quality gain
Triplet lossAnchor, positive and negative supplied explicitly. Older style, still fine
Cosine-similarity / CoSENT losses When your labels are graded scores rather than right-or-wrong. Rare in RAG
A Matryoshka wrapper Combine with any of the above to preserve truncatable dimensions — see document 06
learning rate small — around 2e-5 is the usual start; too high and the model forgets its general ability
epochs 1 to 3; more than that and it memorises
batch size as large as memory allows, because it is the negative count

4 · Where the pairs come from

You need thousands. Realistically, a few thousand good pairs beats tens of thousands of poor ones — and the difference between good and poor is almost entirely about whether the questions are real.

YOU NEED THOUSANDS. A FEW THOUSAND GOOD PAIRS BEATS TENS OF THOUSANDS OF POOR ONES. 1 · production feedback real questions, real language, a known correct chunk free, and the only genuinely representative source 2 · existing artefacts resolved tickets, FAQs, search logs with click-through already in the building, usually unnoticed 3 · generated a model writes questions for each chunk unlimited, and not how users talk The trap in source 3 generated: "What is the default timeout value for the Payments API, and how can it be configured?" real user: "payment timeout too short" Train on the first and evaluate on the first, and both numbers look excellent while production does not move. If your training data and your evaluation data are both synthetic, you are measuring the generator. Generated pairs are acceptable for training and unacceptable for evaluation. And one signal that is not a signal: “the chunk was retrieved”. Using what the current system retrieved as ground truth trains the new model to imitate the old one, mistakes included.

The quality bar, in order: an explicit thumbs-up with the chunk known is a strong pair; the user copying the answer without rephrasing is a good one; the session merely ending normally is weak; and the chunk having been retrieved at all is not evidence of anything — retrieval is the thing you are trying to fix.

5 · Hard negatives: the part that decides quality

If one thing separates a fine-tune that works from one that does not, it is the negatives — and the mechanism is that you are explicitly training against the current model’s mistakes.

QUESTION: “HOW DO I ROTATE THE API SIGNING KEY?” an EASY negative a chunk about the office parking policy the model already knows this is unrelated, so it learns nothing a HARD negative a chunk about rotating the database password plausible, adjacent, wrong — and it teaches a real distinction How they are mined 1 · retrieve top 50 with the current model 2 · drop the known correct chunk 3 · keep what remains as hard negatives you are training against the current model’s mistakes Step 3 assumes exactly one chunk is correct. Often several are. labelled: chunk 41 also correct: chunk 42, the same procedure on a different page mine naively → chunk 42 becomes a “hard negative” → you train the model to push away a correct answer The guards Cap the similarity of mined negatives — discard anything above roughly 0.95 against the positive — or have a stronger model or a human check a sample. Unchecked mining can make a model worse while every training curve looks healthy.
  1. Easy versus hard. A chunk about the parking policy teaches nothing; a chunk about rotating the database password is plausible, adjacent and wrong, and teaches a real distinction.
  2. How they are mined. Retrieve the top 50 with the current model, drop the known correct chunk, and keep what remains — so you are explicitly training against the current model’s mistakes.
  3. The false-negative danger. Mining assumes exactly one chunk is correct. When several are, a correct answer becomes a “hard negative” and you train the model to push it away.
  4. The guards. Cap mined-negative similarity at about 0.95 against the positive, or sample-check with a stronger model or a human.

If one thing separates a fine-tune that works from one that does not, it is the negatives. Random in-batch negatives are mostly easy — which is why they are free, and why they eventually stop teaching.

6 · Why fine-tune, and what to do first

There is one good reason: your domain uses language differently from the general internet, and no off-the-shelf model has ever seen that usage.

USER SAYS         DOCS SAY                        A GENERAL MODEL
"bricked"         "fails to initialise"           no idea these match
"the box"         "edge appliance unit 3"         no idea
"flapping"        "intermittent link state"       no idea
"P1"              "Severity 1 incident"           no idea

A general model learned similarity from the public web, and your internal shorthand was not on the public web. That gap is real, and fine-tuning closes it in a way prompt engineering cannot. Typical reported gains on genuinely domain-specific corpora sit around five to fifteen points of recall@100, with wide variance — and on a corpus of ordinary business prose, expect much less, sometimes nothing.

EVERY RUNG BELOW IS CHEAPER, FASTER AND REVERSIBLE 1 Fix the chunking free 2 Prepend the section path before embedding free 3 Add hybrid retrieval for exact-token queries a deploy 4 Add an off-the-shelf reranker a deploy — try this before 6 or 7 5 Try a different off-the-shelf embedding model a migration 6 Fine-tune the reranker you now own a model 7 Fine-tune the embedding model re-embed everything, forever

Rungs 1 to 5 are things you configure. Rungs 6 and 7 are things you own. That is the real boundary, and it matters far more than the technical difficulty, which is roughly the same for both. The one good reason to cross it: your domain uses language differently from the general internet, and no off-the-shelf model has ever seen that usage.

7 · Embedder or reranker?

This is the middle option most candidates never mention, and it is usually the right answer. The two stages have the same training data and comparable effort, and completely different consequences.

TWO STAGES, TWO MODELS, VERY DIFFERENT CONSEQUENCES STAGE 1 · retrieval, over 8M chunks the bi-encoder made the vectors earlier; the search itself is a distance computation decides recall@100 — what makes it into the pool STAGE 2 · reranking, over the top 100 the cross-encoder runs live, reading question and chunk together and emitting a relevance score decides NDCG@10 — the order within the pool re-embed the corpus yes, all of it no rebuild the index yes no time to revert hours — re-embed again with the old model seconds — swap a model file blast radius if wrong the whole index one ranking stage can fix a chunk missing from the pool yes no — it never sees it The diagnostic: is recall@100 low, or is recall@100 fine while NDCG@10 is poor? The first is the embedder’s problem. The second is the reranker’s.

That single diagnostic is the professional answer to “which one do I fine-tune?” — and the reranker is the middle option most candidates never mention. Same training data, comparable effort, no re-embed, and a rollback measured in seconds.

Why the cross-encoder is more accurate, stated precisely

Because it sees both texts at once, its attention can relate a word in the question directly to a word in the chunk. A bi-encoder cannot: each side was compressed into a vector in isolation, before the other side existed.

That is also exactly why it cannot be used for search. Scoring eight million chunks per query with a model is not a system, it is an outage.

8 · What it costs after the training run finishes

The training is the cheap part — often under an hour on one GPU. Four costs follow it, and three of them are permanent.

CostWhat it means
A full re-embed, immediately New weights mean new geometry, so every stored vector is now meaningless. On eight million chunks that is 2.8 billion tokens through the model and a blue-green reindex. There is no partial migration — mixing old and new vectors is not degraded retrieval, it is random retrieval for the mixed portion
Retraining, forever Content changes, vocabulary changes, new products arrive with new jargon. A model fine-tuned on last year’s language slowly stops matching this year’s questions
An upgrade tax Every future base-model improvement now has a retraining step attached before you can take it. You have opted out of simply adopting the next good thing
Ownership Weights, training data, evaluation, reproducibility and the person who understands all of it. That is a standing commitment, not a project

The question to ask before starting

“Who owns retraining this in eighteen months?” If the honest answer is nobody, the fine-tune is a liability with a quality benefit attached rather than the other way round — and the reranker option in section 7 gets most of the benefit with none of that obligation.

9 · Why there is no partial migration

Everything in the second half of this document follows from one fact: a vector from model A and a vector from model B cannot be compared. Position 500 in the list encodes something that was decided during that model’s training, by that model, for itself.

The arithmetic still runs. Both vectors have 1024 numbers, so a dot product is perfectly computable, returns a valid number in the usual range, and carries no information about meaning whatsoever.

A NAIVE INCREMENTAL MIGRATION, HALFWAY DONE 4 million rows embedded with model B 4 million rows still embedded with model A one table, two geometries, no error anywhere against the B half the scores mean something. Same space, real similarity. against the A half the scores are noise — and noise sometimes scores high. The top ten interleaves real results with arbitrary chunks. Every dashboard is green. error logs clean · latency unchanged · result count unchanged · score values in the usual range And the complaint arrives, three layers downstream, as “the assistant has started making things up”.
  1. Halfway. Four million rows embedded with model A, four million with model B, in one table.
  2. A query arrives, embedded with model B. Against the B half the scores mean something; against the A half they are noise — and noise sometimes scores high.
  3. The top ten interleaves real results with arbitrary chunks. Every dashboard is green, and the complaint arrives as a language-model quality problem three layers downstream.

The distinction interviewers listen for: this is not degradation, it is noise. Degradation means the right answer is present and ranked lower — recoverable and detectable. Noise means the right answer may be absent, and what replaced it has nothing to do with the query. “It gets worse” is a weak answer; “it returns arbitrary results with no error” is the correct one.

The rule

One table, one model, one version. Always. There is no valid partial state, no “mostly migrated”. A table is either entirely model A or entirely model B — which is why the migration has to happen somewhere other than the live table.

10 · The parallel table, and the four phases

Two tables coexist. Each is internally consistent — one model, one version, neither mixed — and traffic goes to exactly one of them at a time. This is blue-green, and model migration is its primary reason for existing.

phase 1dual-write on. Live edits are written to both tables. A config flag and a second write path in one function
phase 2backfill. A bulk job walks the existing chunks, embeds each with the new model, and writes into the new table
phase 3shadow read and compare. Real queries hit both; only the old table’s answers are served
phase 4flip the alias, and keep the old table warm and still written to

Backfill and dual-write handle two different halves of the corpus

Backfill handles the past — the content that already existed when the migration started. It is a one-off batch job that runs for hours or days.

Dual-write handles the present — everything that changes during the migration. It is not a job at all; it is a change to the live ingest pipeline.

Together they cover the whole corpus. Neither alone does, and confusing the two is how a migration ends up with a hole in it.

11 · The ordering rule

This is the single most important operational detail in a model migration, and it costs nothing to get right and a great deal to get wrong.

THE WRONG ORDER — BACKFILL STARTS, DUAL-WRITE FOLLOWS Mon 09:00 Tue 14:00 Wed 08:00 backfill walking the corpus, chunk by chunk dual-write on backfill passes document 88 document 88 is edited — and the edit reaches only the old table Every document edited inside the build window after the backfill passed it is permanently stale in the new table, with nothing to reveal it. On a large corpus over a two-day window, that is thousands of documents — silently wrong, in a table you are about to cut over to. THE RIGHT ORDER — DUAL-WRITE ON FIRST, THEN BACKFILL Mon 08:00 Tue 14:00 Wed 08:00 dual-write on — every live edit reaches both tables, throughout backfill walking the corpus backfill passes document 88 document 88 is edited — and the edit is written to both tables. Caught. Some chunks now get written twice — once by the live edit, once by the backfill passing over them later. That is fine, and it is intentional. The writes are idempotent upserts keyed on chunk id, so writing chunk 4182 twice leaves one row with the correct content. Cost of the overlap: a few redundant embeddings. Cost of the gap: permanently wrong rows, undetectable. You would rather write something twice than miss it once.

This is the single most important operational detail in a model migration, and the ordering is not cosmetic. Backfill handles the past; dual-write handles the present. Together they cover the whole corpus, and neither alone does.

The one hazard the overlap introduces: resurrection

A subtle race, and knowing it is a strong signal. A document is deleted while the backfill is running. The backfill, working from a snapshot taken before the delete, writes it into the new table anyway — and the deleted document is now live in the collection you are about to cut over to.

Three guards, and the third catches everything:

  1. The backfill reads a tombstone list and skips deleted ids at write time.
  2. The backfill takes a watermark timestamp and never overwrites a row whose last-written time is newer than the watermark.
  3. A reconciliation pass after the backfill: compare the id sets between the two tables and remove anything present in the new one and absent from the old. This is the simplest and it also catches bugs in the first two.

12 · Validation, cutover and rollback

Never switch on offline metrics alone. Offline metrics have been wrong before, and shadow reads give you thousands of paired result sets from genuine production queries rather than a gold set of a few hundred.

NEVER SWITCH ON OFFLINE METRICS ALONE. OFFLINE METRICS HAVE BEEN WRONG BEFORE. the query service → an alias chunks_v1 — model A serving 100% of traffic chunks_v2 — model B backfilled and dual-written; nobody reads it yet 100% shadow — real queries, answers discarded 100% compare six things, not one gold-set recall and NDCG · per document type · top-10 overlap rank movement of known-good chunks · latency p50/p95/p99 · score distribution the overlap signal Agreement on eight or nine of the top ten means low risk. Low overlap means more than a model swap happened — find out what, first. Cutover is repointing the alias — seconds, atomic, and no code deploy. Keep dual-writing to v1 afterwards, or your rollback window closes silently over the next few hours.
  1. v1 serves everything while v2 is backfilled and dual-written to.
  2. Shadow read. Send real production queries to both, serve only v1’s answers, and compare offline — on gold-set recall and NDCG, per document type, top-10 overlap, rank movement of known-good chunks, latency percentiles and score distribution.
  3. Read the overlap signal. Agreement on eight or nine of the top ten means the change is low risk. Low overlap means something more than a model swap has happened.
  4. Flip the alias. Seconds, atomic, and deliberately not a code deploy — a deploy is slow to reverse and a config change is not.
  5. Keep v1 warm and still dual-written to, for at least one full incident cycle. Stop writing to it at cutover and it goes stale immediately, closing your rollback window without telling you.

Deleting the old table at cutover is the most common and most expensive mistake in a migration. Keeping it costs storage; not keeping it is the difference between a recoverable regression and an outage.

The decommission checklist

cutover complete for at least one full incident cycle — one to four weeks depending on traffic
no quality regressions reported or detected
gold-set scores stable on the new table
dual-write to the old table turned off
a documented path to rebuild it if ever needed
and only then: drop the table

13 · The model stamp

Everything above assumes you can tell which model produced a given vector. You cannot, unless you recorded it — and if you did not record it at write time, the information is gone.

a model-A vector:  1024 floats, normalised, norm 1.0
a model-B vector:  1024 floats, normalised, norm 1.0

IDENTICAL in every inspectable property.
There is no signature, no header, no watermark.
FieldWhy it is there
model_nameWhich model
model_versionWhich version of it — fine-tunes especially, because the name does not change
dimensionCatches a truncation or configuration mismatch
embedded_atLets you scope a repair to an ingest window
normalisedMakes the invariant from document 06 auditable rather than assumed

Two records, two purposes: the table declares its version and each row stamps the model that produced it. They exist to be compared against each other, and one query is the entire detection mechanism for the failure in section 9:

SELECT model_name, model_version, count(*)
FROM chunks_v2
GROUP BY 1, 2;

healthy  →  exactly one row returned
broken   →  two or more rows, and you know immediately

That query costs nothing and it should run on a schedule. It is the difference between finding a mixed table in an hour and finding it in a hallucination ticket six weeks later.

14 · Symptom → cause

SymptomMost likely causeWhat to check first
Results are arbitrary for some queries and fine for others; no errors anywhere A mixed table — two models in one collection The group-by on model name and version. It should return exactly one row
The new table is missing recent edits after the backfill finished Backfill was started before dual-write The timestamps: when dual-write was enabled versus when the backfill began
A deleted document reappeared during migration The resurrection race — the backfill worked from a pre-delete snapshot Whether a reconciliation pass ran after the backfill
Fine-tuning made retrieval worse, and every training curve looked healthy False negatives in the mined hard negatives — you trained the model to push away correct answers The similarity cap on mined negatives, and a human sample of twenty of them
Offline metrics improved a lot; production did not move at all Training and evaluation are both synthetic, so you measured the generator Where the evaluation questions came from. If a model wrote them, that is the answer
The fine-tune helped for a quarter and then decayed Vocabulary drift — new products, new jargon, new phrasing Whether anybody owns retraining, and when it last ran
Rollback was impossible after a bad cutover The old table was dropped, or dual-write to it stopped at cutover so it went stale Whether writes to the old table continued after the alias flip
Recall@100 is unchanged but answers are better after a change You fixed ordering, not retrieval — which is the reranker’s job Nothing is wrong. Note it: it confirms the diagnostic and tells you which model to invest in next

15 · Interview questions

ArchitectWhen would you fine-tune an embedding model?

When the domain uses language differently from the general internet and no off-the-shelf model has seen that usage — users saying “bricked” where the docs say “fails to initialise”. That gap is real and prompt engineering cannot close it.

But I would walk the ladder first, out loud, because everything below it is cheaper and reversible: fix the chunking, prepend the section path, add hybrid retrieval if the losses are on identifiers, add an off-the-shelf reranker, try a different base model. Only then fine-tune — and even then, probably the reranker rather than the embedder.

ArchitectWhat exactly does fine-tuning teach the model?

Not facts. It will not know your products, prices or policies — those live in the chunks, which is the entire premise of RAG. What it teaches is what “similar” means in your domain: that “bricked” and “fails to initialise” are the same event, and that ERR_5521 and ERR_5522 are not.

The model is a similarity function and fine-tuning reshapes that function. And the consequence is immediate: every vector you have already computed came from the old geometry and is now wrong.

ArchitectEmbedder or reranker — which do you fine-tune?

There is a single diagnostic that answers it. Look at recall@100 against NDCG@10. If recall@100 is low, the right chunk is not reaching the candidate pool at all, and only the embedder can fix that. If recall@100 is fine and NDCG@10 is poor, the right chunk is in the pool and badly ordered — that is the reranker’s job.

And the reranker is far cheaper to be wrong about: no re-embed, no rebuild, rollback by swapping a file, and the blast radius is one ranking stage rather than the whole index. So where both would work, I would do the reranker.

ArchitectWhere do the training pairs come from, and what is the trap?

Three sources, cheapest first: production feedback, which is the only genuinely representative one; existing artefacts like resolved tickets, FAQs and click-through logs, which most organisations already have; and generated pairs, where a model writes questions for each chunk.

The trap is in the third. Generated questions are phrased the way a language model phrases things — “What is the default timeout value for the Payments API?” — and real users type “payment timeout too short”. Train on synthetic and evaluate on synthetic and both numbers look excellent while production does not move. Generated pairs are acceptable for training and unacceptable for evaluation.

ArchitectWhy can you not migrate to a new embedding model incrementally?

Because a vector from one model and a vector from another cannot be compared. The arithmetic runs perfectly — both are 1024 numbers, the dot product returns a valid score in the usual range — and it carries no information about meaning.

And it is worth being precise that this is noise, not degradation. Degradation means the right answer is present and ranked lower, which is recoverable and detectable. Noise means the right answer may be absent and what replaced it has nothing to do with the query. So there is no valid partial state: one table, one model, one version, and the migration happens in a parallel table.

ArchitectWalk me through the migration.

Dual-write on first, then backfill, then shadow read, then flip an alias. The ordering of the first two is the part that matters: if the backfill starts first, every document edited after the backfill has passed it is permanently stale in the new table, with nothing to reveal it.

Dual-write handles the present, backfill handles the past, and the overlap is deliberate — some chunks get written twice, the writes are idempotent upserts, and you would rather write something twice than miss it once. Then shadow-read real queries against both and compare on gold-set recall per document type, top-ten overlap and latency, before repointing the alias. And keep the old table warm and still dual-written to afterwards.

ArchitectHow would you know a table had two models in it?

Only if you stamped it, because the vectors themselves are identical in every inspectable property — same dimension, same norm, no signature. So every row carries the model name and version, the table declares its expected version, and a scheduled group-by compares them. One row returned is healthy; two or more and you know immediately.

That single query is the whole detection mechanism, and without the stamp there is no detection mechanism at all.

Eng managerThe team wants to fine-tune. What do you ask before approving it?

Four questions. What does the gold set say we are losing on, and is it the kind of loss a fine-tune fixes rather than an identifier problem hybrid retrieval would fix? What have we already tried from the cheap end — specifically, has anyone put an off-the-shelf reranker in front of it? Would fine-tuning the reranker get most of the gain without the re-embed? And who owns retraining this in eighteen months?

That last one is the one that changes the decision most often. A fine-tune is not a project, it is a standing commitment: weights, training data, evaluation, reproducibility, and an upgrade tax on every future base model. If nobody owns it, we are buying a liability with a quality benefit attached.

Eng managerHow do you plan a model migration as a piece of work?

As a project with a rollback, not a version bump. Concretely: build the alias layer first if it does not exist, because it is an afternoon and it is what makes cutover reversible. Then dual-write, then backfill sized against the rate limit rather than the token cost — on a large corpus that is the constraint, and it competes with live ingest.

I would budget for two full collections resident for the duration, get the shadow comparison built before the backfill finishes rather than after, and set the decommission gate at one full incident cycle. The failure mode I would guard against hardest is somebody dropping the old table at cutover to reclaim the storage.

Eng managerHow do you tell whether the fine-tune actually worked?

On a held-out set of real questions that never entered training, measured per query type rather than on the average — because a fine-tune that helps conceptual queries and hurts identifier queries can show a flat mean while making the product worse for a specific team.

Then shadow reads before cutover, and after cutover the same production signals as any other change: click-through, thumbs, escalation rate. And I would want the pre-fine-tune numbers recorded, because the most common failure here is having no baseline and therefore no way to answer the question at all.

16 · FAQ

How many pairs do I actually need?

A few thousand is a reasonable working target, and quality dominates quantity — two thousand real questions from production beat twenty thousand generated ones. Below a few hundred you are unlikely to shift the geometry meaningfully and very likely to overfit. If you cannot reach a few thousand real pairs, that is itself an argument for the reranker route or for waiting until you have collected them.

Can I fine-tune and keep Matryoshka truncation working?

Yes, by wrapping your loss in the Matryoshka objective so the training computes it at several prefix lengths. If you fine-tune with an ordinary loss on a Matryoshka-trained base model, you should expect the truncation property to degrade — the model is no longer being asked to keep every prefix useful. It is a one-line change at training time and an expensive discovery later.

Does fine-tuning change the dimension or the token limit?

No. Same architecture, same dimension, same limit — only the weights differ. That is convenient operationally, because your schema and your capacity arithmetic are unchanged. It is also exactly what makes the failure dangerous: a fine-tuned vector is indistinguishable from a base-model vector in every inspectable property, which is why the model stamp has to carry a version and not just a name.

Can I A/B test the new model instead of shadow reading?

You can, and it answers a different and better question — it measures outcomes rather than rankings. The reasons to shadow read first are that it is risk-free, it needs no traffic split, and it gives you paired results for the same query, which an A/B test does not. The strong sequence is shadow read to establish that nothing collapsed, then an A/B test to measure whether anybody is better off.

What if the backfill will take a week?

Then dual-write for a week, which is exactly what it is for — and check the rate limit rather than the cost, because the wall clock is the binding constraint. Two practical mitigations: request a temporary quota increase, since vendors will often grant one for a migration; and prioritise the backfill by document popularity so the highest-traffic content is correct in the new table first, which shortens the window in which a rollback would actually matter.

Is it safe to fine-tune on top of an already fine-tuned model?

Technically yes, and it accumulates risk you cannot easily see. Each round moves further from the base model’s general ability, and the drift is hard to measure because your evaluation set has usually drifted with it. The disciplined pattern is to keep the base model and the full training set, and retrain from the base each time rather than stacking — which also means a base-model upgrade is a retrain rather than a restart.

How do I decide the similarity cap for mined hard negatives?

Start around 0.95 and then look at what it excluded. Take fifty mined negatives just under the cap and read them: if you find genuine answers among them, the cap is too high for this corpus. Corpora with near-duplicate documents — policy versions, product variants, templated reports — need a lower cap, because “plausible and wrong” and “correct on a different page” look almost identical to a similarity score.

Do I need a GPU in production after fine-tuning?

You need somewhere to run the model, which you already did — fine-tuning changes the weights, not the serving requirement. What changes is that you can no longer use a hosted API for that model, so if you were using one, fine-tuning has just moved you to self-hosting for the query path as well as the ingest path. That is a real operational consequence and it belongs in the cost comparison from document 05 before you start.

17 · Cheat sheet

The numbers

embedding model size 22M to 335M parameters — small enough for full fine-tuning on one GPU
training pairs a few thousand good ones; quality beats quantity
learning rate / epochs ~2e-5 / 1 to 3
batch size as large as memory allows — it is the negative count
hard-negative cap discard mined negatives above ~0.95 similarity to the positive
typical gain 5–15 points recall@100 on a genuinely domain-specific corpus; often nothing on ordinary prose
keep the old table one full incident cycle — one to four weeks

The one-liners

The ninety-second version

“Fine-tuning an embedder teaches it what similar means in your domain — that ‘bricked’ and ‘fails to initialise’ are the same event. It does not teach it facts; facts stay in the chunks. The models are small, a hundred million parameters or so, so this is a full fine-tune on one GPU rather than anything LoRA-shaped.

But I would go down the ladder first, because everything below is cheaper and reversible: chunking, section-path prefixing, hybrid retrieval for identifiers, an off-the-shelf reranker, a different base model. And there is a diagnostic that decides which model to tune: if recall@100 is low the right chunk never reaches the pool and only the embedder fixes that; if recall@100 is fine and NDCG@10 is poor, the reranker is the answer — and the reranker needs no re-embed and rolls back in seconds.

If we do change the embedder, it is a migration rather than a version bump, because vectors from two models cannot be compared — mixing them is noise, not degradation. So: a parallel table, dual-write on before the backfill starts, shadow-read real queries against both and compare per document type, then flip an alias. Keep the old table warm and still dual-written to for a full incident cycle, because dropping it is the expensive mistake.”

Where this connects

Thread from this documentResolved in
Rebuilding from the canonical store rather than re-crawling 02 · Parsing hard content
Blue-green, dual-write and the alias layer in general 03 · Identity, updates and deletes
Why cross-model vectors are noise, and the shared-space rule 05 · Choosing an embedding model
Keeping truncatable dimensions through a fine-tune 06 · Dimensions, metrics and Matryoshka
The reranker as a stage, and hybrid retrieval for identifiers 15 · Hybrid retrieval and reranking
The gold set, held-out splits and per-slice measurement 16 · Evaluation and observability

Questions to ask them