Blog / How-to guides

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)

vectorsearchdatabasehow-to

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.

Today the writer lays vectors down interleaved with the index's link records and the packer's copies, so an exact search sweeps past all of them; the fix in the release train lays the vectors down on their own, so the sweep reads only vectors.The vector writeToday: vectors, indexlinks, packer copiesinterleavedExact search sweepsall of itThe fix: vectors laiddown aloneThe sweep reads onlyvectorstodayin the release train

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:

bash
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 4096 are stored and searchable exactly. Vectors of width 132 or 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 413 and 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:

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":"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.

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

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.

Related