Skip to content

Search

⌘K opens the search palette from anywhere in Subspace. It runs one hybrid search across page titles, node text, extracted file text, mail, and memory items, and returns typed result rows (page / node / mem / file / mail) ranked by a fusion of two independent retrieval arms.

Full-text search over tsvector generated columns on kb.nodes.text_tsv and kb.pages.title_tsv, GIN-indexed, ranked with ts_rank_cd (length-normalized, rewards term density). Because the tsvector columns are GENERATED ALWAYS AS ... STORED, the lexical index updates in the same transaction as the write, by construction, there is no reindex lag to reason about.

The top-k hits from each arm are merged with reciprocal-rank fusion (k=60): each result’s score is 1 / (60 + rank) summed across the arms it appears in, then deduped by target so a hit that scores well on both arms floats to the top. If the embedding call fails (provider outage, missing API key), search silently degrades to lexical-only rather than erroring.

A dedicated outbox consumer keeps the vector arm current by reacting to writes rather than polling:

Trigger What gets chunked
A kb-command touches a page Its changed nodes, one chunk per node (ref: node:<nodeId>); a node is tagged kind: memory when it carries props.memory. Deleted or emptied nodes are pruned from the index.
A file finishes upload search.file_text (the extracted text) is chunked into roughly 1,000-character pieces (ref: file:<fileId>:<n>) after the file-text extractor runs.
Mail arrives Subject, snippet, and body-blob text are embedded together as one chunk (ref: mail:<id>).

Embeds are batched and upserted, so redelivering an outbox notice after a crash never double-indexes. Two cost controls keep this cheap in practice:

  • Diffed to dirty nodes only: search.embeddings.text_hash stores an md5 of the last embedded text; unchanged text under the same model skips the embedding API call entirely on the next pass.
  • Debounced per page: a burst of edits to the same page coalesces into one batched embed call of the final text, instead of one call per keystroke-triggered command. The window is SUBSPACE_INDEX_DEBOUNCE_MS (0 means synchronous indexing; the default is 0 under PROVIDERS=fake and 1500ms otherwise). A debounced index lost to a crash before it flushes heals itself on the page’s next edit, or on a full reindex, since everything the indexer produces is derived state.

Under PROVIDERS=fake (the default for tests and local sandboxes), embeddings come from a deterministic hashed-character-trigram function: 256 live dimensions, zero-padded to 1536, tagged with model fake-trigram-256.

Run subspace reindex (pnpm --filter @subspace/server reindex) to rebuild everything derived from scratch: it re-extracts search.file_text from stored blobs, then truncates and re-embeds every node, file, and mail chunk. Stop the server first, it boots its own embedded Postgres the same way the server does. Because new outbox consumers start reading from the current head rather than replaying history, run a reindex once after pointing Subspace at a pre-existing corpus so search has something to serve immediately.

When a query looks more like a question than a lookup, the palette adds an “Ask /rag” row below the ranked results. Pressing ⇧↵ on that row files /rag <your query> on the page you currently have open and starts the same child-node agent run there, streaming a sourced answer as new bullets instead of a results list. See RAG for how the underlying /rag, /rag-pageindex, and /rag-direct agents retrieve and cite.

Agents get the same retrieval machinery through dedicated tools rather than the ⌘K UI: kb.search runs the identical hybrid RRF query (optionally filtered to node, memory, file, or mail kinds) and returns up to 10 ranked hits as text; kb.grep complements it with an exhaustive exact-or-regex scan over every bullet for “every occurrence of X” queries that ranked retrieval can’t answer completely; kb.read (and the node-scoped kb.node.read) fetch full page or subtree content once a search has narrowed down a target. See the full agent tools reference for every tool’s schema and approval behavior.