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.

Credentials and access management

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.

TermWhat it means
VectorA 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 modelThe 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.
CollectionA named group of vectors, like a table in a regular database. Created automatically the first time you write to it.
MetadataKey–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 / scoreHow results are ranked. Vector search returns a distance (smaller = closer match); keyword and hybrid search return a score (larger = better match).
Crux of search

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"}}
  ]}'
Put the searchable text in a "text" metadata field

If 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.

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

SymptomLikely 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