revector
Declarative, versioned schema & config migrations for Qdrant — Alembic for vector collections.
revector brings ordered, reversible, database-tracked migrations to Qdrant — the piece that, unlike for relational databases, didn't exist yet. You write declarative YAML migrations, commit them next to your code, and apply or roll them back with a single static binary. No Python venv, no external state store.
Schema, not data. revector manages collection schema and config — collections, payload indexes, named vectors, aliases, and all tunable knobs. Moving points between instances is a solved problem (see
qdrant/migration); that's explicitly out of scope. The one data operation revector does help with — re-embedding — is handled through an exec-hook.
Why
Qdrant collections drift. You tune hnsw_config, add a payload index, introduce
a second named vector for a new model, flip quantization on. Today those changes
live in ad-hoc scripts or a teammate's shell history. revector makes them:
- Versioned — each change is a file with a
revisionanddown_revision, forming an ordered chain (Alembic's model). - Tracked — applied revisions are recorded inside Qdrant itself, in a
dedicated
_revector_migrationscollection. No external database. - Reversible (honestly) — downgrades are auto-derived where safe and refused loudly where they'd lose data, instead of pretending.
- Idempotent & resumable — Qdrant has no transactional DDL and builds indexes asynchronously, so every step is safe to re-run after a failure.
Where to next
- New here? → Install → Quick start → the full Tutorial (local Docker, from scratch).
- Writing a migration? → Migration files and the Operations reference.
- Want to know every knob? → Specs.
- Existing collection? → Adopting an existing collection.
Install
Once a release is cut, prebuilt binaries for Linux, macOS, and Windows are attached to each GitHub Release by cargo-dist, with installers:
# Shell (Linux/macOS) — downloads the right prebuilt binary
curl --proto '=https' --tlsv1.2 -LsSf \
https://github.com/diegoglozano/revector/releases/latest/download/revector-installer.sh | sh
# Windows (PowerShell)
powershell -c "irm https://github.com/diegoglozano/revector/releases/latest/download/revector-installer.ps1 | iex"
Or build from source (requires Rust 1.82+):
cargo install --path . # from a checkout
cargo build --release # ./target/release/revector
Homebrew and crates.io are planned — see ROADMAP.md.
Quick start
# 1. Scaffold a config + migrations/ directory
revector init
# 2. Create your first migration
revector new "create products collection"
# → migrations/1718480000_create_products_collection.yaml
# 3. Edit the file (see Migration files), then apply
export REVECTOR_URL=http://localhost:6334
revector up
# 4. Inspect state
revector status
# 5. Roll back the last migration
revector down
Want the full, hands-on version — starting a local Qdrant in Docker and building a collection from scratch? See the Tutorial.
See the Commands reference for every subcommand and flag, and Migration files for the YAML format.
Tutorial: from zero to a versioned collection
This is the hands-on walkthrough: start a local Qdrant in Docker, create a
collection from scratch with revector, evolve it across a few migrations, roll a
change back, and tear it all down. By the end you'll have a migrations/
directory you could commit next to your code.
Everything here runs locally and disposably — no cloud account, no external state store.
Prerequisites
- Docker (to run Qdrant locally).
- revector on your
PATH— see Install. The quickest route from a checkout iscargo install --path .; verify withrevector --help.
1. Start Qdrant locally (latest Docker image)
Qdrant publishes an official image. Pull the latest and run it, exposing both
the REST (6333) and gRPC (6334) ports — revector talks gRPC on 6334:
docker run -p 6333:6333 -p 6334:6334 \
--name qdrant-revector-tutorial \
qdrant/qdrant:latest
Leave that running in its own terminal. To confirm it's up, the REST API and a
web dashboard are on 6333:
curl http://localhost:6333/healthz # → healthz check passed
# or open the dashboard: http://localhost:6333/dashboard
Tip — persist data across restarts. The container above is ephemeral; add
-v "$(pwd)/qdrant_storage:/qdrant/storage"to keep the data on disk. For a throwaway tutorial you don't need it.
2. Initialize a revector project
In a fresh working directory:
revector init
This creates two things:
migrations/— where your migration files live.revector.toml— project config, pre-pointed athttp://localhost:6334, which is exactly where our Docker container is listening:
# revector.toml
url = "http://localhost:6334"
migrations_dir = "migrations"
# api_key = "..." # or set REVECTOR_API_KEY
# tracking_collection = "_revector_migrations"
Because the default already matches local Docker, you don't have to set anything
else. (If you'd run Qdrant elsewhere, you'd set url here or export
REVECTOR_URL. See Configuration.)
3. Write your first migration — create a collection
Scaffold a migration. revector new creates a timestamped file chained onto the
current head (here, nothing yet, so it becomes the base of the chain):
revector new "create products collection"
# → migrations/1718480000_create_products_collection.yaml
Open that file. It's a commented template; replace the up: section so it
creates a products collection with a 768-dim cosine text vector:
revision: "1718480000_create_products_collection"
down_revision: null # null marks the base of the chain
description: create products collection
up:
- op: create_collection
name: products
spec:
vectors:
"": # "" is the unnamed/default vector
size: 768
distance: Cosine
hnsw_config:
m: 16
ef_construct: 128
# No explicit `down`: revector auto-inverts create_collection to
# delete_collection on a rollback.
(Your revision and filename will use a real timestamp — leave them as
scaffolded; the 0001_… ids elsewhere in the docs are just hand-authored
examples.)
4. Validate, then apply
First check the chain parses and resolves — offline, no Qdrant needed. This is the same check you'd run in CI:
revector validate
Now apply it. revector connects to Qdrant, creates the collection, and records
the revision as applied inside Qdrant itself (in a _revector_migrations
collection):
revector up
Preview first without touching Qdrant by adding --dry-run to print the plan.
5. Inspect what happened
Check revector's own view of the world:
revector status
You'll see 1718480000_create_products_collection marked applied. And the
collection really exists — ask Qdrant directly:
curl http://localhost:6333/collections/products
or browse it in the dashboard at http://localhost:6333/dashboard. Notice
there's also a _revector_migrations collection — that's revector's tracking
store, living inside the same Qdrant instance (no external database). See
How state is tracked.
6. Evolve the schema — a second migration
Real projects change. Let's index a payload field and turn on scalar quantization. Scaffold another migration — it chains automatically onto the previous head:
revector new "index category and quantize"
Edit it so up adds an index and patches the config, with an explicit down
(config patches aren't auto-reversible because Qdrant doesn't hand back the
prior values):
revision: "1718480100_index_category_and_quantize"
down_revision: "1718480000_create_products_collection"
description: index the category field and enable scalar quantization
up:
- op: create_payload_index
collection: products
field_name: category
schema: keyword
- op: update_collection
collection: products
quantization_config:
scalar:
type: int8
quantile: 0.99
always_ram: true
down:
- op: update_collection
collection: products
quantization_config: disabled
- op: delete_payload_index
collection: products
field_name: category
schema: keyword
Apply just the new one:
revector up
revector status # both revisions now applied
revector up is idempotent and resumable — if a step fails partway (Qdrant has
no transactional DDL), just run it again and it picks up where it left off.
7. Roll a change back
Made a mistake, or just want to see reversibility work? Roll back the last migration:
revector down # undoes the quantize/index migration
revector status # the second migration is pending again; the first still applied
down rolls back one step by default; pass --steps N or --to <rev> to go
further, and --yes to skip the confirmation prompt (required in
non-interactive shells like CI). revector refuses rollbacks that would
silently lose data instead of pretending — see the
operations reference for each op's reversibility.
Re-apply when you're ready:
revector up
8. (Optional) Check for drift
If someone changes the collection by hand outside revector, you can catch it. Declare the expected shape in a spec file and diff it against the live collection:
revector diff products --spec expected.yaml
See Drift detection (diff) for the spec format and how it
avoids false positives from Qdrant's normalized defaults.
9. Tear down
When you're done experimenting:
docker rm -f qdrant-revector-tutorial
Your migrations/ directory and revector.toml remain — that's the artifact
you'd commit to version control. Pointed at a staging or prod Qdrant (via
REVECTOR_URL), the exact same files reproduce this schema there. revector up
is safe to run in CI: it takes an advisory lock so parallel jobs don't race.
Where to next
- Changing an embedding model later? → Model migration (end-to-end recipe).
- Adopting a collection that already exists? → Adopting an existing collection.
- Every subcommand and flag → Commands; every op → Operations.
Migration files
A migration is a YAML file with a revision id, a link to its parent, and up /
optional down operation lists. Each operation names itself with an op: key.
revision: "0001_products"
down_revision: null # null marks the base of the chain
description: create products collection
up:
- op: create_collection
name: products
spec:
vectors:
"": # "" is the unnamed/default vector
size: 768
distance: Cosine
hnsw_config:
m: 16
ef_construct: 128
- op: create_payload_index
collection: products
field_name: category
schema: keyword
# Optional. If omitted, revector auto-inverts the `up` ops in reverse order
# and refuses the downgrade if any step is irreversible.
down:
- op: delete_collection
name: products
Required fields
| Field | Type | Notes |
|---|---|---|
revision | string | Unique id. Auto-scaffolded by revector new as <timestamp>_<slug>. |
down_revision | string | null | Parent revision. null marks the base of the chain. |
description | string | Free-text label for revector status output. |
up | list of operations | Steps applied on revector up. |
down | list of operations | Optional. If omitted, revector auto-inverts up in reverse order. |
What each op: does
See the Operations reference for a full description, example, spec fields, and reversibility behavior of every operation.
Safety
- Confirmation. Rollbacks (
down, and atothat moves backwards) prompt before proceeding. Pass-y/--yesto skip the prompt; in a non-interactive shell (CI) revector refuses rather than guessing, so--yesis required there. - Advisory lock.
up/down/to/stamptake a lock record in the tracking collection for the duration of the run, so two concurrent runs (e.g. parallel CI jobs) don't stomp on each other. If a previous run died and left a stale lock,--forceoverrides it. (Best-effort — Qdrant has no compare-and-set — but it reliably catches the common case.) - Checksums. revector records the SHA-256 of each migration file when applied; editing an already-applied file fails loudly on the next run. See How state is tracked.
Commands
| Command | Description |
|---|---|
revector init | Create migrations/ and a starter revector.toml. |
revector new <name> | Scaffold a new migration chained onto the current head. |
revector status | Show applied vs pending revisions, checksums, and reversibility. |
revector up [--to <rev>] [--dry-run] | Apply pending migrations. |
revector down [--to <rev>] [--steps N] [--dry-run] | Roll back migrations (default: 1 step). |
revector to <rev> [--dry-run] | Migrate to an exact revision (up or down). |
revector validate | Parse all migrations and resolve the chain offline — no Qdrant connection. Good as a CI / pre-commit check. |
revector stamp <rev|head|base> [--dry-run] | Mark the DB as being at a revision without running any ops — for adopting an existing collection (Alembic's stamp). |
revector diff <collection> --spec <file.yaml> | Compare a declared collection spec against the live collection. |
--dry-run prints the plan without touching Qdrant.
Global flags
These flags work on every subcommand:
| Flag | Env | Description |
|---|---|---|
--config <FILE> | Path to a revector.toml (default: ./revector.toml). | |
--url <URL> | REVECTOR_URL | Qdrant gRPC URL. |
--api-key <KEY> | REVECTOR_API_KEY | Qdrant API key. |
--migrations-dir <DIR> | Migrations directory. | |
-v, -vv | Increase log verbosity (debug, trace). | |
-y, --yes | Skip confirmation prompts (required in non-interactive shells for rollbacks). | |
--force | Override a held or stale migration lock. |
Set REVECTOR_LOG=revector=debug for verbose logging (equivalent to -v).
Configuration
Settings are layered (highest precedence first): CLI flags → REVECTOR_*
environment variables → revector.toml → defaults.
# revector.toml
url = "http://localhost:6334"
migrations_dir = "migrations"
# api_key = "..." # or REVECTOR_API_KEY
# tracking_collection = "_revector_migrations"
| Setting | Env | Default |
|---|---|---|
url | REVECTOR_URL | http://localhost:6334 |
api_key | REVECTOR_API_KEY | none |
migrations_dir | REVECTOR_MIGRATIONS_DIR | migrations |
tracking_collection | REVECTOR_TRACKING_COLLECTION | _revector_migrations |
Set REVECTOR_LOG=revector=debug for verbose logging (or pass -v / -vv).
Operations
Each step inside up: / down: is identified by an op: key. The table
summarizes effect and auto-reversibility; click through for a detailed
description, runnable example, and the full list of spec fields.
op: | Effect | Auto-reversible? |
|---|---|---|
create_collection | Create a collection from a full spec | ✔ → delete_collection |
delete_collection | Drop a collection | ✘ (data loss) |
update_collection | Patch hnsw_config, quantization_config, optimizers_config, or per-vector params in place | ✘ (prior state unknown) |
create_vector | Add a named dense vector (Qdrant v1.18+) | ✔ → delete_vector |
create_sparse_vector | Add a named sparse vector | ✔ → delete_vector |
delete_vector | Drop a named vector | ✘ (data loss) |
create_payload_index | Index a payload field | ✔ → delete_payload_index |
delete_payload_index | Remove a payload index | ✔ iff schema: is given |
create_alias | Point an alias at a collection | ✔ → delete_alias |
delete_alias | Remove an alias | ✘ (target unknown) |
switch_alias | Atomically repoint an alias (zero-downtime swap) | ✘ (prior target unknown) |
exec | Run a shell command (the re-embedding escape hatch) | ✘ unless down provided |
When an operation isn't auto-reversible, supply an explicit down: block — then
revector down uses it verbatim.
The shapes referenced by these operations (CollectionSpec, VectorSpec,
HnswConfigSpec, …) are documented on the Specs page.
create_collection
Create a new collection from a full CollectionSpec.
Example
up:
- op: create_collection
name: products
spec:
vectors:
"": # "" is the unnamed/default vector
size: 768
distance: Cosine
image: # multiple named vectors
size: 512
distance: Dot
on_disk: true
sparse_vectors:
keywords:
on_disk: false
hnsw_config:
m: 16
ef_construct: 128
optimizers_config:
default_segment_number: 2
shard_number: 2
replication_factor: 2
on_disk_payload: true
Fields
| Field | Type | Required | Description |
|---|---|---|---|
name | string | yes | Collection name. |
spec | CollectionSpec | yes | Full collection specification. |
spec fields
| Field | Type | Description |
|---|---|---|
vectors | map<name, VectorSpec> | Named dense vectors. Use "" as the key for the unnamed/default vector. |
sparse_vectors | map<name, SparseVectorSpec> | Named sparse vectors. |
hnsw_config | HnswConfigSpec | Collection-level HNSW defaults. |
quantization_config | QuantizationSpec | Collection-level quantization defaults. |
optimizers_config | OptimizersConfigSpec | Optimizer thresholds. |
shard_number | uint | Number of shards (immutable on single-node). |
replication_factor | uint | Replication factor. |
write_consistency_factor | uint | Write consistency factor. |
on_disk_payload | bool | Store the whole collection payload on disk. |
Reversibility
Auto-reversible → delete_collection. Dropping the
new collection destroys whatever was written into it in the interim, so the
downgrade is destructive but unambiguous.
delete_collection
Drop a collection entirely.
Example
up:
- op: delete_collection
name: legacy_products
# delete_collection is irreversible — declare an explicit `down`
# (typically a full create_collection) if you need to be able to roll back.
down:
- op: create_collection
name: legacy_products
spec:
vectors:
"":
size: 768
distance: Cosine
Fields
| Field | Type | Required | Description |
|---|---|---|---|
name | string | yes | Collection to drop. |
Reversibility
Irreversible. Dropping a collection destroys all its points; revector
cannot reconstruct them. revector down refuses unless you supply an explicit
down: block — typically a create_collection op
matching the original spec.
update_collection
Patch tunable collection-level configuration in place. Only the fields you set are sent to Qdrant — unset fields are left untouched.
Example
up:
- op: update_collection
collection: products
hnsw_config:
ef_construct: 256
quantization_config:
scalar:
type: int8
quantile: 0.99
always_ram: true
optimizers_config:
indexing_threshold: 20000
vectors: # patch existing named-vector params
image:
on_disk: true
# update_collection is not auto-reversible (previous values aren't recorded).
# Spell out the inverse explicitly:
down:
- op: update_collection
collection: products
quantization_config: disabled
Fields
| Field | Type | Required | Description |
|---|---|---|---|
collection | string | yes | Collection to update. |
hnsw_config | HnswConfigSpec | no | Override collection-level HNSW params. |
quantization_config | QuantizationSpec | no | Set or replace quantization (scalar / product / binary / disabled). |
optimizers_config | OptimizersConfigSpec | no | Tune optimizer thresholds. |
vectors | map<name, VectorParamsDiff> | no | Patch params of existing named vectors (on_disk, hnsw_config, quantization_config). |
At least one of those four fields must be set, otherwise revector refuses the op as a no-op.
Note.
sizeanddistanceof a named vector are immutable. Per-vectorhnsw_config/quantization_configcannot be set atcreate_vectortime either (Qdrant's add-vector API doesn't accept them); apply them with a follow-upupdate_collectionstep as shown above.
Reversibility
Not auto-reversible. The previous values aren't stored, so revector cannot
synthesise an inverse. Provide an explicit down: block:
- To restore prior numeric values, repeat
update_collectionwith the original numbers. - To turn quantization back off, use
quantization_config: disabled.
create_vector
Add a new named dense vector to an existing collection. Requires Qdrant v1.18+.
Example
up:
- op: create_vector
collection: products
name: image
spec:
size: 512
distance: Dot
on_disk: true
datatype: float16
To tune the new vector's hnsw_config or quantization_config, follow it with
an update_collection step — Qdrant's add-vector API
doesn't accept those at create time.
Fields
| Field | Type | Required | Description |
|---|---|---|---|
collection | string | yes | Collection to add the vector to. |
name | string | yes | Vector name (must be unique within the collection). |
spec | VectorSpec | yes | Vector configuration. |
spec fields
| Field | Type | Description |
|---|---|---|
size | uint | Dimensionality. Immutable once created. |
distance | Cosine | Euclid | Dot | Manhattan | Distance metric. Immutable. |
on_disk | bool | Store vectors on disk rather than in RAM. |
datatype | float32 | uint8 | float16 | Element storage type. |
hnsw_config | HnswConfigSpec | Ignored at create time — apply via update_collection. |
quantization_config | QuantizationSpec | Ignored at create time — apply via update_collection. |
Reversibility
Auto-reversible → delete_vector. Note the downgrade
discards any embeddings written to the vector in the meantime — a deliberate,
declared choice.
create_sparse_vector
Add a named sparse vector to an existing collection.
Example
up:
- op: create_sparse_vector
collection: products
name: keywords
spec:
on_disk: true
full_scan_threshold: 5000
Fields
| Field | Type | Required | Description |
|---|---|---|---|
collection | string | yes | Collection to add the vector to. |
name | string | yes | Sparse vector name (must be unique within the collection). |
spec | SparseVectorSpec | yes | Sparse-vector configuration. |
spec fields
| Field | Type | Description |
|---|---|---|
on_disk | bool | Store the sparse index on disk. |
full_scan_threshold | uint | Postings-list size below which Qdrant performs a full scan instead of using the index. |
Reversibility
Auto-reversible → delete_vector. The downgrade drops
any sparse vectors stored under this name.
delete_vector
Drop a named vector (dense or sparse) from a collection.
Example
up:
- op: delete_vector
collection: products
name: text_v1
# delete_vector is irreversible — declare an explicit `down` if rollback matters.
down:
- op: create_vector
collection: products
name: text_v1
spec:
size: 768
distance: Cosine
Fields
| Field | Type | Required | Description |
|---|---|---|---|
collection | string | yes | Collection that holds the vector. |
name | string | yes | Vector name to drop. |
Reversibility
Irreversible. Dropping a vector destroys its embeddings; revector cannot
restore them. Supply an explicit down: to recreate the vector definition
(though the embeddings themselves will need to be regenerated — typically via an
exec step).
create_payload_index
Create an index on a payload field so it can be used in filters efficiently.
Example
up:
- op: create_payload_index
collection: products
field_name: category
schema: keyword
- op: create_payload_index
collection: products
field_name: price
schema: float
- op: create_payload_index
collection: products
field_name: location
schema: geo
Fields
| Field | Type | Required | Description |
|---|---|---|---|
collection | string | yes | Collection that holds the field. |
field_name | string | yes | Payload field to index. |
schema | PayloadSchemaType | yes | Field type: keyword, integer, float, geo, text, bool, datetime, uuid. |
Reversibility
Auto-reversible → delete_payload_index with the
same schema: carried over, so the inverse can recreate the index if needed.
delete_payload_index
Drop an index from a payload field. The field stays in the payload — only the index goes away.
Example
up:
- op: delete_payload_index
collection: products
field_name: category
schema: keyword # optional, but needed for auto-rollback
Pass schema: when you want the downgrade to be auto-reversible — revector
uses it to reconstruct the matching create_payload_index.
Fields
| Field | Type | Required | Description |
|---|---|---|---|
collection | string | yes | Collection that holds the index. |
field_name | string | yes | Payload field whose index to drop. |
schema | PayloadSchemaType | no | Original field schema. Required for auto-reversibility. |
Reversibility
- Auto-reversible iff
schema:is supplied → recreates the index viacreate_payload_index. - Without
schema:, revector has no way to know the original field type and refuses the auto-downgrade; supply an explicitdown:.
create_alias
Point an alias at a collection. Aliases are how you keep callers stable while rebuilding collections underneath.
Example
up:
- op: create_alias
collection: products_v2
alias: products
Fields
| Field | Type | Required | Description |
|---|---|---|---|
collection | string | yes | Collection the alias should resolve to. |
alias | string | yes | Alias name. |
Reversibility
Auto-reversible → delete_alias.
delete_alias
Remove an alias. The target collection stays in place.
Example
up:
- op: delete_alias
alias: products_v1_legacy
# Spell out the inverse — revector does not record the previous target.
down:
- op: create_alias
alias: products_v1_legacy
collection: products
Fields
| Field | Type | Required | Description |
|---|---|---|---|
alias | string | yes | Alias name to remove. |
Reversibility
Not auto-reversible — the previous target isn't recorded anywhere, so
revector can't know which collection to re-point at on downgrade. Supply an
explicit down: that calls create_alias.
switch_alias
Atomically repoint an alias to a different collection — the zero-downtime swap used to roll out a rebuilt collection.
Example
up:
- op: switch_alias
alias: products
to_collection: products_v2
# Spell out the inverse — revector does not record the previous target.
down:
- op: switch_alias
alias: products
to_collection: products_v1
Fields
| Field | Type | Required | Description |
|---|---|---|---|
alias | string | yes | Alias to repoint. |
to_collection | string | yes | New target collection. |
Reversibility
Not auto-reversible — the previous target isn't stored. Supply an explicit
down: that switches the alias back to the prior collection.
exec
Run a shell command as a migration step — the escape hatch for steps a generic binary can't own (most often re-embedding). See the Re-embedding guide for the canonical pattern, and the Model migration recipe for the full end-to-end flow.
Example
up:
- op: exec
name: re-embed with the new model
command: "python scripts/reembed.py --collection products --target text_v2"
workdir: ./scripts
# `exec` has no automatic inverse. Spell out a compensating command if one
# exists, or omit `down` and revector will refuse the downgrade.
down:
- op: exec
name: restore embeddings from snapshot
command: "python scripts/restore_text_v1.py"
Fields
| Field | Type | Required | Description |
|---|---|---|---|
command | string | yes | Command line, executed via sh -c. Inherits environment and stdio. |
name | string | no | Human-readable label for status / log output. |
workdir | string | no | Working directory. Defaults to the project root. |
A non-zero exit aborts the migration.
Reversibility
Not auto-reversible. Provide an explicit down: block if a compensating
command exists; otherwise revector refuses the downgrade.
Specs
The shapes referenced by operations. These types are the on-disk vocabulary —
they are deliberately decoupled from qdrant-client, so a client-crate upgrade
can't silently change the meaning of a committed migration.
CollectionSpec
The full specification of a collection. Used by
create_collection and as the desired
state passed to revector diff.
| Field | Type | Description |
|---|---|---|
vectors | map<name, VectorSpec> | Named dense vectors. Use "" as the key for the unnamed/default vector. |
sparse_vectors | map<name, SparseVectorSpec> | Named sparse vectors. |
hnsw_config | HnswConfigSpec | Collection-level HNSW defaults. |
quantization_config | QuantizationSpec | Collection-level quantization defaults. |
optimizers_config | OptimizersConfigSpec | Optimizer thresholds. |
shard_number | uint | Number of shards (immutable after create on single-node). |
replication_factor | uint | Replication factor. |
write_consistency_factor | uint | Write consistency factor. |
on_disk_payload | bool | Store the whole collection payload on disk. |
VectorSpec
Configuration of a single (dense) named vector.
| Field | Type | Description |
|---|---|---|
size | uint | Dimensionality. Immutable once created. |
distance | Distance | Distance metric. Immutable in place. |
on_disk | bool | Store vectors on disk rather than in RAM. |
hnsw_config | HnswConfigSpec | Per-vector HNSW overrides. Ignored at create_vector time — apply via update_collection. |
quantization_config | QuantizationSpec | Per-vector quantization overrides. Ignored at create_vector time — apply via update_collection. |
datatype | Datatype | Element storage type. |
SparseVectorSpec
Configuration of a single named sparse vector.
| Field | Type | Description |
|---|---|---|
on_disk | bool | Store the sparse index on disk. |
full_scan_threshold | uint | Postings-list size below which Qdrant performs a full scan instead of using the index. |
VectorParamsDiff
Used inside update_collection.vectors to
patch the in-place tunables of an existing named vector. size and
distance are deliberately excluded — they are immutable.
| Field | Type | Description |
|---|---|---|
on_disk | bool | Move the vector on / off disk. |
hnsw_config | HnswConfigSpec | Per-vector HNSW params. |
quantization_config | QuantizationSpec | Per-vector quantization. |
HnswConfigSpec
HNSW index parameters. Only fields you set are sent — unset means "leave alone".
| Field | Type | Description |
|---|---|---|
m | uint | Number of edges per node in the index graph. |
ef_construct | uint | Size of the dynamic candidate list during construction. |
full_scan_threshold | uint | Vector count below which Qdrant uses a full scan instead of the index. |
max_indexing_threads | uint | Maximum threads to use when building the index. |
on_disk | bool | Store the HNSW graph on disk. |
payload_m | uint | m value for the dedicated payload-filtered graph. |
QuantizationSpec
Tagged union — set exactly one variant. Use disabled inside
update_collection to turn quantization off.
# scalar
quantization_config:
scalar:
type: int8
quantile: 0.99
always_ram: true
# product
quantization_config:
product:
compression: x8
always_ram: true
# binary
quantization_config:
binary:
always_ram: true
# disable (update_collection only)
quantization_config: disabled
ScalarQuantizationSpec
| Field | Type | Description |
|---|---|---|
type | string | Quantization type. Only int8 exists today. Default: int8. |
quantile | float | Quantile used to clip outliers when computing the scale. |
always_ram | bool | Keep quantized vectors in RAM. |
ProductQuantizationSpec
| Field | Type | Description |
|---|---|---|
compression | string | Compression ratio — one of x4, x8, x16, x32, x64. |
always_ram | bool | Keep quantized vectors in RAM. |
BinaryQuantizationSpec
| Field | Type | Description |
|---|---|---|
always_ram | bool | Keep quantized vectors in RAM. |
OptimizersConfigSpec
Optimizer thresholds and behavior. All fields optional.
| Field | Type | Description |
|---|---|---|
deleted_threshold | float | Fraction of deleted points that triggers segment vacuum. |
vacuum_min_vector_number | uint | Minimum vectors per segment before vacuum is considered. |
default_segment_number | uint | Target number of segments. |
max_segment_size | uint | Maximum segment size in KB. |
memmap_threshold | uint | Segment size in KB above which Qdrant memory-maps it. |
indexing_threshold | uint | Vector count above which a segment becomes indexed. |
flush_interval_sec | uint | Interval (seconds) between automatic flushes. |
Distance
Cosine · Euclid · Dot · Manhattan
Datatype
float32 · uint8 · float16
PayloadSchemaType
keyword · integer · float · geo · text · bool · datetime · uuid
Drift detection (diff)
revector diff compares a declared collection spec against the live collection.
It is declaration-driven: only fields you actually wrote in the spec are
compared. A field you leave unset means "don't care", never "must be unset" —
this avoids the classic Alembic-autogenerate false positives caused by Qdrant
normalizing and defaulting config on read.
revector diff products --spec products.spec.yaml
# collection `products` has 1 difference(s):
# vectors.<default>.size : declared 1024 | live 768
The spec file uses the same CollectionSpec
shape as a create_collection op's spec: block — without the surrounding
op: / name: keys, since diff already knows the collection from its CLI
argument.
# products.spec.yaml
vectors:
"":
size: 1024
distance: Cosine
hnsw_config:
m: 16
ef_construct: 128
diff exits non-zero when differences are found, so it slots into CI as a
drift guard. Folding the full migration chain into a desired-state spec is
future work — today you write the spec file by hand.
Model migration (end-to-end recipe)
You're switching embedding models — a new model, a new dimensionality, or a new
distance metric. In Qdrant a vector's size and distance are immutable:
the engine will not alter them in place, and you must not recreate the whole
collection under a live service. The supported path is a three-beat dance:
add a new vector → re-embed your points into it → drop the old vector
revector owns the first and third beats as ordinary, reversible schema
operations. The middle beat — running your model over your points — is the
one thing a generic binary can't own, so revector shells out to your command via
the exec op. This page walks the whole
thing end-to-end, including the checkpoints where you stop and verify before
anything destructive happens.
If you only want the mechanics of the exec-hook itself, see Re-embedding (the exec-hook). This guide is the full recipe built on top of it.
Pick a strategy
There are two shapes, and the right one depends on which vector you're changing:
| Strategy | Use when | Cutover | Rollback |
|---|---|---|---|
| Named-vector swap (in place) | The collection uses named vectors and you can add a second one alongside the old. | Switch your query code from text_v1 to text_v2. | Roll back to "both vectors exist". |
| Collection rebuild + alias | You're changing the default/unnamed vector, or you want a clean blue/green collection. | switch_alias flips reads atomically. | switch_alias back to the old collection. |
The named-vector swap is the default — it's cheaper (no full copy of the collection) and keeps point ids stable. Reach for the alias rebuild when the vector you need to change is the unnamed default (which can't sit alongside a second default) or when you want a fully isolated new collection you can smoke- test before any traffic touches it.
Strategy 1 — Named-vector swap
Scenario: products has a named text_v1 vector (768-dim, an old model) and
you're moving to a 1024-dim model under the name text_v2.
Split the work across three migrations so each destructive boundary is its own deliberate, reversible step:
0005_add_text_v2_vector add the new (empty) vector reversible
0006_reembed_text_v2 exec-hook: fill it with the new model
0007_drop_text_v1_vector remove the old vector one-way
Migration 1 — add the new vector
revision: "0005_add_text_v2_vector"
down_revision: "0004_..."
description: add text_v2 vector for the new embedding model
up:
- op: create_vector
collection: products
name: text_v2
spec:
size: 1024
distance: Cosine
# down: auto-inverts to delete_vector (the vector is still empty here).
create_vector auto-reverses to
delete_vector, so this migration is freely reversible — at this point the new
vector holds no embeddings worth keeping.
Migration 2 — re-embed via the exec-hook
revision: "0006_reembed_text_v2"
down_revision: "0005_add_text_v2_vector"
description: re-embed products into text_v2 with the new model
up:
- op: exec
name: re-embed products → text_v2
command: "python scripts/reembed.py --collection products --target text_v2"
# `exec` has no automatic inverse. Re-running migration 1's down would already
# drop text_v2 and its data; only spell out a `down` here if you need a
# distinct compensating action.
down:
- op: exec
name: clear text_v2 (no-op placeholder)
command: "true"
The command runs via sh -c, inherits the environment and stdio, and a
non-zero exit aborts the migration — so make your script fail loudly. A
minimal, resumable re-embed script:
# scripts/reembed.py
import argparse
from qdrant_client import QdrantClient, models
from my_embeddings import embed # your new model
def main() -> None:
ap = argparse.ArgumentParser()
ap.add_argument("--collection", required=True)
ap.add_argument("--target", required=True) # the new vector name
ap.add_argument("--source-field", default="text") # payload field to embed
ap.add_argument("--batch", type=int, default=256)
args = ap.parse_args()
client = QdrantClient(url="http://localhost:6334")
offset = None
while True:
points, offset = client.scroll(
collection_name=args.collection,
with_payload=True,
with_vectors=False,
limit=args.batch,
offset=offset,
)
if not points:
break
texts = [p.payload[args.source_field] for p in points]
vectors = embed(texts) # batch-encode with the NEW model
client.update_vectors(
collection_name=args.collection,
points=[
models.PointVectors(id=p.id, vector={args.target: v})
for p, v in zip(points, vectors)
],
)
if offset is None: # last page
break
if __name__ == "__main__":
main()
Notes that make this safe in production:
- Idempotent.
update_vectorsonly writes the namedtargetvector, so re-running after a crash just overwrites the same points — exactly what you want, since Qdrant has no transactional DDL and revector'sexecis resumable. - Only the target vector is touched. The old
text_v1and all payloads are left intact, which is what keeps migration 1's rollback lossless. - Fail loudly. Let exceptions propagate (non-zero exit) so a partial run aborts the migration instead of silently leaving half-embedded points.
Apply and verify
export REVECTOR_URL=http://localhost:6334
revector up # applies 0005 then 0006
revector status # confirm both are recorded as applied
Now verify before you drop anything. Point a few real queries at text_v2,
compare recall against text_v1, and confirm every point actually has the new
vector (a point missing from your --source-field would be skipped). This is
the checkpoint the three-migration split exists to give you.
If something's wrong, you can still roll all the way back — nothing destructive has happened yet:
revector down # undo 0006 (exec down)
revector down # undo 0005 → delete_vector text_v2
Migration 3 — drop the old vector (one-way)
Once you've cut your query code over to text_v2 and you're happy, retire the
old vector in its own migration:
revision: "0007_drop_text_v1_vector"
down_revision: "0006_reembed_text_v2"
description: drop the retired text_v1 vector
up:
- op: delete_vector
collection: products
name: text_v1
# No down: dropping a vector destroys its data. revector refuses the downgrade
# rather than pretend it's reversible.
revector up # applies 0007 — deletes text_v1
delete_vector is irreversible: revector will refuse a downgrade past this
point instead of pretending the old embeddings can come back. Keeping it in a
separate revision means everything before it stays rollback-able, and the
destructive step is an explicit, reviewable boundary.
Strategy 2 — Collection rebuild + alias
Use this when you're changing the default/unnamed vector, or you want a
fresh collection you can smoke-test in isolation before any read touches it.
Callers talk to a stable alias the whole time; you build products_v2
alongside products_v1 and flip the alias atomically.
Assume your app already reads through the products alias (if it reads the raw
collection name, add a create_alias first and switch your client to the
alias).
Migration 1 — build the new collection
revision: "0005_build_products_v2"
down_revision: "0004_..."
description: build products_v2 with the new model dimensions
up:
- op: create_collection
name: products_v2
spec:
vectors:
"":
size: 1024 # new model's dimensionality
distance: Cosine
hnsw_config: { m: 16, ef_construct: 128 }
# down: auto-inverts to delete_collection products_v2 (still empty).
Migration 2 — re-embed into the new collection
revision: "0006_reembed_products_v2"
down_revision: "0005_build_products_v2"
description: embed all points into products_v2 with the new model
up:
- op: exec
name: re-embed products_v1 → products_v2
command: "python scripts/reembed_into.py --source products_v1 --target products_v2"
Here the script reads points (and payloads) from products_v1, embeds with the
new model, and upserts full points into products_v2. Same idempotency and
fail-loud rules as Strategy 1; the difference is you're writing whole points
into a separate collection rather than one named vector in place.
Migration 3 — flip the alias (the zero-downtime cutover)
revision: "0007_switch_products_alias"
down_revision: "0006_reembed_products_v2"
description: point the products alias at products_v2
up:
- op: switch_alias
alias: products
to_collection: products_v2
# switch_alias does NOT record the previous target — spell out the inverse.
down:
- op: switch_alias
alias: products
to_collection: products_v1
switch_alias is atomic, so reads
move from v1 to v2 with no gap. Because you supplied a down, rolling this
migration back instantly repoints the alias to the old collection — your
blue/green safety net. Keep products_v1 around until you're confident, then
retire it in a final, one-way migration:
revision: "0008_drop_products_v1"
down_revision: "0007_switch_products_alias"
description: drop the retired products_v1 collection
up:
- op: delete_collection
name: products_v1
# No down — delete_collection destroys data; revector refuses the downgrade.
Checklist & gotchas
- Split destructive steps into their own migration.
delete_vector/delete_collectionare one-way; isolating them keeps every earlier step reversible and gives reviewers an explicit checkpoint. - Verify between re-embed and drop. Check recall and coverage on the new vector/collection before the destructive migration runs — that gap is the whole point of the split.
- Make the embed script resumable.
execre-runs cleanly after a failure; useupdate_vectors(named-vector swap) or idempotent upserts (rebuild) so a retry overwrites rather than duplicates. - Let the script fail loudly. A non-zero exit aborts the migration; swallow errors and you'll record a "successful" half-embedded state.
- Coordinate the model side with the
qdrant-model-migrationskill — revector orchestrates the schema and the cutover; that skill drives the embedding work itself. - Don't move points between clusters with revector. That's data movement —
use
qdrant/migration. revector only re-embeds in place via the exec-hook.
Re-embedding (the exec-hook)
Changing a vector's size or distance is structural — Qdrant can't mutate it
in place. The path is: add a new named vector → re-embed points with your model
→ drop the old vector. The re-embedding step is the one thing a generic binary
can't own, so revector shells out to your command via the
exec op:
up:
- op: create_vector
collection: products
name: text_v2
spec: { size: 1024, distance: Cosine }
- op: exec
name: re-embed with the new model
command: "python scripts/reembed.py --collection products --target text_v2"
- op: delete_vector # irreversible — make this a separate, deliberate migration
collection: products
name: text_v1
The command runs via sh -c, inherits the environment and stdio, and a
non-zero exit aborts the migration.
Recommended split
Put the destructive step (delete_vector) in a separate migration applied
after you've verified the new vector is healthy. Two migrations instead of one
gives you:
- A safe rollback point — you can roll back to "both vectors exist" without losing data.
- An explicit checkpoint where someone has to look at the new state before the old one is destroyed.
Full recipe
This page covers the exec-hook mechanics. For the complete, copy-pasteable walkthrough — three migrations, a resumable re-embed script, the verify checkpoint, rollback at each stage, and the alias-based collection-rebuild variant — see Model migration (end-to-end recipe).
Adopting an existing collection
If you already have a collection that matches an early migration, stamp lets
revector take over without re-creating it:
# Tell revector the DB is already at 0002 (no operations run)
revector stamp 0002_index_and_quantize
# Now apply everything after it normally
revector up
stamp accepts a revision id, the literal head (latest revision in the
chain), or base (clear all tracking, mark nothing applied). It records every
revision up to and including the target as applied, and removes any recorded
revisions above it.
Use --dry-run to preview the change to the tracking collection without
writing.
Recommended bootstrap
- Write the migrations that would have produced your current schema, in order. Don't apply them.
- Run
revector validateto make sure the chain parses and resolves. - Run
revector stamp head --dry-runand confirm what would be written. - Run
revector stamp headfor real. - From now on, new schema changes go through
revector new+revector up.
CI/CD integration
revector is a single static binary that reads its config from flags or
REVECTOR_* environment variables and runs fully non-interactively — which is
exactly what a pipeline needs. The natural split is:
- On every pull request — run
revector validate(offline, no Qdrant) so a broken or unorderable chain never merges. Optionally addrevector diffas a drift guard. - On deploy / merge to
main— runrevector upagainst the target Qdrant so the schema moves forward in lockstep with the code that depends on it.
The two safety nets that make this trustworthy in automation are built in: the SHA-256 checksum guard refuses to proceed if an already-applied migration file was edited, and the advisory lock (held as a point in the tracking collection) stops two concurrent pipeline runs from applying at once. Neither needs configuration.
Non-interactive usage
Three things matter when running outside a TTY:
-
Pass connection details as env vars, not flags, so secrets stay out of process listings and logs:
export REVECTOR_URL="$QDRANT_URL" export REVECTOR_API_KEY="$QDRANT_API_KEY" # from your CI secret store -
Use
-y/--yesfor anything that prompts.upis non-destructive and runs unattended, butdownandto(when rolling back) require confirmation in an interactive shell —-yis mandatory for them in CI. -
Rely on exit codes. Every command exits non-zero on failure, so no extra
grep-the-output logic is needed.revector diffexits non-zero when it finds drift;validateexits non-zero on a malformed or unresolvable chain.
Installing the binary in a runner
Use the prebuilt installer — no Rust toolchain required:
curl --proto '=https' --tlsv1.2 -LsSf \
https://github.com/diegoglozano/revector/releases/latest/download/revector-installer.sh | sh
Pin to a specific release in CI rather than tracking latest, so a new release
can't change behavior under you — swap latest/download for
download/vX.Y.Z. (See Security & supply chain for
verifying release artifacts.)
GitHub Actions
Validate on every pull request
This job needs no Qdrant and no secrets — it just parses the migration files and resolves the chain offline. Keep it fast and required.
# .github/workflows/migrations.yml
name: migrations
on:
pull_request:
paths:
- "migrations/**"
- "revector.toml"
jobs:
validate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install revector
run: |
curl --proto '=https' --tlsv1.2 -LsSf \
https://github.com/diegoglozano/revector/releases/latest/download/revector-installer.sh | sh
- name: Validate migration chain
run: revector validate
Apply on deploy
Gate this on your deploy event (a push to main, a tag, or an environment
deployment) and pull the Qdrant URL and API key from repository or environment
secrets. The GitHub environment: key lets you require a manual approval before
the migration runs.
apply:
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
environment: production # optional: gate behind a required reviewer
env:
REVECTOR_URL: ${{ secrets.QDRANT_URL }}
REVECTOR_API_KEY: ${{ secrets.QDRANT_API_KEY }}
steps:
- uses: actions/checkout@v4
- name: Install revector
run: |
curl --proto '=https' --tlsv1.2 -LsSf \
https://github.com/diegoglozano/revector/releases/latest/download/revector-installer.sh | sh
- name: Show plan
run: revector status
- name: Apply pending migrations
run: revector up
revector up is idempotent: every operation checks existence before
create/delete, so a re-run after a partial failure (or a duplicate pipeline
trigger) is safe. A run with nothing pending is a no-op that exits zero.
Preview the plan on the PR
To see what a merge would change before it merges, run up --dry-run against
a staging instance — it prints the ordered plan without touching anything:
- name: Plan against staging
env:
REVECTOR_URL: ${{ secrets.STAGING_QDRANT_URL }}
REVECTOR_API_KEY: ${{ secrets.STAGING_QDRANT_API_KEY }}
run: revector up --dry-run
Drift detection as a guard
If you want CI to catch a live collection that has drifted from its declared
spec (someone hand-edited config in the Qdrant dashboard, say), add a
diff step. It exits non-zero on any difference, so a failing job
flags the drift:
- name: Check for drift
run: revector diff products --spec specs/products.spec.yaml
This is declaration-driven — only fields you actually wrote in the spec are compared — so it won't false-positive on Qdrant's read-time defaulting. See the drift detection guide for the spec-file shape.
GitLab CI
The same shape maps onto any runner. Store QDRANT_URL and QDRANT_API_KEY as
masked/protected CI/CD variables.
stages: [validate, deploy]
validate-migrations:
stage: validate
image: debian:stable-slim
before_script:
- apt-get update && apt-get install -y curl
- curl --proto '=https' --tlsv1.2 -LsSf
https://github.com/diegoglozano/revector/releases/latest/download/revector-installer.sh | sh
- export PATH="$HOME/.cargo/bin:$PATH"
script:
- revector validate
apply-migrations:
stage: deploy
image: debian:stable-slim
rules:
- if: $CI_COMMIT_BRANCH == "main"
variables:
REVECTOR_URL: $QDRANT_URL
REVECTOR_API_KEY: $QDRANT_API_KEY
before_script:
- apt-get update && apt-get install -y curl
- curl --proto '=https' --tlsv1.2 -LsSf
https://github.com/diegoglozano/revector/releases/latest/download/revector-installer.sh | sh
- export PATH="$HOME/.cargo/bin:$PATH"
script:
- revector status
- revector up
Rollbacks in automation
Rollbacks are deliberately less automatic, because a down can be destructive
or irreversible (revector refuses to auto-invert a step that needs unrecorded
prior state — see Migration files). If you do wire a
rollback step — for example a manually-triggered job — it must pass -y and
should target an explicit revision rather than relying on step counts:
- name: Roll back to a known-good revision
run: revector to 0002_index_and_quantize -y
Prefer revector to <rev> -y over down --steps N in pipelines: it's explicit
about the end state and idempotent if re-run.
Checklist
-
revector validateruns on every PR that touchesmigrations/. -
Secrets come from the CI secret store as
REVECTOR_API_KEY/REVECTOR_URL, never committed torevector.toml. - The apply job is gated on a deploy event and, ideally, a required reviewer / protected environment.
-
revector status(orup --dry-run) runs beforeupso the plan is in the logs. -
The installer is pinned to a release tag, not
latest. -
Any rollback job passes
-yand targets an explicit revision.
How state is tracked
Applied revisions are stored as points in the _revector_migrations collection
(a dummy 1-d vector plus a payload of revision id, parent, checksum, and
timestamp). Because the checksum of each migration file is recorded, revector
refuses to proceed if a migration was edited after being applied — catching
silent divergence between your files and the database.
You can rename the tracking collection via the
tracking_collection config option (or the
REVECTOR_TRACKING_COLLECTION env var), if _revector_migrations clashes with
naming conventions in your cluster.
Advisory lock
While up / down / to / stamp is running, revector writes a lock record
into the tracking collection so a second concurrent process refuses to start.
If a previous run died mid-flight and left a stale lock, pass --force to
override it.
This is best-effort — Qdrant has no compare-and-set primitive, so two processes that both check the lock at the exact same moment can both proceed. In practice the window is small and the common case (parallel CI jobs racing for the same DB) is caught reliably.
Inspecting the tracking collection
The tracking collection is a normal Qdrant collection — you can query it with the Qdrant API or dashboard if you need to debug. Each point's payload looks roughly like:
{
"revision": "0002_index_and_quantize",
"down_revision": "0001_create_products",
"checksum": "sha256:…",
"applied_at": "2025-01-15T12:34:56Z"
}
Never edit these payloads by hand — use revector stamp instead.
Scope & limitations
- Schema, not data. revector manages collection schema and config —
collections, payload indexes, named vectors, aliases, and tunable knobs.
Moving points between instances is out of scope; use
qdrant/migrationfor that. The one data operation revector helps with — re-embedding — goes through the exec-hook. - Linear chains only (single base, single head) in v1 — branching/merging is rejected with a clear error.
- Per-vector
hnsw_config/quantization_configcan't be set atcreate_vectortime (Qdrant's add-vector API doesn't accept them); apply them with a follow-upupdate_collectionstep. diffreads a standalone spec file; folding the full migration chain into a desired-state spec is future work.
Security & supply chain
- Dependency scanning.
cargo-denyruns in CI (and weekly, to catch newly-published advisories) checking vulnerabilities, licenses, banned/duplicate crates, and that every dependency comes from crates.io. Config:deny.toml. - Automated updates. Dependabot opens weekly PRs for Rust deps and GitHub Actions, so security patches land promptly.
- Build provenance. Release binaries carry
SLSA build-provenance attestations
generated by cargo-dist, so you can verify an artifact was built by this
repo's CI:
gh attestation verify revector-x86_64-unknown-linux-gnu.tar.xz \ --repo diegoglozano/revector - Reproducibility.
Cargo.lockis committed andcargo publish --lockedis used, so published builds resolve to pinned versions. - Minimal runtime surface. The known advisories in the tree are confined to
dev-dependencies (the test harness) and don't ship in the binary; see
deny.tomlfor the tracked exceptions.
Development
cargo test # unit + logic tests
cargo clippy --all-targets # lints
cargo fmt # format
Integration tests spin up a real Qdrant via testcontainers (Docker required); they skip automatically when Docker is unavailable. To run them against an already-running Qdrant instead:
REVECTOR_TEST_URL=http://localhost:6334 cargo test --test integration
Building the docs site
The site you're reading is built with mdBook:
cargo install mdbook # one-time
mdbook serve docs --open # live preview on http://localhost:3000
mdbook build docs # static site → docs/book/
docs/book/ is gitignored — CI builds and publishes the site to GitHub Pages
on every push to main. The source lives under docs/src/.