Skip to content

Retrieval ​

kontext searches locally with BM25 and merges whatever connected adapters return.

The local index ​

A Tantivy index per worktree, derived from three sources:

SourceDocumentsURI
Store entriesone per entry: title, body, tags, paths, idkx:<id>
Repository docsMarkdown matching sources.include (root *.md, every README.md, AGENTS.md, CLAUDE.md, docs/**), split at ## headingsfile:<path> or file:<path>#<section>
Commit historysubject, body and trailers of the last index.max_commits commits (5000); file names are searchable but kept out of snippetsgit:<sha>

Noise is filtered: "Merge branch …" commits, Co-authored-by/Signed-off-by lines, files over sources.max_bytes, and anything that looks like a secrets file.

The index refreshes incrementally before a query (entries and docs by modification time and size, commits by SHA), at most once per second per process. When another process refreshed it (a CLI capture, kontext status, a second agent session in the same worktree), a long-running MCP server notices the changed manifest and reloads its reader before answering. A new schema version is built in a temporary directory and renamed into place, never by deleting a directory another process is building. kontext reindex rebuilds it from scratch.

Identifiers ​

Code-shaped words are expanded so both forms match: orderTotal also indexes order total, user_account_balances indexes user account balances. A query for an identifier additionally searches its words as a phrase, so RunContext finds "run context" without matching every document that says "run".

Folding and stemming ​

Two shadow fields, title_f and body_f, hold the same text lower-cased, ASCII-folded and run through an English stemmer. They are searched beside the exact fields at a lower boost, so namietka finds námietka, adapter finds "Adapters are configuration", and an exact identifier still ranks first. There is no stemmer for Slovak; folding covers diacritics, not inflection.

Ranking ​

BM25 over title (boost 2.5), identifier-expanded auxiliary text (1.5), body, and the folded shadow fields at a lower boost. The whole query also counts as a phrase (boost 2): it ranks an entry with the exact wording first without adding hits of its own. Local results have weight 1.0; adapters default to 0.9 (weight per adapter).

Within the local source, each source has a weight before the groups are fused: entries 1.0, doc chunks 0.7, commits 0.6, a nested store's entries 0.95, so a reviewed entry ranks above a doc chunk or a commit at equal rank. The weights are built in ([search] keys for them are planned) and were set on the graded eval set (tests/eval/qrels.tsv, nDCG@5, recall@10, MRR); a change to ranking needs that measurement. A superseded, deprecated or rejected hit is weighed down by half, never hidden, and its status shows the successor: superseded · → superseded by [<id>] <Title>. The phrase bonus is computed from the cleaned query words, without the identifier expansions. A query that is exactly an entry id or a path takes a shortcut to that entry.

ctx_search sends the query to every adapter with a search op, in parallel, with a 12-second deadline. With [search] adapters = "fallback" it does so only when the local sources found fewer than min_local_hits hits, or when the caller names an adapter or passes adapters: true; the default, always, asks them every time. Sources are fused by weighted reciprocal rank (each hit scores weight / (8 + rank)), so no source's score scale (BM25 here, cosine similarity there) crowds the others out, and a single source keeps its own order.

Only proven copies are dropped: a hit with a URI seen before, an adapter's copy of a local entry (a synced …/<id>.md), and a hit whose whole normalized title and text equal an earlier one. Of two such copies, a superseded one is never kept over its active successor. Adapter hits are ordered by their mapped score, not by the order of the response's buckets; hits from a nested store carry its marks. Two claims with the same opening are not the same claim: a hit that only starts like an earlier one is kept and shown under it with the part that differs (↳ similar, differs: …). A slow or failing adapter never blocks the answer: it is reported in a note and cools down for 30 seconds.

Restrict a search with kinds (decision, convention, learning, incident, architecture, doc, commit) and sources (local and/or adapter names).

ctx_why ​

"Why is this like this?" combines several signals, depending on the target:

TargetAnswer
path or directoryknowledge whose paths cover it, the module summary, decision-shaped commits among the last 60 touching it (plus the latest three), history adapters
path:line / path:start-endthe same, plus the commits that last changed those lines (git blame)
commit shamessage, the knowledge it relates to (via commits or paths), changed files
symbol or topiccode adapters locate it (CodeGraph, Serena), then local knowledge and history, then history adapters

Commits are ranked by decision signals (wording such as replace, migrate, instead of, because; Conventional-Commit type; breaking changes; explanation length) and by kontext trailers (Decision: …).

ctx_log ​

The decision timeline: date, id, title, status, supersession and the commit that added the file. kontext log --json for scripts.

Released under the MIT or Apache-2.0 license.