Blog / How-to guides

Semurg as a graph database: load edges, walk them to any depth

23 October 2026 · Semurg · 4 min read · Markdown

Last checked on r11.126.20261025.70c5311 (25 Oct 2026): every command below ran on a fresh node of that release. The current release is r11.127.20261026.f8fafdc (26 Oct 2026), and the commands have not been re-checked on it yet (how)

graphdatabasehow-topython

A graph in Semurg is a set of edges. An edge's id is computed from its two ends and its relation, so writing the same edge twice still leaves one edge. A walk starts from the nodes you name and follows outgoing edges level by level, as deep as you ask, and answers with how many nodes it reached, how many edges it followed, and how many nodes it first reached at each level.

Your edges, one record each; a walk starts from the seeds you name and follows outgoing edges level by level, answering with the nodes reached, the edges followed and how many nodes were first reached at each level.Your edges, one recordeachThe nodes you startfromA walk: outgoingedges, level by levelNodes reached, edgesfollowed, nodes firstreached per level

Load edges and walk them, from Python

pip install semurg, then build each edge with semurg.container.edge(source, target) (or edge(source, target, relation)) and send them with ingest. The program below writes a small graph, a short chain with one branch, and walks it out from the first node of the chain. It reads your node's API token from SEMURG_API_TOKEN: the installer prints it, and semurg.open() reads it for you.

python
import os
from semurg import Client, container

c = Client("http://localhost:4000", token=os.environ["SEMURG_API_TOKEN"])

# 1 -> 2 -> 3 -> 4, and a branch 2 -> 5
edges = [container.edge(1, 2), container.edge(2, 3), container.edge(3, 4), container.edge(2, 5)]
print("written", c.ingest(edges)["containers"])

walk = c.query("traverse", {"seeds": [1], "hops": 3})["result"]
print("levels", walk["witness"]["levels"])
print("nodes", walk["nodes_visited"], "edges", walk["edge_hops"])

Expected: four edges written; levels 1, 1, 2, 1 (node 1, then 2, then 3 and 5, then 4); five nodes reached (node 1 included) over four edges

semurg.open() starts a local node with Docker and returns the same client, so on an x86_64 Linux machine with Docker the first line can be c = semurg.open(). A large graph goes in batches: each ingest call takes a list of edges, and the edges are checked before anything is sent (an edge built wrong is refused by its position in the list, never stored wrong).

What a walk answers

  • nodes_visited: the distinct nodes reached, the starting nodes included.
  • edge_hops: the edges followed.
  • witness.levels: how many nodes were first reached at each level, starting with the seeds, so you can see where the walk grew and where it stopped. levels_complete is true when every node of the cluster reported its counts; false means the per-level counts are partial, and "elided" means the walk went too deep to list every level.

A walk takes up to 100000 starting nodes in one call ("seeds": [...]) and any depth ("hops"). Over HTTP it is one call:

bash
curl -sS http://localhost:4000/v1/query \
  -H "Authorization: Bearer $SEMURG_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"op":"traverse","seeds":[1],"hops":3}'

Read a node's neighbours

neighbour_ids answers each node's outgoing neighbours, or only how many there are with "count_only": true:

bash
curl -sS http://localhost:4000/v1/query \
  -H "Authorization: Bearer $SEMURG_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"op":"neighbour_ids","ids":[2],"direction":"out"}'

Each id in the answer says its state: its edges (an empty list for a node with none), a page of them when there are more than you asked for, a count with count_only, or absent when nothing was ever written under that id.

When something is refused

  • 422: a malformed request, with the field named.
  • 413: more than 100000 starting nodes in one call.
  • 501: starting nodes that exist but cannot be walked on this release.
  • 503: the node is under its memory floor, or busy with other walks; retry.

What it does not do yet

  • A walk answers counts and a witness per level, not the list of nodes it reached.
  • No shortest-path door, and walks follow outgoing edges only.
  • No graph query language (Cypher or GQL).
  • Edges load as records built by the clients (semurg.container in Python), not as JSON.

Where it stands, and run it yourself

Graph is on the benchmark page against Neo4j, ArangoDB, FalkorDB, ArcadeDB and SurrealDB, each on all of the server's cores, in memory and on a graph far bigger than memory, with the release and the machine beside each figure. Graph on one server and a graph bigger than memory walk through those results. To run the same comparison on your machine:

bash
curl -fsSL https://one.semurg.io/bench | sudo bash -s -- --graph

The runner loads the same graph into every engine, checks that each answers every question the same way, and only then times it.

Next

How these commands were checked

Our test farm ran each of the 1 command block marked runnable, exactly as written, with the answer checked against the expectation printed under it, against a fresh node of the published release r11.126.20261025.70c5311. A post is published only when every one of them passed there.

Related