quiver
Documentation menu

Python client

A typed client for the REST API, sync and async, in clients/python. It needs Python 3.10 or newer and depends only on httpx; numpy is optional.

pip install ./clients/python             # from a clone of the repository
pip install "./clients/python[numpy]"    # numpy arrays as vectors

The distribution is quiver-db; the package you import is quiver.

Connecting

from quiver import Client
 
client = Client("http://localhost:6900")   # api_key="...", timeout=30.0
print(client.health())
print(client.info())
{'status': 'ok', 'version': '0.1.0'}
{'collections': 0, 'points': 0, 'snapshots_enabled': True, 'storage': 'file', 'version': '0.1.0'}
ArgumentDefaultMeaning
urlhttp://localhost:6900the server's REST address
api_keynonesent as Authorization: Bearer <key>
timeout30.0seconds per request
ca_certnonePEM file of CA certificates to trust, for an https:// server with a private CA
client_certnonePEM client certificate (optionally followed by its key), for mutual TLS
client_keynonePEM key for client_cert, if kept separately
http_clientnoneyour own httpx.Client (then url, api_key, timeout and the TLS arguments are ignored)

Client is a context manager (with Client(...) as client:), and close() releases its connection pool.

Collections

from quiver import Flat, HNSW, KDTree
 
movies = client.get_or_create_collection(
    "movies",
    dimension=4,
    metric="cosine",  # "cosine" | "euclidean" | "dot_product" | "manhattan" | "hamming"
    index=HNSW(m=16),  # or Flat() / KDTree()
)
print(movies, movies.info())
Collection('movies') CollectionInfo(name='movies', dimension=4, metric='cosine', index={'type': 'hnsw', 'm': 16, 'ef_construction': 128, 'ef_search': 64, 'seed': 6512614579346866177}, count=0, tombstones=0, field_indexes=[], quantization='none', on_disk=False, vectors={}, sparse_vectors={})
Client methodDoes
create_collection(name, dimension, metric=, index=, field_indexes=(), quantization="none", on_disk=False, vectors=None, sparse_vectors=None, expiry=None)creates it; ConflictError if it exists. See field indexes, quantization ("scalar" or "binary"), on disk, named vectors and expiry (Expiry(field, default_ttl=None))
get_collection(name)a handle to an existing collection; NotFoundError otherwise
get_or_create_collection(name, dimension, ...)either of the above
collection(name)a handle, without asking the server
list_collections()every collection's CollectionInfo
delete_collection(name)drops it
rename_collection(name, new_name)renames it; returns a handle under the new name
list_aliases(){alias: collection}
set_alias(alias, collection) / delete_alias(alias)create or re-point / remove an alias
health(), info()server status; version, storage engine and totals

HNSW(m=16, ef_construction=128, ef_search=64, seed=None) and KDTree(balance_factor=3) carry the index parameters.

Points

upsert accepts any mix of Point objects, dicts (the id is optional and generated when missing) and (id, vector) or (id, vector, metadata) tuples. Large iterables, generators included, are sent in batches of batch_size (default 1,000). It returns how many points were written.

from quiver import Point
 
print(movies.upsert([
    Point("alien", [0.9, 0.1, 0.0, 0.2], {"genre": "sci-fi", "year": 1979}),
    {"id": "arrival", "vector": [0.8, 0.3, 0.1, 0.1], "metadata": {"genre": "sci-fi", "year": 2016}},
    ("amelie", [0.0, 0.2, 0.9, 0.4], {"genre": "romance", "year": 2001}),
]))
generated = movies.insert([0.1, 0.1, 0.1, 0.9], {"genre": "drama"})
print(len(generated), len(movies))
print(movies.get("amelie", with_vectors=False), movies.get("nope"))
3
36 4
Point(id='amelie', vector=[], metadata={'genre': 'romance', 'year': 2001}) None

insert adds one point and returns its id, generated by the server when you give none. get returns None for a missing point; get_many(ids) skips missing ones.

numpy

With numpy installed, any 1-D array works where a vector is expected:

import numpy as np
 
query = np.array([1.0, 0.0, 0.0, 0.1], dtype=np.float32)
print([hit.id for hit in movies.search(query, k=2)])
['alien', 'arrival']

Collection API

MethodDoes
upsert(points, batch_size=1000)insert or replace points; returns the count written
insert(vector, metadata=None, id=None)insert one point; returns its id
get(id, with_vectors=True) / get_many(ids, with_vectors=True)fetch points
search(vector, k=10, offset=0, filter=, ef=, max_distance=, with_vectors=False, with_metadata=True, using=None)search; using names a named vector, offset pages
search_by_id(id, k=10, ..., using=None)neighbours of a stored point, without it
recommend(positive, negative=(), k=10, ..., using=None)like the positives, unlike the negatives
search_batch(vectors, k=10, filter=, ef=, ..., using=None)up to 256 searches in one request
search_sparse(vector, k=10, using=, filter=, max_distance=, ...)sparse search
search_groups(vector, group_by=, groups=10, group_size=3, id=, sparse=, using=, filter=, ...)grouped search; returns Group(key, hits)s
discover(target=None, context=(), k=10, ...)discovery; targets and (positive, negative) examples are ids or vectors
query(prefetch, k=10, filter=, fusion="rrf", rrf_k=60, rescore=None, ...)hybrid search; fusion is "rrf", "relative_score" or "dbsf", rescore=Rescore(vector, using=None); returns ScoredResults
scroll(cursor=None, limit=100, with_vectors=False, filter=None, order_by=None)one ScrollPage, in id order or by a field ("year" or OrderBy("year", "desc"))
iter_points(page_size=500, with_vectors=False, filter=None, order_by=None)every (matching) point, following the cursors
count(filter=None)how many points (match)
facet(field, filter=None, limit=10)the most common values of a field, as FacetValue(value, count)s
update_metadata(metadata, ids=, filter=, mode="merge")change metadata; see data operations
delete(ids) / delete_where(filter)delete; returns how many existed
info() / len(collection)configuration and count
flush() / compact()force a checkpoint / reclaim deleted slots
create_field_index(field) / drop_field_index(field)index a metadata field / drop its index
update(index=, quantization=, on_disk=, vectors=, sparse_vectors=, expiry=)change settings; expiry=Expiry(...) turns expiry on, expiry=False off; returns the CollectionInfo
rename(new_name)rename the collection; the handle follows it
create_snapshot() / snapshots()back this collection up / list its snapshots

Search results are SearchResult(id, distance, metadata, vector, vectors, sparse_vectors) objects. Snapshot methods on the client are list_snapshots(collection=None), get_snapshot(id), download_snapshot(id, path), upload_snapshot(path_or_bytes), restore_snapshot(id, collection=None, replace=False) and delete_snapshot(id); see snapshots.

Named and sparse vectors

from quiver import Flat, Point, Prefetch, SparseVector, SparseVectorParams, VectorParams
 
papers = client.create_collection(
    "papers",
    dimension=4,
    vectors={"title": VectorParams(2, metric="euclidean", index=Flat())},
    sparse_vectors={"text": SparseVectorParams("idf")},  # or just ["text"]
)
papers.upsert([
    Point("p1", [0.1, 0.2, 0.3, 0.4], {"year": 2021},
          vectors={"title": [0.5, 0.5]}, sparse_vectors={"text": {17: 1.0, 4: 0.5}}),
])
papers.search([0.5, 0.5], k=5, using="title")
papers.search_sparse(SparseVector([17], [1.0]), k=5, using="text")
hits = papers.query(
    [Prefetch(vector=[0.1, 0.2, 0.3, 0.4]), Prefetch(sparse={17: 1.0}, using="text", weight=2.0)],
    k=10,
)
print([(h.id, h.score) for h in hits])

VectorParams(dimension, metric="cosine", index=None, quantization="none", on_disk=False) configures a named vector. Sparse vectors are SparseVector(indices, values) or a {index: value} dict. Prefetch(vector= or sparse=, using=None, limit=None, weight=1.0, ef=None, max_distance=None) is one search of a hybrid query.

Settings, renames and aliases

papers.update(index={"ef_search": 128}, vectors={"title": {"on_disk": True}})
papers.rename("papers-v1")            # papers.name is now "papers-v1"
client.set_alias("papers", "papers-v1")
print(client.list_aliases())          # {'papers': 'papers-v1'}

index= takes an index object, which replaces the settings, or a dict merged into them.

Errors

Every exception derives from QuiverError and carries .status_code and .code (the server's error code):

ExceptionRaised for
InvalidRequestError400, 413, 415, 422 and other 4xx
AuthenticationError401: a missing or wrong API key
PermissionDeniedError403: a read-only key used for a write
NotFoundError404: no such collection, alias, point, snapshot or route
ConflictError409: the name is taken by a collection or an alias
ServerError5xx
QuiverConnectionErrorthe server could not be reached
from quiver import ConflictError, InvalidRequestError, NotFoundError
 
try:
    movies.search([1.0, 0.0], k=1)
except InvalidRequestError as e:
    print(e.status_code, e.code, e.message)
try:
    movies.search_by_id("nope")
except NotFoundError as e:
    print(e.status_code, e.code)
try:
    client.create_collection("movies", dimension=4)
except ConflictError as e:
    print(e.status_code, e.code)
400 dimension_mismatch dimension mismatch: expected 4, got 2
404 point_not_found
409 already_exists

Async

AsyncClient and AsyncCollection mirror the sync API with async methods, and iter_points is an async iterator. collection() is a plain method:

import asyncio
from quiver import AsyncClient
 
 
async def main():
    async with AsyncClient("http://localhost:6900") as client:
        docs = await client.get_or_create_collection("docs", dimension=3)
        await docs.upsert([("a", [1, 0, 0]), ("b", [0, 1, 0])])
        print([hit.id for hit in await docs.search([1, 0, 0], k=1)])
        same = client.collection("docs")  # no await
        print([p.id async for p in same.iter_points()])
 
 
asyncio.run(main())
['a']
['a', 'b']