Build vector + graph RAG on one engine. No new query language.
- Vectors and graph on one copy of the data: retrieval and traversal over the same stored records.
- Write relationships as you write the data; afterwards every read is a get, a walk or a scan.
- No query planner to coax and no indexes to declare.
- Four doors answer today: the REST API, the WebSocket, the binary wire and clients for the languages you already use. Each door reads AND writes; the Elixir and Rust clients carry every data operation, the others carry the memory calls and the core reads and writes.
- Admin console: Coming soon.
- The binary wire is the engine's own compact frame protocol, and it reads and writes: a node takes an ingest over the wire as well as over HTTP, and a passage written on either is found by a search on the other. The clients encode and decode those frames today; the published wire specification follows.
Evaluating Semurg for a team? The evaluation guide takes you from a running node to your own data and the published benchmarks in an afternoon.
Four doors, one engine
The first five minutes
Install, prove the token, write, read. Every command below is one the installer prints or the release's own install test runs against a node you just installed. There is no account.
1. Install. The installer ends with a SUCCESS banner that prints your API token. Keep it.
curl -fsSL https://one.semurg.io/install | sudo bash
One prerequisite, and the installer checks it. The data directory must sit on a real disk filesystem, ext4 or xfs. A network share, a container overlay or a shared folder from a desktop virtual machine cannot take the writes this engine makes, and the installer stops and says so rather than running on top of one. On a normal Linux machine this is already true and there is nothing to do.
What it runs on today. Linux on x86-64: every release is installed and tested on Ubuntu 24.04, and the same release runs in Docker on any x86-64 Linux host. Any x86-64 processor from roughly the last ten years works: the release carries builds for processors with and without AVX-512 and picks the right one when it starts. To try it, 2 cores and 4 GB of memory are enough (the Docker quickstart below runs comfortably in a 4 GB container); the data directory needs a few GB free and grows with what you store. No GPU is needed. Apple Silicon Macs and Windows: native releases are planned, built from the same Ubuntu release; today, run the node on a Linux machine or VM you can reach and point your agents at it. On Windows, WSL2 and the Docker image in Docker Desktop are being tested now.
Already running a node? Take the new release with semurg update --apply (the node checks the
signed manifest, downloads, verifies, swaps and restarts, and rolls back if the new release does not come up). That line is for a node installed with step 1: the Docker image carries no semurg command.
Re-running the install command on a node that already has a completed install is REFUSED on purpose, because a
plain re-run would rewrite its environment from a fresh hardware scan and could leave your stored bytes
unserved; say what you mean with SEMURG_INSTALL_MODE=upgrade if you want the installer to do it.
2. Put the token in your shell. It is stored on the node, so you can read it back at any time.
TOKEN=$(sudo grep '^SEMURG_API_TOKEN=' /etc/semurg/semurg.env | cut -d= -f2-)
1b. Or run it in Docker. The same node, from the published image at one.semurg.io/semurg. Every
/v1 call needs the token exactly as on bare metal; Docker prints it in the container log at first
start and keeps it on the node.
docker run -d --name semurg -p 127.0.0.1:4000:4000 \ --ulimit memlock=-1 --ulimit nofile=1048576:1048576 --security-opt seccomp=unconfined \ -v semurg-data:/var/lib/semurg one.semurg.io/semurg until docker logs semurg 2>&1 | grep -q 'Your API token'; do sleep 1; done TOKEN=$(docker exec semurg cat /var/lib/semurg/.api_token)
--security-opt seccomp=unconfined is not optional: Docker's default profile
blocks the system calls the engine reads and writes with, and without it the container exits
at start with a message naming this flag. --ulimit memlock=-1 is what lets it register its
buffers. Binding to 127.0.0.1 keeps the node on the host until you decide otherwise.
The image is pulled from Semurg's own read-only registry at one.semurg.io, never from Docker Hub.
No licence key, ever, on one node. A single node runs the full engine, writes included, with nothing to obtain. A stale or foreign key file on the node changes nothing. A key exists only to join a second node to a cluster.
3. Prove the gate is on. Without the token the API answers 401 on purpose. With it, 200. A node needs a few seconds to start: if you pasted the steps above back to back, wait until curl -s http://127.0.0.1:4000/api/health answers before you run these two lines.
curl -si http://127.0.0.1:4000/v1/whoami | head -1 curl -si -H "Authorization: Bearer $TOKEN" http://127.0.0.1:4000/v1/whoami | head -1
4. Write something. A passage is the simplest thing to write: text goes in, and the node keeps one copy.
curl -s -H "Authorization: Bearer $TOKEN" -H 'content-type: application/json' \
-d '{"passages":[{"text":"Semurg keeps one copy of your data and answers every kind of question about it."}]}' \
http://127.0.0.1:4000/v1/ingest
5. Read it back. A search answers with three numbers: how many stored items matched, how many it looked at, and how many bytes that took. This search matches a needle of up to 20 bytes, so keep the needle to 20 bytes or fewer: a longer passage is stored in pieces it matches one by one.
curl -s -H "Authorization: Bearer $TOKEN" -H 'content-type: application/json' \
-d '{"op":"search","needle":"Semurg keeps"}' \
http://127.0.0.1:4000/v1/query
A new node starts empty. To try a traversal on data that is already there, ask for a small public
sample graph (a Panama Papers subset from the ICIJ Offshore Leaks data) on the node's first boot: run the installer with
SEMURG_PRELOAD_PANAMA=1, or add -e SEMURG_PRELOAD_PANAMA=1 to the docker run line; a
search's "looked at" count then includes the sample. See the blog for worked examples on data that
carries relationships of your own. Similarity search over your own passages
(vector_search, which returns the matching text rather than a count) needs an embedding model on the node;
a new install downloads one on its first boot unless you declined, and until one is installed the API says so plainly if you call it. Every call above works on a fresh node.
Your coding agent
Two things a coding agent can do with the node you just installed, in this order: remember across sessions, and build your app on it. The first is one command. The second is a short file you choose to add.
A. Give your agent a memory. One command connects Claude Code to the node's seven memory tools over MCP. The
token stays an environment-variable reference in the agent's settings, so export it in the shell you start the agent
from (TOKEN is the variable from step 2 or 1b above). claude mcp list should then show
semurg as connected. Claude Code itself installs with the line on the Agents page.
export SEMURG_API_TOKEN="$TOKEN"
claude mcp add-json --scope user semurg '{"type":"http","url":"http://127.0.0.1:4000/v1/mcp","headers":{"Authorization":"Bearer ${SEMURG_API_TOKEN}","X-Semurg-Scope":"default"}}'
claude mcp list
What the agent stores lives on the node, not in the chat: a later session, or a different agent, that connects to the same node and the same scope reads it back. Replace the address if your node is not local. The Agents page records Claude Code as Connected: its own client listed the node's memory tools. Cursor and the OpenAI Codex CLI use the same node and the same seven tools; their configuration is on that page too, marked as results pending until we have run each one ourselves.
B. Check the node by hand first. Six short calls, with no agent and no model involved. They use their own
scope, rules-check, so they do not touch your app's memory. Each prints the answer and then its HTTP
status.
export SEMURG_API_TOKEN="$TOKEN"
NODE_URL=http://127.0.0.1:4000
mem() { curl -sS -w '\nHTTP %{http_code}\n' -H "Authorization: Bearer $SEMURG_API_TOKEN" -H 'content-type: application/json' "$NODE_URL/v1/memory/$1" ${2:+-d "$2"}; }
mem status
mem remember '{"scope":"rules-check","facts":{"entities":[{"name":"Acme","type":"Company"},{"name":"Dana","type":"Person"}],"relations":[{"source":"Dana","target":"Acme","type":"WORKS_AT"}]},"passages":[{"text":"Dana works at Acme.","mentions":["Dana","Acme"]}],"source":"rules check"}'
mem recall '{"scope":"rules-check","subject":"Dana"}'
mem context '{"scope":"rules-check","query":"Where does Dana work?"}'
mem recall '{"scope":"rules-check","subject":"Nobody"}'
mem remember '{"scope":"rules-check","content":"Dana works at Acme."}'
All six answer HTTP 200. The fifth, a name that matches no stored entity, answers
state: nothing_stored with no items and a note saying what was tried: an empty answer that says so,
never an invented one. The sixth sends raw text as content: it is stored now, with no model, cut into
passages and linked to the entities it names that are already stored; the reply says
extraction: known_names_only and carries a hint: send the entities and relations you found in the
text as facts in the same call, and they are stored at once (the node's own extraction model reads text sent without
facts only when it is installed).
C. Let your agent build on it (optional). If you want a coding agent to build an app that keeps its data in
the node, a short rules file helps it start with the right calls instead of guessing. It is a recommendation, not a
requirement: nothing on the node depends on it, and you can edit or delete any line. Each recommendation carries its
reason, so you can keep the ones that fit your app and drop the rest. Save it at the root of the repository as
CLAUDE.md (Claude Code), AGENTS.md or .cursorrules (agents that read those
names).
# Semurg for this app's memory and retrieval (recommended, not required)
This file recommends keeping what the app must remember in a Semurg node. Each
recommendation says why, so you can keep the ones that fit and drop the rest.
## Connection
- Node: $NODE_URL (a base URL, for example http://127.0.0.1:4000).
- Token: $SEMURG_API_TOKEN.
Recommended: keep the token and the node address out of source files and out
of commits. Why: anyone who can read the repository could then use the node.
- MCP: POST /v1/mcp on the node (streamable HTTP, bearer token).
HTTP: POST /v1/memory/remember, /v1/memory/recall, /v1/memory/relate,
/v1/memory/forget, /v1/memory/context; GET /v1/memory/status.
- Recommended: when a memory call fails, call memory_status first.
Why: it says whether memory is on for this node and which scopes hold facts.
## The seven memory calls
memory_status, memory_remember, memory_recall, memory_relate, memory_forget,
memory_context, memory_document.
- One app, many end users. Recommended: send user_id (optional, on every
memory call; no ':', at most 256 bytes) naming the end user.
Why: each end user's memory is then their own inside your key, and two end
users never meet.
- One connection per end user. Recommended: send the X-Semurg-User header with
that end user's id (over MCP or HTTP).
Why: the connection is then pinned to that user. A call that names a
different user_id is refused with user_id_pinned_mismatch, so a mistake in
your code cannot read or delete another user's memory.
- When an end user leaves. Recommended: POST /v1/memory/delete_user
{"user_id": "..."} (over HTTP; it is not an MCP tool).
Why: one call forgets everything that end user stored.
- Where the app's durable memory lives. Recommended: the node, and not a second
store (another database file, cache or search index) for the same data.
Why: a second copy is a second thing to keep in step, and the node keeps the
relationships between things, which is what recall and context walk. If the
app needs another store for a different reason, that is your call.
- remember. Recommended: you are the extractor. Whenever you store text as
"content", also send the entities and relations you found in it as "facts"
in the same call.
Text with its facts: {"scope":"<app>","content":"Dana works at Acme.",
"facts":{"entities":[{"name":"Dana","type":"Person"},{"name":"Acme","type":"Company"}],
"relations":[{"source":"Dana","target":"Acme","type":"WORKS_AT"}]}}
Facts alone: {"scope":"<app>","facts":{"entities":[{"name":"Acme","type":"Company"}],
"relations":[{"source":"Dana","target":"Acme","type":"WORKS_AT"}]},
"source":"<where it came from>"}
Passages: {"scope":"<app>","passages":[{"text":"Dana works at Acme.",
"mentions":["Dana","Acme"]}]}
Raw text: {"scope":"<app>","content":"Dana works at Acme."}
Why: facts you send are stored at once, and you extract better and faster
than the node's own model, which reads text sent without facts only in the
background and slowly on a CPU (or not at all when none is installed; the
reply's model_extraction says which, and a reply to text without facts
carries a hint). Raw text is stored now: it is cut into passages and each
passage is linked to the entities it names that are already stored or that
you send beside it as facts (the reply says extraction: agent_facts or
known_names_only, and unlinked_passages counts the passages no entity reaches). Each mention of a
passage must appear in its text (letter case aside), or the passage is
refused by name. content and passages cannot ride in one call. A name is at
most 1024 bytes and one call holds at most 1000 entities; more is refused
with 422, so send several calls.
- recall. Recommended: pass the name of one entity, as stored or close to it:
{"scope":"<app>","subject":"Dana","k":20}
Why: recall finds the entity by its name (exact, a similar spelling or letter
case, or one whole word of a longer name), walks the graph from it and
returns the connected facts in both directions. A name that matches nothing
answers HTTP 200 with state nothing_stored and no items: not an error, not a
guess. Read state before you use the items.
- context. Recommended: ask it a question with the entity names in it:
{"scope":"<app>","query":"Where does Dana work?"}
Why: each word or phrase of the query that names a stored entity (exact, a
similar spelling or letter case, one whole word of a longer name, or the
start of a word) or a stored type becomes a seed. The graph is walked "hops"
steps (default 2) and the ranked facts come back, with any passage that names
those entities. When nothing matches, state is nothing_stored and items is
empty. A stored name that shares one ordinary word with a longer question can
still be a seed (a question about the capital of France can reach a stored
"Capital One"). Recommended: read the reply's grounding before you answer.
Why: grounded means the cited items cover your question; grounded_by_meaning
means a passage found by meaning answers a reworded question, so answer from
it when it does; related means they share only some of its words, so use only
what really answers; nothing_stored means say you do not know.
- compact. Recommended: send "compact": true on recall and context.
Why: the same answer, each item once, in a fraction of the bytes. Leave
it off only when you need the diagnostics (how each seed was found and the
walk it took).
- time. Recommended: when a fact changes, remember the new value with "date"
set to when it became true; to ask about the past, send as_of (what the node knew then) or
valid_at (what was true then) on recall or context, as an ISO 8601 date or
instant.
Why: a relation's values come back newest first, so the first is the current
one, and a date that does not parse is refused by name, never ignored.
- scope. Recommended: one scope per app, sent in the body of every HTTP call.
Why: a scope keeps one app's memory apart from another's, and the HTTP
routes answer 422 "missing scope" without it. Over MCP, the X-Semurg-Scope
header sets the connection's default.
- forget. Recommended: use it with the entity's exact name for an entity that
should no longer be returned, and check with a recall afterwards.
Why: the node marks the entity deleted, so recall and context should no
longer return it or a fact with it at either end; the recall is how you
confirm that on your node. A name that matches nothing is refused with 404
and up to three close names to check. A later remember that names it brings
it back. Every passage LINKED to it is hidden whole, with whatever else it
says; a passage that names it without that link stays, and the reply counts
it in passages_retained. To drop one fact use the fact form, to drop one text
the passage form.
- ids. Recommended: treat ids as strings and send them back unchanged.
Why: they are 64-bit numbers, and a JavaScript Number cannot hold one. A
passage id sent to forget as a JSON number is refused with id_not_string,
because the number may already have been rounded on the way.
## What to expect
- Recall and context find stored names by their spelling (exact, letter case,
a similar spelling, one whole word of a longer name, the start of a word),
and by MEANING once the node's embedding model is installed (it downloads on
the first boot). Recommended: read the reply's meaning field before you rely
on a paraphrase.
Why: until the model is installed, replies say meaning: not_armed and a
paraphrase that shares no name with the question is not found.
- Every memory tool and memory route, with its parameters, is listed in
https://one.semurg.io/llms.txt (the data routes, /v1/ingest and /v1/query
with its operations, are in the blog's API reference),
which is generated from the source. Recommended: read it instead of guessing
a route. Why: it is the one list that matches the running code.
- A refusal names what to change. Recommended: change that, and do not retry
the same call unchanged. Why: the same call is refused the same way.
Your code's history, held in Semurg
Put a git repository's history into your node, and your coding agent can ask it who changed a file, when and why, and what the file looked like at any point. It reads the history; it never changes your repository.
Six tools for your agent. The MCP connection from step A carries them beside the memory tools:
code_status: what the node holds of a repository and how fresh it is. Call it first: every other answer is as of the last commit it took in.code_at: a file as it was at a commit, a branch, a tag or a point in time.code_history: a file's history, newest first: who, when, the message, and what each commit did to it.code_blame: the commit that produced the file as it is at a revision, and the commits that changed it before that. Whole-file today: line-by-line blame is not built, and asking for it is refused by name.code_diff: the files changed between two revisions, and for one file its text before and after.code_changes_since: the commits after a revision, oldest first, each with the files it changed: what an agent reads to catch up.
Putting a repository in is one command on a node installed with step 1, and taking new commits in again later adds only those. Its how-to appears here with the next release, once we have run it from outside on that release. Files whose names or contents look like credentials are never stored. Read-only: the node holds the history and answers questions about it, and your commits still go through git.
How you work with it
Write the relationships
You say what connects to what as you write. That is the work, and it is done once, at write time.
Read three ways
Get one thing by id, walk from it to what it connects to, or scan a run of it. Those are the reads.
No query language
Ordinary code in the language you already use. There is nothing new to learn and nothing to tune.
The clients
A client points at the base URL of a node you operate, with a token if you set one. There is no Semurg-hosted control plane in the middle, and nothing about your data leaves your network.
- Python:
pip install semurg(PyPI, Apache-2.0, no dependencies, Python 3.9 or newer). Start a node with the Docker command above, thenremember,recallandcontextin five lines: the example is on the PyPI page. - Eleven languages: C++, C#, Elixir, Erlang, F#, Go, Java, Julia, OCaml, Python and Rust, in one
archive: one.semurg.io/dl/clients.tar.gz, with
CONNECTING.md, the guide to pointing a client at your node. The Python client was installed from PyPI and run against a live node on the current release (remember, recall, context); the other ten have not yet been run against it, and the four verbs below are the ones each client carries. Elixir needs Elixir 1.18 or newer and Rust needs a 2024-edition toolchain (rustup), which the Ubuntu 24.04 packages do not meet; C++ needslibcurl4-openssl-dev, Erlang needsrebar3and OCaml needsdune. The archive'sfetch_batch,benchmarks,dashboardandbenchmethods call routes that no longer exist and are being removed; the four verbs below work in every client - What they cover today: health, status, ingest and query in all eleven; the cluster and licence reads in five (Python, Go, Elixir, C#, Julia). The retrieval, analytics and generate verbs are REST-only today and are being added to the clients now.
- For a coding agent: the memory calls (status, remember, recall, relate, forget, context) are in the Python, Go, Java, C#, F#, Elixir, Erlang, Rust and OCaml clients; Julia has remember, recall and context; C++ reaches them over HTTP. The tools, routes and calls are in one plain-text file, generated from the source: one.semurg.io/llms.txt, and as a page with every command on it: one.semurg.io/commands.
- The same small surface in each, written in that language's idiom, so the mental model carries across your stack.
- A PostgreSQL wire door: the node also answers the PostgreSQL wire protocol on
127.0.0.1:5432, so an existing PostgreSQL driver can read from it. Today it is read-only, plus session control; any other statement is refused by name. When the node has an API token, the door asks for a token as the password (SCRAM-SHA-256); it admits without a password only on a node with no token, and it opens on an address other than loopback only when a token is set. It has no TLS yet, so reach it from another machine through a tunnel.