Config TOML
XERJ reads one TOML file. Every key has a production-ready default, so the smallest working config is an empty file. The table below is the full surface; the sections below it walk each group with a runnable example.
The config file is read from --config /path/to/xerj.toml on the command line, else from the path in XERJ_CONFIG. Nothing else is searched: with neither set, the built-in defaults are used even when ./xerj.toml or /etc/xerj/xerj.toml exists.
Full key table
xerj.v1.XerjSearch, the same engine calls as REST, same authorization). Cleartext h2c even when [tls] is on: off loopback with TLS enabled the node refuses to start unless tls.allow_insecure_grpc_h2c = true.analysis.analyzer.default, else standard. Setting it away from the default logs a warning at startup._source and quantizes it per query, so resident memory is unchanged and the field also gives up HNSW-served kNN (#392). "binary" has no quantizer behind it and is refused at startup rather than silently stored at full precision; "scalar4" is not a value this key accepts at all (the 4-bit quantizer in the vector crate is unreachable from config or mapping). Not wired as a node-wide default: only the "binary" refusal reads it; quantization comes from each field's index_options in the mapping. Setting it away from the default logs a warning at startup.from + size a search may ask for, for any index whose settings carry no index.max_result_window (that setting wins when present, above or below this). 1–2147483647. Page past it with search_after or a point-in-time cursor.rerank search stage. It is the only search-time feature that sends document text off this node, and it does nothing until a key is set. false refuses every rerank request with HTTP 403, whatever the environment holds, and nothing is sent.[server]
Network listeners and the data directory. Most deployments only touch data_dir and the bind address.
[server]
rest_port = 8080 # native /v1/* API
es_compat_port = 9200 # ES wire-compatible API
grpc_port = 8081 # gRPC (xerj.v1.XerjSearch), plaintext h2c
bind_address = "0.0.0.0" # not a default — the default is "127.0.0.1" (loopback only)
allow_insecure_network_bind = true # not a default (false) — required for a non-loopback bind while TLS is off
data_dir = "/var/lib/xerj" # not a default ("./data") — an absolute path is strongly recommended
The default bind is loopback, so an unconfigured node is not reachable from the network. Exposing it while tls.enabled = false refuses to start unless allow_insecure_network_bind says the cleartext exposure is intended — enable TLS and neither line is needed.
[auth]
Static API-key authentication. The first-run admin key is written to <data_dir>/admin.key; subsequent starts reuse it. Clients pass Authorization: ApiKey <key> on every request.
[auth] enabled = true admin_api_key = "" # blank → auto-generated on first run # Or provide your own: # admin_api_key = "ak_live_c8f9a4…"
[tls]
TLS termination at the server. In Kubernetes or behind a load balancer, leave this off and terminate at the proxy instead — one place to rotate certs, one place to log handshakes.
[tls]
enabled = true # not a default (false) — this block switches TLS on
cert_path = "/etc/xerj/certs/server.crt" # not a default ("") — your certificate
key_path = "/etc/xerj/certs/server.key" # not a default ("") — your private key
[storage]
The WAL and flush tuning. wal_sync is the durability knob everyone looks for — pick "sync" for financial/compliance workloads, "batched" for everything else, and "async" only in benchmarks.
[storage] wal_sync = "batched" wal_batch_ms = 100 # fsync every 100 ms wal_max_size_mb = 1024 # roll WAL every 1 GiB flush_size_mb = 512 # flush memtable at 512 MiB flush_interval_secs = 30
[merge]
Segment compaction. size_tiered is the only implemented policy: it merges same-size segments, which is cheap and write-optimal. log_structured is a name in the enum with no merge policy behind it, so the server refuses to start on it rather than silently running size-tiered merging for an operator who picked a levelled policy for its read amplification. If full-range scans are what hurts, that is a real gap in this build — setting the key will not change it.
[merge] strategy = "size_tiered" # the only one implemented — log_structured is refused at startup min_segments = 10 # accepted, not wired max_segment_mb = 8192 # 8 GiB cap io_rate_mb_per_sec = 100 # accepted, not wired — merges are not throttled max_concurrent = 1 # accepted, not wired (see XERJ_MERGE_PARALLELISM) tier_floor_mb = 4 min_merge_count = 4 # the real per-tier merge trigger max_merge_count = 16 # caps peak merge RAM
[compression]
See Compression for the encoding catalog. This section picks the outer block codec effort only — the inner per-column encodings are chosen automatically at write time.
[compression] enabled = true # accepted, not wired — no uncompressed write path exists level = "balanced" # Zstd L1 / L3 / L6, applied at MERGE (flush is always L3) block_size_docs = 128 # accepted, not wired — 16-4096, range-checked at startup
[fts]
This key is accepted but not wired: a text field without an analyzer of its own uses the index's analysis.analyzer.default when the index declares one, and standard otherwise. Set the analyzer per field or per index in the mapping when creating an index. See Analyzers for the built-ins.
[fts] default_analyzer = "standard" # accepted, not wired — see the table
[vector]
Vector search settings. Unfiltered kNN is served by a persisted HNSW graph with exact rescoring (measured recall@10 1.00 on the official bench query); filtered kNN and SQ8 fields run the exact scan. The hnsw_* keys are accepted and validated but not yet wired: the graph builds with fixed M=16 / ef_construction=200, and the query-time beam width comes from the request's num_candidates (floored at 800). Quantization trades recall for scoring precision, not for memory — see default_quantization below and #392.
[vector]
default_metric = "cosine" # accepted, not wired — similarity comes from the mapping
hnsw_m = 16 # accepted, not wired — the graph builds with M=16
hnsw_ef_construction = 200 # accepted for compat; no effect on results
hnsw_ef_search = 100 # accepted for compat; no effect on results
default_quantization = "none" # "scalar8" = int8 precision, 1–2% recall loss.
# NOT a RAM saving today — see issue #392
max_dimensions = 16384 # accepted, not wired — nothing checks dims against it
[logs]
Time-series retention is not implemented in this build: both keys are accepted and nothing reads them, no retention job runs, and log data is kept until you delete it. Setting either away from its default logs a warning at startup.
[logs] retention_days = 90 # accepted, not wired — nothing deletes log data time_partition = "1h" # accepted, not wired
[embedding]
Delegates vector generation to an OpenAI-compatible endpoint. Leave default_endpoint empty if clients provide vectors themselves. Token limits are model-specific; the chunker in the ai crate splits long documents to fit the model's window.
[embedding]
# OpenAI:
default_endpoint = "https://api.openai.com/v1/embeddings" # not a default ("" disables auto-embedding)
default_model = "text-embedding-3-small" # not a default ("")
batch_size = 64
timeout_ms = 5000
# Or a local Ollama:
# default_endpoint = "http://localhost:11434/v1/embeddings"
# default_model = "nomic-embed-text"
[limits]
Hard caps to protect the server from runaway queries and mapping explosions. Lower these on shared nodes, raise max_query_memory_mb for aggregation-heavy workloads. Leave max_process_memory_mb out to auto-size the process-wide cap from the machine (8 / 16 / 32 GiB tiers); set an explicit MiB ceiling only to raise headroom on a large host — for example if a big xerj autoindex trips 429 circuit_breaking_exception.
[limits] max_query_memory_mb = 512 max_concurrent_searches = 64 max_fields_per_index = 500 # max_process_memory_mb = 0 # omit → AUTO (8/16/32 GiB by machine); 0 = whole machine
[indexing]
Turbo mode knobs. Turbo is opt-in per-request via POST /v1/indices/:name/turbo-ingest or the X-Turbo: true header on _bulk; none of the three settings below changes anything in this build (each logs a warning at startup when set away from its default).
[indexing] turbo_batch_size = 1000 # accepted, not wired turbo_parallel = true # accepted, not wired turbo_fast_analyzer = false # accepted, not wired
[cluster]
Multi-node mode. Default is off — single-node doesn't need a consensus layer. When enabled, the embedded Raft implementation replicates metadata only (schemas, shard assignments, node roster). See Clustering for the full story.
[cluster]
enabled = true # not a default (false) — this block switches cluster mode on
port = 9300 # intra-cluster gRPC + Raft
peers = [ # not a default ([]) — your node roster; label = address (#1171)
"10.0.0.11:9300=10.0.0.11:9300",
"10.0.0.12:9300=10.0.0.12:9300",
"10.0.0.13:9300=10.0.0.13:9300",
]
tick_ms = 50
auth_secret = "<secret>" # not a default ("") — required when enabled, 16+ chars, same on every node
[rerank]
This feature sends your data off the machine. A search that carries a rerank block POSTs the question and the text of up to window hits (default 30, maximum 300) to a third-party relevance judge, and reorders the hits by the probability that comes back. It is the only search-time feature that sends document text off the node. Two other features send text off the node, both operator configuration and off by default: [embedding] default_endpoint (proxy embeddings) and the WAL tap. The neural model download and cluster Raft traffic carry no document text. Nothing is sent until an operator sets a key, a request only triggers it by asking for it, and enabled = false forbids it outright. Settings are resolved once at startup; a value in this file wins over the environment. Ranking quality with the provider's model has not been verified by this project. Full reference: docs/RERANK.md.
[rerank]
enabled = true
api_key = "ts-..." # not a default ("") — or set TYPESAFE_API_KEY
endpoint = "https://api.typesafe.ai/v1/systemone" # not a default ("") — this is where "" resolves to
Check it without printing the key: GET /_xerj/rerank (superuser only) returns configured, whether a key is set and whether it came from config or env, the endpoint with any query string removed, and the server-side limits.
Source · engine/xerj.default.toml · engine/crates/xerj-common/src/config.rs