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
xerj_search — keyword / structured (match, term, bool, range).xerj_semantic_search — semantic query; server embeds the query text.xerj_vector_search — top-level knn, exact nearest-neighbor.xerj_hybrid_search — hybrid query; rrf or linear fusion.xerj_memory_store — store a memory (text auto-embedded).xerj_memory_recall — recall top-k by meaning / keyword / vector.DELETE /_memory/:ns drops the namespace.semantic_text, dense_vector, …).POST = auto-id, PUT /_doc/:id = explicit id.index, create, update, delete.Routes: engine/crates/xerj-api/src/router.rs · handlers: es_compat.rs, memory_api.rs
xerj_search — keyword & structured
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.
| Field | Type | Meaning |
|---|---|---|
field | string | The semantic_text field to search. |
query | string | Natural-language query. Embedded server-side. |
k? | int | Neighbors to retrieve (default 10). |
filter? | query | Pre-filter clause applied before scoring. |
boost? | float | Score 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).
| Field | Type | Meaning |
|---|---|---|
field | string | The dense_vector field. |
query_vector | float[] | Query embedding (same dims as the field). vector also accepted. |
k? | int | Neighbors to return (default 10). |
num_candidates? | int | Accepted 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.
| Field | Type | Meaning |
|---|---|---|
text | string | The memory. Auto-embedded + BM25-indexed. |
metadata? | object | Arbitrary tags; subfields filterable as metadata.x. |
vector? | float[] | Your own embedding; enables kNN recall. Fixes the namespace's vector dims. |
id? | string | Explicit id; a UUID is generated if omitted. |
dedup? | bool | If true, skip storing a near-duplicate of an existing memory. |
dedup_threshold? | float | Cosine 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.
| Field | Type | Meaning |
|---|---|---|
query? | string | Text query — BM25, or embedded when semantic:true. |
semantic? | bool | Embed query server-side and recall by similarity. |
vector? | float[] | Recall by kNN against a supplied embedding. Wins over query. |
k? | int | Number of memories to return (default 10). |
filter? | query | Metadata pre-filter, e.g. {"term":{"metadata.kind":"preference"}}. |
recency_weight? | float | Blend 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.