04 · DATA MODEL

Vectors & kNN

XERJ stores your vectors in full at ingest time and maintains a persisted HNSW graph alongside them. An unfiltered knn on a full-precision cosine field (≥1,024 docs) is served by an approximate HNSW beam search whose candidates are exact-rescored, so returned scores match the exact path bit-for-bit — measured recall@10 1.00 on the official bench query, 100-probe mean 0.976 (ES 8.13.4 same protocol: 0.937). num_candidates sets the beam width (floored at 800 to match Elasticsearch's per-segment candidate semantics). A filter, non-cosine similarity, SQ8 quantization, a small index, or a stale graph falls back to the exact brute-force scan: every candidate is scored and the true nearest neighbours returned (recall 1.00; latency scales with vectors scanned). Distance metric and quantization are set in config. See KNN query below.

Embedding backends — how vectors get made

Everything above assumes a vector already exists on the doc. XERJ can produce that vector for you from a semantic_text field, and you pick how once at the server with --embed-mode (or embedding.mode in config, or XERJ_EMBED_MODE). The mapping and the semantic query are identical across all three:

MODE
SHIPS / NEEDS
WHAT IT IS
lexical
built-in · default
Feature-hash embedder. Offline, deterministic, zero download. Maps overlapping and related vocabulary into vector space — strong when question and answer share terms, not a neural sentence encoder.
neural
built-in · ~90 MB on first use
BERT sentence encoder (all-MiniLM-L6-v2) running in-process via candle — pure Rust, no Python, no external service. The model auto-downloads (~90 MB) once, or point embedding.local_model_dir at a pre-staged copy for a fully air-gapped install. Captures meaning with no shared term (rotate ≈ regenerate).
proxy
external endpoint
Bring your own model: XERJ calls any OpenAI-compatible /v1/embeddings (OpenAI, Cohere, a local vLLM). Set embedding.default_endpoint. Embedding happens at ingest time.

Turn on the built-in neural embedder with a single server flag — no mapping or query change:

xerj --insecure --data-dir ./data --embed-mode neural

See the Semantic search & RAG recipe for the end-to-end walkthrough.

hnsw_* config keys — accepted, no effect on search

KEY
TYPE
DEFAULT
DESCRIPTION
hnsw_m
u32
16
Accepted and validated; the built-in HNSW graph currently builds with fixed M=16 — this key is not yet wired to the build.
hnsw_ef_construction
u32
200
Accepted; must be ≥ hnsw_m. The graph currently builds with fixed ef_construction=200 — this key is not yet wired to the build.
hnsw_ef_search
u32
100
Accepted but not read by the serving path — the ANN beam width comes from the request's num_candidates (floored at 800).
default_metric
enum
"cosine"
"cosine" · "dot_product" · "euclidean".

Quantization

Quantization is off by default[vector] default_quantization = "none", full-precision f32. Opt in per field with index_options.type: int8_hnsw (or int8_flat) on the mapping, or index-wide by setting the key.

A 4-bit quantizer exists in the vector crate but no config or mapping value reaches it, so scalar4 is not a mode you can select in this build.

A mapping's quantization value is honoured or refused — never accepted and ignored (#275, fixed). "scalar8" (alias "int8") and "none" are the values this build implements. Anything else — "binary", "scalar4", "int4", or a typo such as "sq8" — is now a 400 mapper_parsing_exception naming the value and the accepted set, on both PUT /<index> and PUT /<index>/_mapping, and the index is not created. Before the fix those values answered 200, were echoed back verbatim by GET /_mapping, and were then ignored, so the field stored full-precision f32 while the mapping you read back to confirm it said otherwise. This is the same guard Config::validate applies to [vector] default_quantization at startup, now at the per-field surface that sets the same field.

The strictness is confined to XERJ's own quantization key. Elasticsearch's index_options.type stays permissive — hnsw, flat, int4_* and bbq_* mappings still create, served by the exact f32 scan — because an ES mapping must not be rejected on a wire-compatibility surface.

KNN query

{
  "knn": {
    "field":      "embedding",
    "query_vector": [0.12, 0.08, -0.31, ...],
    "k":          20,
    "num_candidates": 200
  }
}

num_candidates is the ANN beam width on the HNSW-served (unfiltered, ≥1,024-doc) path — larger values trade latency for recall, floored at 800 to match Elasticsearch's per-segment candidate semantics. On the exact scan (filtered kNN, small indexes, SQ8 fields) it has no effect on results.

Hybrid — BM25 + KNN in one planner pass

{
  "hybrid": {
    "fusion": "rrf",
    "queries": [
      { "match": { "message": "kernel panic on reboot" } },
      { "knn":   { "field": "embedding", "query_vector": [...], "k": 50 } }
    ]
  }
}

Source · engine/crates/xerj-engine/src/index.rs (run_knn_hnsw + exact fallback)