quiver
Documentation menu

Collections & points

Everything in Quiver lives in a collection: a named set of points that share a vector dimension, a distance metric and an index. A point is an id, a vector and optional JSON metadata. A collection can also give its points named vectors of other dimensions, and sparse vectors for keyword search.

Collections

Create one with a name and a dimension. The metric defaults to cosine and the index to HNSW; both are covered in indexes & metrics.

curl -X POST localhost:6900/collections -H 'content-type: application/json' \
  -d '{"name": "articles", "dimension": 4, "metric": "euclidean", "index": {"type": "flat"}}'
{"name":"articles","dimension":4,"metric":"euclidean","index":{"type":"flat"},"count":0,"tombstones":0}
SettingRule
name1–64 characters from [A-Za-z0-9_-], not starting with -
dimension1 to 65,536
metriccosine (default), euclidean, dot_product (or dot), manhattan (or l1), hamming
index{"type": "hnsw"} (default), {"type": "flat"} or {"type": "kd_tree"}, with optional parameters
quantizationnone (default), scalar or binary; see indexes & metrics
on_diskfalse (default); true keeps the vectors in a file, see storage
vectorsnamed dense vectors, see below
sparse_vectorsnamed sparse vectors, see below
expirylets points expire, see below

The dimension, the metric and the set of named and sparse vectors are fixed once the collection exists; index, quantization and on-disk settings can change later. Creating a name that is taken answers 409 already_exists; the Python and Rust get_or_create_collection return the existing collection instead. In the embedded crate, get_or_create_collection with a different configuration is an error.

count is the number of live points. tombstones counts deleted points whose slots have not been reclaimed yet; see compaction.

Named and sparse vectors

A point always has its main vector. A collection can add named dense vectors, say an embedding of the title next to one of the body, each with its own dimension, metric, index, quantization and on_disk setting. Every point must carry every named vector.

It can also add sparse vectors: vectors given by their non-zero components, typically term weights from a tokenizer or a model like SPLADE, for keyword-style search. Points may leave them out. A sparse vector's modifier is none (plain dot products) or idf, which weights each query term by its inverse document frequency in the collection; with term frequencies as values that gives BM25 scores.

curl -X POST localhost:6900/collections -H 'content-type: application/json' -d '{
  "name": "papers",
  "dimension": 4,
  "vectors": {"title": {"dimension": 2, "metric": "euclidean", "index": {"type": "flat"}}},
  "sparse_vectors": {"text": {"modifier": "idf"}}
}'
curl -X POST localhost:6900/collections/papers/points -H 'content-type: application/json' -d '{
  "points": [{
    "id": "p1",
    "vector": [0.1, 0.2, 0.3, 0.4],
    "vectors": {"title": [0.5, 0.5]},
    "sparse_vectors": {"text": {"indices": [17, 4], "values": [1.0, 0.5]}}
  }]
}'

Names follow the rules for collection names, and one name cannot be both dense and sparse. Sparse vectors need as many values as indices, unique indices and finite values; they are stored sorted by index, without zeros. Points come back with vectors and sparse_vectors when vectors are requested. Searching explains how to query them.

Changing settings

PATCH /collections/{name} changes the index, quantization and on-disk settings of the main vector and of named vectors, the modifier of sparse vectors, and expiry. Absent fields stay as they are. index is a JSON Merge Patch over the current index settings: {"ef_search": 128} changes just that parameter, while a different type replaces the index (its other parameters take their defaults).

curl -X PATCH localhost:6900/collections/papers -H 'content-type: application/json' -d '{
  "index": {"ef_search": 128},
  "quantization": "scalar",
  "vectors": {"title": {"on_disk": true}},
  "sparse_vectors": {"text": {"modifier": "none"}}
}'

It answers the collection's info. Any index change other than ef_search rebuilds that index, and on_disk moves the vectors to or from disk; both can take a while on a large collection. Searches carry on meanwhile, writes wait. Dimensions, metrics and the set of vectors cannot change: unknown fields such as dimension are rejected with 400 invalid_body.

Expiring points

With expiry, points expire at a time held in their metadata: field names the metadata field (a dot-separated path, as in filters) holding a Unix time in seconds. Once that time has come, the point is gone from every read: searches, get, listing, counts and filters, as well as metadata updates and deletes. A background sweep then deletes it for good, every 60 seconds by default (--expiry-interval, see configuration). Points without the field never expire.

curl -X POST localhost:6900/collections -H 'content-type: application/json' \
  -d '{"name": "sessions", "dimension": 3, "expiry": {"field": "expires_at", "default_ttl": 86400}}'
curl -X POST localhost:6900/collections/sessions/points -H 'content-type: application/json' \
  -d '{"points": [{"id": "s1", "vector": [0.1, 0.2, 0.3], "metadata": {"expires_at": 1893456000}}]}'

With default_ttl (in seconds), points written without the field get it, set to the time of the write plus default_ttl and rounded up to a whole second, so every point expires. Writing a point again restarts its clock, as does replacing its metadata; merging metadata keeps its expiry time unless the patch sets a new one. To keep a point longer, set a later time.

The field is indexed while expiry is on (it shows up in field_indexes and cannot be dropped) and must hold a number: anything else is refused with 400 invalid_argument. PATCH with {"expiry": {...}} turns expiry on for an existing collection, after checking the points already there, and {"expiry": null} turns it off; expired points not yet swept away then come back. A default_ttl set later applies to points written from then on.

Renaming

POST /collections/{name}/rename with {"name": "new-name"} renames a collection, files included, and answers its info. Aliases that pointed to it follow it; snapshots keep the name they were taken under. In the embedded crate, handles to the old collection are closed, as if it had been dropped; use the one rename_collection returns.

Aliases

An alias is a second name for a collection, usable wherever a collection is named: searches, upserts, snapshots and so on. Re-pointing an alias is atomic, so clients can query products while you build products-v2 and switch over in one step:

curl -X PUT localhost:6900/aliases/products -H 'content-type: application/json'   -d '{"collection": "products-v1"}'
# later, once products-v2 is ready
curl -X PUT localhost:6900/aliases/products -H 'content-type: application/json'   -d '{"collection": "products-v2"}'
curl localhost:6900/aliases
curl -X DELETE localhost:6900/aliases/products
{"aliases":[{"alias":"products","collection":"products-v2"}]}

Aliases and collections share one namespace: a collection cannot take an alias's name (409 already_exists), and aliases point to collections, not to other aliases. Dropping or renaming a collection needs its own name, not an alias. Dropping a collection removes the aliases pointing to it, and a snapshot cannot be restored under a name an alias holds. Aliases are saved in aliases.json in the data directory.

Points

curl -X POST localhost:6900/collections/articles/points -H 'content-type: application/json' -d '{
  "points": [
    {"id": "intro", "vector": [0.1, 0.2, 0.3, 0.4], "metadata": {"lang": "en", "tags": ["guide"]}},
    {"vector": [0.4, 0.3, 0.2, 0.1], "metadata": {"lang": "fr"}}
  ]
}'
{"ids":["intro","3068766d-961b-4c37-9155-02c829012e5e"],"upserted":2}

Ids are strings of 1 to 512 bytes. Leave the id out and the server generates a UUID v4 (yours will differ from the one above); the response lists every id in request order, generated ones included.

Upsert inserts new ids and replaces existing ones: the whole point, vector and metadata both. To change only the metadata, use a metadata update. A batch is validated before anything is written, so one bad point rejects the whole request.

Metadata is any JSON value, usually an object. Filters look inside objects, so use one if you want to filter. null metadata is the same as none. It may nest at most 64 levels deep.

Fetch a point by id; with_vectors=false leaves the vector out:

curl localhost:6900/collections/articles/points/intro
{"id":"intro","metadata":{"lang":"en","tags":["guide"]},"vector":[0.10000000149011612,0.20000000298023224,0.30000001192092896,0.4000000059604645]}

Vectors are stored as 32-bit floats, which is why 0.1 reads back as 0.10000000149011612.

The ids ., .., lookup, delete, count and metadata cannot be fetched with GET /collections/{name}/points/{id}: URLs collapse the first two, and the others are routes of their own. Fetch them with POST /collections/{name}/points/lookup, which the Python and Rust clients do for you.

Vector rules

Every vector, stored or used as a query, must:

  • have exactly the dimension of its vector (the main one or a named one) (dimension_mismatch for a query; an upsert names the point in an invalid_argument error);
  • contain only finite numbers (JSON cannot carry NaN or infinity anyway, but the embedded API and gRPC check);
  • not be all zeros, with the cosine metric (each named vector has its own metric);
  • have a squared length of at most f32::MAX / 4 (about 8.5 × 10³⁷), so that every distance between two valid vectors stays finite. Larger vectors fail with vector magnitude too large.

Stored vectors must also fit their datatype: whole numbers from 0 to 255 for uint8, and at most 65504 in magnitude for float16 (which rounds them). Queries need not.

With the cosine metric, vectors are normalised to unit length when they are stored, and queries are normalised too. A point you read back has the normalised vector:

curl -X POST localhost:6900/collections -H 'content-type: application/json' \
  -d '{"name": "unit", "dimension": 2}'
curl -X POST localhost:6900/collections/unit/points -H 'content-type: application/json' \
  -d '{"points": [{"id": "a", "vector": [3, 4]}]}'
curl localhost:6900/collections/unit/points/a
{"name":"unit","dimension":2,"metric":"cosine","index":{"type":"hnsw","m":16,"ef_construction":128,"ef_search":64,"seed":6512614579346866177},"count":0,"tombstones":0}
{"ids":["a"],"upserted":1}
{"id":"a","vector":[0.6000000238418579,0.800000011920929]}

Deleting and compaction

Delete by id with DELETE /collections/{name}/points/{id}, or several at once, or by filter, with POST /collections/{name}/points/delete (see data operations). Drop a whole collection, its points and its files, with DELETE /collections/{name}.

A delete leaves a tombstone behind. Once a collection has at least 1,024 tombstones and they make up at least 30% of its slots, Quiver compacts it: it copies the live points into a fresh store and rebuilds the index. POST /collections/{name}/compact does it now and returns the collection's info.

curl -X DELETE localhost:6900/collections/articles/points/intro
curl -X POST localhost:6900/collections/articles/compact
{"name":"articles","dimension":4,"metric":"euclidean","index":{"type":"flat"},"count":1,"tombstones":0}

The delete answers 204 No Content with an empty body, or 404 point_not_found if there was no such point; the compaction answers the collection's info.

Limits

LimitValue
Collection name1–64 characters, [A-Za-z0-9_-]
Dimension1–65,536
Point id1–512 bytes
Metadata depth64 levels
Named dense vectors16 per collection
Sparse vectors16 per collection, 65,536 non-zero values each
Ids per lookup or delete request10,000
REST request body64 MiB (--max-body-mb)
gRPC message64 MiB