Four kinds of data, one route: objects, streams, search and places
12 October 2026 · Semurg · 5 min read · Markdown
Verified: every command below ran on a fresh node of r11.78.20261012.bc0d94f (how)
objectsstreamingsearchgeohow-to
Most applications need more than one kind of data. Files and blobs, a stream of events, text that has to be searchable, places on a map. Usually each is its own system. In Semurg they are the same store, written and read through the same route, POST /v1/query, with an op that says which kind of data the call is about. This guide shows one write and one read for each of four kinds.
Each block puts your token in the shell first; replace <your token> with yours.
Objects: store a value of any size under an id
An object is a value of any size, from a few bytes to a large file, stored under a numeric id. Values travel as base64 over HTTP.
export SEMURG_API_TOKEN=<your token>
curl -sS http://localhost:4000/v1/query \
-H "Authorization: Bearer $SEMURG_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{"op":"object_put","objects":[{"id":900000000001,"value_b64":"aGVsbG8gc2VtdXJn"}]}'Expected: the object is written (or skipped, if this node already holds exactly that value)
Read it back by id. Each object in the answer says its state: present with its value, absent if the id holds nothing, deleted if it was deleted, and a few more states that name exactly why a value could not be returned. None of them is a silent empty value.
export SEMURG_API_TOKEN=<your token>
curl -sS http://localhost:4000/v1/query \
-H "Authorization: Bearer $SEMURG_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{"op":"object_get","ids":[900000000001]}'Expected: the object, present, with the same base64 value that was stored
To delete, send {"op":"object_delete","ids":[...]}; the answer says how many were deleted, and a later read answers deleted with the generation it happened at. One call over HTTP returns at most 64 MiB of values; a value larger than that answers too_large rather than arriving cut short.
Streams: append messages, read them in order
A stream is a named topic that messages are appended to and read from in order. A message is acknowledged only once it is readable.
export SEMURG_API_TOKEN=<your token>
curl -sS http://localhost:4000/v1/query \
-H "Authorization: Bearer $SEMURG_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{"op":"stream_produce","topic":"orders","values":["order placed","order paid"]}'Expected: the first and last sequence numbers of the two messages, and the topic's cursor
Read everything after a cursor. A consumer keeps the cursor it was given and asks again from there; with wait_ms it waits for the next message instead of polling.
export SEMURG_API_TOKEN=<your token>
curl -sS http://localhost:4000/v1/query \
-H "Authorization: Bearer $SEMURG_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{"op":"stream_read","topic":"orders","cursor":0,"max":10}'Expected: the messages after the cursor, each with its sequence number and its value in base64, and the new cursor
And how many messages a topic holds, for one topic or many in one call:
export SEMURG_API_TOKEN=<your token>
curl -sS http://localhost:4000/v1/query \
-H "Authorization: Bearer $SEMURG_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{"op":"stream_count","topics":["orders"]}'Expected: the count for the orders topic
Document search: index text, find it by word
Index documents by id, then ask which documents contain a word. Words are lower-case runs of letters and digits.
export SEMURG_API_TOKEN=<your token>
curl -sS http://localhost:4000/v1/query \
-H "Authorization: Bearer $SEMURG_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{"op": "index_documents",
"docs": [{"id": 900000000101, "body": "the submarine surfaced at dawn"},
{"id": 900000000102, "body": "a yellow submarine song"}]}'Expected: the number of documents and words indexed
The documents that contain a word, as ids in ascending order, with how many there are:
export SEMURG_API_TOKEN=<your token>
curl -sS http://localhost:4000/v1/query \
-H "Authorization: Bearer $SEMURG_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{"op":"term_postings","word":"submarine"}'Expected: both documents' ids for the word submarine
For counts only, {"op":"term_docs","words":["submarine","dawn"]} answers how many documents hold each word, in the order you asked. To remove documents from the index, send delete_documents with the same documents you indexed. A word that is not a single word is refused by name, and a store that has never been indexed says so; neither is answered with a zero.
Places: points, and which are near a center
Store a point for an entity. Coordinates are signed integers in millionths of a degree: latitude, then longitude.
export SEMURG_API_TOKEN=<your token>
curl -sS http://localhost:4000/v1/query \
-H "Authorization: Bearer $SEMURG_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{"op": "geo_put",
"points": [[900000000201, -33856800, 151215300],
[900000000202, -33873300, 151206900],
[900000000203, 51507400, -127800]]}'Expected: three points written
Then ask which of a set of candidates lie within a radius of any of a set of centers. Centers are ids too, so only ids travel. Here the center is a point in Sydney, and the candidates are a second point in Sydney and one in London:
export SEMURG_API_TOKEN=<your token>
curl -sS http://localhost:4000/v1/query \
-H "Authorization: Bearer $SEMURG_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{"op":"geo_within","candidates":[900000000202,900000000203],"centers":[900000000201],"radius":20000}'Expected: only the second Sydney point
Be precise about what the radius means: it is in millionths of a degree, tested as a straight distance on the latitude and longitude grid, not in metres along the earth's surface. A degree of longitude is shorter than a degree of latitude away from the equator, so choose the radius for your own latitudes. The test only looks at the candidates and centers you send, never the whole store, so its cost grows with your lists, not with your data.
What these have in common
All four are the same store, so the ids are yours to connect. A document's id can be an entity in the knowledge graph, an object can be the file a passage came from, a point can belong to a customer the memory already knows. Each answer names its state, so an empty or partial answer always says why.
When something is refused
422: the body is malformed, and the message names the field: a bad id list, value, topic, cursor, point or radius.413: the call is past a pack's bounds; send less per call.501: this release does not carry that kind of data.503on a stream write: nothing was acknowledged; send it again.401: the token is missing or wrong.
Next
- Hybrid records: a vector, attributes, text and links in one record, searched and walked in one call.
- Look after your node: the calls that tell you how it is doing.
How these commands were checked
Our test farm ran each of the 9 command blocks marked runnable, exactly as written, with the answer checked against the expectation printed under it, against a fresh node of the published release r11.78.20261012.bc0d94f. A post is published only when every one of them passed there.