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.
- HTTP (
polign.Client) — pure standard library, zero dependencies. Talks JSON to the server's HTTP listener (default:23000). - gRPC (
polign.GrpcClient) — the[grpc]extra. Wire-compatible with the Go client, against the gRPC listener (default:23001).
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 )
| Argument | What it does |
|---|---|
values | The query embedding. Omit it (and set text) for a pure BM25 search. |
k | How many hits to return (default 10). |
ef | Search beam width override; 0 = server default. Larger = more accurate, slower. |
filter | Metadata 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. |
text | BM25 query. Alone = text search; together with values = hybrid. |
fusion | Fusion(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. |
cold | Serve straight from object-storage segments instead of the in-memory index. Requires a segment store. |
nprobe | IVF probe-count override for cold queries; 0 = searcher default. |
rescore | Accuracy/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:
| Exception | HTTP | gRPC |
|---|---|---|
InvalidArgumentError | 400 | INVALID_ARGUMENT |
AuthenticationError | 401 | UNAUTHENTICATED |
PermissionDeniedError | 403 | PERMISSION_DENIED |
NotFoundError | 404 | NOT_FOUND |
ConflictError | 409 | ALREADY_EXISTS |
NotOwnerError | 421 | FAILED_PRECONDITION |
RateLimitError | 429 | RESOURCE_EXHAUSTED |
ServerError | 500 | INTERNAL |
NotEnabledError | 501 | UNIMPLEMENTED |
UnavailableError | 503 | UNAVAILABLE |
ConnectionError | — | network 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
- Embed documents and queries with the same model — distances are only meaningful within one embedding space.
- Auto-created collections use the server's default metric (L2) and hybrid IVF index; metric and index tuning are not yet exposed over the wire.
- Metadata is
str → stronly; filter scalars (numbers, booleans) compare against the stored string by their literal form ("0.5","true").
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.
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.