Vector indexing (optional)¶
Every collection starts — and can happily stay — index-free: vector search is a SIMD brute-force scan over the 4-bit quantized codes, and up to roughly a million vectors that scan is as fast as (or faster than) a graph index while using a fraction of the memory. See the benchmarks.
For collections that grow into the multi-million range, raggio offers an optional
IVF index — an additional object you attach to a collection and can remove again at
any time. It is ScaNN-style coarse partitioning: k-means splits the collection into
nlist shards, each a quantized turbovec index of its own; a query scores the query
against the centroids and scans only the nprobe closest shards.
When to add an index¶
Don't index below ~1M vectors. Measured on the real 553k × 1536 benchmark corpus,
every IVF configuration that preserved recall@10 ≥ 0.95 was at best 1.6x faster than
the flat scan — and most were slower, because each probed shard carries a fixed
~0.4 ms search cost (bench/ivf_probe.py, ADR 0001). The flat scan also batches
concurrent queries into one kernel pass; the index cannot, so high-QPS workloads lose
more.
Consider indexing above ~2M vectors. The flat scan is linear in collection size
while the indexed scan is roughly nprobe / nlist of it. On the same corpus scaled to
2.2M vectors (jittered near-duplicates), measured per-query scan latency:
| Configuration | p50 | recall@10 | speedup |
|---|---|---|---|
| flat scan | 18.1 ms | 0.985 | 1x |
| IVF nlist=256, nprobe=16 (default) | 5.1 ms | 0.980 | 3.5x |
| IVF nlist=256, nprobe=8 | 2.7 ms | 0.974 | 6.8x |
| IVF nlist=256, nprobe=4 | 1.3 ms | 0.957 | 13.6x |
On the harder unscaled real corpus the recall cost of nprobe=16 was about 1 point
(0.951 vs 0.961 flat). Treat these as the shape of the curve, not a guarantee — the
nprobe search parameter lets you trade recall against speed per query.
For calibration, the full served picture at 552k (REST round-trip, DGX Spark,
nlist=256 nprobe=16 — bench/results-vec-regression.md vs
bench/results-ivf-553k.md): serial p50 improves 9.3 → 6.8 ms and serial QPS
101 → 144, but recall@10 drops 0.976 → 0.960, concurrent QPS stays flat (~300, the
index gives up the flat scan's query micro-batching), and large filtered searches
get slower (8.0 → 15.8 ms p50 — big allowlists are intersected per probed shard).
Attach took 40 s, detach 24 s, both online.
Rules of thumb:
- ≲ 1M vectors, or recall matters more than milliseconds → no index.
- ≳ 2M vectors in a larger container, single-query latency dominates → attach one.
- Heavy concurrent or filtered load on ≲ 1M vectors → no index.
Costs¶
- Memory: ~0.5–1 MB fixed per shard (
bench/shard_mem_probe.py) on top of the same quantized codes — e.g.nlist=256on 2.2M × 1536-d ≈ 1.14x the flat index. - Disk: attach/detach rebuilds need the original vectors, so raggio retains an
fp16 copy of every ingested vector in
meta.db(dim × 2bytes per record — 3 KB/record at 1536 dims). This is always on, disk-only, and also what makes the index fully reversible. - Rebuild: attaching/removing streams the collection through a rebuild — tens of seconds per million vectors, run as a background job. Searches keep serving from the old representation until the swap; expect transiently ~2x index RAM plus ~0.4 GB for the k-means training sample at 1536 dims. If the container lacks that headroom the job fails with a clear error (instead of an OOM kill) — raise the memory limit and re-attach.
API¶
# attach (or rebuild) — 202 + job id; poll /jobs/{id}
curl -X POST :8000/collections/mail/index -H "x-api-key: $KEY" --json '{}'
# optional parameters: {"nlist": 256, "nprobe": 16}
# defaults: nlist ≈ rows/8192 rounded to a power of two (16..1024), nprobe 16
# check
curl :8000/collections/mail -H "x-api-key: $KEY"
# -> ..., "index": {"type": "ivf", "nlist": 256, "nprobe": 16}
# per-query recall/speed knob (ignored on unindexed collections)
curl :8000/collections/mail/search -H "x-api-key: $KEY" \
--json '{"query": {"vector": [...]}, "k": 10, "nprobe": 32}'
# retune the default nprobe without rebuilding (indexed collection, nprobe only)
curl -X POST :8000/collections/mail/index -H "x-api-key: $KEY" --json '{"nprobe": 8}'
# remove — rebuilds the flat index from retained vectors, 202 + job id
curl -X DELETE :8000/collections/mail/index -H "x-api-key: $KEY"
Attach and detach run through the same durable job queue as ingest: they survive
crashes (the job replays), are idempotent, and GET /collections/{name}/jobs/{job_id}
reports progress. Ingest, deletes, and upserts keep working while an index is
attached — new vectors are routed to their nearest shard.
Caveats¶
- Filtered search: metadata filters and scopes work unchanged. Small result sets
(≤128 candidates) probe exactly the shards that own them — no recall loss. Larger
filtered sets are subject to the same
nproberecall trade as plain searches. - Centroids are fixed at attach time. After heavy churn (say, the collection doubles or its content distribution shifts), re-POST the index to retrain.
- Legacy rows: collections ingested before vector retention lack the fp16 copy. Attach backfills them by re-embedding text through the collection's embedding endpoint — correct only if that endpoint produced the original vectors. Vector-only legacy rows make the attach job fail with a count; re-ingest them first.