A new way to answer questions over your data: the model writes a small program, the engine runs it over your own facts. No embeddings to build, no GPU to rent, no answer you cannot check. Four CPU cores, offline, and hundreds of times cheaper than RAG or an LLM reading the data.
It is off by default. A node becomes a XERJ Query-1 node when you start it
with xerj --facts-model-dir <DIR>. Without the flag,
rc.101 behaves exactly like rc.100.
You write a plain question: "What is Bo Chen's height?", "Which river
is longer, X or Y?", "How many people have office Berlin?". The model
reads the question plus a numbered listing of the facts in the
namespace and emits a few program steps: LOOKUP #12,
CHAIN, LT / GT / EQ / NOT,
PICK_LT, or for counts SET → FIND → FILTER → AGG
COUNT. It never writes the answer itself.
Request · a plain question (an evaluation item; the names are synthetic)
POST /_facts/world-dev-90014/_ask
{"question": "Which river carries less water, Arhalga or Gacunas?"}
Response · the program the model wrote, then the trace and the answer
program LOOKUP #10 ; LOOKUP #7 ; LT ; RETURN step 0 LOOKUP #10 → 6956 m3/s doc3@v1/discharge, Arhalga River step 1 LOOKUP #7 → 7567 m3/s doc6@v1/discharge, Gacunas River step 2 LT → TRUE 6956 < 7567 answer Arhalga River
What #10 and #7 mean
#10 and #7 are not words from your prompt. For each question the engine lists the facts in the namespace as a short numbered listing (#0, #1, #2 …). LOOKUP #10 means: read record 10 of that listing. Here record 10 is the Arhalga River's discharge, 6,956 m3/s, and record 7 is the Gacunas River's, 7,567 m3/s. Every step of the trace names the record it used, with its source document and span.
Each step points at a stored fact: subject, relation, typed value, source document and the character span the value came from. The executor runs the program in microseconds and returns the answer plus the trace: which record, which version, which span. Nothing in the answer is generated; it is read off the store.
Request
POST /_facts/hr/_ask
{"question": "How tall is Ana Bel?"}
Response · answer with the trace
{
"final_status": "ANSWER",
"final_answer": "170 cm",
"program": [["LOOKUP", 0], ["RETURN"]],
"evidence": [{
"fact_id": "f1",
"doc_id": "d1", "version": 1,
"char_start": 11, "char_end": 14, "quote": "170",
"content_sha256": "…"
}],
"published_rev": 9
}
A verifier checks the program against the store before anything is served. A pointer that resolves to nothing returns UNKNOWN. Two stored facts that disagree return CONFLICT, and that propagates through any comparison built on them. A count is served only when the executor can close the set and a second, independently seeded model writes the same program.
A fact that was never written
{"question": "What is Ana Bel's salary?"}
→ {"final_status": "UNKNOWN"}
Two heights stored for the same person, neither supersedes the other
{"question": "How tall is Bo Chen?"}
→ {"final_status": "CONFLICT", "evidence": []}
A count, served only with partner agreement
{"question": "How many people have office Berlin?"}
→ {"final_status": "ANSWER", "final_answer": "3",
"path": "counting", "partner_agreement": true}
There is no vector step on this path. The "index" the model uses is the facts store the engine already keeps: typed records, an inverted index and doc-values. Writing 9,496 facts took 8.8 seconds and 15.4 MB on disk; no encoder ran, no vector memory was allocated, and there is nothing to redo when a model changes. The same binary still offers BM25 full-text search, aggregations and kNN for documents.
Index build, measured, per arm (same 5,000 records)
XERJ Query-1 8.8 s wall · 5.5 CPU-s · 15.4 MB · no LLM call BM25 RAG 0.02 s · no LLM call Embedding RAG 19.6 s · 1.28 GB peak RSS (bge-small, 8 threads) GraphRAG 267 LLM calls · $14.27 · 72 min serial
Still a full search engine on the same port
POST /hr/_search {"query": {"match": {"bio": "designer Berlin"}}}
POST /hr/_search {"aggs": {"by_office": {"terms": {"field": "office.keyword"}}}}
POST /vecs/_search {"knn": {"field": "v", "query_vector": [...], "k": 2}}
Facts go in through /_facts/{ns}/_bulk: text, number,
date or boolean values, a stable subject_id per entity,
and the source document with the quote span. A correction
supersedes the old fact and the answer changes, while the old answer
stays readable at its revision. A second, disagreeing fact turns the
answer into CONFLICT until one is withdrawn. Every ask can be pinned
to a revision. About 44 ms per ask on four x86 cores; about 120 ms
at the median in the int8 serving configuration.
Write facts with their source
POST /_facts/hr/_bulk
{
"documents": [{"doc_id": "d1", "version": 1,
"text": "Ana Bel is 170 cm tall and works in Berlin."}],
"facts": [{"fact_id": "f1", "subject": "Ana Bel", "subject_id": "emp:1",
"relation": "height", "kind": "number", "value": "170 cm",
"doc_id": "d1", "version": 1, "char_start": 11, "char_end": 14}]
}
→ {"committed": {"from_rev": 1, "to_rev": 9}}
Correct it, then ask again, then ask as of before
POST /_facts/hr/_bulk
{"edits": [{"op": "correct",
"doc": {"doc_id": "d1", "version": 2, "text": "Ana Bel is 172 cm tall…"},
"fact": {"fact_id": "f7", "subject_id": "emp:1", "relation": "height",
"kind": "number", "value": "172 cm", "doc_id": "d1", "version": 2},
"supersedes": "f1"}]}
→ {"committed": {"from_rev": 13, "to_rev": 14}}
{"question": "How tall is Ana Bel?"} → "172 cm" (evidence f7)
{"question": "How tall is Ana Bel?", "revision": 9} → "170 cm"
DELETE /_facts/hr/f9 → a withdrawn fact; CONFLICT clears
Agent memory (store, recall, forget; lexical ranking today) and
brains (link, unlink, ego with evidence, overview) are separate
surfaces, available over HTTP and as MCP tools. Brain names ending
in -facts are reserved for facts namespaces. The model
reads facts namespaces only; memory is for notes an agent wants
back later, brains are for the links between them.
Store a note, recall it, link it
POST /_memory/agent1 {"text": "Ana Bel is the Falcon tech lead", "tags": ["falcon"]}
POST /_memory/agent1/_recall {"query": "who leads Falcon", "k": 2}
→ two notes with scores (lexical ranking)
POST /_graph/agent1/link
{"src": "mem:falcon", "dst": "person:ana-bel", "type": "led_by",
"evidence": {"quote": "…", "source": "…", "offset": 0}}
→ 201 {"edge_id": "…"}
GET /_graph/agent1/ego?node=person:ana-bel&hops=1 → the edges, with evidence
The same surfaces as MCP tools
xerj_memory_store · xerj_memory_recall xerj_brain_link · xerj_brain_unlink · xerj_brain_ego · xerj_brain_overview xerj_facts_ask (listed only when the node runs the model: 24 tools on, 23 off)
One store merged from 95 collections of the standard evaluation, 160 questions with known answers, scored by the same frozen rubric. Costs are per million questions, ARITHMETIC: the measured per-1,000-question list price times 1,000. Opus 5.5 at $4 per million input tokens and $20 per million output tokens; CPU at the AWS c7g.xlarge on-demand rate of $0.145 per hour ($0.03625 per vCPU-hour) at 4.1 CPU-seconds per question, the live run.
| Approach | Returns | Checkable | Index time | Cost / million q | Wrong of 160 | Offline on CPU |
|---|---|---|---|---|---|---|
| Keyword search (BM25) | documents | no answer to check | 0.02 s | CPU only | n/a | yes |
| Embeddings + RAG on Opus 5.5 (top-10) | generated text | no | 19.6 s, 1.28 GB | $19,850 | 14 | no |
| BM25 + RAG on Opus 5.5 (top-10) | generated text | no | 0.02 s | $20,950 | 9 | no |
| GraphRAG on Opus 5.5 | generated text | no | 72 min, 267 LLM calls, $14.27 | $65,390 | 9 | no |
| Opus 5.5 reading the whole store (40 q per call; 1 q per call: $694,300) | generated text | no | none | $19,410 | 6 | no |
| Opus 5.5 with perfect retrieval | generated text | no | assumes perfect retrieval | $28,840 | 5 | no |
| XERJ Query-1 | program + trace | yes, step by step | 8.8 s, 15.4 MB | $42 | 1 | yes |
Source: the head-to-head measured on 2026-11-05 for the XERJ Query-1 announcement (n = 160, 95% paired bootstrap intervals; the score gap is statistically clear against embedding RAG and BM25 top-5, the wrong-answer gap against every LLM pipeline). Records were pre-extracted for every arm; questions are from the model's own evaluation distribution. The XERJ cost is the live run on a shared aarch64 box; the int8 configuration on a quiet x86 box measures lower.
xerj autoindex yields records and a graph, not facts
namespaces). The river example in the announcement uses records that came
from the evaluation pipeline.
devices
relation returns the member count (ANSWER 3) as verified. Disclosed here and
in the docs; the real fix is in the next model.
--facts-model-dir the node
is rc.100: no /_facts route, no model load, the same MCP tool list.
set counts, partner-verified · needs subject_id · no sums in rc.101
Set counts over facts that carry a subject_id: "how
many X have Y", "distinct values of a field". The model writes a set
program, the executor runs it over the store, and the count is
served only when a second, independently seeded model writes the
same program. Not sums, totals or thresholds in this release.
Facts with subject ids, then a count
facts: [{"subject": "Ana Bel", "subject_id": "emp:1", "relation": "office", "value": "Berlin"},
{"subject": "Bo Chen", "subject_id": "emp:2", "relation": "office", "value": "Berlin"},
{"subject": "Cy Dao", "subject_id": "emp:3", "relation": "office", "value": "Berlin"},
{"subject": "Di Eck", "subject_id": "emp:4", "relation": "office", "value": "Munich"}]
{"question": "How many people have office Berlin?"}
→ {"final_status": "ANSWER", "final_answer": "3",
"path": "counting", "partner_agreement": true, "program_sha256": "…"}
{"question": "How many people have role engineer?"} → "2"
Without subject ids the set cannot be closed
→ {"final_status": "UNKNOWN", "path": "facts", "counting": {"status": "INCOMPLETE"}}
LOOKUP #10 reads record 10 of it. The trace names every record it used./_facts/{ns}/_bulk or the MCP tool. It does not yet extract facts from arbitrary documents on its own. Full-text search over your documents is the ordinary search, and it works alongside.Install the engine with the model bundle, start the node with the flag, write a few facts, ask a question. The trace is the product.
curl -fsSL https://xerj.org/get | sh -s -- --with-model xerj --facts-model-dir ~/.local/share/xerj/model --insecure --data-dir ~/xerj-data curl -s localhost:9200/_facts | jq '.model.state' # "loaded"
The write contract, the two ask routes and the counting rules are in the model quickstart and the facts API reference. The announcement, with the measurements and how the model was trained, is at /blog/xerj-query-ai-model.
XERJ Query-1 was trained on donated GPU time. If it is useful to you, fund the next model.