---
title: "Which documents hold this word? Semurg as a search index, and how far it goes today"
slug: semurg-as-a-search-index
date: 2026-10-12
author: Semurg
section: how-to
tags: [search, full-text, database, how-to]
summary: Index documents' text under your own ids, count how many documents hold each word and list the documents that hold one, with the limits of word search today.
status: published
covers: [route:POST /v1/query, op:index_documents, op:term_docs, op:term_postings, op:delete_documents]
---
Search here means the plainest question a search index answers: which documents contain this word, and how many. Semurg keeps, for
every word, a small record of how many documents hold it, and an append-only list of which ones, so counting a word is one read and
listing its documents is one sweep. It does not rank results yet, and this post says so before you invest in it.

> Diagram word-records: A document's text splits into words; each word keeps a count record and an append-only list of the documents that hold it, so how many hold a word is one read and which ones is one sweep in id order.

## Index documents

Send each document's text under your own whole-number id. Run this once: indexing the same document again counts it twice.

```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": "index_documents",
       "docs": [{"id": 900000000601, "body": "invoice export drops the currency column"},
                {"id": 900000000602, "body": "the currency selector resets after login"},
                {"id": 900000000603, "body": "export to spreadsheet is slow"}]}'
```

```expect
expect: three documents indexed
assert: json:.ok==true and .docs==3
```

Words are runs of the letters a to z and the digits; anything else separates words, and there is no stemming, so
`export` and `exports` are different words.

## Count and list

`term_docs` counts the documents holding each of a list of words in one call; `term_postings` lists the documents holding one word, in
ascending id order:

```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": "term_postings", "word": "currency"}'
```

```expect
expect: the two documents that mention currency
assert: json:.ok==true and .ids==[900000000601,900000000602]
```

To find documents holding two words, list each word's documents and keep the ids both lists share. To change a document, delete it
with the text it was indexed with, then index the new text; `delete_documents` takes the same shape as `index_documents`.

## Where it stands

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, as a stress test of Semurg itself. The re-measured search results will be on the
[benchmark page](/benchmark), wins and losses alike, with the release and the machine they ran on. Until then this post states
no comparison.

## What it does not do yet

- Rank results by relevance: a word's documents come back in id order.
- Phrases, AND, OR or NOT in one query: combine word lists in your own code.
- Words with letters outside a to z, or stemming.
- Delete a document without sending the text it was indexed with; and indexing the same document twice counts it twice.
- Import from another search engine, such as OpenSearch.

## Next

- [Four kinds of data, one route](/blog/objects-streams-search-and-places): search beside objects, streams and places.
- [Nearest vectors, filtered, then walked](/blog/hybrid-records-vector-filter-walk): a word as a filter on a vector search.
