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.

PhaseTriggerEffect
WritingFirst insert of an _emb_* fieldHNSW graph grows; tombstones accumulate
CompactingTombstone ratio > 10%Tier compaction merges; dead nodes reclaimed
ReadingSIMILAR TO query, /search, or HYBRID_SEARCHBeam search + filtered refine
Spillingmax_live exceededSpill 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.

ParameterDefaultRangeEffect
M12816–256Out-degree per node (layer > 0). Higher = better recall, more RAM.
M025632–512Out-degree at layer 0 (the data layer). Typically 2× M.
ef_construction350100–1000Beam width during build. Higher = better recall, slower build.
ml1/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

WorkloadMef_constructionNotes
High recall, low QPS256500Best recall@10; 2× RAM
Balanced (default)128350Good recall, fast search
Low latency, high QPS64200Lower recall; ~half RAM
Binary embeddings32100For 1-bit codes

HNSW parameters (search-time)

ParameterDefaultRangeEffect
ef_searchmax(k, 10)k–1000Beam width at query. Higher = better recall, slower.
limit (k)query-supplied1–1000Top-K returned
filternoneallowlistPre-filter or post-filter (adaptive at 25% selectivity)
-- 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=200

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

MetricUse caseCost vs cosine
Cosine (default)Semantic similarity (most embedding models)
L2SquaredFace recognition, image search~1×
InnerProduct (MIPS)DPR-style retrievers~0.9×
SELECT * FROM Face WHERE SIMILAR TO '[...]' FIELD=_emb_face METRIC=L2 LIMIT 10

Quantization

TierBytes per dimRecall lossUse when
f32 / full (default)40%Default (RELATA_VECTOR_QUANT=full); small indexes
FP162<0.5%2× memory savings
int81±0.4%4× memory savings; opt in via RELATA_VECTOR_QUANT=int8
PQ~0.12–5%Billion-scale cold tier
Binary hash1/85–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:

ComponentPurposeTrigger
IVF bucketCluster by k-means++ centroidsAlways on (cold tier)
Posting listPer-centroid vector listPaged from object store via LRU
nprobeNumber of centroids to searchDefault 32
DiskANN graphObject-store-backed HNSW for SSD traversalCold 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 = unbounded

Search-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=100

Pre-filter vs post-filter

For selective predicates (e.g. "only vectors where tenant_id = 'org-acme'"), Relata chooses automatically based on selectivity:

SelectivityStrategyWhy
> 25%Post-filterMost 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 shapePushed?
col = 'literal' (equality)Yes
col IN ('a', 'b', …)Yes
Multiple AND-joined equality/IN predicatesEach pushable one is pushed; non-pushable ones stay post-fusion
Range comparisons (<, <=, >, >=, !=)No — post-fusion only
OR branchesNo — post-fusion only
Predicate on a non-text columnNo — 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=10
  • applied=true — the allowlist was handed to the ANN leg.
  • applied=false with over_fetch=true — pushdown was skipped but the ANN fetch was widened to compensate.
  • applied=false with over_fetch=false — no WHERE clause, 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

OperationLatency (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

PitfallSymptomFix
ef_search too lowRecall@10 <0.9Raise EF_SEARCH=100 or higher
M too low for high-dimensional embeddingsRecall plateausRebuild with M=256
Filtered search recall cliffRecall@10 drops sharply at <5% selectivityUse pre-filter explicitly
Index RAM exceeds budgetOOM warning at startupReduce max_resident or shard the type
Stale tombstone accumulationSearch latency drifts upwardTrigger compaction (relata compact --type <T>)

See also