Search
Search
Every any server keeps a local search index — BM25 full-text plus semantic vectors — over the chats, documents, property values and runtime-dataset records of every space it holds. One endpoint, POST /v1/spaces/:spaceId/search, queries it in three modes; nothing leaves the machine.
The shape of it
space change feed index.db (per space) /search
┌──────────────────┐ chunkers ┌───────────────────┐ fts ┌──────────┐
│ chat_messages │ ───────────▶ │ doc: objectId: │ ──────▶ │ │
│ editor_blocks │ text only │ dataset: │ │ hybrid │
│ objects (props) │ │ recordId │ vector │ (RRF) │
│ runtime datasets │ │ data, scope, hash │ ──────▶ │ │
└──────────────────┘ │ vector?, pending? │ └──────────┘
└───────────────────┘
- Indexing is a background consumer of each space's change feed. It never blocks writes and never touches the CRDT — the index is derived state that can be deleted and rebuilt.
- Full-text search is always available, with no external dependency. New writes are searchable within about a 250 ms debounce.
- Vector search activates when an embedder is configured. The default runs an embedding model in-process (llama.cpp, auto-downloaded); an unreachable embedder only pauses the vector side — full-text keeps working.
- Hybrid is the default query mode: both legs fused by reciprocal rank, degrading to full-text on its own when the embedder cannot help. The reply says which mode actually ran.
Why it matters. Search over an encrypted, local-first database has to run where the plaintext is — on the device. There is no server-side index to leak, no query log anywhere else, and the same index works offline.
A first query
curl -s http://127.0.0.1:7001/v1/spaces/$SPACE/search \
-H 'content-type: application/json' \
-d '{"query": "zeppelin disaster", "limit": 5}'
any search $SPACE "zeppelin disaster" --limit 5
{
"hits": [
{ "scope": "chat", "objectId": "…", "dataset": "chat_messages",
"recordId": "…", "data": "the zeppelin disaster of 1937",
"score": 0.0328 }
],
"mode": "hybrid",
"vectorStatus": "used"
}
Hits carry identity, not full records — hydrate them with a dataset query when you need the whole row.
What is indexed
| Content | Dataset in hits | Scope | Unit |
|---|---|---|---|
| Chat messages | chat_messages |
chat |
one message (text only) |
| Editor documents | editor_blocks |
basic |
a ~1.5 KB window of consecutive blocks |
| Object name / description | prop |
basic |
one entry per built-in |
| User property values | prop |
props (full-text only) |
"<prop name>: <value>" |
| Runtime-dataset records | the dataset's own name | basic (or the declared scope) |
one record, by its x-search mapping |
Programs, miniapps and file bytes are never indexed.
Reading further
Full-text searchBM25, phrases, prefixes, require/exclude, stop words
Vector searchSemantic recall, the ANN index, similarity scores
Hybrid rankingReciprocal-rank fusion, vectorStatus, weighting knobs
How indexing worksChunkers, scopes, gating, removal, freshness
Embedderslocal, ollama, openai, auto, none — and outage semantics
EvaluationWhat was measured and why the defaults are what they are