Change an embedding model
You have a working generation and you are changing the model. The new vectors go in a new column, beside the old ones, and your queries switch when you say so.
Copy the specification and change two things
Section titled “Copy the specification and change two things”model: identifier: text-embedding-3-small # was: bge-small-en reported_dimension: 1536 # was: 384target: column: embedding_v2 # was: embeddingThe model change is what you set out to do. The column change is what makes it possible: two generations over one table need two columns, because a new generation that overwrote the old one would leave you nothing to go back to.
prepare refuses if you forget:
error: column "embedding" on articles holds generation 31122cc8322d, and thisrun is generation 9aef540a9038: a generation writes its own column so theprevious one is still there to go back to. Give this one its own target.columnin the specificationKeep the previous specification file. You will need it to maintain the previous generation during the stabilization window.
Check what changed
Section titled “Check what changed”ptah inference plan --spec spec-v2.yaml --db-url "$DB" --current <previous-generation>--current tells the plan which generation queries read now, so it can describe
the change rather than a fresh build. ptah inference status on the previous run
prints the generation identity to pass.
Build it
Section titled “Build it”Same sequence as a first generation, with a new run identifier. New is the part that matters: a run records the generation it was prepared for, and every verb refuses a run prepared for a different one.
run 2026-08-31-v2 is for generation 547ab65200da and this specificationproduces b115a08fbd46Reuse the previous generation’s run id and you meet that refusal at prepare,
before anything is written. It is a refusal rather than a warning because the
alternative was silent: the second prepare used to add its columns and
register its generation, then say “leaving it as it is”, and the backfill
after it resumed the first generation’s finished cursor and reported rows it had
not embedded.
export RUN=$(date +%Y-%m-%d)-v2
ptah inference prepare --spec spec-v2.yaml --db-url "$DB" --run-id "$RUN"ptah inference backfill --spec spec-v2.yaml --db-url "$DB" --run-id "$RUN"ptah inference catchup --spec spec-v2.yaml --db-url "$DB" --run-id "$RUN"ptah inference index --spec spec-v2.yaml --db-url "$DB" --run-id "$RUN"Both columns now hold vectors. Your application is still reading the first, and nothing about the second has affected it.
Compare before switching
Section titled “Compare before switching”This is the step worth not skipping. The new model is not automatically better for your data.
ptah inference evaluate --spec spec-v2.yaml --db-url "$DB" \ --corpus corpus.yaml \ --baseline <previous-generation> --baseline-spec spec-v1.yaml \ --max-ndcg-regression 0.02The corpus file is described in Evaluation corpus reference.
--baseline-spec is the previous generation’s own specification file, and it is
required with --baseline. Scoring a generation embeds every query with its
model and searches its column; a generation identity carries neither, so the
identity alone names the comparison and the file is what makes it. Ptah refuses
--baseline without it, and refuses a file whose identity is not the one named.
With both, the evaluation compares the two generations over the same
questions and refuses when the new one is worse by more than you allowed. The
numbers it reports carry the query parameters they were taken under, because
recall measured at one ivfflat.probes setting is not comparable to recall
measured at another.
Verify and cut over
Section titled “Verify and cut over”ptah inference verify --spec spec-v2.yaml --db-url "$DB" --run-id "$RUN"ptah inference cutover --spec spec-v2.yaml --db-url "$DB" --run-id "$RUN"# prints the plan digest, then:ptah inference cutover --spec spec-v2.yaml --db-url "$DB" --run-id "$RUN" \ --approve <digest> --approver "your name" --stabilize-for 24h--stabilize-for is what makes a rollback possible at all. It is not enough on
its own — see Rollback and retire for what keeping the
previous generation current actually requires.
Then change your application
Section titled “Then change your application”The pointer moved; your SQL did not. Deploy the change that reads
embedding_v2, with the operator matching the new metric if it changed.
Order matters here, and both orders are defensible:
- Cut over, then deploy. The window between the two is a window in which your application reads the old generation, which is still correct.
- Deploy behind a flag, then cut over, then flip the flag. More moving parts, and no window.
What does not work is deploying the new column first: the vectors are there, but the pointer still says the old generation is active, so a rollback would move the pointer back under an application that no longer reads it.