A new way to answer questions over your data: the model writes a small program, the engine runs it over facts you have written. No embeddings to build and no GPU to rent. Lookups and comparisons come with the facts they used; counts come with the program's hash and a second model's agreement, not the records. Four CPU cores, offline, and in our 160-question test hundreds of times cheaper per question than RAG or an LLM reading the data (ARITHMETIC at list prices).
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 serves no /_facts route and the same MCP tools as rc.100,
but its storage format changed: the first write moves a data folder to
format 4, which rc.100 cannot open.
You write a plain question: "What is Bo Chen's height?", "Which river
is longer, X or Y?", "How many people have office Berlin?" (counting needs a stable subject_id
on each person; without one the answer is UNKNOWN). 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 · an evaluation item from our teacher_dev set (the world and its names are invented)
POST /_facts/world-dev-90014/_ask
{"question": "Is the Arhalga River's discharge exceeded by that of the Gacunas River?"}
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 yes
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 the 9,496 facts of our 160-question test (each question's records in its own namespace) took 8.8 seconds and 15.4 MB on disk on a shared ARM server (1.8 to 3.0 s on a laptop); 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 (writing the 9,496 facts of the 160 test questions into 160 small namespaces on a shared ARM server; 1.8 to 3.0 s on a laptop) 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 35 to 45 ms per ask on a tiny namespace (3 documents, 6 facts) on a laptop with 4 model threads; about 220 ms at the median on our 160-question test (namespaces of 6 to 148 facts). The 120 ms we published earlier came from an unreleased int8 setting on one 40,000-fact namespace with a single repeated lookup question.
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 in rc.101; main adds xerj_sources, so rc.102 will list 25 and 24)
160 questions with known answers from our own development evaluation, scored by the same frozen rubric. The language-model arms read one merged store of 5,000 records (4,305 distinct: 13 collections were stored twice); 3,732 of those records come from public data (MoMA and Tate collection metadata, OurAirports, USGS earthquakes, UCI Online Retail, the NASA Exoplanet Archive, Python package metadata), the rest from invented worlds. XERJ Query-1 and the oracle read only each question's own records. 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 on a shared ARM server (0.9 on a quiet x86 laptop).
| Approach | Returns | Checkable | Index time | Cost / million q | Wrong of 160 (declined) | Offline on CPU |
|---|---|---|---|---|---|---|
| Keyword search (BM25) | records | no answer to check | 0.02 s | CPU only | n/a | yes |
| Embeddings + RAG on Opus 5.5 (top-10) | a short written answer | no record pointer (our prompt asked for none) | 19.6 s, 1.28 GB | $19,850 | 14 (37) | no |
| BM25 + RAG on Opus 5.5 (top-10) | a short written answer | no record pointer | 0.02 s | $20,950 | 9 (27) | no |
| GraphRAG on Opus 5.5 | a short written answer | no record pointer | 267 LLM calls, $14.27; 72 min one call at a time, 12 min at 6 in parallel | $65,390 | 9 (24) | no |
| Opus 5.5 reading the whole store (40 q per call; 1 q per call: $694,300) | a short written answer | no record pointer | none | $19,410 | 6 (24) | no |
| Opus 5.5 given each question's own records (oracle; our 2026-10-31 reference run, re-scored) | a short written answer | no record pointer | none (needs perfect retrieval) | $28,840 | 5 (18) | no |
| XERJ Query-1 (each question's own records) | program + trace | lookups: each step cites a fact; counts: program hash only | 8.8 s, 15.4 MB (160 small namespaces, 9,496 facts) | $42 | 1 (35) | yes |
Source: the head-to-head measured on 2026-11-05 (n = 160, 95% paired bootstrap intervals). The Score gap (our credit scale, the Opus 5.5 reference reader = 100) is statistically clear against embedding RAG and BM25 RAG top-5, and not clear against BM25 top-10, GraphRAG, Opus reading the whole store or the oracle. The wrong-answer gap is clear against GraphRAG and every RAG setup, borderline against Opus reading the whole store (0 to 11) and not clear against the oracle (−1 to 10). Records were pre-extracted for every arm. All 160 questions come from the model's development set. Retrieval quality of the search arms (R@k, nDCG), the latency split and the data sets: benchmarks and methodology.
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 there
is no /_facts route, no model load and the same MCP tool list as
rc.100. Storage still changed: rc.101's first write moves a data folder to
format 4, which rc.100 cannot open.
set counts, partner-verified · needs subject_id · no sums in rc.101 (a sum question can come back as a member count)
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 (over MCP you can ask with xerj_facts_ask, but no MCP tool writes facts). 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.XERJ_DISABLE_FEEDBACK=true or --disable-feedback.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 mostly on rented GPU time paid for with donations, plus a few machines of our own. If it is useful to you, fund the next model.