Python SDK

polign_db ships a Python package, polign, in the repo under sdk/python — two interchangeable transports with the same operations and identical semantics: put, put_many, get, get_many, list, delete, search.

Install

Not on PyPI yet — install straight from the repo:

# HTTP client — no dependencies
pip install "git+https://github.com/Polign/polign_db.git#subdirectory=sdk/python"

# with the gRPC transport (adds grpcio + protobuf)
pip install "polign[grpc] @ git+https://github.com/Polign/polign_db.git#subdirectory=sdk/python"

Quick start

from polign import Client

client = Client("http://localhost:23000")

# Upsert. Collections are auto-created on first put, inferring their
# dimension from the vector. Values accept lists or numpy arrays.
client.put("docs", "doc-1", embedding, metadata={"title": "Cats", "url": "/cats"})

# Nearest-neighbour search (distance: smaller = closer)
for hit in client.search("docs", values=query_embedding, k=10):
    print(hit.id, hit.distance, hit.metadata)

Swap in gRPC by changing two lines — the rest of the code is identical:

from polign import GrpcClient

client = GrpcClient("localhost:23001")

The operations

client.put("docs", "doc-1", values, metadata={"k": "v"})  # upsert, returns id
client.put_many("docs", [Vector(id="a", values=va), Vector(id="b", values=vb)])
                                               # batch upsert, one request
v = client.get("docs", "doc-1")                # Vector(id, values, metadata)
vs = client.get_many("docs", ["doc-1", "doc-2"])  # batch get, one request
page = client.list("docs", limit=100, offset=0)  # page.vectors, page.total
client.delete("docs", "doc-1")                 # True; False if absent (no error)
hits = client.search("docs", values=q, k=10)   # [Hit(id, distance, score, metadata)]

put_many is the bulk-load path — one request per batch instead of one per vector, up to 5000 vectors per batch. The server validates the whole batch up front (an invalid batch applies nothing); on a rarer mid-batch failure earlier vectors remain applied, and since puts are idempotent upserts you simply retry the batch. Chunk larger loads into batches of 5000.

Search options

from polign import Fusion

client.search(
    "docs",
    values=q,                      # vector leg (either values or text required)
    k=10,
    ef=64,                         # HNSW beam width override (0 = server default)
    filter={"lang": "en"},         # metadata predicate (dict language, see below)
    text="quick brown fox",        # BM25 leg (needs a segment index server-side)
    fusion=Fusion(method="linear", alpha=0.6),  # hybrid fusion; default RRF
    cold=True, nprobe=8,           # serve from object-store segments
)
ArgumentWhat it does
valuesThe query embedding. Omit it (and set text) for a pure BM25 search.
kHow many hits to return (default 10).
efSearch beam width override; 0 = server default. Larger = more accurate, slower.
filterMetadata predicate. Bare values are equality (AND across keys); per-key operators ($eq, $ne, $in, $gt/$gte/$lt/$lte, $exists) and the composers $and/$or/$not express richer predicates. Identical on both transports (the gRPC client converts the dict to the wire's typed tree). Applied in-graph on both the in-memory and cold paths.
textBM25 query. Alone = text search; together with values = hybrid.
fusionFusion(method="rrf", rrf_k=60) (default) or Fusion(method="linear", alpha=…)alpha is the vector-leg weight, default 0.5. Only valid when both legs are present.
coldServe straight from object-storage segments instead of the in-memory index. Requires a segment store.
nprobeIVF probe-count override for cold queries; 0 = searcher default.
rescoreAccuracy/speed dial for compressed (IVF-PQ) collections: 0 = server default (accuracy-first), > 0 = exact-rescore pool of that size, < 0 = fastest approximate ranking with no exact rescore. Other collection types ignore it.

Metadata filters

The filter dict extends the plain equality map with Mongo-style operators — every {"key": "value"} filter keeps working unchanged. String range bounds compare lexicographically (store sortable encodings like RFC3339 timestamps); number bounds compare numerically:

client.search("docs", values=q, k=10, filter={
    "author": "acme",                              # bare value = equality
    "score": {"$gte": 0.5},                        # numeric range
    "ts": {"$gte": "2026-01-01T00:00:00Z"},        # lexicographic range
    "tag": {"$in": ["a", "b"]},
    "$or": [{"lang": "en"}, {"lang": {"$exists": False}}],
})

Each Hit carries a distance (vector metric, smaller-is-closer) and a score (relevance, larger-is-better: BM25 on a text search, the fused score on a hybrid one, 0.0 on a pure vector search). Text and hybrid search read the object-store segment index, so they need the server started with -segment-stores and a persistor running — see the install & use guide.

Auth & TLS

client = Client(
    "https://db.example.com:23000",
    api_key="plgn_<key_id>_<secret>",     # sent as Authorization: Bearer
)

Both constructors take api_key and timeout (default 30 s), and both clients are safe to share across threads. Everyday reads and writes never need a key — an API key is only checked on the collection-management calls of a server running the collections registry (-byo-store). For TLS, use an https:// URL (HTTP client, with an optional ssl_context) or pass credentials=grpc.ssl_channel_credentials() (gRPC client, plaintext by default like the Go client).

Errors

All errors subclass polign.PolignError, so callers never touch HTTP status codes or gRPC status types:

ExceptionHTTPgRPC
InvalidArgumentError400INVALID_ARGUMENT
AuthenticationError401UNAUTHENTICATED
PermissionDeniedError403PERMISSION_DENIED
NotFoundError404NOT_FOUND
ConflictError409ALREADY_EXISTS
NotOwnerError421FAILED_PRECONDITION
RateLimitError429RESOURCE_EXHAUSTED
ServerError500INTERNAL
NotEnabledError501UNIMPLEMENTED
UnavailableError503UNAVAILABLE
ConnectionErrornetwork failure

In fleet mode, NotOwnerError means another node owns the resource; over HTTP its owner attribute names the node to reconnect to (over gRPC the owner is in the error message).

Notes & caveats

Contributing to the SDK?

Unit and integration tests (the latter builds and boots the Go server) and the command for regenerating the vendored gRPC stubs after a proto/vectordb.proto change live in the SDK README's Development section.

Other ways in

The same operations are available from the command line, over raw HTTP/JSON (curl examples in Install & deploy polign_db), the gRPC wire API, and the Go client library.