---
title: "Vectors in the same store as your graph: exact search, filters, and where we stand"
slug: semurg-as-a-vector-database
date: 2026-10-12
author: Semurg
section: how-to
tags: [vector, search, database, how-to]
summary: Store float vectors under your own ids, search them exactly or through the index, and filter by attributes and words, all on the store that also holds your graph and documents, and where it stands today.
status: published
covers: [route:POST /v1/ingest, route:POST /v1/query, op:vector_top_k, op:hybrid_ingest, op:hybrid_query]
---
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.

> Diagram vector-layout: 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.

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 runnable kind=shell-node needs=token
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"}'
```

```expect
expect: the three vectors are stored
assert: rc=0;absent~"ok":false
```

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 runnable kind=shell-node needs=token
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=="}'
```

```expect
expect: the two nearest to [1, 0, 0, 0]: the vector itself, then [0.9, 0.1, 0, 0]
assert: json:.answer=="exact" and ([.result[0][][1]]|sort)==[900000000301,900000000302]
```

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](/blog/hybrid-records-vector-filter-walk) 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](/benchmark), 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](/blog/hybrid-records-vector-filter-walk): vector, attributes, text and links in one record, searched and walked in one call.
- [Connecting over REST](/blog/connect-over-rest): the token and every route.
