Skip to content

Getting started

Run the container

podman build -t raggio .
podman run -p 8000:8000 -v raggio-data:/data \
  -e ROOT_API_KEY=change-me \
  -e EMBEDDING_BASE_URL=https://api.openai.com/v1 \
  -e EMBEDDING_API_KEY=sk-... \
  -e EMBEDDING_MODEL=text-embedding-3-small \
  raggio

Any OpenAI-compatible /embeddings endpoint works. For Azure AI Foundry, set EMBEDDING_BASE_URL to the full deployment URL — the key is sent as both Authorization: Bearer and api-key, so both providers work unchanged.

Experimental: free-threaded Python

podman build --build-arg PYTHON=3.14t -t raggio:py314t . builds the same image on free-threaded CPython 3.14t, for evaluation only (ADR 0006). It compiles turbovec from source and fails the build if anything re-enables the GIL; /healthz reports gil_enabled.

No embedding endpoint?

raggio works without one: ingest pre-computed vectors and search with mode: "vector" + a raw query vector, or use mode: "text" (BM25), which never calls an embedding endpoint.

Configuration

All configuration is via environment variables:

Variable Default Description
ROOT_API_KEY — (required) Admin key with full access to every endpoint
EMBEDDING_BASE_URL — OpenAI-compatible embeddings endpoint
EMBEDDING_API_KEY — Key for the embeddings endpoint
EMBEDDING_MODEL — Model name sent with each embeddings request
EMBEDDING_DIM probed Skip the dimension-probe call at collection creation
DATA_DIR /data Storage root (mount a volume here)
MAX_RESIDENT_COLLECTIONS 4 LRU cap on in-memory collections
COLLECTION_IDLE_TTL 900 Seconds before an idle collection is offloaded to disk
NATIVE_BM25 auto Stage-2 BM25 scorer: auto runs the native raggio_native extension when it is installed (the container image builds it), 0 forces the pure-Python scorer. Both return the same results; GET /healthz reports which one runs
IVF_SEARCH_THREADS min(12, CPUs) Threads per indexed collection that scan IVF shards in parallel; 1 restores the serial loop
SYNC_BATCH_JOBS 8 Ingest jobs covered by one vector-index sync; each job turns done only after the sync that covers it. 1 syncs after every job
SYNC_BATCH_MS 1000 Milliseconds after a batch's first job at which the batch syncs even if it holds fewer jobs (checked as each job joins); 0 closes each batch at its first job
GC_FREEZE 0 1 collects and freezes the startup heap before serving, so later garbage collections skip it. Meant for the experimental 3.14t image, whose collector walks the whole heap every time

Authentication

Every request needs a key, passed as X-API-Key: <key> or Authorization: Bearer <key>.

  • The root key (ROOT_API_KEY) can do everything, including collection CRUD and listing collections.
  • Each collection may set its own collection key at creation (collection_key). That key grants access to that collection only: its info, documents, search, and jobs. If no collection key is set, only the root key can access the collection.

First collection

ROOT=change-me

# dim is probed from the embedding endpoint if omitted
curl :8000/collections -H "x-api-key: $ROOT" \
  --json '{"name": "kb", "collection_key": "kb-secret", "bit_width": 4}'

Collection options:

Field Default Description
name required [a-zA-Z0-9_-]{1,64}
dim probed Vector dimension; must be a positive multiple of 8
bit_width 4 Vector quantization: 4 (≈8× smaller) or 2
model / base_url env defaults Per-collection embedding override
collection_key none Per-collection API key
tokenizer unicode61 BM25 tokenizer: unicode61 or trigram — see Search

Ingest

Ingest is asynchronous: the API journals the job and returns 202 with a job_id immediately.

curl :8000/collections/kb/documents -H "x-api-key: kb-secret" --json '{
  "documents": [{
    "doc_id": "contract-42",
    "summary": {"text": "Master agreement with ACME covering 2026 pricing."},
    "chunks": [
      {"id": "contract-42#0", "text": "...", "position": 0,
       "metadata": {"src": "sharepoint", "date": "2026-03-01"}},
      {"id": "contract-42#1", "text": "..."}
    ]}]}'
# -> {"job_id": 1}

curl :8000/collections/kb/jobs/1 -H "x-api-key: kb-secret"
# -> {"job_id": 1, "status": "done", ...}

Documents are pre-chunked by you. Each chunk (and the optional document summary) takes text (embedded server-side), a ready vector, or both. Re-ingesting an existing chunk or document id upserts it.

curl :8000/collections/kb/search -H "x-api-key: kb-secret" --json '{
  "query": {"text": "acme pricing terms"},
  "mode": "hybrid",
  "k": 5,
  "filter": {"src": "sharepoint", "date": {"gte": "2026-01-01"}},
  "expand": {"siblings_topk": 3, "summary": true}}'

See Search for the three modes, filtering, and result expansion.

Develop

uv sync --group bench
uv run pytest

The project needs uv 0.12 or newer (required-version in pyproject.toml). The bench group adds orjson, which the benchmark harness in bench/ (and its tests) imports; the server itself never needs it.

With a Rust toolchain, also build the native stage-2 BM25 scorer (ADR 0004). REQUIRE_NATIVE=1 makes its parity tests fail, instead of skip, when the extension is missing. uv sync is exact: a later sync without --extra native uninstalls it.

uv sync --group bench --extra native
REQUIRE_NATIVE=1 uv run pytest

Build these docs locally:

uv sync --group bench --group docs
uv run mkdocs serve