Semantic search¶
By default kata search is lexical: it matches the words you type against issue
titles, bodies, and comments. That misses issues that describe the same problem
in different words — the case that matters most when an agent searches before
creating a duplicate. Semantic search adds a vector (embedding) leg so a query
like "auth redirect duplicates" can surface an issue titled "Login callback
double-submits on Safari" even though they share no keywords.
Semantic search is opt-in. With no embedding endpoint configured, kata
search behaves exactly as before and the daemon makes no network calls. This
page explains how to turn it on and how it behaves; for the exact config fields
see Configuration, and for the
command flags see the CLI reference.
How it works¶
When you configure an embedding endpoint, the daemon embeds each issue's title and body, chunking long text instead of truncating it so long issues get full coverage, and stores the resulting vectors in a sidecar database next to the main one. A search then runs two legs and fuses them:
- the lexical leg — the existing full-text search, unchanged; and
- the vector leg — embeds your query and finds issues whose vectors are closest to it.
Results are merged with reciprocal rank fusion, so an issue that ranks well in either leg surfaces, and an issue that ranks well in both rises to the top.
Embeddings are produced by an OpenAI-compatible /embeddings endpoint that
you point kata at. That can be a local runtime (Ollama, LM Studio, or a
llama.cpp server) or a hosted provider (OpenAI, Voyage, and others) — kata only
speaks the wire format and never bundles a model.
Enabling it¶
- Run an embeddings endpoint. For a fully local setup, install Ollama and pull an embedding model:
- Add a
[search.embeddings]block to<KATA_HOME>/config.toml:
[search.embeddings]
base_url = "http://localhost:11434/v1" # any OpenAI-compatible /embeddings
model = "nomic-embed-text"
base_url and model are both required once the section exists. A hosted
provider also needs a key — set api_key or, better, api_key_env pointing
at an environment variable. See
Configuration for every
field (dims, batch_size, timeout_seconds, fingerprint_salt,
trust_private_network).
-
Restart the daemon (or start it —
katawill pick up the config). It begins embedding existing issues in the background immediately. -
Confirm it is live and watch the backfill drain. The reconciler state is in the
embeddingsobject of the JSON health response:
That is the whole setup. New and edited issues are embedded automatically from then on.
Search modes¶
kata search takes three mutually exclusive flags; with none of them it runs in
auto mode:
| Mode | Flag | Behavior |
|---|---|---|
| auto (default) | (none) | Hybrid when embeddings are configured, lexical otherwise. |
| lexical | --lexical |
Full-text search only — today's behavior. |
| hybrid | --hybrid |
Fuse the lexical and vector legs. |
| semantic | --semantic |
Vector results only. |
Auto is the right default for almost everything, including agents: it transparently
improves recall when embeddings are on and is unchanged when they are off. Reach
for --lexical when you want an exact keyword/identifier lookup, or --semantic
when you want pure concept matching regardless of wording.
The effective mode is reported back on every response. In --json and --agent
output it appears as a mode field (lexical, hybrid, or semantic); see the
agent output reference for the exact shape.
Freshness: lexical is instant, semantic is eventual¶
Lexical search is always up to date the moment you write an issue — creating an issue and immediately searching for it works exactly as before, which keeps the agent search-before-create flow reliable.
The vector index is eventually consistent. A background reconciler embeds new and edited issues a few seconds after they change (sooner against a fast local endpoint, longer against a busy cloud API). A brand-new issue is not in the vector leg until its first embedding lands — it is still found lexically, so nothing becomes unsearchable. An edited issue keeps serving its previous vector (so the vector leg may rank it on the old text) until the reconciler re-embeds it; the lexical leg already reflects the new text in the same results. Either way the staleness is brief and bounded by reconciler lag.
You can watch the reconciler in kata health --json under embeddings:
configured— whether an endpoint is set;embedded— how many issues are embedded at their current revision for the configured generation;skipped— how many issues were deliberately stamped without vectors after the configured model rejected their content;backlog— how many issues are waiting to be (re-)embedded; decreases as each issue is persisted and trends to 0;rate_per_secondandeta_seconds— a smoothed measured rate and estimated time remaining; omitted until at least two positive progress samples exist;started_atandlast_progress_at— when the current backfill began and when its most recent issue was persisted, useful for distinguishing slow progress from a stalled endpoint;last_success_at— when the reconciler last completed a batch;last_error_status— the HTTP status of the most recent embedding-endpoint error response, if any. It is set only when the endpoint answered with an HTTP error; an unreachable endpoint (transport failure) leaves it unset.
When the endpoint is unavailable¶
If the embedding endpoint is unreachable for a query, behavior depends on the mode:
- auto degrades to lexical results and labels the response so the fallback is
never silent: a
# mode=lexical degraded: …note in human output,degradedanddegraded_reasonfields in--json, and adegraded=<reason>field in--agent. - explicit
--hybrid/--semanticdo not degrade — they return an error (HTTP 503) so a caller that asked for semantic results knows it did not get them. They return 400 when embeddings are not configured at all.
A persistent endpoint problem shows up as a growing backlog and a stale
last_success_at in health; the reconciler backs off and retries. When the
endpoint answers with an HTTP error, last_error_status carries that status —
a misconfiguration (bad key, wrong model) is reported there rather than
silently looping. An endpoint that is unreachable outright (transport error,
no HTTP response) leaves last_error_status unset; the growing backlog and
stale last_success_at are the signal in that case.
Changing the model¶
Each stored vector belongs to a generation keyed by a fingerprint of the
model, dimensionality, and text recipe it was produced under. If you switch
model, dims, or fingerprint_salt, kata builds a new generation in the
background. While that backfill runs, the vector leg is unavailable —
queries embedded under the new model cannot be scored against the old
generation's vectors, so auto mode serves labeled degraded lexical results
and explicit --hybrid/--semantic requests return 503. Lexical search is
unaffected throughout. When the new generation finishes filling, kata cuts
over to it automatically, semantic results resume, and the old generation's
storage is reclaimed. If you keep the same model name but its weights changed
underneath you (for example a re-pulled Ollama tag), bump fingerprint_salt
to force the same re-embed.
Privacy and federation¶
Configuring an endpoint sends issue titles and bodies to it on every embed —
that is the consent boundary. For sensitive projects, prefer a local endpoint
(such as Ollama on loopback) so issue text never leaves the host. The embedding
API key is only ever sent to the configured base_url origin.
Embeddings are local derived state and do not federate: each daemon embeds
only what it stores, and no vectors are sent to or pulled from federated hubs.
They live in a sidecar database, not in kata.db, and are not included in
JSONL backup/export — a restore or a
storage-format upgrade re-embeds from scratch rather than carrying vectors
forward. Archives exported by older kata versions that still contain
embedding records import cleanly: those records are skipped and the
reconciler rebuilds the vectors.
Scope and limits¶
- Only issue title and body are embedded today; comments are searchable lexically but a paraphrase that lives only in a comment will not vector-match.
- Semantic search requires the SQLite backend. A daemon configured with a
non-SQLite database and
[search.embeddings]set fails to start with a configuration error; PostgreSQL support is not yet implemented.
For the design rationale and internals, see the semantic search design note.