Blog / How-to guides

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.

One query route takes an op that names the kind of data: objects by id, messages on a stream in order, documents found by word and points found near a center, all in the one store where the ids connect.Your callOne route: an op namesthe kindObjects: a value underan idStreams: messages inorderSearch: documents bywordPlaces: points near acenterOne store: the idsconnect

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.

bash
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.

bash
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.

A producer appends messages to a topic, each acknowledged once readable; a consumer reads everything after its cursor and, with a wait, waits for the next message instead of polling.Your producerA topicRead after your cursorWait for the nextmessageacknowledged once readableno polling
bash
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.

bash
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:

bash
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.

bash
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:

bash
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.

bash
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:

bash
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.
  • 503 on a stream write: nothing was acknowledged; send it again.
  • 401: the token is missing or wrong.

Next

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.

Related