FOR AI · 03

Endpoint contract

The exact wire contract for the six agent operations plus the data endpoints they build on. Everything is on port 9200, JSON in and JSON out. Optional fields are marked ?. Response shapes are shown as they actually come back from a live XERJ.

At a glance

METHOD
PATH
DESCRIPTION
POST
xerj_search — keyword / structured (match, term, bool, range).
POST
xerj_semantic_searchsemantic query; server embeds the query text.
POST
xerj_vector_search — top-level knn, exact nearest-neighbor.
POST
xerj_hybrid_searchhybrid query; rrf or linear fusion.
POST
xerj_memory_store — store a memory (text auto-embedded).
POST
xerj_memory_recall — recall top-k by meaning / keyword / vector.
GET
List recent entries in a namespace (bounded, recent first).
DELETE
Forget one memory. DELETE /_memory/:ns drops the namespace.
PUT
Create an index with a mapping (semantic_text, dense_vector, …).
POST/PUT
Index a document. POST = auto-id, PUT /_doc/:id = explicit id.
POST
NDJSON bulk: index, create, update, delete.

Routes: engine/crates/xerj-api/src/router.rs · handlers: es_compat.rs, memory_api.rs

Standard ES query DSL on POST /{index}/_search: match (BM25 full-text), term/terms (exact), range, bool (must/should/must_not/filter), plus from, size, sort, _source, aggs, highlight. See the full query-types reference.

POST /notes/_search
{"query": {"match": {"body": "deployment"}}, "size": 5, "_source": ["body"]}

→ {"hits": {"total": {"value": 1, "relation": "eq"},
            "hits": [{"_id": "1", "_score": 0.28…,
                      "_source": {"body": "How do I roll back a bad deployment?"}}]}}

xerj_semantic_search — retrieve by meaning

Requires a field mapped as semantic_text (see creating an index). XERJ auto-embeds that field on ingest into a companion <field>_vector; the semantic query embeds the query text the same way and ranks by vector similarity — no client-side embedding, no external key.

FieldTypeMeaning
fieldstringThe semantic_text field to search.
querystringNatural-language query. Embedded server-side.
k?intNeighbors to retrieve (default 10).
filter?queryPre-filter clause applied before scoring.
boost?floatScore multiplier for this clause.
POST /notes/_search
{"query": {"semantic": {"field": "body", "query": "undo a failed rollout", "k": 2}},
 "_source": ["body"]}

→ top hit _id "2" — "Reverting a broken release to the previous version." (shares no words)

Honesty: the zero-config built-in embedder is deterministic and lexical, not a large neural model — great for offline, reproducible retrieval. For production semantic quality, map the field with an inference_id / inference_endpoint pointing at your own OpenAI-compatible embeddings service; the query shape is unchanged.

xerj_vector_search — exact kNN

Bring your own embeddings: map a dense_vector field, store vectors on each doc, and pass a query_vector as a top-level knn block (ES 8.x shape).

FieldTypeMeaning
fieldstringThe dense_vector field.
query_vectorfloat[]Query embedding (same dims as the field). vector also accepted.
k?intNeighbors to return (default 10).
num_candidates?intAccepted for ES compatibility.
POST /notes/_search
{"knn": {"field": "embedding", "query_vector": [0.1, 0.9, 0.0], "k": 2},
 "_source": ["body"]}

→ [("1", 1.0), ("2", 0.9998)]   # exact cosine scores

Honesty: knn is exact (brute-force) at query time — every candidate vector is scored, so recall is exact and there is no approximate query-time traversal to tune. (An HNSW graph is built at ingest; the query path does not do approximate graph search.)

xerj_hybrid_search — fuse keyword + vector

Run several sub-queries and fuse their rankings in one call. Each entry in queries is {"query": {…}, "weight"?: float}; fusion is "rrf" (reciprocal-rank fusion, default) or "linear" (weighted score sum).

POST /notes/_search
{"query": {"hybrid": {
   "queries": [
     {"query": {"match": {"body": "rollback deployment"}}},
     {"query": {"knn": {"field": "embedding", "query_vector": [0.1,0.9,0.0], "k": 3}}}
   ],
   "fusion": "rrf"
 }},
 "_source": ["body"]}

→ fused ranking over both sub-queries (RRF k=60)

Honesty: only rrf and linear are implemented. Requesting "fusion":"learned" fails loudly:

→ "reason": "parse error: hybrid fusion learned is not yet supported; use rrf or linear"

xerj_memory_store

POST /_memory/{namespace}. The namespace is created on first store. A memory needs at least text or a vector. Text is always BM25-indexed and auto-embedded (so it is semantically recallable); a supplied vector additionally enables kNN recall.

FieldTypeMeaning
textstringThe memory. Auto-embedded + BM25-indexed.
metadata?objectArbitrary tags; subfields filterable as metadata.x.
vector?float[]Your own embedding; enables kNN recall. Fixes the namespace's vector dims.
id?stringExplicit id; a UUID is generated if omitted.
dedup?boolIf true, skip storing a near-duplicate of an existing memory.
dedup_threshold?floatCosine threshold for dedup (default 0.95).
POST /_memory/agent-demo
{"text": "The user prefers metric units and a dark UI theme.", "metadata": {"kind": "preference"}}

→ 201 {"created": true, "id": "77eff57b-…", "namespace": "agent-demo"}
   # dedup hit → 200 {"created": false, "deduplicated": true, "id": "…", "score": 0.97}

xerj_memory_recall

POST /_memory/{namespace}/_recall. The body picks the mode: an explicit vector → kNN; else "semantic": true with a query → server-side semantic recall; else query alone → BM25; empty body → recent memories.

FieldTypeMeaning
query?stringText query — BM25, or embedded when semantic:true.
semantic?boolEmbed query server-side and recall by similarity.
vector?float[]Recall by kNN against a supplied embedding. Wins over query.
k?intNumber of memories to return (default 10).
filter?queryMetadata pre-filter, e.g. {"term":{"metadata.kind":"preference"}}.
recency_weight?floatBlend recency into ranking in [0,1] (0 = pure relevance).
POST /_memory/agent-demo/_recall
{"query": "what display settings does the user like?", "semantic": true, "k": 2}

→ {"hits": [{"id": "77eff57b-…", "text": "The user prefers metric units and a dark UI theme.",
             "metadata": {"kind": "preference"}, "score": 0.6555…}],
   "namespace": "agent-demo"}

Recall of an unknown namespace returns {"hits": [], "namespace": …} — isolation is structural: each namespace is a physically separate index, so a recall in one can never read another.

List a namespace

GET /_memory/{namespace} returns a bounded, recent-first view for debugging/audit (up to 100 entries) — not a full export.

GET /_memory/agent-demo
→ {"namespace": "agent-demo", "exists": true, "count": 2,
   "entries": [{"id": "…", "text": "…", "metadata": {…}, "score": 1.0}, …]}

Forget

DELETE /_memory/agent-demo/77eff57b-…   → {"forgotten": true,  "id": "77eff57b-…", "namespace": "agent-demo"}
DELETE /_memory/agent-demo              → {"dropped": true, "namespace": "agent-demo"}

Data endpoints the ops build on

To use semantic, knn, or hybrid over your own data you first create an index and put documents in it. These are the standard ES-compatible calls (full reference: ES-compat API).

# create an index with a semantic_text + dense_vector mapping
PUT /notes
{"mappings": {"properties": {
   "body":      {"type": "semantic_text"},
   "embedding": {"type": "dense_vector", "dims": 3, "similarity": "cosine"}
}}}
→ {"acknowledged": true, "shards_acknowledged": true, "index": "notes"}

# index a document with an EXPLICIT id (PUT) …
PUT /notes/_doc/1
{"body": "How do I roll back a bad deployment?", "embedding": [0.1, 0.9, 0.0]}
→ {"_index": "notes", "_id": "1", "result": "created", …}

# … or with an AUTO id (POST, no id in the path)
POST /notes/_doc
{"body": "A note without an id", "embedding": [0.2, 0.7, 0.1]}
→ {"_id": "<uuid>", "result": "created", …}

# bulk NDJSON (Content-Type: application/x-ndjson)
POST /logs/_bulk
{"index":{"_id":"1"}}
{"service":"auth","level":"error","message":"login failed"}
→ {"errors": false, "items": [{"index": {"status": 201, …}}]}

Freshly indexed documents become searchable after a refresh; call POST /{index}/_refresh in a test loop, or rely on the periodic refresh in normal operation.