Metadata-Version: 2.4
Name: polign
Version: 0.1.0.post1
Summary: Python client for polign_db, a vector database with hot/cold search and hybrid BM25 fusion
Project-URL: Homepage, https://polign.com
Project-URL: Documentation, https://polign.com/python.html
Project-URL: Issues, https://github.com/Polign/polign/issues
Author: Polign
License: Apache-2.0
Keywords: embeddings,polign,similarity search,vector database,vector search
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Topic :: Database
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Requires-Python: >=3.9
Provides-Extra: grpc
Requires-Dist: grpcio>=1.80; extra == 'grpc'
Requires-Dist: protobuf<7,>=6.31; extra == 'grpc'
Description-Content-Type: text/markdown

# polign — Python client for polign_db

A thin Python client for [polign](https://polign.com) with
two interchangeable transports:

- **HTTP** (`polign.Client`) — zero dependencies, pure stdlib. Talks JSON to
  the server's HTTP listener (default `:23000`).
- **gRPC** (`polign.GrpcClient`) — install with the `[grpc]` extra.
  Talks to the gRPC listener (default `:23001`).

Both expose the same seven operations with identical semantics: `put`,
`put_many`, `get`, `get_many`, `list`, `delete`, `search`.

## Install

```bash
pip install polign             # HTTP client, no dependencies
pip install 'polign[grpc]'     # + gRPC transport (grpcio, protobuf)
```

## Quick start

```python
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:

```python
from polign import GrpcClient

client = GrpcClient("localhost:23001")
```

## Operations

```python
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", ["a", "b"])       # batch read, byte-exact values:
                                               # never a compressed reconstruction
                                               # (get may return one on a cold-
                                               # flushed collection); unknown ids
                                               # omitted, request order kept
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)]
```

### Search options

```python
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 (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
)
```

`text` alone runs a pure BM25 search; `values` + `text` runs hybrid search
fused server-side. `hit.score` is the BM25/fused relevance (larger = better)
and is `0.0` on a pure vector search.

`filter` takes the same dict language on both transports: bare values are equality
(ANDed across keys); per-key operator objects (`$eq`, `$ne`, `$in`, `$gt`,
`$gte`, `$lt`, `$lte`, `$exists`) and the composers `$and`/`$or`/`$not`
express richer predicates:

```python
filter={
    "tenant": "acme",
    "score": {"$gte": 0.5},
    "$or": [{"lang": "en"}, {"lang": {"$exists": False}}],
}
```

## Auth and tenants

```python
client = Client(
    "https://db.example.com:23000",
    api_key="plgn_<key_id>_<secret>",     # sent as Authorization: Bearer
    tenant="acme/search/prod",            # org/project/namespace
)
```

Servers started without `-auth-stores` need no credentials. With TLS enabled
server-side, use an `https://` URL (HTTP) or pass
`credentials=grpc.ssl_channel_credentials()` (gRPC).

## Errors

All errors subclass `polign.PolignError`:

| Exception               | HTTP | gRPC                 |
|-------------------------|------|----------------------|
| `InvalidArgumentError`  | 400  | `INVALID_ARGUMENT`   |
| `AuthenticationError`   | 401  | `UNAUTHENTICATED`    |
| `PermissionDeniedError` | 403  | `PERMISSION_DENIED`  |
| `NotFoundError`         | 404  | `NOT_FOUND`          |
| `NotOwnerError`         | 421  | `FAILED_PRECONDITION`|
| `RateLimitError`        | 429  | `RESOURCE_EXHAUSTED` |
| `ServerError`           | 5xx  | `INTERNAL`           |
| `ConnectionError`       | —    | `UNAVAILABLE`        |

`NotOwnerError.owner` names the owning node in fleet mode — reconnect there
and retry.

## Notes & caveats

Caveats shared by both transports:

- 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.
- **Bulk loads should use `put_many`** — one request per batch instead of one
  per vector. The server validates the whole batch up front (id, non-empty
  values, uniform dimension, at most 5000 vectors per batch: 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.
- Metadata is `str -> str` only; filter scalars (numbers, booleans) compare
  against the stored string by their literal form (`"0.5"`, `"true"`).
