Vectors in the same store as your graph: exact search, filters, and where we stand
12 October 2026 · Semurg · 4 min read · Markdown
Verified: every command below ran on a fresh node of r11.78.20261012.bc0d94f (how)
Semurg stores vectors in the same engine as its graph, its documents and its time series, so a vector search and a walk over what the results are connected to happen in one place, on one copy of the data. This post covers storing your own vectors, searching them exactly and through the index, filtering, and where it stands today.
These are your vectors: you compute them with whatever model you use.
Store vectors under your ids
Send the vectors as one block of little-endian single-precision floats, row by row, encoded in base64, with their width d and one id per row:
export SEMURG_API_TOKEN=<your token>
curl -sS http://localhost:4000/v1/ingest \
-H "Authorization: Bearer $SEMURG_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{"d": 4,
"ids": [900000000301, 900000000302, 900000000303],
"vectors_b64": "AACAPwAAAAAAAAAAAAAAAGZmZj/NzMw9AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAA"}'Expected: the three vectors are stored
The block above is three vectors of width four: [1, 0, 0, 0], [0.9, 0.1, 0, 0] and [0, 1, 0, 0]. The rules a store holds you to:
- Ids are distinct whole numbers from one up to, but not including,
2^60; zero is refused. - Widths up to
4096are stored and searchable exactly. Vectors of width132or less also get a k-nearest-neighbour index; wider ones are searched exactly only. - One request carries a bounded batch; a larger one is refused with
413and a request to send successive batches. - A component that is not a finite number is refused.
- A stored vector is not updated in place on this release: writing a different vector to an id that already holds one is refused with
409. Store the new vector under a new id.
Search exactly
vector_top_k takes one or more query vectors in the same base64 form, with q queries of width d, and returns the k nearest for each, as distance and id pairs. Without anything else it searches every stored vector, so the answer is exact:
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":"vector_top_k","q":1,"d":4,"k":2,"queries_b64":"AACAPwAAAAAAAAAAAAAAAA=="}'Expected: the two nearest to [1, 0, 0, 0]: the vector itself, then [0.9, 0.1, 0, 0]
Every answer says which search produced it: exact, or indexed when you asked for the index.
Search through the index
Add ef, the number of candidates the index search keeps, and the answer comes from the stored k-nearest-neighbour index instead of every vector. It visits only the candidates its search walks, not every vector, and it is approximate in which vectors it reaches, never in the distances it reports. It needs vectors of width 132 or less. A store whose vectors have no index answers 409 and names why; ask again without ef for the exact answer.
Filter while you search
When the nearest vectors must also pass a condition, write them as hybrid records, each with a vector, numeric attributes, a text and links, and query with filters: an attribute range, a word in the text, or a link from a given record. On this release the filtered search is exact too: it reads past the nearest candidates until it has k that pass, up to a bound. If the bound is reached before k pass, it answers 409 rather than a short list, so a filtered answer is never silently incomplete. The hybrid records post walks through it.
Where it stands today
We have taken our published benchmark rows down while every domain is re-measured on new hardware, with a dataset in RAM and one bigger than RAM running at the same time. The vector rows we had published showed the exact search slower than the engines teams use for vectors, and we know where the time goes: the search reads far more bytes than the vectors it needs, because vectors were written to disk interleaved with the index's own link records and the background packer's copies, so a search had to read past them. The fix is in how vectors are written, so that a search reads only vectors, and it is in our release train. The re-measured vector results will be on the benchmark page, with the release and the machine they ran on.
What it does not do yet
- It does not turn text into vectors on a node without an embedding model; store the vectors your own model computes.
- It does not update a stored vector in place; use a new id.
- Filtering inside the search itself, without the bound above, is not in the published release.
Next
- Hybrid records: vector, attributes, text and links in one record, searched and walked in one call.
- Connecting over REST: the token and every route.
How these commands were checked
Our test farm ran each of the 2 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.