rift
Developer

Architecture

Rift works as a toolkit for agents such as Claude Code and Codex - practically any agentic appliance that supports MCP.

When an agent has Rift configured as one of its MCP servers, the rift mcp process starts and checks whether a rift server is already running for that particular folder. We'll call this folder a workspace throughout this documentation.

Each rift mcp process is short-lived and isolated, while development operations depend on state shared across requests and agents. To bridge that gap, rift mcp acts as a small proxy: it forwards every request to one rift server running locally for the workspace. The two processes communicate over Streamable HTTP.

Since v0.0.47

A foreground repository server can hold several workspaces from the same Git repository. rift mcp adopts it when the workspace accepts its settings; automatic startup continues to start a workspace server. Each workspace retains its own index, configuration, watcher, and databases.

The rift server coordinates source discovery and providers. Source resolvers identify project files, dependencies, and generated code. Providers analyze that source and produce facts such as symbols, syntax nodes, relationships, and diagnostics.

The server stores searchable provider output in an embedded index under .rift. The search tool always retrieves lexical matches; when rift.toml selects an embedding model, a local model adds vector ranking.

Server lifecycle

Since v0.0.11

When the server starts, it writes .rift/server.json following the ServerLock model. The file gives each rift mcp process the port and token it needs to reach that server. For example:

{
  "port": 12345,
  "token": "Zk3mQ8xW1vY5uT9rN2bH7cJ4dL6fP0sA8gE5iK2oM7q",
  "pid": 4242,
  "identity": {
    "version": "0.0.45+b006b8433ba06679f06a3c7f0743d65634d32c34",
    "schema_digest": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
  }
}

When no live server stands behind the file, rift mcp starts one. An exclusive file lock elects one starter if several processes race; the winner binds a port and publishes the file, and the rest connect to it. The winner holds the lock from its election onward, while its index prepares source in the background.

The proxy adopts that server only when its identity matches the proxy's own: the package version with the git commit the binary was built from, and the served MCP schema's SHA-256. Requests therefore never cross between binaries or tool schemas that interpret them differently.

When the identities differ, the version decides:

  • A server of an older version is asked to stop, and the proxy starts one from its own binary once the election releases. Every release bumps the version, so the first request after an update moves the workspace to the new binary.
  • A server of the same version from another build, or of a newer version, refuses each request, so two development builds never displace each other and an older binary never displaces a newer server. Stopping the running server lets the next request start one that matches.

A reader that cannot reach the recorded port treats the file as stale. The election lock releases with its holder, so a stale file never has a live server behind it, and the next starter replaces the file. A server on its way out closes its port before it releases the lock, and the next starter waits for the release before electing a fresh one.

To enforce a cross-process boundary between OS users, the server persists a token in .rift/server.json, which only the file's owner can read. Every request carries this token, and the server rejects one without it. Processes running as the same OS user can read the same token and are not separated by this boundary.

A browser introduces another route to a localhost port: a remote page can issue requests through DNS rebinding or a forged origin. The HTTP server therefore accepts only the literal loopback Host for its bound port. It also rejects a present Origin unless that origin is loopback; non-browser MCP clients omit Origin and still authenticate with the token.

You might be asking why there's a server at all. Assume your agent launches a couple of subagents. Each of them might also want to read the codebase through Rift, and each gets its own rift mcp, because MCP's stdio transport has the client launch its server as a subprocess.

None of those short-lived processes can hold the things that are expensive and shared - the index - so a long-lived server holds them and every rift mcp reaches the same one.

And why HTTP rather than a socket? Either way rift mcp has to find the running server, and HTTP lets an HTTP-capable agent connect to the port without rift mcp.

On startup the server reads rift.toml from the workspace root and accepts the workspace. The configuration allows you to describe:

  • which embedding model, if any, ranks search
  • which exact language entries select source files

A change to the file takes effect for later requests once the server accepts it again. An invalid file keeps the workspace unusable until you fix it.

A rebuild that fails - the tree crossed a configured bound, a file could not be read - leaves the last published snapshot served. A read answers from that snapshot and carries a stale_index warning naming the failure. The next filesystem event or background validation retries the rebuild. A successful rebuild clears the warning, including recovery after the tree returns to the publication's exact bytes.

The server stops itself once no request remains active and the idle_timeout span passes after the last response completes. rift server stop ends it sooner. The server removes its lock file on the way out, and the next rift mcp starts a fresh one. The databases under .rift survive the exit.

Since v0.0.62

A served workspace keeps three databases under .rift:

  • the index database, .rift/index, holds the lexical index search ranks from
  • the vectors database, .rift/vectors, holds the vector ranking's rows. The server creates it at the first vector operation, and never while vector ranking is disabled
  • the metrics database, .rift/metrics, holds the server's own log records; under repository serving, each workspace's metrics database holds the records that name that workspace, and a record that names none reaches stderr alone

Each database has its own connections and its own writes, so a long index write delays no log record and no other database's write. rift server logs reads the metrics database alone, with or without a running server, and answers while the index database is locked, missing, or refused.

rift server status reports which of these states the workspace is in - a serving server with its port, pid, and version, a server still indexing, a stale .rift/server.json, or no server

  • and changes nothing. After taking its election and opening storage, a server that fails to start, or panics, records why under rift server logs --level error before it exits. A start refused by the election reports its error on standard error. A server rift server start spawned writes its standard error to .rift/server.stderr, truncated at each start.

Source discovery

Before Rift analyzes code, source resolvers discover what belongs to the project and which packages it depends on. The built-in project resolver catalogs workspace files.

Dependency resolvers read what the workspace's package managers resolved. Each resolver claims one manifest file name, and every visible manifest with that name reaches it:

  • the Cargo resolver claims Cargo.toml and reads the Cargo.lock beside it
  • the uv resolver claims pyproject.toml and reads the uv.lock beside it
  • the npm resolver claims package.json and reads the package-lock.json beside it
  • the Bun resolver claims package.json too and reads bun.lock the same way, so a manifest reaches whichever of the two has a lockfile beside it

A resolver reads manifests and lockfiles, never a package's source, and it never downloads a package or writes to the tree. What the resolvers read becomes the dependency context, and a read that needs a package's declarations asks the global index for them.

Each resolver reads at most 256 manifests per workspace, taken in path order from the visible paths carrying its manifest file name. Selecting the top-level manifests compares every manifest pair, so that work grows with the square of the manifest count.

A workspace holding more manifests names the packages those 256 declare, and the resolver reports the rest as a degradation. A read whose scope reaches packages carries it as a package_context_degraded warning: the next.js repository holds 876 package.json files, so the warning reads 620 of 876 package.json manifests were not read: at most 256 are read per workspace.

Each readable project file becomes a source unit in the source catalog, named by a SourceUnitId such as rift://source/project/src/lib.rs; a package file the global index answers from takes the same form. Its location says whether it belongs to the project, a dependency, the standard library, or an external source. Its source_kind separately says whether a human authored it or a tool generated it.

Generated project code and generated dependency code therefore keep both facts. Declarations invented by a language tool have source_kind: "synthetic" and no source unit.

The project resolver publishes a new catalog revision when workspace files or generated-source metadata change. Fact providers consume that catalog. A parser such as tree-sitter can describe syntax in a supplied file, but cannot infer which package owns it or which generator produced it.

Dependency context

Since v0.0.39 · Rust, Python, TypeScript

The dependency context lists the packages the workspace depends on. The resolvers reach the workspace through a reader that offers a file read and a directory listing and nothing else, so they run no toolchain and read no environment value.

Each entry carries the package manager, the name, and exactly one selector: the exact version a lockfile pins, or the version requirement a manifest declares. A manifest value is reported as a requirement unless its form pins one exact version - Cargo's 1.2.3 is a caret requirement and =1.2.3 a pin - so a manifest-only project never invents a resolved version. Entries merge by manager, name, and selector, and a requirement is dropped where some input pins that package's version. The [dependencies] packages list merges with what the files state, under the same rules.

rift://map lists this context. The server reads it when it builds the index, and again when a change touches a manifest, lockfile, or version pin it read, or when a language with a standard library gains its first visible file or loses its last. Every other file change leaves it untouched.

A uv sync that installs from an unchanged uv.lock changes no visible file, since .venv holds its own .gitignore. The context therefore keeps the site-packages listing of each project environment it read beside a uv.lock, and each request lists it again: a listing that differs reads the context again before the request is answered.

Since v0.0.46

An entry's availability says whether a public registry serves the package. An entry crates.io, npm, or the Python Package Index serves is canonical, as is every standard library entry, and the global index answers for it. Every other source has a value of its own, and no global package index answers for it:

  • path - a path outside the workspace, or an archive at a path, such as helper = { path = "../helper" }
  • git - a git repository
  • private_registry - a registry other than the public one its package manager reads
  • url - a URL no registry serves, such as a uv wheel URL or an npm tarball URL

A path dependency inside the workspace, such as a Cargo workspace member or an npm workspaces member, is project source. The local index already covers it, so the context names no entry for it.

Standard libraries

Since v0.0.46 · Rust, JavaScript, TypeScript, Python

Code in every language calls its standard library, and no manifest names it. The context therefore adds one entry for each standard library a language the workspace uses relies on, under the manager stdlib. A language counts when the [languages] policy selects it for at least one visible file, so a Rust repository holding one tool.py names stdlib/python too:

LanguageEntriesVersion, in the order it is read
Ruststdlib/rustrustc --version, then the channel rust-toolchain.toml or rust-toolchain names
JavaScript, TypeScriptstdlib/node, npm typescript.nvmrc, .node-version, volta.node in package.json, .tool-versions, mise.toml, then node --version, then engines.node
Pythonstdlib/pythonthe project environment's .venv/pyvenv.cfg, then .python-version, then requires-python in pyproject.toml

The npm typescript package ships the lib.*.d.ts files that declare the ECMAScript built-ins, such as Array and Promise. It goes out at the version the lockfile pins, and otherwise as the requirement >=0, which the global index answers from its newest collected release.

A whole version goes out exact and a partial one as a requirement: 1.98 in rust-toolchain.toml goes out as ~1.98, and 3.12 in .python-version as ==3.12.*. A named channel or alias, such as stable or lts/iron, goes out as >=0, and so does a library no source names.

rustc --version and node --version run under [dependencies] resolution = "auto" alone, each bounded by command_timeout, and neither installs a toolchain a pin names but the machine lacks. A probe that answers nothing leaves the static reading and a package_context_degraded warning naming the entry. When rust-toolchain.toml pins 1.80.0 and the machine lacks that toolchain, the entry goes out as stdlib/rust@1.80.0, and the warning carries rustc --version: error: toolchain '1.80.0-aarch64-apple-darwin' is not installed.

The [languages.<name>] stdlib key turns a language's entries off. The entries join the request to the global index alone: nothing is installed, downloaded, or written.

Global index

Since v0.0.43 · Rust, Python, TypeScript

A package's declarations come from the global index, which holds facts for collected OSS package versions. A global search reaches collected package and runtime facts selected by the dependency context. Explicit packages replaces versions of named packages or adds packages the context lacks; an omitted list keeps the snapshot context unchanged. An all search merges global results with the primary project and accepted extra projects. A package declaration is a Symbol like any other, and its returned id selects one exact get_symbol read.

The local server analyzes no package source itself. Package analysis describes how one package's files become the declarations the global index answers from.

When global access is disabled or the global API cannot answer, the read answers the project hits it has, and one typed warning names why package facts are missing. A package the global publication holds no release of answers package_absent or package_requirement_absent, a version it lacks answers from the nearest collected release under package_substituted, and an entry no public registry serves answers package_unavailable. The MCP reference states each warning.

Providers

Providers analyze cataloged source at different depths and publish immutable Contribution records in provider-owned symbol spaces. Rift normalizes applicable Contributions into workspace-local SymbolRecord values, then assembles readable Symbol values for one captured index revision.

A condition the caller must weigh before relying on the answer - a stale index, an absent capability - arrives as a typed entry in the result's warnings.

Syntax

Rift ships syntax providers based on tree-sitter by default. They parse readable project, dependency, and generated units into declarations, syntax structure, and parse diagnostics. Project declarations also carry their syntax nodes and byte ranges.

The server invalidates a changed path's syntax facts and search-index entries.

Language engines

A language engine resolves facts the grammar alone cannot provide, such as references to a declaration in another file. Rift's LSP client requests references, call hierarchy, and diagnostics over LSP.

Each [languages.<identity>] table selects one named or inline process for an exact language identity: rust-analyzer for rust, or typescript-language-server for typescript and typescript:tsx. Several identities may select the same named process. LSP profiles register directories as separate projects, each with its own configuration and language processes.

search uses the engine for relationship traversal: the engine is the one source of the references an incoming walk follows and of the calls an outgoing walk follows. Source and configuration must match the captured index before and after the engine request.

Before a running engine's next request, the server sends it each publication's changed files that match a file watcher the engine registered, as one workspace/didChangeWatchedFiles notification. An engine that learns of file changes through its client, as rust-analyzer does, then answers from the published source, including files the walk never opens.

The embedded ty engine serves Python over an in-memory transport through the same LSP session contract. Its version ships with the Rift binary.

History

The history source resolver reads the workspace's git objects in place, with no checkout or git subprocess. That access serves revision-addressed reads: get_symbol, search, and nodes accept a rev, and the server indexes that commit's tree - the same syntax providers, over committed bytes - without touching the working tree.

Since v0.0.13 · Rust, JavaScript, TypeScript, Markdown, JSON, YAML, TOML, Python

A change comparison reads the same objects: the server lists the paths two sides hold differently, parses those paths alone on each side, and matches their declarations. Since v0.0.46 Against the working tree, the head side is the current publication, and the base side reads each changed file in the form the working tree holds it.

Since v0.0.46

Walking git on every read costs more the further back it reaches, so the server keeps a history store: tables derived from git and filled before any read asks. For each commit the [providers.history] strategy selects, the store holds the commit's author, time, and message, the paths it changed, the files it renamed without a changed byte, and the declarations whose shape changed. Git stays the source of truth, and a stored commit id names the git object it came from.

One store serves every worktree of a repository. It lives in the .rift/ folder of the common git directory, .git/.rift/ in a plain checkout, so worktrees analyze the commits they share once; when that directory refuses writes, the store sits in the worktree's own .rift/ folder instead.

A history task fills the store. The server spawns it at start, and it analyzes newest first, so recent history answers soon, writing bounded batches at the share of one core cpu_share sets. When HEAD moves, the task analyzes only the commits the store lacks and deletes the ones outside every worktree's newest max_revisions commits.

No request waits for the task. A symbol timeline and a commit search answer from what the store holds: a timeline whose served revision the store has not analyzed yet answers complete: false, and a commit search answered before the fill lands carries a history_store_filling warning naming how far it has got.

The task follows a declaration across files as it analyzes each commit. A deletion and an addition carrying one blob id are a rename, and a declaration an added file holds with the exact bytes a deleted file held is a move; a blob id or a declaration with more than one candidate pairs nothing, since no evidence says which addition answers which deletion.

Custom

Custom integrations implement a source resolver, a fact provider, or both. The contracts stay separate even when one integration implements both capabilities. A family with no serving provider surfaces as a typed entry in warnings, which keeps an empty answer distinct from a complete answer with no facts.

One integration can connect several cooperating pieces through a provider composition. Like a scikit-learn pipeline, the composition gives each step a stable name and feeds its output into compatible inputs.

The composition API can change the flow before Rift starts:

  • insert, replace, or remove a named step
  • transform one step's input or output
  • branch one output into several providers, then publish each provider's Contributions
  • nest one composition inside another

Provider compositions support several flow shapes:

  • syntax and semantics consume the same source catalog, then publish separate Contributions
  • history maps syntax analysis over source catalogs keyed by Git revision, then aggregates revision-qualified facts
  • a custom external step can produce generated source or facts for another provider to analyze

A transform that changes source units or facts becomes a named, revisioned step and publishes new Contributions, so later answers still identify its inputs.

For example, a CSS integration can send templates and configuration to a registered Tailwind CLI, parse its generated CSS with a CSS syntax provider, and publish those facts beside authored CSS. The server runs the command with declared inputs, environment, timeout, and output limits; the composition step decodes its output.

Rift validates the composition's inputs, outputs, merge rules, and cycles before it runs. The server then owns scheduling, caching, and invalidation; MCP requests do not assemble or modify the composition. Composition controls data flow and provider selection. It cannot establish Contribution equivalence or SymbolRecord identity.

The workspace resource assigns each provider a stable ProviderId and reports its lifecycle state. A provider derives an immutable Contribution collection from one tree revision, then publishes the collection atomically.

While a provider catches up after a change, requests may serve its previous revision, and the result carries a stale_index entry in warnings naming the two tree revision digests. A provider with no usable revision contributes nothing, and the result's warnings say so.

A read captures one tree revision, one index revision where it searches, and the provider revisions it uses. Later writes create new revisions; they do not alter the captured result. A request addresses one page with page_index, and a page past the end returns an empty page with the true total_pages.

Normalization validates equivalence evidence without using provider order, candidate names, or provider composition as identity evidence. It retains unresolved and conflicting records instead of forcing one SymbolId. When Rift assembles a readable Symbol, provider order selects scalar presentation fields and combines list facts without discarding conflicting Contributions.

Index

Since v0.0.8 · Rust, JavaScript, TypeScript, Markdown, JSON, YAML, TOML, Python

The server keeps an immutable index over the project's source. Tree-sitter extracts declarations and syntax nodes from the files the workspace's source policy includes.

Since v0.0.47

The server starts with an empty publication, then publishes discovered paths and prepared files as indexing advances. Reads answer from prepared source and carry local_index_preparing while their selected local scope is incomplete. A nodes request can read its supplied file ahead of background preparation.

Every publication owns its preparation counts and map together, so progress describes the answer the caller received. A tree change can reduce those counts; completion applies to the current selection of files.

The server watches the workspace and validates the source each current-tree tool read uses, so an answer never combines state from two index revisions. The implementation page describes the watcher, the rebuild lane, and the swap that keep this true.

When the published index lags the tree a read captured, the result says so: a stale_index entry in warnings carries the digests of the tree revision the index covers and the revision the read captured, so the caller knows the answer may miss the newest writes.

Since v0.0.9 · Rust, JavaScript, TypeScript, Markdown, JSON, YAML, TOML, Python

Each rebuild derives lexical units for search.

Since v0.0.25

The rebuild derives one unit per indexed symbol, and chunks from every bounded UTF-8 file without a NUL byte whose effective language entry or [search.text].include selects it. Visible Markdown, MDX, reStructuredText, and notebook documentation also enter collection. Syntax providers enrich files their effective language entry selects. The units persist in the index database at .rift/index, stamped with the tree revision that published them.

The rebuilt index becomes current as soon as its syntax index is built, and the units are written behind that publication. The store records which bytes each file's rows were derived from, so a write, startup's included, touches only the files that moved; the first load of a large workspace still runs for longer than a read is willing to wait, so the publication never waits for it.

A search that captures the publication before its transaction lands is ranked by identifier matching alone and carries a lexical_ranking_unavailable warning naming the tree revision the store is still committing; the store's ranking joins the answer once the transaction ends. A transaction that refuses leaves the previous rows and stamp intact and is recorded in rift server logs, and the next publication compares every file with what the store recorded rather than applying its change on top of rows the store missed.

The vector ranking's vectors live in the vectors database and are not part of that transaction. Embedding one declaration can cost more than a read is willing to wait, so vectors are refreshed after the index publishes, and a search whose vectors are still being written ranks lexically and reports the tier's readiness in warnings.

Both tiers answer under one tree revision: the full-text store reads its stamp and its rows in one transaction, and the vectors a pass published carry the tree they were described for. A store that has moved past the tree a request captured returns nothing, and the request captures the publication the store already answers for. What lexical_ranking_unavailable tells a caller about is a store that has not yet committed the captured tree, one that missed a commit, one whose every database connection stayed in use past [search] busy_timeout, or one that will not answer at all because it refused to load or holds no indexed tree.

Since v0.0.32

A file past the per-file byte bound, one its syntax provider refuses under a node or depth bound, one holding a declaration the Contribution contract refuses, or one standing past the [source] declarations capacity, is left out of the index: it answers no search or lookup, counts nowhere in rift://map, and is named in rift server logs with the bound it crossed or the field the contract refused. One minified bundle therefore never blocks the workspace, and neither does a workspace declaring more than the index holds: the files that fit are served and the rest are reported. The [source] exclusion remains the way to keep such files out of the walk altogether.

Since v0.0.46

Under the default [search.text] large_files = "split", a file past [providers.syntax] max_file stays in the index as text the syntax provider does not parse: search reads it in chunks, its declarations are absent, and every answer names it in a large_file_unparsed warning. Under skip the per-file byte bound is max_file, so such a file is left out as above, and a file past max_chunk keeps no text in search, which a search whose paths reach it counts in a large_file_skipped warning.

search is for reads where the exact name isn't known. It ranks declaration names, signatures, documentation, and file content from the index, and it answers a regex pattern from the same stored text.

The document shape

Every index publishes the same document shape, whether it holds the served project or a package version the global index collected. A document carries a stable identity, the project path or source unit it lives at, its language, its package when it came from one, a content digest, and the searchable fields below:

FieldWhat it holdsWeight
nameA declaration's name, or a file's final path segment with its extension10
qualified_nameThe declaration's qualified name6
identifier_termsThe names split on case, acronym, and separator boundaries4
signatureThe rendered signature, when a provider published one2
documentationThe attached doc comment, when a provider published one1.5
file_contentAn indexed file's text, parsed source and plain text alike1

A fact no provider published stays absent. Nothing substitutes a declaration's own text into the signature or documentation field, because the two are weighed apart and filling one with the other would count the same bytes twice. The weights are what make a name hit outrank a hit buried in a comment, and the answer reports which field placed each hit, so a documentation match and a name match are distinguishable rather than both reported as "the ranking found it".

Since v0.0.46

The index holds each file's text once, in the file's own documents, and a declaration's document carries the derived fields alone. Keeping one copy is what lets the regex search below read every match from one set of rows: a copy split between declarations and the rest of the file would put a match that crosses a declaration's edge in neither.

A word only a function's body holds therefore ranks the function's file. The read path then finds the query's terms in the first file documents of the ranked list, maps each position to the smallest declaration containing it, and answers those declarations in place of the first file document, scored together by BM25 over the terms each one holds.

The project path, the source unit, the package identity, the language, and the kind stay metadata. None of them enters the ranked text: indexing one tree below two different host roots must produce one order, and a path in the ranked text would make the clone directory part of the score.

The index records a corpus revision covering the field shape, the derivation the identifier terms were split with, the tokenizer, and the weights. A store stamped with another revision holds rows this build cannot read, so it answers as if nothing had been published and the next write clears it before it writes.

Documentation

search with target: "documentation" returns blocks from selected documentation sources. Workspace selection includes .md, .markdown, .mdx, .rst, and .ipynb, plus .txt selected by [search.text].include. Source visibility applies before collection.

Since v0.0.46

The selection leaves out change logs, licenses, codes of conduct, and archived, deprecated, or translated copies by default. The [documentation] table overrides that default for the workspace.

Each regular source keeps its existing text chunks. Notebook markdown and code cells receive separate content owners; outputs and raw notebook JSON stay outside ranking. Collection records block ranges, headings, links, source digests, and exact-symbol references beside those owners. It neither executes code nor follows links to acquire more sources.

Before fusion, the read maps existing ranked documents to block identities and keeps the best position once per ranking input. A heading and its text chunk therefore return one documentation hit. Under target: "all", an attached comment keeps its symbol result when that symbol answered; a file match outside the comment remains a file result.

For example, a guide containing the inline code Compass can contribute context to get_symbol with its established id and include: ["documentation"]. The reference resolves only when the selected declaration set establishes one exact target. Ambiguous names remain unresolved. The read returns at most 32 references per symbol and 16 KiB of documentation excerpts across the page, with typed warnings for missing bytes or reached bounds.

A package's documentation comes from the global index. Global documentation requests use the package REST client only after the service advertises the corresponding capability. Workspace paths and source bytes remain local.

Query phases

A caller's text is parsed once, into quoted phrases and unquoted terms, and every member is rendered as a quoted string literal, so an FTS operator a caller typed cannot become an operator the store executes. The parse is bounded: 4,096 bytes of query, 32 members, 256 bytes per member. A question carrying more terms than the bound narrows rather than failing - every quoted phrase survives and the longest remaining terms fill what is left - because a long question deserves an answer, not a refusal. More quoted phrases than the bound is still refused: dropping one would answer a question nobody asked.

Two expressions come out of one parse. The precise phase requires every member, which is what answers a multi-term question exactly. The broad phase keeps every phrase required and widens only the unquoted terms with OR, so "impact radius" graph search renders as "impact radius" AND ("graph"* OR "search"*).

Every selected index runs the precise phase first and the results fuse together. Only when that merged answer leaves the candidate pool short does the broad phase join, and its identities append after every precise one. One index's widened hit therefore cannot displace another index's exact hit.

Ranking inputs and fusion

The rankings below answer, and their answers fuse together:

  • identifier matching over the declaration names, for the identifiers the query carried;
  • full-text matching over the document's searchable fields, by BM25 under the weights above;
  • vector similarity over the embedded declarations, when a model is configured and its vectors are in.

The identifier input reads the query the way a reader would. The whole query counts when it is one identifier-shaped token, a dotted name buried in prose counts along with its final segment, and any token that splits into more than one word counts: where does encode_path get its escape set reaches encode_path without the caller quoting it. A plain prose word contributes nothing here, because it reaches the full-text ranking through its own input.

Each ranking scores in units nothing converts between: a match class, a BM25 rank, and a cosine. What they agree on is the position they put a candidate in, so fusion reads positions alone and combines them by weighted reciprocal rank. [search.ranking] carries that decision: fusion_k sets how sharply a top rank counts, and identifier_weight, lexical_weight, and vector_weight set each input's share. Only the inputs that answered take part, so an unavailable vector ranking contributes nothing and leaves the order the other two state. Several selected indexes may each contribute one kind; that kind's share then splits among them, so reaching for the global index does not double what full-text matching is worth.

Fusion holds no project path and resolves no symbol. It answers ordered identities, and the read path resolves them afterwards, which is what lets package hits from the global index rank through the same code. A hit's score is derived from its place in the final order: it is comparable inside one answer and nowhere else.

Regex patterns

Since v0.0.46 · Rust, JavaScript, TypeScript, Markdown, JSON, YAML, TOML, Python

A regex pattern answers from a trigram index over the same stored file text, so the text still lives in one place. A trigram index records which rows hold each run of three characters.

The pattern's query plan derives the trigrams any match must contain - load_config\( requires loa, oad, and every other run of three characters in load_config( - and the index answers the rows holding them. The pattern's matcher then verifies those rows over the text the published index holds, line by line as ripgrep reads it.

The trigram index is filled in the background after the rows it covers are written, so no search waits for it. A row it doesn't hold yet is verified beside the rows it selects, and when those rows pass the search's bounds, the answer covers the rows the index holds and warns pattern_index_preparing.

The plan never rules out a row that could hold a match: a class too large to expand, a repetition that can match nothing, and a literal shorter than three characters each require nothing. A pattern that requires nothing, such as [0-9a-f]{40}, verifies every file its paths select. Each match maps to its smallest containing declaration through the same lookup body matches use, which is how a symbol hit names the declaration holding a match.

Vector ranking

Since v0.0.16 · Rust, JavaScript, TypeScript, Markdown, JSON, YAML, TOML, Python

Full-text matching answers a query that shares a token with the code. A query that shares none - a description of what a declaration does, in the words a bug report would use - reaches nothing that way, and no amount of ranking repairs that, because the words are not there.

An embedding puts the query and the declaration in one space, so a paraphrase finds code it has no word in common with. It replaces nothing. A caller who quotes a real name - a symbol, a configuration key, a line from a traceback - has handed the full-text ranking an exact token to score, while the embedding maps that name into a space where its near neighbours crowd it.

A declaration is embedded as its own source under its qualified name. The name carries the module the source doesn't state; the source carries the identifiers and documentation an encoder trained on code reads best. The path is left out, because the full-text ranking already covers it.

Every model runs through one contract, whether it is an encoder this process loaded or a service answering the OpenAI embedding shape. Rift keeps what that contract does not cover: acquiring the weights, cutting a pass into batches under batch_inputs and max_in_flight, and narrowing the coordinates to the stored format once at the boundary.

Vectors are filed under the identity of the space they were embedded into: the source kind, the endpoint origin for a remote service, the model and its resolved revision, the width, and the document and query transformations. Any change there mints another identity and the held vectors are dropped rather than scored against, because two spaces are two coordinate systems. Credentials never enter it.

A remote endpoint sends declaration text outside the project while the vectors are built, and query text while a caller searches. The configuration page states both transfers. The local default sends nothing: it starts without a key and makes no request once the weights are acquired.

Preparation runs behind the answers. A search issued before the vectors are in is answered by the other two inputs and says so, rather than waiting for a model to load.

Retrieve symbol

get_symbol reads one logical symbol by its canonical id. The answer separates the symbol from its declaration bindings and carries requested source inline. An immutable view keeps a later read and its declaration continuation on the same selection.

A Symbol carries id when the index established one identity for the declaration; a symbol the providers could not settle answers without an id, and a retained provider disagreement lands as a symbol_disagreement entry in the result's warnings.

Asking for history attaches the symbol's committed timeline to the same answer.

On this page