Get started
One static binary, no dependencies. This page goes from download to a durable, searchable collection: install the server, add your data, and search it by meaning, keyword, or both. Running it for real is the operations guide, which covers services, scaling, and per-collection buckets.
Install
On macOS and Linux, the install script detects your platform, verifies the release checksum, and puts all six binaries on your PATH:
curl -fsSL https://get.polign.com | sh
On macOS, Homebrew works too and keeps the binaries updated:
brew install polign/tap/polign
For manual or pinned installs, each release archive ships static binaries for linux/darwin (amd64/arm64) with S3, GCS, and Azure Blob support built in. Download from the releases page, unpack, run:
tar -xzf polign_db_darwin_arm64.tar.gz
# for development: in-memory, zero config, write and query immediately
./polign-server
Running the server with persistent storage
Running the polign-server should give you an HTTP API on port 23000 and a gRPC API on port 23001. The archive also contains polign, a CLI that talks to http://localhost:23000 by default (point it elsewhere with -url or $POLIGN_URL), see the
CLI reference for details.
If you prefer working from code, the Python SDK covers the same operations. Using it from Python below shows it in action.
The bucket must already exist, and the server reads AWS credentials from the standard chain: AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY environment variables, ~/.aws/credentials, or an IAM role, plus AWS_REGION. Nothing polign-specific to configure:
export AWS_ACCESS_KEY_ID=AKIA... AWS_SECRET_ACCESS_KEY=... AWS_REGION=us-east-1
aws s3 mb s3://my-bkt --region us-east-1 # create the bucket once
./polign-server -store s3://my-bkt/polign
Any S3-compatible endpoint (MinIO, R2) works. Just point the standard AWS endpoint configuration at it.
Use the gcs:// scheme with the standard Google auth chain. GOOGLE_APPLICATION_CREDENTIALS pointing at a service-account key, or ambient credentials on GCE/GKE. The bucket must already exist:
export GOOGLE_APPLICATION_CREDENTIALS=/path/to/service-account.json
gcloud storage buckets create gs://my-bkt # create the bucket once
./polign-server -store gcs://my-bkt/polign
The service account needs read/write access to the bucket's objects (e.g. roles/storage.objectAdmin on that bucket).
Use the az:// scheme. An Azure container name is unique only within a storage account, so the account is part of the address, as in az://account/container/prefix. Credentials come from AZURE_STORAGE_CONNECTION_STRING, else AZURE_STORAGE_KEY (a shared key for that account), else the standard Azure identity chain: service principal env vars, workload or managed identity, or your az login. The container must already exist:
az storage container create --account-name myaccount --name my-ctr # create the container once
./polign-server -store az://myaccount/my-ctr/polign
The identity needs blob read/write on the container (e.g. the Storage Blob Data Contributor role). A local Azurite emulator works. Point AZURE_STORAGE_CONNECTION_STRING at it.
The fs: scheme needs no credentials at all. Point it at a directory the server process can write:
./polign-server -store fs:/var/lib/polign
Same durable data plane, no cloud account: the write-ahead log, snapshots, and segments all live under that directory. Good for development and single-node installs; anything the bucket would hold is on that one disk, so back it up accordingly.
-store turns on the full data plane inside that one process: a durable write-ahead log in the bucket (.wal/, appended before every ack), persistence, bucket-backed serving with fresh reads, a bounded local disk cache, and the background maintenance that keeps big collections memory-efficient. The API is identical with or without it, so upgrading later is adding the flag.
Running it as a systemd service, exposing it beyond localhost with TLS, and pinning release versions are covered in Operate in production.
Terminology
polign_db has a small vocabulary.
| Term | What it means |
|---|---|
| Vector | A list of numbers that represents the meaning of something, a sentence, an image or a product. Two things with similar meaning get vectors that are numerically close together. |
| Embedding model | The tool that turns your text (or images) into vectors. You bring any model you like (OpenAI, Cohere, a local model) and store what it produces. |
| Collection | A named group of vectors, like a table in a regular database. Created automatically the first time you write to it. |
| Metadata | Key–value labels you attach to each vector — each value a string, number, or boolean. The metadata is returned with every search hit, and usable for filtering. |
| Distance / score | How results are ranked. Vector search
returns a distance (smaller = closer match); keyword and hybrid search return
a score (larger = better match). |
Always embed your documents and your search queries with the same model. Vectors from different models live in different "spaces" and comparing them produces plausible-looking results. Pick one model per collection and stick with it.
Add your data
Each record is three things: an id you choose, the vector from your embedding model, and optional metadata. Writing a record whose id already exists overwrites it.
# one record: id "guide-1" in the collection "articles"
polign put articles guide-1 \
-values 0.12,-0.03,0.98 \
-meta title="Getting started" -meta url=/blog/start \
-meta text="How to get started with..."
Real vectors are long, so pass a file instead:
-values @embedding.json reads a JSON array, and -values - reads one
from stdin, so you can pipe your embedding script straight in. The first write creates the
collection and fixes its dimension: write a 768-number vector first, and every later vector
must also have 768 numbers.
Everything the CLI does is plain HTTP underneath (the endpoints are in the
HTTP API reference). Over HTTP, metadata values can be typed —
strings, numbers, booleans — and batch requests take up to 5,000 vectors each. Reads render
metadata values as strings unless the request opts in with
"typed_metadata": true (or ?typed=true on GET).
# the same put, as raw HTTP — note the typed values curl -X PUT localhost:23000/v1/collections/articles/vectors/guide-1 \ -d '{"values": [0.12, -0.03, 0.98], "metadata": {"title": "Getting started", "score": 0.85, "published": true}}' # batch upsert — validated as a whole before anything is applied curl -X POST localhost:23000/v1/collections/articles/vectors:batch \ -d '{"vectors":[ {"id":"guide-2","values":[...],"metadata":{"title":"Filters"}}, {"id":"guide-3","values":[...],"metadata":{"title":"Hybrid search"}} ]}'
"text" metadata fieldIf you store each record's text under the metadata key "text" (as above),
keyword and hybrid search below work on it out of the box. That's the field the text
index reads by default. Keyword search also needs the server running with
-store; a no-flags server does vector search only.
Search
All searching is one command: polign search. What you give it decides which
kind of search you get.
Search by meaning
Embed your question with the same model you used for your documents, and pass the
resulting vector as -values. -k is how many results you want
back:
polign search articles -values @query.json -k 5 ID DISTANCE SCORE METADATA guide-1 0.0014 - {"title":"Getting started","url":"/blog/start",…} guide-3 0.0102 - {"title":"Hybrid search"}
Hits come back closest-first, each with its id, distance, and metadata, so the title and URL you stored earlier arrive with the result, ready to display.
Search by keyword
Give it -text instead of -values and you get classic keyword
search (BM25, the ranking used by traditional search engines). No embedding model involved,
so exact words, names, and codes match reliably:
polign search articles -text "getting started" -k 5
Search both ways at once (hybrid)
Give it -values and -text together and the server runs
both searches and merges them into one ranking. This is usually the best default for search
over documents.
polign search articles -values @query.json -text "getting started" -k 5
How the two lists merge is adjustable through fusion, rank-based by default
or a weighted blend; the options are in the
HTTP API reference.
Narrow results with filters
A -filter limits any search to records whose metadata matches. The simplest
form is exact match:
# only records where lang is "en"
polign search articles -values @query.json -k 5 -filter '{"lang": "en"}'
Filters compose: value lists ($in), ranges ($gte, $lt, …),
existence checks, and $and/$or/$not combinations.
Values are typed, so number filters compare numerically and booleans match by value:
# well-scored and published, since Jan 2026, English or untagged
polign search articles -values @query.json -k 5 \
-filter '{"score": {"$gte": 0.8}, "published": true,
"ts": {"$gte": "2026-01-01"},
"$or": [{"lang": "en"}, {"lang": {"$exists": false}}]}'
Filtering happens during the search, not after it. Asking for 5 results with a filter still returns the 5 best matching results, not whatever survives a trim.
Everyday housekeeping
Three more operations round out the API. You can fetch a record, page through a collection, and delete:
# fetch one record by id polign get articles guide-1 # list records in a collection polign list articles # delete a record polign delete articles guide-1
And that's nearly the complete surface: put, get, list, delete, query, plus batch put and batch get (plain HTTP routes, also wrapped by the client libraries). There is no index to create, rebuild, or babysit. As a collection grows, the server re-indexes it in the background and swaps the new index in without downtime. Every flag of every command, including the ones this walkthrough skips, is in the CLI reference.
Using it from Python
Everything above works identically through the Python client. Same operations, same options, without hand-writing HTTP calls.
# pip install polign from polign import Client client = Client("http://localhost:23000") client.put("articles", "guide-1", embedding, metadata={"title": "Getting started", "score": 0.85, "text": "How to get started with..."}) for hit in client.search("articles", values=query_embedding, k=5, filter={"lang": "en"}): print(hit.id, hit.distance, hit.metadata["title"])
The polign
package's HTTP client is pure standard library with zero dependencies, and
pip install "polign[grpc]" adds a gRPC transport with the identical
API. The Python SDK guide covers batching, hybrid search,
error handling, and the gRPC transport; other languages talk to the
HTTP API or the gRPC API directly.
When something doesn't work
| Symptom | Likely cause and fix |
|---|---|
| Write rejected with a dimension error | The collection's dimension was fixed by its first vector, and this one has a different length. Check which model produced each vector; if the first write was a mistake, delete the collection's records and reload with consistent vectors. |
| Connection refused | The server isn't running, or you're on the wrong port. HTTP is
23000, gRPC is 23001. The polign CLI and curl
talk to the first, and the SDK's GrpcClient dials the second. |
| Search returns results, but they're bad | Almost always a model mismatch: the query was embedded with a different model (or version) than the documents. Re-embed one side so both use the same model. |
| Keyword or hybrid search returns nothing | The text index needs two things: the server running with -store, and
your searchable text stored in the "text" metadata field. The index is
built in the background, so a just-written record may take a little while to appear
in keyword results, but vector search sees it immediately. |
| Filter matches nothing | String values are matched exactly, including case, so {"lang": "EN"}
won't match records stored with "en" (numbers compare numerically and
booleans by value). Fetch one record by id and check what its metadata actually
looks like. |
| My data vanished after a restart | The server was running without -store, which is in-memory by design.
|
Where to go next
- Operate in production: run it as a service, scale the fleet, control the bill, and give each collection its own bucket.
- HTTP API, Python SDK, and gRPC API are the complete references.
- How it works covers the design and the consistency contract behind all of this.
- The live demo runs hybrid search over Wikipedia on polign_db.