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 vectorsThe 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'}| Argument | Default | Meaning |
|---|---|---|
url | http://localhost:6900 | the server's REST address |
api_key | none | sent as Authorization: Bearer <key> |
timeout | 30.0 | seconds per request |
ca_cert | none | PEM file of CA certificates to trust, for an https:// server with a private CA |
client_cert | none | PEM client certificate (optionally followed by its key), for mutual TLS |
client_key | none | PEM key for client_cert, if kept separately |
http_client | none | your 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 method | Does |
|---|---|
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}) Noneinsert 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
| Method | Does |
|---|---|
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):
| Exception | Raised for |
|---|---|
InvalidRequestError | 400, 413, 415, 422 and other 4xx |
AuthenticationError | 401: a missing or wrong API key |
PermissionDeniedError | 403: a read-only key used for a write |
NotFoundError | 404: no such collection, alias, point, snapshot or route |
ConflictError | 409: the name is taken by a collection or an alias |
ServerError | 5xx |
QuiverConnectionError | the 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_existsAsync
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']