Code search that ranks like code.
Definitions above usages. The files your codebase depends on above the ones it doesn't. Served from an in-memory trigram index in milliseconds, with a CLI, a web UI, REST and gRPC APIs, and MCP for coding agents.
Documentation · Install · First search · Guides · How it works · Benchmarks
Most code search tools rank by text. grep and ripgrep return lines in file order;
an IDE's "find in files" does the same with a nicer list. fast_code_search indexes
your tree once, keeps the index in memory, and ranks every hit the way a reader of
the code would:
| Signal | Effect |
|---|---|
| The line defines the symbol (tree-sitter, 12 languages) | 3× |
| The file is imported by other files (resolved import graph) | 1 + 0.5·log10(dependents) |
Exact case, match at the start of the line, file under src/ or lib/ |
2× / 1.5× / 1.5× |
| Test and example paths | demoted |
Matching follows ripgrep (rg -F -i), checked against it on every change, so
fn main finds the phrase and returns fn main() definitions first, Widget returns the type's definition
before its two hundred uses, and a widely imported module outranks a scratch file
that happens to mention the same word. refs NAME flips it around: every call site
and type mention of an identifier, across languages. The weights live in one file,
src/search/ranking.rs.
Release archive (Linux, macOS, Windows): download from
releases, unpack, and
put fast_code_search_server and fcs on your PATH. Each archive includes a
config template, the startup unit files and the guides.
With cargo (Rust 1.89+; the source build also needs protoc):
cargo binstall --git https://github.com/jburrow/fast_code_search fast_code_search # downloads the release archive
cargo install --git https://github.com/jburrow/fast_code_search --bins # builds from sourceContainer (indexes the mounted tree, serves the web UI on :8080):
docker run --rm -p 8080:8080 -v "$PWD:/src:ro" ghcr.io/jburrow/fast_code_searchRelease archives are signed through Sigstore and carry a .sha256; the
install guide
shows how to verify one.
fast_code_search_server --init ~/.config/fast_code_search/config.toml # then add your paths
fast_code_search_server # builds the index, opens :8080
fcs 'fn main' # from another terminalfcs -e 'fn\s+\w+\(' -g '*.rs' -C 2 # regex, Rust files only, two lines of context
fcs refs helper # references; `fcs symbols helper` for definitions
fcs -l TODO | xargs $EDITOR # files only
vim -q <(fcs --format vimgrep TODO) # quickfix list; exit codes follow grep (0 / 1 / 2)The web UI is at http://127.0.0.1:8080, the JSON API under /api/
(curl "localhost:8080/api/search?q=fn%20main"), gRPC on :50051
(--no-grpc if you don't need it). To keep the
server running on a developer machine, start it at login:
docs/RUN-AT-STARTUP.md (systemd, launchd, Task Scheduler).
A coding agent that greps re-reads the tree on every question and gets lines in
file order. Point it at the server instead and it gets ranked answers from the
in-memory index in milliseconds, plus the import graph, which grep cannot give
it at all. The server speaks the Model Context Protocol
at /mcp:
claude mcp add --transport http fast_code_search http://127.0.0.1:8080/mcpClients that only launch commands can use fcs mcp, which relays over stdio:
{ "mcpServers": { "fast_code_search": { "command": "fcs", "args": ["mcp"] } } }.
What an agent can then answer, and the tool it uses:
| The agent wants to… | Tool |
|---|---|
| find where something is defined, used or mentioned (text, regex, symbols, references) | search_code |
| read source with line numbers it can cite | read_file |
| know what a file depends on, and what depends on it | file_dependencies |
| check what a change could break, and which tests cover it, before editing | change_impact |
| understand how two files are connected | import_path |
| get oriented in an unfamiliar repository: main folders, hubs, cycles | dependency_overview |
| check whether files it just wrote are indexed yet (when file watching is off) | index_status |
For example, asked to change the ranking weights, an agent can first call
change_impact on src/search/ranking.rs and get back, from this repository:
Changing src/search/ranking.rs can affect 10 other files (2 tests, 8 other code), up to 2 imports away.
Tests (hops away, path):
2 src/search/engine/tests.rs
2 tests/integration_tests.rs
Every tool is read-only and shares the REST API's deadlines and caps. The arguments are in docs/API.md. Import edges cover Rust, Python and JavaScript/TypeScript; search covers every language.
- Dependency explorer (GRAPH in the web UI): a file's imports and
importers, what a change to it could reach (and which tests), the import
chain between two files, and a folder map with cycles marked, beside the
source. The same queries are under
/api/graph/. - Query syntax shared by the CLI, UI and API: literal text like
rg -F(several words are one phrase), narrowed byfile:src/,lang:rust,case:yes,word:yes; a MATCH CASE toggle that means the same in every mode; regex with literal pre-filtering through the trigram index, a cheat-sheet and suggestions when a pattern finds nothing; symbols-only and references modes. - Live index. A file watcher applies edits, renames and deletes in place; a branch switch re-indexes a burst of files with one pass over the index.
- Persistent index. Atomic, checksummed save and mmap load; a restart of a 60k-file index takes seconds and reconciles what changed while it was down. A lock keeps a second server from writing the same index.
- Tells you what is wrong. The Index page reports which APIs started, where the index is saved, and startup or storage problems (a busy port, a disk out of space or inodes).
- Bounded by design. Every search runs under a deadline and a match budget, page sizes and offsets are capped, and regexes have size limits. Results are in rank order with each file's hits together.
fcs(docs/CLI.md): grep-style output and exit codes,--json, and an offline fallback that searches the saved index when the server is down.- MCP for coding agents at
/mcp: search, read files and walk the import graph from Claude Code or any MCP client; see For coding agents. - Semantic search (optional, experimental, behind the
semanticfeature): natural-language queries over TF-IDF or embedding vectors on its own ports; see docs/semantic/SEMANTIC_SEARCH_README.md.
Recorded against a server indexing this repository; scripts/docs/record-demos.sh
re-records them.
Definitions, then references. SYMBOLS keeps only the lines that define a name; REFERENCES lists every place it is used.
Regex help. A cheat-sheet of patterns you can run with one click; a broken pattern says where it is wrong and offers fixes; a pattern that finds nothing offers the close variants that do.
Dependency explorer. What a file imports and what imports it, the source of any neighbour, and everything a change could reach, tests included.
Three things are measured, on every push to main, by one
workflow
on a GitHub-hosted runner:
- Latency over two pinned real repositories, tokio 1.45.0 and Django 5.2, indexed from scratch each run. Every query is timed 30 times in-process after warm-up; the tables report the median (p50) and the 95th percentile (p95). Lower is better.
- Ranking quality: does the first result answer the query? Twenty labelled queries over the same repositories, scored as precision@1 and precision@5. Higher is better; the build fails below 0.90 / 0.95.
- Against scan tools: the same queries through ripgrep, ugrep and
fcson the same tree and machine, warm cache, median of seven runs.
Every run publishes its raw JSON with the machine specification, and the trend page charts each measurement per commit with an explanation of what each chart means. The methodology page says what is deliberately not claimed.
The numbers below are from
run 34254363545
on commit f46e7bf (2026-09-08, GitHub-hosted ubuntu-latest, 4 vCPU):
6,237 files, 38.9 MB of text, 242k symbol references.
| Measure | Value |
|---|---|
| Full build (read, trigrams, symbols, imports, merge) | 4.3 s (1,460 files/s) |
| Resident memory after build | 118 MB |
| Index save / reconciling load | 0.20 s / 0.25 s (21.5 MB on disk) |
| Text search, common word, p50 / p95 | 0.95 ms / 1.03 ms |
| Text search, identifier, p50 | 1.5 ms |
| Regex with literal / case-insensitive / no literal, p50 | 1.4 ms / 0.7 ms / 1.7 ms |
| Symbol search, p50 | 3.7 ms |
| Reference search, p50 | 0.7 ms |
| Incremental update of one file, p50 / p95 | 10 ms / 14 ms |
The same run's Criterion suite over a synthetic corpus puts a common-word text
search at 0.39 ms and a no-match query at 0.45 µs. A nightly job indexes the
rust-lang/rust 1.89.0 tree and records throughput and memory in its job summary.
Speed is only half of it. The
ranking-quality suite
names the file and line a reader would want first for each query (struct Runtime → the definition, not a doc comment; Field → the model field, not the
GDAL one) and currently scores precision@1 = 1.00, precision@5 = 1.00. A
ripgrep backtest
runs 124 queries through fcs and rg on the same trees and fails CI on any line one
finds and the other does not; where fcs deliberately differs from grep, ripgrep and
git grep is
documented. The
comparison
page puts ripgrep at 30–80 ms per query on these trees against about a
millisecond in the engine, and says plainly when a scan tool is the better
choice. Reproduce any of it locally:
cargo run --release --example corpus_bench -- path/to/repo [more paths]
cargo run --release --example ranking_quality -- bench-corpus/tokio bench-corpus/django
cargo run --release --example rg_backtest -- bench-corpus/tokio bench-corpus/django tests/fixtures/backtest
scripts/bench/compare.sh path/to/repo # ripgrep / ugrep / fcs on the same queries
cargo benchWhy a server rather than a faster grep: a scan tool pays the read cost on every query, which is unbeatable once and a poor fit for a query per keystroke. The index pays it once. docs/design/PRIOR_ART.md compares the architecture with ripgrep, Zoekt and GitHub code search.
Indexing reads files in parallel, extracts trigrams into Roaring-bitmap posting lists and symbols with tree-sitter, resolves imports into a dependency graph, and merges into the engine in batches so searches stay live during a build. A query intersects the posting lists of its trigrams to get candidates, scans them in parallel under a match budget, scores each hit with the signals above, and pages deterministically. Details: docs/DEVELOPMENT.md and the How it works chapter of the documentation.
| Area | Source |
|---|---|
| Trigram index, file store, persistence | src/index/ |
| Query execution, ranking, regex analysis, watcher | src/search/ |
| Symbol and reference extraction | src/symbols/ |
| Import resolution | src/dependencies/ |
| gRPC, REST + web UI, CLI | src/server/, src/web/, src/cli/ |
Symbol-aware ranking covers Rust, Python, JavaScript, TypeScript, Go, C, C++, Java, C#, Ruby, PHP and Bash; JSON, TOML, YAML, HTML, CSS and Markdown get structural symbols; everything else is indexed and searchable without the ranking boosts.
The documentation site is built from this repository on every push. It is organised the way questions arrive:
- Start here — Install · First index, first search · Web UI · Editors · Command-line client · Coding agents (MCP) · Run at startup
- Guides — Configuration cookbook · Performance and memory · Troubleshooting · Upgrading · Deployment
- Reference — API and configuration ·
Query syntax ·
Matching compared with grep and ripgrep ·
fcscommand line · Server command line · Configuration template · Index file format - How it works — Indexing pipeline · Candidates and regex analysis · Ranking · Incremental updates · Persistence · Memory and limits
- Benchmarks — Methodology · Latest results · Against ripgrep and ugrep · Ranking quality · Correctness against ripgrep · Trends per commit
- Project — Changelog · Roadmap · Contributing · Development · Security policy · Code of conduct · Prior art · Glossary
The same pages live as Markdown under docs/ and read fine on GitHub;
the site adds generated reference pages, search and the live benchmark data.
The server has no authentication and returns full file contents, so both listeners bind to loopback by default. Put it behind an authenticating proxy before exposing it; see SECURITY.md.
MIT — see LICENSE.



