Per-Vector Version Tags: The Missing Column Behind Every Embedding Migration
A new embedding model lands. The benchmark numbers are 4% better. A staff engineer files the ticket: "Upgrade embeddings to v3." Two weeks later the index has been re-embedded, the alias has been swapped, and the team has shipped the change behind a feature flag. Six weeks later, support tickets pile up. Search results "feel off." A retro is scheduled. Nobody can explain what regressed because nothing crashed and every dashboard is green.
The problem is not the model swap. The problem is that the vector store has no idea which vectors came from which model. There is no column for it. There is no migration table tracking which records have been backfilled. There is no alembic_version row, no schema_migrations table, no pg_dump of the previous state. The team treated an embedding upgrade like a config flip, and the vector store had no schema-level concept that would have stopped them.
Embedding migrations need the same artifact that database migrations have relied on for two decades: a per-record version tag, written into every vector, queried on every read, and used as the gating criterion for cutover and rollback. It is the single column most teams forget to add, and adding it later costs more than adding it up front.
What Postgres Taught Us That Vector Stores Forgot
Relational databases got migration tooling early. ALTER TABLE is online for most changes. Tools like pg_dump, gh-ost, pt-online-schema-change, Liquibase, Flyway, and Alembic give engineers versioned, repeatable, rollback-aware schema changes. Every migration carries a version number. Every row knows which schema it conforms to. The toolchain assumes you will mess up at least once and need to back out gracefully.
Vector stores ship without any of this. There is no ALTER VECTOR TABLE that re-embeds rows in place. There is no migration framework that tracks which rows have been processed. There is no rollback semantic for "swap the embedding model" beyond "rebuild the whole index from scratch." Pinecone, Weaviate, Qdrant, Milvus, and pgvector all expose primitives for storing vectors with metadata, and all of them leave the migration discipline as a homework assignment for the application team.
Most teams don't notice the gap until they hit it. They treat the vector store like a write-mostly KV store. They embed the corpus once, write the vectors, and query them. Then a better model comes along, and the team discovers that the only operation the vector store actually supports is "delete and rewrite." There is no online schema change. There is no rolling cutover. There is no backfill machinery. The team builds it from scratch, badly, under time pressure, and ships.
The discipline that has to land is not exotic. It is the same discipline a senior backend engineer would apply to changing a column type in a 50M-row table. The reason it doesn't get applied is that nobody has labeled the work as a schema migration, because the schema column the migration would need is missing.
The Per-Vector Version Tag Is the Column You Forgot
The starting point is dull and concrete: every vector you write needs a version tag in its metadata. Not on the index. Not on the namespace. On the row. The tag identifies the embedding model that produced it — model name, model version, and any preprocessing parameters that changed (chunk size, normalization, prompt template). A reasonable shape is a single short string like [email protected]/chunk-512/norm-l2. Pinecone, Weaviate, Qdrant, and pgvector all support arbitrary metadata on records; the cost of adding the field is approximately zero, and the cost of not having it later is approximately one engineer-month.
Once the tag is on every vector, several things become possible that were not possible before:
- Read-time filtering. Queries can scope to a specific embedding version. During cutover, production traffic queries the new tag while a small percentage of validation traffic queries both and compares.
- Mixed-state safety. If the backfill is half done, the retrieval layer knows which vectors are in the new "language" and which are still in the old one. It does not silently mix them and produce incoherent neighborhoods.
- Per-record backfill tracking. The migration job can
SELECT WHERE version = 'v1'to find what's left. There is no separate ledger to keep in sync. The vector store IS the ledger. - Rollback without re-embedding. If the new model regresses, flipping reads back to the old tag is a config change. The old vectors still exist. Nothing has been deleted. The rollback is bounded by feature-flag latency, not by re-embedding cost.
- https://medium.com/@kandaanusha/vector-database-reindexing-pipeline-87efa1d1cd19
- https://medium.com/google-cloud/migrating-vector-embeddings-in-production-without-downtime-8a0464af6f55
- https://mixpeek.com/guides/embedding-portability-versioning
- https://www.shshell.com/blog/multimodal-rag-module-22-lesson-3-index-versioning
- https://docs.cloud.google.com/spanner/docs/backfill-embeddings
- https://zilliz.com/ai-faq/how-do-i-handle-versioning-of-embedding-models-in-production
- https://particula.tech/blog/update-rag-knowledge-without-rebuilding
- https://docs.pinecone.io/troubleshooting/create-and-manage-vectors-with-metadata
- https://docs.vectorize.io/build-deploy/data-pipelines/understanding-metadata/
- https://www.arxiv.org/pdf/2509.23471
