Skip to content

Change the stored model ​

migrate() converts stored vectors to a new model in place. Use it after the column is adopted and search on the current space is working.

The default strategy is convert: a local converter model or a direct UniVec hosted converter translates the stored vectors. On remote mode the converter lives on postvec-server. To keep stored vectors as they are and convert only each query, stay on search a retired space.

A migration waits at two operator steps: awaiting_finalize (column swap) and, if an ANN index existed, awaiting_index (rebuild then a second finalize).

Migration lifecycle and its two manual finalize stepsA migration converts rows, then waits in awaiting_finalize until migration_finalize is called, which performs the column swap. An entry that had an ANN index then waits again in awaiting_index for the rebuilt index and a second finalize; an entry without one goes straight to done. migration_abort is available only before the swap.migrate()ROWS CONVERTEDFINALIZE · SWAPFINALIZENO INDEXABORTrunningconvert, or re-embedawaiting_finalizewaits for youawaiting_indexswap done, rebuild ANNabortedonly before the swapdone
sql
SELECT postvec.migrate(
  'public.docs', 'body',
  new_model => 'baai-bge-m3'
) AS migration_id \gset

SELECT migration_id, resolved_via, state,
       rows_done, rows_total, progress_pct, error
  FROM postvec.migration_status(:migration_id);

Repeat the status query until state = 'awaiting_finalize', then:

sql
SELECT postvec.migration_finalize(:migration_id);

Expected

After the first finalize, an entry that had an ANN index normally enters awaiting_index. The column swap is already done; the entry is live on the new model. Run the suggested concurrent index, then finalize again.

sql
SELECT suggested_index_sql
  FROM postvec.migration_status(:migration_id);
-- run that CREATE INDEX CONCURRENTLY in autocommit, then:
SELECT postvec.migration_finalize(:migration_id);
SELECT state FROM postvec.migration_status(:migration_id);

In psql, \gexec executes suggested_index_sql.

Strategies ​

strategyWhat it does
convert (default)Translate existing vectors. Needs a direct converter to the target.
reembedEmbed current source text (renders the template) with the new model.
autoConvert when a route exists, otherwise re-embed.

enable(), adopt() and migrate() emit a NOTICE when the target embed model is provider-backed. strategy => 'reembed' onto a provider sends every existing row as text. A direct hosted conversion emits a second NOTICE and sends stored vectors instead.

sql
SELECT postvec.migrate(
  'public.docs', 'body',
  new_model => 'baai-bge-m3',
  strategy => 'convert'
);
sql
SELECT postvec.migrate(
  'public.docs', 'body',
  new_model => 'baai-bge-m3',
  strategy => 'reembed'
);
sql
SELECT postvec.migrate(
  'public.docs', 'body',
  new_model => 'baai-bge-m3',
  strategy => 'auto'
);

reindex is manual (default) or blocking. Use manual mode for tables that serve application traffic. On managed PostgreSQL, an entry with index_mode => 'auto' finishes awaiting_index by itself: the server builds the index concurrently and marks the migration done.

Local and hosted converter routes ​

Install the converter first: change the stored model (CLI) (model pull on an embedded host or postvec-server, or provider add univec --convert-to).

A kind = "convert" UniVec entry is a hosted direct route. The migration sends every stored vector to UniVec. migration_status().resolved_via names the converter:

json
{"kind":"direct","model":"univec-convert-snowflake-to-bge-m3"}

A hosted converter is a direct route only. If more than one direct converter has the same source and target, postvec prefers a local converter, then the first hosted one by name.

Abort ​

sql
SELECT postvec.migration_abort(:migration_id);

migration_abort() is available before the swap. It drops the new column and keeps the original one. Writes made during the migration reached only the new column, so the rows it holds a vector for (and chunks re-chunked meanwhile) are queued again for the original model; their stored vectors stay until the new ones land.

Observed entries ​

An adopt(sync => false) entry has no write path. migrate() refuses it unless observed_writes_quiesced => true, with writes remaining stopped through finalization. Otherwise later writes can be missed.

Chunked entries ​

Counts refer to chunks. Conversion sends vectors. New writes during the migration embed with the new model into the scratch column.

Finalization constraints ​

migrate() checks column metadata (defaults, NOT NULL, constraints, comments, ACLs, stats/storage) before creating the _new scratch column. finalize takes ACCESS EXCLUSIVE and re-inspects. If dependent views or indexes appeared, it returns a retryable error. The inspection walks pg_partition_tree.

Validation constraints ​

Conversion requires correct source-model provenance

An incorrect model assertion at adopt() produces invalid converted vectors. Provenance must be confirmed first; otherwise use strategy => 'reembed'.

awaiting_index means the column is already live

The data is on the new model. Build the index, then finalize again.

Only one migration may be active per entry

migration_status() reports the active migration. It can be aborted before the column swap when a different target or strategy is required.