Vector index parameters reference
How to tune Relata's HNSW + DiskANN + IVF vector indexes. Covers index creation, parameter selection, distance metrics, quantization, and search-time knobs.
Index lifecycle
A vector index lives on a typed table for every column whose name matches
_emb_* (the convention for embedding slots). The index is keyed on
(object_type, modality, model_tag, tenant_id) — multi-tenant by default.
| Phase | Trigger | Effect |
|---|---|---|
| Writing | First insert of an _emb_* field | HNSW graph grows; tombstones accumulate |
| Compacting | Tombstone ratio > 10% | Tier compaction merges; dead nodes reclaimed |
| Reading | SIMILAR TO query, /search, or HYBRID_SEARCH | Beam search + filtered refine |
| Spilling | max_live exceeded | Spill to DiskANN warm tier (object-store-backed flat segments) |
By default, the index is lazy — the first query that touches an _emb_*
field triggers build. Set RELATA_ANN_EAGER=true to build at insert time
(higher ingest cost; lower first-query latency).
HNSW parameters (index-time)
These are set at index creation (first insert) and cannot be changed without a rebuild.
| Parameter | Default | Range | Effect |
|---|---|---|---|
M | 128 | 16–256 | Out-degree per node (layer > 0). Higher = better recall, more RAM. |
M0 | 256 | 32–512 | Out-degree at layer 0 (the data layer). Typically 2× M. |
ef_construction | 350 | 100–1000 | Beam width during build. Higher = better recall, slower build. |
ml | 1/ln(M) | (computed) | Level-decay factor for random level assignment. |
Memory cost: ~3 KB × vectors at f32 / 1536-d (OpenAI). ~1 KB × vectors
at int8 / 384-d (MiniLM). For 1 M vectors at f32/1536-d: ~3 GB.
Choosing M and ef_construction
| Workload | M | ef_construction | Notes |
|---|---|---|---|
| High recall, low QPS | 256 | 500 | Best recall@10; 2× RAM |
| Balanced (default) | 128 | 350 | Good recall, fast search |
| Low latency, high QPS | 64 | 200 | Lower recall; ~half RAM |
| Binary embeddings | 32 | 100 | For 1-bit codes |
HNSW parameters (search-time)
| Parameter | Default | Range | Effect |
|---|---|---|---|
ef_search | max(k, 10) | k–1000 | Beam width at query. Higher = better recall, slower. |
limit (k) | query-supplied | 1–1000 | Top-K returned |
filter | none | allowlist | Pre-filter or post-filter (adaptive at 25% selectivity) |
Tuning ef_search
-- Tighten ef_search for fast low-recall queries:
SELECT * FROM Vec WHERE SIMILAR TO '[...]' FIELD=_emb_text LIMIT 10 EF_SEARCH=20
-- Loosen for high recall:
SELECT * FROM Vec WHERE SIMILAR TO '[...]' FIELD=_emb_text LIMIT 10 EF_SEARCH=200The default max(k, 10) is fine for most workloads. For high-recall
applications (face recognition, dedup), set ef_search = 5 * k.
Distance metrics
Today, cosine is the default and most-optimised metric.
| Metric | Use case | Cost vs cosine |
|---|---|---|
Cosine (default) | Semantic similarity (most embedding models) | 1× |
L2Squared | Face recognition, image search | ~1× |
InnerProduct (MIPS) | DPR-style retrievers | ~0.9× |
SELECT * FROM Face WHERE SIMILAR TO '[...]' FIELD=_emb_face METRIC=L2 LIMIT 10Quantization
| Tier | Bytes per dim | Recall loss | Use when |
|---|---|---|---|
f32 / full (default) | 4 | 0% | Default (RELATA_VECTOR_QUANT=full); small indexes |
| FP16 | 2 | <0.5% | 2× memory savings |
| int8 | 1 | ±0.4% | 4× memory savings; opt in via RELATA_VECTOR_QUANT=int8 |
| PQ | ~0.1 | 2–5% | Billion-scale cold tier |
| Binary hash | 1/8 | 5–15% | Deep-1-bit first-pass filter |
The HNSW hot tier defaults to full (raw f32 — RELATA_VECTOR_QUANT=full). Set RELATA_VECTOR_QUANT=int8 for a 4× memory reduction at ±0.4% recall. The IVF cold tier uses int8 with
optional PQ (32–64× compression).
Cold tier (DiskANN + IVF)
When the index exceeds RELATA_DISKANN_MAX_RESIDENT, the cold tier engages:
| Component | Purpose | Trigger |
|---|---|---|
| IVF bucket | Cluster by k-means++ centroids | Always on (cold tier) |
| Posting list | Per-centroid vector list | Paged from object store via LRU |
nprobe | Number of centroids to search | Default 32 |
| DiskANN graph | Object-store-backed HNSW for SSD traversal | Cold tier |
# Bound RAM-resident vectors in the cold tier staging area
RELATA_VECTOR_COLD_RESIDENT_MAX=200000 # default 100 000
# Bound total HNSW-resident vectors (warns past this)
RELATA_DISKANN_MAX_RESIDENT=1000000 # 0 / unset = unboundedSearch-time knobs
/search body
{
"query": "alice",
"vector": [0.1, 0.2, ...],
"vector_field": "_emb_text",
"metric": "cosine",
"limit": 10,
"ef_search": 100,
"filter": {"tenant_id": "org-acme"}
}SQL SIMILAR TO
SELECT * FROM Vec
WHERE SIMILAR TO '[0.1, 0.2, ...]'
FIELD=_emb_text
METRIC=COSINE
LIMIT 10
EF_SEARCH=100Pre-filter vs post-filter
For selective predicates (e.g. "only vectors where tenant_id = 'org-acme'"),
Relata chooses automatically based on selectivity:
| Selectivity | Strategy | Why |
|---|---|---|
| > 25% | Post-filter | Most candidates survive; ANN is faster |
| <25% | Pre-filter (allowlist) | Few candidates; skip ANN entirely |
HYBRID_SEARCH WHERE pushdown
HYBRID_SEARCH's WHERE clause always applies correctly as a post-fusion
filter — every result is guaranteed to match it. Separately (an optimization,
not a correctness requirement), Relata also tries to push cheap predicates
into the ANN leg itself, so a selective filter doesn't burn the vector
search's top_k budget on rows that get dropped afterward anyway (the
filtered-ANN "recall cliff" failure mode).
Which predicates push down:
| Predicate shape | Pushed? |
|---|---|
col = 'literal' (equality) | Yes |
col IN ('a', 'b', …) | Yes |
Multiple AND-joined equality/IN predicates | Each pushable one is pushed; non-pushable ones stay post-fusion |
Range comparisons (<, <=, >, >=, !=) | No — post-fusion only |
OR branches | No — post-fusion only |
| Predicate on a non-text column | No — post-fusion only |
When pushdown applies, the candidate row-id allowlist is intersected with any
ACL read-allowlist before reaching the ANN leg — a row must both be readable
by the caller and match the pushable predicate to be rankable. Pushdown
is skipped when the allowlist comes back empty, or is larger than
top_k × 10 (the scan cost outweighs the recall benefit at that
selectivity — the post-fusion filter handles it correctly regardless).
Adaptive over-fetch: whenever the filter (or part of it) is not covered
by an applied pushdown, the ANN leg's requested top_k is widened up to 4×
(bounded by RELATA_MAX_VECTOR_K), giving the post-fusion filter more raw
candidates to work with.
Reading EXPLAIN:
EXPLAIN HYBRID_SEARCH FROM Case QUERY 'wire fraud' LIMIT 10 WHERE status = 'open'
step | stage | detail
-----|----------|----------------------------------------------------------
0 | Scan | Case access=ANN (HYBRID_SEARCH) est_rows=12345
1 | Pushdown | pushable_predicates=1 allowlist_size=42 budget=500 \
applied=true effective_k=50 over_fetch=false \
reason="applied: 1 pushable predicate(s), allowlist=42"
2 | Limit | n=10applied=true— the allowlist was handed to the ANN leg.applied=falsewithover_fetch=true— pushdown was skipped but the ANN fetch was widened to compensate.applied=falsewithover_fetch=false— noWHEREclause, or the allowlist came back empty.
EXPLAIN ANALYZE is not yet supported for HYBRID_SEARCH; use plain
EXPLAIN above for the pushdown plan.
Performance characteristics
| Operation | Latency (1 M vectors, int8, 384-d) |
|---|---|
| Build (parallel bulk insert) | ~3 min on 16 cores |
| Point search (k=10) | <5 ms p99 |
| Filtered search (1% selectivity) | <8 ms p99 |
| Insert (single vector) | <100 µs |
| Soft delete (tombstone) | <10 µs |
| Compaction (10% tombstones) | ~5 s for 1 M vectors |
Common pitfalls
| Pitfall | Symptom | Fix |
|---|---|---|
| ef_search too low | Recall@10 <0.9 | Raise EF_SEARCH=100 or higher |
| M too low for high-dimensional embeddings | Recall plateaus | Rebuild with M=256 |
| Filtered search recall cliff | Recall@10 drops sharply at <5% selectivity | Use pre-filter explicitly |
| Index RAM exceeds budget | OOM warning at startup | Reduce max_resident or shard the type |
| Stale tombstone accumulation | Search latency drifts upward | Trigger compaction (relata compact --type <T>) |