quiver
Documentation menu

Searching

A search returns the k points nearest to a query, closest first. The query is a vector, the id of a point already in the collection, or a sparse vector; hybrid queries fuse several searches into one ranking. The examples use this collection:

curl -X POST localhost:6900/collections -H 'content-type: application/json' \
  -d '{"name": "books", "dimension": 3}' > /dev/null
curl -X POST localhost:6900/collections/books/points -H 'content-type: application/json' -d '{
  "points": [
    {"id": "dune",       "vector": [0.9, 0.1, 0.0], "metadata": {"genre": "sci-fi",  "year": 1965}},
    {"id": "foundation", "vector": [0.8, 0.2, 0.1], "metadata": {"genre": "sci-fi",  "year": 1951}},
    {"id": "hobbit",     "vector": [0.1, 0.9, 0.2], "metadata": {"genre": "fantasy", "year": 1937}},
    {"id": "emma",       "vector": [0.0, 0.2, 0.9], "metadata": {"genre": "romance", "year": 1815}}
  ]
}'
{"ids":["dune","foundation","hobbit","emma"],"upserted":4}

POST /collections/{name}/search:

curl -X POST localhost:6900/collections/books/search -H 'content-type: application/json' \
  -d '{"vector": [1.0, 0.0, 0.0], "k": 2, "filter": {"genre": "sci-fi"}, "with_vectors": true}'
{
  "results": [
    {"distance": 0.006116271018981934, "id": "dune", "metadata": {"genre": "sci-fi", "year": 1965},
     "vector": [0.9938837289810181, 0.11043152958154678, 0.0]},
    {"distance": 0.036913156509399414, "id": "foundation", "metadata": {"genre": "sci-fi", "year": 1951},
     "vector": [0.9630868434906006, 0.24077171087265015, 0.12038585543632507]}
  ]
}
FieldDefaultMeaning
vectorthe query; give exactly one of vector, id and sparse
idsearch with this point's stored vector; the point itself is left out
sparsea sparse query, {"indices": [...], "values": [...]}
usingthe main vectorthe named vector to search; required with sparse
k10how many results, 1 to 10,000
offset0skip this many of the best results first; k + offset is at most 10,000
filternonea filter on metadata
efthe collection's ef_searchHNSW beam width for this query; ignored by exact indexes
max_distancenonedrop results farther than this
with_vectorsfalseinclude each result's stored vector
with_metadatatrueinclude each result's metadata

Fewer than k results come back when fewer points match. Stored vectors are what you get back: with cosine, the normalised ones.

Recall and ef

With an HNSW index the results are approximate. Raise ef to trade speed for recall on one query, or set ef_search on the collection for all of them; indexes & metrics has measured numbers. ef is never narrower than k.

Search by id

Give id instead of vector to find a stored point's neighbours. The point is excluded inside the index, so it does not use up one of the k:

curl -X POST localhost:6900/collections/books/search -H 'content-type: application/json' \
  -d '{"id": "dune", "k": 2, "with_metadata": false}'
{"results":[{"distance":0.016214847564697266,"id":"foundation"},{"distance":0.7856534719467163,"id":"hobbit"}]}

An id that does not exist answers 404 point_not_found.

Recommend

POST /collections/{name}/recommend finds points like the positive examples and unlike the negative ones. The query is the mean of the positives; with negatives, it is pushed away from them: 2 · mean(positive) − mean(negative). The examples themselves never appear in the results.

curl -X POST localhost:6900/collections/books/recommend -H 'content-type: application/json' \
  -d '{"positive": ["hobbit"], "negative": ["dune"], "k": 2}'
{"results":[{"distance":0.598021388053894,"id":"emma","metadata":{"genre":"romance","year":1815}},{"distance":1.1261796951293945,"id":"foundation","metadata":{"genre":"sci-fi","year":1951}}]}

It takes the same k, offset, filter, ef, max_distance, with_vectors and with_metadata as a search. At least one positive is required, the examples together may number at most 10,000, and every example must exist (404 point_not_found names the first missing one).

Discovery

POST /collections/{name}/discover steers a search with context pairs: each pair has a positive and a negative example, and a point is on the right side of a pair when it is closer to the positive one. Examples are point ids or vectors; examples given by id never appear in the results.

With a target, Quiver takes the target's nearest neighbours (10 × (k + offset), at least 100) and ranks them first by how many pairs they are on the right side of, then by distance to the target. distance is the distance to the target, so results are not always in distance order:

curl -X POST localhost:6900/collections/books/discover -H 'content-type: application/json' -d '{
  "target": [1.0, 0.0, 0.0],
  "context": [{"positive": "hobbit", "negative": "foundation"}],
  "k": 2, "with_metadata": false
}'
{"results":[{"distance":1.0,"id":"emma"},{"distance":0.006116271018981934,"id":"dune"}]}

Without a target, it is a context search: every point that passes the filter is scored by how far it is on the wrong side of the pairs, and distance is that penalty, 0 for a point on the right side of every pair. This scans the whole collection (or what a field index narrows it to), so filter it on large collections:

curl -X POST localhost:6900/collections/books/discover -H 'content-type: application/json' -d '{
  "context": [{"positive": [0.0, 1.0, 0.0], "negative": [1.0, 0.0, 0.0]}],
  "k": 3, "with_metadata": false
}'
{"results":[{"distance":0.0,"id":"hobbit"},{"distance":0.0,"id":"emma"},{"distance":0.7223151326179504,"id":"foundation"}]}
FieldDefaultMeaning
targetnonean id or a vector
context[]up to 64 {"positive": ..., "negative": ...} pairs, each side an id or a vector
using, k, offset, filter, ef, max_distance, with_vectors, with_metadataas for a search

Give a target, at least one pair, or both. max_distance applies to the target distance, or to the penalty without a target.

Paging results

offset skips the best results, so k 10 with offset 20 returns results 21 to 30. The search still finds k + offset points and drops the first offset, so deep pages cost as much as one big search, and k + offset is capped at 10,000:

curl -X POST localhost:6900/collections/books/search -H 'content-type: application/json' \
  -d '{"vector": [1.0, 0.0, 0.0], "k": 2, "offset": 2, "with_metadata": false}'
{"results":[{"distance":0.8921672105789185,"id":"hobbit"},{"distance":1.0,"id":"emma"}]}

Searches by id, sparse searches, batch entries, recommendations, discovery and hybrid queries take offset too. With HNSW, pages come from separate approximate searches, so a point near a page boundary can occasionally show up on two pages or none; raise ef if that matters. To read a whole collection page by page, scroll it instead.

Grouping results

POST /collections/{name}/search/groups returns the best groups values of a metadata field, each with up to group_size of its nearest points. Use it to get one result per document when a collection stores several chunks of each:

curl -X POST localhost:6900/collections/books/search/groups -H 'content-type: application/json' -d '{
  "vector": [1.0, 0.0, 0.0], "group_by": "genre", "groups": 2, "group_size": 2, "with_metadata": false
}'
{"groups":[{"hits":[{"distance":0.006116271018981934,"id":"dune"},{"distance":0.036913156509399414,"id":"foundation"}],"key":"sci-fi"},{"hits":[{"distance":0.8921672105789185,"id":"hobbit"}],"key":"fantasy"}]}
FieldDefaultMeaning
group_bythe metadata field (a dotted path) to group by
groups10how many groups
group_size3at most this many hits per group
vector, id, sparse, using, filter, ef, max_distance, with_vectors, with_metadataas for a search

Groups are ordered by their best hit, and hits within a group by distance. Group keys are strings, numbers and booleans; 1 and 1.0 are one group. A point whose field holds an array joins the group of each element, and points without the field, or with an object or null there, are left out. Quiver searches for groups × group_size × 2 points and widens the search until the groups are full or the collection runs out, so a group that stays short has no more points. groups × group_size is at most 10,000.

Distance thresholds

max_distance drops results farther than the threshold. The results are still capped at k, and with HNSW the threshold applies to the approximate top k: it filters what the search found rather than searching until it runs out of close points.

curl -X POST localhost:6900/collections/books/search -H 'content-type: application/json' \
  -d '{"vector": [1.0, 0.0, 0.0], "k": 10, "max_distance": 0.1, "with_metadata": false}'
{"results":[{"distance":0.006116271018981934,"id":"dune"},{"distance":0.036913156509399414,"id":"foundation"}]}

Named vectors

In a collection with named vectors, using picks the one to search; the query must have that vector's dimension. It works for searches by vector and by id (which then use the point's named vector), recommendations and batch searches.

curl -X POST localhost:6900/collections/papers/search -H 'content-type: application/json' \
  -d '{"vector": [0.5, 0.5], "using": "title", "k": 5}'

with_vectors returns every vector of each result: vector, vectors (named) and sparse_vectors.

sparse with using naming a sparse vector scores points by the dot product of their sparse vector with the query, weighted by IDF if the sparse vector's modifier is idf. Only points sharing at least one index with the query come back, best first. The search is exact.

curl -X POST localhost:6900/collections/papers/search -H 'content-type: application/json' \
  -d '{"sparse": {"indices": [17], "values": [1.0]}, "using": "text", "k": 5}'
{"results":[{"distance":-0.5,"id":"p1"}]}

To keep "lower is closer" everywhere, a sparse hit's distance is its negated score; max_distance applies to it the same way. filter works as for dense searches; ef has no effect.

POST /collections/{name}/query runs several searches, typically one dense and one sparse, and fuses their rankings. By default it uses reciprocal rank fusion: a point at rank r (from 1) of a search scores weight / (k + r) there, and its scores add up across searches. Rank fusion needs no calibration between distances and keyword scores.

curl -X POST localhost:6900/collections/papers/query -H 'content-type: application/json' -d '{
  "prefetch": [
    {"vector": [0.1, 0.2, 0.3, 0.4], "limit": 50},
    {"sparse": {"indices": [17, 4], "values": [1.0, 1.0]}, "using": "text", "weight": 2.0}
  ],
  "fusion": {"type": "rrf", "k": 60},
  "k": 10,
  "filter": {"year": {"$gte": 2000}}
}'
{"results":[{"id":"p1","score":0.04918033},{"id":"p7","score":0.016129032}]}
FieldDefaultMeaning
prefetch1 to 16 searches, each with exactly one of vector and sparse
prefetch[].usingthe main vectorthe named vector to search; required with sparse
prefetch[].limitmax(k + offset, 50)how many of its best hits take part
prefetch[].weight1multiplies its contribution; 0 ignores it
prefetch[].ef, prefetch[].max_distanceas for a search
fusion{"type": "rrf", "k": 60}rrf (with its constant k), relative_score or dbsf
rescorenone{"vector": [...], "using": ...}: re-rank the candidates by this dense vector instead of fusing
k10how many results
offset0skip this many of the best results
filternoneapplies to every search
with_vectors, with_metadatafalse, trueas for a search

Results carry a score instead of a distance: higher is better.

Score fusion

Rank fusion ignores how much better one hit is than the next. The two score fusions keep that: they turn each search's distances into scores (negated distances), scale them to a common range, multiply by the weight and add them up across searches.

  • relative_score scales each search's scores to 0–1, its best hit to 1 and its worst to 0. A search with a single hit gives it 1.
  • dbsf (distribution-based score fusion) scales each search's scores so that its mean minus three standard deviations maps to 0 and its mean plus three to 1. One outlier skews it less than relative_score. A search whose hits all score the same gives them 0.5.
curl -X POST localhost:6900/collections/books/query -H 'content-type: application/json' -d '{
  "prefetch": [{"vector": [1.0, 0.0, 0.0]}, {"vector": [0.0, 1.0, 0.0]}],
  "fusion": {"type": "relative_score"},
  "k": 3, "with_metadata": false
}'
{"results":[{"id":"foundation","score":1.1205607652664185},{"id":"hobbit","score":1.1084964275360107},{"id":"dune","score":1.0}]}

Rescoring

With rescore, the prefetches only gather candidates: every point any of them found is scored by its exact distance to the rescore vector, and the best k come back with the negated distance as their score. fusion and the weights then play no part (a weight of 0 still contributes candidates). Use it for two-stage search: a cheap first pass, such as a small named vector or a binary-quantized one, then exact re-ranking by a larger one.

curl -X POST localhost:6900/collections/books/query -H 'content-type: application/json' -d '{
  "prefetch": [{"vector": [1.0, 0.0, 0.0], "limit": 3}],
  "rescore": {"vector": [0.0, 0.0, 1.0]},
  "k": 2, "with_metadata": false
}'
{"results":[{"id":"hobbit","score":-0.7843344211578369},{"id":"foundation","score":-0.8796141147613525}]}

The prefetch found dune, foundation and hobbit; emma is closest to the rescore vector but was never a candidate. using names the dense vector to rescore against, the main one if absent.

Quantized vectors on disk

A vector with scalar quantization and on_disk keeps its 8-bit codes in memory and the exact vectors on disk. Searches walk the index with the codes, fetching 4 × k candidates, then re-rank them by their exact distances from disk. Returned distances and vectors are exact.

Binary quantization always works this way, with the exact vectors on disk or in memory, and fetches 20 × k candidates.

POST /collections/{name}/search/batch runs up to 256 searches in one request. Each entry takes every search field above, so entries can mix vectors, ids, sparse queries and named vectors. Results come back as one list per search, in order:

curl -X POST localhost:6900/collections/books/search/batch -H 'content-type: application/json' -d '{
  "searches": [
    {"vector": [0.0, 1.0, 0.0], "k": 1, "with_metadata": false},
    {"id": "emma", "k": 1, "with_metadata": false}
  ]
}'
{"results":[[{"distance":0.029505014419555664,"id":"hobbit"}],[{"distance":0.5789401531219482,"id":"hobbit"}]]}

If any entry is invalid, the whole batch fails.

From Python

from quiver import Client
 
books = Client("http://localhost:6900").collection("books")
 
print([h.id for h in books.search([1.0, 0.0, 0.0], k=2, filter={"year": {"$lt": 1960}})])
print([h.id for h in books.search_by_id("dune", k=1)])
print([h.id for h in books.recommend(["hobbit"], negative=["dune"], k=1)])
print([h.id for h in books.search([1.0, 0.0, 0.0], k=10, max_distance=0.1, ef=128)])
print([[h.id for h in hits] for hits in books.search_batch([[0, 1, 0], [0, 0, 1]], k=1)])

Named, sparse and hybrid searches, on the papers collection:

from quiver import Prefetch
 
papers = Client("http://localhost:6900").collection("papers")
papers.search([0.5, 0.5], k=5, using="title")
papers.search_sparse({17: 1.0}, k=5, using="text")
papers.query(
    [Prefetch(vector=[0.1, 0.2, 0.3, 0.4]), Prefetch(sparse={17: 1.0, 4: 1.0}, using="text", weight=2.0)],
    k=10,
)
['foundation', 'hobbit']
['foundation']
['emma']
['dune', 'foundation']
[['hobbit'], ['emma']]

Paging, groups, discovery, score fusion and rescoring:

from quiver import Rescore
 
print([h.id for h in books.search([1.0, 0.0, 0.0], k=2, offset=2)])
for group in books.search_groups([1.0, 0.0, 0.0], group_by="genre", groups=2, group_size=2):
    print(group.key, [h.id for h in group.hits])
print([h.id for h in books.discover([1.0, 0.0, 0.0], [("hobbit", "foundation")], k=2)])
print([h.id for h in books.query(
    [Prefetch(vector=[1.0, 0.0, 0.0]), Prefetch(vector=[0.0, 1.0, 0.0])], k=3, fusion="dbsf")])
print([h.id for h in books.query(
    [Prefetch(vector=[1.0, 0.0, 0.0], limit=3)], k=2, rescore=Rescore([0.0, 0.0, 1.0]))])
['hobbit', 'emma']
sci-fi ['dune', 'foundation']
fantasy ['hobbit']
['emma', 'dune']
['hobbit', 'foundation', 'dune']
['hobbit', 'foundation']

search_groups takes id= or sparse= (with using=) instead of a vector. In discover, the target and each side of a pair is an id or a vector.

search_batch sends the same k, filter and ef with every vector. The Rust client has SearchRequest::new(vector, k), SearchRequest::by_id(id, k), SearchRequest::sparse(using, vector, k) and .using(name), and QueryRequest for hybrid queries.

Errors

StatusCodeWhen
400invalid_argumentk outside 1–10,000 or k + offset above it, not exactly one of vector, id and sparse, sparse without using, an unknown or wrong-kind using, an invalid sparse vector, more than 256 searches in a batch or 16 prefetches in a query, a negative weight, no positive example, groups or group_size of 0 or a product above 10,000, a discovery with neither target nor context, or more than 64 context pairs
400dimension_mismatchthe query vector has the wrong length
400invalid_filterthe filter does not parse
400invalid_bodymalformed JSON or an unknown field
404not_foundno such collection
404point_not_foundthe id, or an example, does not exist