Changelog
All notable changes to this project will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
Unreleased
0.18.1 - 2026-10-16
Small fixes to piped fcs paths and MCP totals. No re-index or config
change is needed.
Fixed
- Piped
fcsoutput andfcs -lprint short paths relative to the current folder even when the configuration lists a root as a relative path or with..in it (they were long but still valid paths). - MCP
search_codesays “total not known” instead of “the scan stopped early” when it has no exact total; the note under it gives the reason (for example, the index is still being built).
0.18.0 - 2026-10-15
Fixes from the 2026-10-15 review of what fcs promises against what it does. Paging is now consistent, busy files show their best hits, several silent cut-offs now say so, and the server keeps its index current by default.
Upgrading: file watching is now on by default. A config without a
watch key now watches its folders; add watch = false under [indexer]
to keep the old behaviour. No re-index is needed. A file with many matches now shows
its best-scoring hits rather than its first ones, so result order changes
for broad searches. search.per_file_matches now defaults to 1,000 (was
100). Files that were skipped as binary only because of control
characters, and SVG files, are now indexed, as ripgrep searches them.
Changed
watchdefaults totrue. The server follows edits, renames and deletes unless the config sayswatch = false, matching what the README and guides describe.--initwriteswatch = true. Thebuild/anddist/default excludes stay, and remain listed as a difference from ripgrep in the matching reference.
Fixed
- Offset paging no longer repeats or skips hits. The per-file cap and match budget grew with the page’s end, so page two could be ranked from a different set than page one (an MCP walk of 305 hits returned 155 distinct ones). Both are now constant below result 1,000 and constant per page beyond it, so consecutive pages join up.
- A busy file shows its best hits. The per-file cap kept a file’s first N matches in line order; it now keeps its N best by score.
- Searches during index build say so. Results carry a new
indexinglimit (web UI: “index still building”; MCP andfcsnotes) and no exact-looking total. fcspiped andfcs -lno longer stop silently at 50.-lpages through every hit and lists every matching file; other output prints a stderr note when more hits exist, with the--offsetfor the next page. Piped and-lpaths are relative to the current folder when the configuration’s roots are known, sofcs -l foo | xargs $EDITORopens the right files.- Two indexed folders with the same name no longer collide. Each root gets a distinct label (extended with parent folders when names clash), so the viewer opens the right file.
- Go to definition with watch off no longer jumps to a line that no longer holds the symbol; a cached definition is used only if its name is still on that line.
- Symbol search totals are honest. When a file limit leaves files
unread, the total is shown as unknown and the
max_fileslimit is reported;fullranking reads every file. - Ranking no longer depends on where the repository sits on disk.
The
src/,lib/and test signals use the root-relative path. - Test files are demoted by their names too (
_test.go,.spec.ts,.test.js,__tests__/,test_*.pyand similar), not only by folder. - Ignore rules match ripgrep:
.git/info/excludeand the global gitignore (core.excludesFile) are honoured by indexing and watch. - Binary detection matches ripgrep: a file is binary only if its first 8 KB contains a NUL byte. SVG is no longer on the binary extension list.
case:andword:accept only yes/no style values; anything else is searched as text. A query that holds only filters now explains how to add text to find.- The index size statistic counts indexed bytes, not files on disk.
- The web UI no longer merges
Foo.tsandfoo.tsinto one group.
Documentation
- The book’s matching, search limits, memory and ranking pages and the CLI guide describe the new behaviour and every remaining difference from ripgrep.
0.17.0 - 2026-10-15
Search limits are measured, raised and configurable. Page 1 of a search for a common word now holds most of what an unlimited search would rank first, and MCP agents can page through every hit.
Upgrading: no re-index is needed. Page 1 of a broad search now ranks
different (better) hits than 0.16.x. If your config sets
server.fast_rank_files, server.max_fast_rank_files or
server.max_page_offset, move them to the new [search] section; the
old keys still work but log a deprecation warning.
Changed
- Page 1 ranks far more of a broad query’s matches. A first page collected 512 matches before ranking (the “match budget”); it now collects 32,768. On a 372k-file corpus (Linux, LLVM, Rust, Kubernetes) page 1 held 10% of the exhaustive top 50 for words with over 100k hits, and now holds 81%, at about 80 ms median. Each collected match costs about 230 bytes while the search runs, about 7.5 MB per search at the new floor. The measurements are on the book’s new “Search limits” page.
- Every search reads files best first. Files are read in order of the per-file score fast ranking used, for every mode, instead of in discovery order below 5,000 candidate files. The budget therefore keeps the hits of the most likely files. Ordering side effect: with the bigger budget and the new order, page 1 of a broad search shows different (better ranked) hits than before; the result list is still plain rank order, and no hit is hidden.
- No more 2,000-file stop. Above 5,000 candidate files, auto ranking
read only the best 2,000 and stopped, even when those files held too few
hits to fill the page, so a rare string in a big repository could come
back nearly empty. Searches now read until the budget is spent or the
deadline passes. A server can still set a cap (
search.fast_rank_files), reported asmax_filesas before.rank=fullnever stops at a file cap;autoandfasthonour one. - MCP
search_codereturns 50 hits by default (was 20), allows up to 1,000 (was 100), takesoffset,max_filesandrank, and lists 20 files whose path matches (was 5). When more hits exist it says the offset of the next page.
Added
[search]configuration section for every search limit, read by the web UI, REST, gRPC,fcsand MCP:fast_rank_files,max_fast_rank_files,max_page_offset,min_match_budget,match_budget_multiplier,max_match_budget,per_file_matches,default_page_size,max_page_size,max_path_matches,max_timeout_secs,max_context_lines. Contradictory values (a page size abovemax_page_size, a budget floor above its ceiling) stop the server at startup, naming the key. The web UI’s LOAD MORE follows the server’smax_page_size.[mcp]configuration section:default_limit,max_limit,file_matches,max_context_lines,read_file_lines,max_read_file_lines,graph_default_limit,graph_max_limit.examples/limits_bench.rs: measures what each limit costs and how far it moves page 1 from the exhaustive ranking, on any corpus.
Deprecated
server.fast_rank_files,server.max_fast_rank_filesandserver.max_page_offsetmoved to[search]. The old keys still work and log a warning; a[search]value wins.
Fixed
- Symbol search read only the best max(200, 8 x page end) files holding a
matching symbol, and reported its total as exact. It now reports
max_fileswith the files it read when there were more.
0.16.2 - 2026-10-14
Watch mode keeps answering searches on very large trees, and Python projects whose imports were listed as external now resolve. Re-index a Python project to pick up the import fixes.
Changed
- Web UI: Semantic is no longer in the header nav, since semantic search
is not being developed for now. The page still works at
/semantic.html(and shows its tab while open).
Fixed
- Watch mode on large trees no longer answers searches with “index is being updated” while changes stream in. Applying a batch held the write lock for its whole duration, including reading and parsing the files, and a per-directory scan of every indexed path made that seconds long on big trees (15 s for one batch on 200,000 files). Now only installing the parsed files takes the write lock, in steps of at most 512 files with a pause between them; directory lookups cost the size of the directory; the watcher parses on its own threads instead of the pool searches use; and a search waits up to five seconds for a step to finish instead of failing at once. On one million files with 2,000 files rewritten a second, no search was turned away and the slowest took 0.2 s.
- Python: an absolute import (
import pkg.mod,from pkg import x) in a file nested more than 8 folders below its package root resolved to nothing, so the dependency explorer listed the project’s own modules as external packages. Every ancestor folder is now tried. A package in a sibling folder put onPYTHONPATH(lib/mypkgimported fromapps/) is still not found; the graph support page lists it as a known gap. - Python: in a repository whose root folder has an
__init__.py(run with that folder onsys.path), every absolute import of the project’s own packages was listed as external, because a package folder is never a root. The outermost package folder is now tried as a root when it is the repository root or no folder above it holds an indexed file. - Web UI: search history now records searches made by typing, not only ones submitted with Enter. A query counts once the box sits unchanged for two seconds or you move on (into the results, a preview or a filter); carrying on typing replaces that entry rather than leaving prefixes.
0.16.1 - 2026-10-14
Existing indexes are rebuilt once on the first start. The on-disk
index format is now version 8 (case-insensitive search folds case like
rg -i), and symbols are re-extracted for the new reference kinds and
Python references; the rebuild also picks up the dependency-graph fixes
for TypeScript, Python and Rust. Results are
now in rank order with each file’s hits together. The v0.16.1 binaries
report their version as 0.16.0.
Added
- Reference kinds: each reference is a
call,type,valueorattribute, shown on the result (web UI tag,fcs refs[call], APIreference_kind, MCP[reference: call]), and thekind:operator filters on it:refs size kind:call,-kind:attributeto leave out property reads. The index file format is unchanged (the kind shares the column’s word); symbols re-extract once on the first start. - Search by imports:
rdeps:PATHlimits a search to that file and the files that import it,deps:PATHto the file and what it imports (:Nhops,:*for any distance,-rdeps:/-deps:to leave them out, a trailing/for a folder). An import that lands on a package’s entry file (mod.rs,__init__.py,index.ts) reaches the modules behind it without costing a hop, one entry file at a time, and only when the file names something the module defines; negated filters walk strictly. Unknown, ambiguous or graph-less anchors are errors, never an empty result. Responses say how many files were searched and warn when unresolved project imports may leave files out; hits carry theirhopsand nearer files rank higher. In the query everywhere, plusdeps/rdepson the API, gRPC, MCPsearch_codeandfcs --deps/--rdeps(the only way to scope a regex), the web UI’s FILTER panel, the dependency popover and the explorer’s Search importers / Search its imports buttons. - Python references find far more uses: decorators, base classes, type annotations, functions and classes used as values, attribute reads and exceptions, where they used to find calls only (Flask: 57% of the uses Jedi finds, now 98%). Parameters and local variables that share a function’s name are no longer listed as its references. Existing indexes re-extract symbols once on the first start.
examples/ref_oracle.rsandsrc/check/oracle/py_refs.pycompare Python references with Jedi’s; CI runs them on Flask and Click.- Book page “Reference support”: what the References mode finds in each language, and what it misses. C and C++ record no references; Python records calls only; TypeScript, Java and C# miss many type positions.
- Imports that resolve to no indexed file are counted, and the ones written
as a path into the project (
./x,.x,crate::,@/), which point at a broken import or an unsupported form, are called out: in the dependency explorer’s folder map, inGET /api/graph/modules(unresolved_imports,unresolved_local), in MCP’sdependency_overview, and per file infile_dependencies. - Docs: “How the import graph is built” explains how Python and JavaScript/TypeScript imports are extracted and resolved, how an import is judged internal (resolves to an indexed file) or external, and how the method is checked against grimp and TypeScript.
fcs check [PATH…]: checks fcs on your own code. It indexes the folders in memory, compares queries made up from the files withrg -F(exit 1 on any difference, with each differing query shrunk), and compares the import graph with grimp (Python) and TypeScript’s own resolver when they are installed. The graph oracles’ scripts moved tosrc/check/oracle/.- Web UI: the file viewer, the hover preview and the dependency explorer’s source pane mark every hit in the whole file in a strip beside the scrollbar (query matches; import lines in the explorer). Click the strip to jump to the nearest one, including lines not laid out yet (#141). The preview’s ↑/↓ now step through every match in the file, not only the lines on screen.
- Web UI themes: Bold (the existing look, still the default), Corporate and
Corporate dark. On a first visit the UI asks which to use, previewing each
live on hover and putting the chosen theme back on mouse-out (one click
picks a theme and closes the picker); the palette button in the header (replacing the
?link, which went to the Docs page also in the navigation) changes it later. The choice is kept per browser. New[server]settings:default_theme("bold","corporate"or"corporate-dark") for people who haven’t chosen, andtheme_picker = falseto skip the first-run question on a shared server. - The ripgrep backtest’s
--check-defaults(on in CI) walks every query the way a web UI user pages through it under the server’s default limits, with Auto and Fast ranking: SEARCH ALL FILES when files were skipped, then LOAD MORE to the end. Every match must be shown exactly once. - The ripgrep backtest makes up queries from the corpus (
--generate N --seed S), checks them againstrg -Fand shrinks any that differ to the smallest form. CI runs 500 per run. - Dependency-graph oracles:
examples/graph_oracle.rscompares the import graph with grimp on Django and with TypeScript’s own resolver on Vite, listing every edge fcs misses or draws wrongly. CI fails on any regression. Django now matches grimp exactly; Vite’s remaining misses are tsconfigpaths. - Capped results say which limit cut them. Search responses carry
limits_hit(max_files,match_budget,per_file_cap,timeout, each with a sentence for people); gRPC sends the names in thefcs-limits-hitresponse header,fcsprints a “results capped” note on stderr, MCP’ssearch_codeadds a “Results capped” line (also when nothing was found), and the web UI names the limit in the count’s tooltip and shows “stopped at time limit” beside the time.
Changed
- Import graph: a Rust path into the crate used without a
use(crate::a::b::f(),super::x::Y) is an import of the module it names, so the dependency explorer, rankings andrdeps:see it. Rebuild the index to pick it up (upgrading from 0.15.x rebuilds it anyway). file:andlang:narrow each other:file:src/ lang:rustis the Rust files undersrc/, where it used to be every file undersrc/plus every Rust file. Repeating one operator still widens it.- Web UI: the preview icon is back at the end of each match line (#153 had moved the preview to the line number, with nothing to show it). Hovering the icon previews the file; clicking it pins the preview, which then stays open until Escape, a click outside it or its ×, as does clicking or scrolling inside it. The line number opens the file and no longer previews it on hover. A kept preview now does what the file viewer does: Ctrl/⌘-click or double-click a name for its definition (Back in the viewer returns to the preview’s line), click a line number to select it, Copy link, Explore imports. Its Open button is now Expand, which shows the same file in the large viewer at the preview’s line (after ↑/↓ or a selected line, not always the original hit).
- Web UI: results show one line of context either side of each hit by
default (was the match line only). Filter → Context lines still sets it
and is remembered once you pick a value;
context=0in a link shows the match line only. - Web UI: the Docs page is a one-page cheat sheet (query operators, regex,
keyboard shortcuts,
fcsand MCP, the main API calls) with links into the documentation book. It used to be a hand-kept copy of the API reference that had drifted from the book. It shows the server’s version and uses the server’s own address in the MCP example. - Web UI: denser results, about twice as many hits per screen. A hit is its code with the line number in a gutter instead of a header row above it; the kind shows as a small tag on definitions and references; the line number opens the file there (and previews it on hover), the file name opens the file, and copy and the explorer link show on hover. Language and dependency badges are quiet outlines, and on a phone they hide so the file name isn’t cut off.
- Web UI: one code viewer for the file viewer, the hover preview and the dependency explorer, so line numbers, highlighting and “Show more” look and work the same in each.
- Web UI: one rule for labels and buttons. Labels are written in sentence case and the theme decides whether they show in capitals (Bold does), so buttons, toggles and headings on every page now look alike. The search button reads SEARCH instead of EXECUTE_QUERY. The Index pages lost their emoji (SVG icons instead) and use the same cards and buttons as the rest, and the dependency explorer’s buttons and chips match the search page.
- Web UI: file names are never re-cased. The dependency explorer’s pane title, extension badges and the compact search box in the header showed them in capitals; they now show as written.
- Web UI: one FILTER button (the duplicate in the results bar is gone). It
shows how many path filters are set, the results bar lists them with an
× to remove each, and the panel has Clear path filters. The boxes are
now called SEARCH ONLY IN and SKIP PATHS and take
,as well as;(a comma inside{…}is still glob alternation). The API, CLI and MCP still separate patterns with;only. - Web UI: the results count includes path matches (“2 FILES · 1 RESULT”), the per-hit ranking score moved to the line number’s tooltip, and the dependency badge reads “imported by N” instead of “N deps”.
- Web UI: the indexing bar no longer flashes on every page load (it moved the search box), the focus hint says Ctrl+K or ⌘K to match the platform, and MAPPED is gone from the header stats (it read 0 B until searches ran).
- Web UI: the SEMANTIC page served by the keyword server says semantic search runs as a separate server, instead of claiming the keyword server is down (it probed a hard-coded port 8080).
- The INDEX page’s sample files are shown relative to the indexed folder.
- Web UI internals: all six pages share one header (
static/shell.js) and one stylesheet of design tokens (static/app.css); every colour, shadow and font is a token, Tailwind’s colours point at the same tokens, and a test fails on hard-coded colours. This is groundwork for themes and should look the same, apart from the navigation links on the non-search pages now matching the search page. The semantic pages use the same SVG icons as the rest, so the 3.9 MB icon font is gone. - A burst of file changes is applied once the tree has been quiet for 200 ms (at most two seconds), instead of after a fixed two-second debounce, so edits show up in search sooner.
- The web API, the gRPC service and offline
fcsnow run every search through one shared request path (search::request): the same default page size, caps, deadline and offset limit, and the same dispatch to the engine. Offlinefcsnow enforcesserver.max_page_offsetlike the server does. A new test checks that all three return the same hits in the same order.
Fixed
- Results are in rank order again, with each file’s hits together: files in the order of their best hit, then every hit of that file by score. Since v0.12.0 hits were interleaved by file (every file’s best hit before any file’s second), and since #147 regardless of score, so a file’s other hits could be pages away. This was never asked for. On the ranking suite, precision@5 goes from 0.90 to 1.00; precision@1 is unchanged.
- Web UI: a file’s hit badge counted only the hits on the page (“1 hit” for
a file with seven). Each result now carries
file_hits(andfile_hits_capped) in the web API, gRPC andfcs --json. A file cut at a page boundary reads “3 of 7 hits”, and Show 4 more hits in this file fetches the rest in place, kept across Load more. The ripgrep backtest checks every first-page count against the full result and that each file’s hits are together. - Dependency graph: TypeScript and JavaScript imports that go through a
tsconfig.json/jsconfig.jsonpathsalias orbaseUrl(@app/orders,components/Button) now link to the file they name, including settings inherited throughextends. They were treated as npm packages, so repos that rely on aliases had few edges. Imports of a workspace package by its package.jsonname(@acme/shared) now link to that package’s entry point. With no tsconfigbaseUrlorpaths, imports are also tried from the repository root.import x = require('./x')is picked up. - Python:
from .missing import x(orimport pkg.missing.sub) where the module does not exist no longer draws an edge to the package above it; the import is unresolved, as Python fails it. - Case-insensitive search now folds case exactly as ripgrep’s
-idoes (Unicode simple case folding):sfinds the longſ,kthe Kelvin sign,μthe micro signµ, and the Greek symbol forms (ϐ,ϑ, …) their letters. The index stores folded text, so the on-disk index format is now version 8 and an existing index is rebuilt once on upgrade. Found byfcs checkon Django. - LOAD MORE past 1000 results repeated some results and skipped others on broad searches, because each deeper page ranked a different sample of the matches. Every page that ends at result 1000 or later now ranks the same sample, so the pages fit together.
- Web UI: the hover preview shows the whole file and scrolls. It used to fetch 12 lines either side of the hit, so a FILES-box row (previewed from line 1) showed 13 lines in a window-tall panel, and the mouse wheel scrolled the page behind it, which closed it. It now opens at the hit (or the top of the file, with no line marked, for a file row), scrolls inside itself, has ↑/↓ buttons to step through the query’s matches in the file, and an Open button for the file viewer. Recently viewed files are cached, so hovering again or opening the file doesn’t re-fetch it.
- File watching no longer leaves deleted files in the index. Moving a file into a directory and deleting that directory soon after left the file’s old name indexed forever, because the two events were merged into one that named only the directory. Watch events are now hints: every path an event names is checked against the disk, directories are reconciled, and when events are lost (an OS queue overflow) the watched folders are reconciled instead.
- Files changed while the initial index was being built could be indexed with old content, or stay indexed after being deleted or moved. Content that is no longer on disk is now never merged into the index.
- Python imports resolve the way Python 3 does.
from pkg import submoduleandfrom . import submodulenow point at the submodule, not at the package’s__init__.py. An absolute import no longer resolves to a same-named file next to the importer (import jsoninside a package reaches a siblingjson.pyno more, andfrom django import …insidebackends/no longer meansbackends/django.py).src/layouts resolve. On Django the graph went from 87% to 100% of grimp’s edges, with 287 wrong edges removed. - The dependency graph of a watched index now ends up the same as a fresh build’s. Every file’s imports are kept, and a change resolves again the imports it can affect: a file that appears takes over imports that had fallen back to its package, a file that goes hands them back, and an edit that removes an import no longer leaves its edge behind when the imported file is saved in the same burst. The initial build also resolves every import once more at the end, so the graph no longer depends on the order files were indexed in.
- JavaScript/TypeScript:
import(/* comment */ './x'), imports with the olderassert { type: 'json' }and.cjswritten for a.ctsfile now give edges. - Whole-word search now uses the same word characters as ripgrep’s
-w(Unicode\w): combining marks such as Indic vowel signs and decomposed accents count as part of a word, soword:yesno longer matches inside Hindi, Malayalam or Telugu words. - Whole-word search with MATCH CASE no longer misses a match that overlaps
a rejected one (
}}at the end offield}}}), and the same on the Unicode case-folding path.
0.15.2 - 2026-10-13
Search works differently in this release. Plain-text search is now a
literal, like rg -F: several words are one phrase, quotes and a leading
- are part of the text, and the -term exclusion and quoted phrases are
gone. In the web UI, a regex ignores case unless MATCH CASE is on. See
Changed below before upgrading scripts or saved queries.
Added
- Search finds files by path. Typing or pasting a path (
src/web/api.rs,api.rs, an absolute path, Windows\separators) lists the matching files in a FILES box above the results in the web UI, first in MCPsearch_code, and above the hits infcson a terminal. File names rank above directories. Content results are unchanged. API:files=Non/api/searchaddsfilesandtotal_filesto the response. - Web UI: MATCH CASE toggle. It means the same in every mode: off,
upper and lower case are the same in plain text and regex alike; on,
they are matched exactly. The results header says which applied
(“ignoring case” or “matching case”). MCP
search_codetakescase_sensitive. - Web UI: SEARCH ALL FILES. When a query could be in more than 5,000
files, fast ranking opens only the 2,000 most likely (“Searched 2,000 of
N files”); the button opens the rest. API:
max_fileson/api/search,fcs --max-files. - Configuration:
fast_rank_files(default 2000) sets how many files fast ranking opens,max_fast_rank_files(default 0, no limit) caps what a request may ask for, andmax_page_offset(default 100000, 0 = no limit) sets how deep a client may page. Responses report the cap asmax_files_allowedwhen one is set.
Changed
- Plain-text search is a literal, like
rg -F. Several words are one phrase:fn mainfinds lines holdingfn main, not every line holdingfnormainin files that contain both. Quotes and a leading-are part of the text:"fn main"finds the words with their quotes and-Wallfinds-Wall. The-termexclusion and quoted phrases are gone;file:,-file:,lang:,-lang:,case:andword:still work. On the command line, put a query starting with-after--. - Paging is no longer refused past offset 10,000: the limit is
max_page_offset(100,000 by default), and a deep page gets a match budget that reaches it. - Web UI: with MATCH CASE off a regex ignores case, like plain search. The
API and
fcs -ekeep matching a regex’s case by default, as ripgrep does. - The search row stacks below 1024 px wide so the input keeps its room.
0.15.1 - 2026-10-13
Same changes as 0.15.0, with the version number set correctly. The v0.15.0 tag was pushed before the version bump, so its binaries report 0.14.0 and its release has no changelog notes. Use 0.15.1.
Added
- Web UI: go to definition. Ctrl+click (⌘+click on a Mac) a name in the file viewer or a result snippet to open where it is defined (a list when there are several, a text search when there are none), with Back (Alt+←) in the viewer. Double-click a name for Definition, References and Search. Uses the existing symbol and reference search.
- Web UI: the file viewer has a shareable link. Click a line number to
select a line; Copy link copies a URL whose fragment
(
#file=…&line=…) reopens the search with that file at that line. - Web UI: syntax colouring for Dart, Dockerfile, Elixir, Erlang, Haskell,
Protobuf and Scala (
static/highlight-languages.min.js, rebuilt byscripts/update-hljs-languages.sh), and for more extensions of languages already bundled (.pyi,.mts,.kts,.vue,.ini,.diff,.graphql, …). show_root_nameindexer option (defaulttrue). Set it tofalseto show result paths without the indexed folder’s name (src/main.rsinstead ofmyapp/src/main.rs) in the web UI, REST API,fcsand MCP. Only applies when a single path is indexed;file:filters are unaffected.- Index freshness. With file watching off, the server only sees files as
they were at its last scan. Search responses,
/api/statusand the progress socket now carryindex(watching,synced_at_ms,updated_at_ms), and/api/status?paths=…says whether each path is current, stale, not indexed yet or deleted. When watching is off, the web UI shows “Index synced 6 min ago · watching off” beside the search time,fcsprints the same warning on stderr (-qhides it),fcs statusreports it, MCPsearch_coderesults end with it, and a new MCP tool,index_status, lets an agent check the files it just wrote.
Fixed
- Web UI: search result context and hover previews are syntax-highlighted in one pass, so doc comments and other multi-line comments are shown as comments instead of having their words coloured as code (also when the snippet starts partway into a comment).
- Web UI: the exact hit is outlined on the matched line, where it used to blend into the line’s yellow background.
- Web UI: Dockerfiles and Makefiles in subfolders (
deploy/Dockerfile) are recognised by name and highlighted, instead of shown as plain text. - Web UI: code is shown without font ligatures, so
!=,=>and===appear as typed rather than as≠and arrows.
0.14.0 - 2026-10-08
Added
- A ripgrep backtest:
examples/rg_backtest.rsruns every query inbenches/rg_backtest.toml(124 queries, including cases adapted from ripgrep’s and Zoekt’s own regression tests) through fcs and throughrgover tokio 1.45.0, Django 5.2 and the edge-case files intests/fixtures/backtest, and fails on any line one finds and the other does not. It also checks that a page of results is repeatable and that LOAD MORE ends with every hit exactly once. Runs in CI on changes to the engine. Two new documentation pages: how the backtest works and how to extend it (“Correctness against ripgrep”), and where fcs’s matching agrees with or differs from grep, ripgrep and git grep (“Matching compared with grep and ripgrep”). - MCP server for coding agents at
/mcpon the web server (streamable HTTP), with six read-only tools:search_code,read_file,file_dependencies,change_impact,import_pathanddependency_overview.fcs mcprelays it over stdio for clients that launch a command. Browser requests from foreign origins are refused. - Dependency explorer (GRAPH in the web UI,
/graph.html): a file’s import neighbourhood, the impact of changing it (with the affected tests), the shortest import chain between two files, and a folder-level module map with import cycles marked, beside the file’s source with its import lines linked. Search results, the deps popover and the file viewer link into it, and it links back to keyword search limited to the files on screen. /api/graph/neighborhood,/impact,/path,/modules,/importsand/filesover the import graph (seedocs/API.md).- Distribution:
cargo binstallmetadata pointing at the release archives; a container image (ghcr.io/jburrow/fast_code_search) built per release with a documenteddocker runline; release archives signed keylessly through Sigstore with the verification command in the release notes; a crates.io publish job and a Homebrew-tap update job (formula generated from the archives’ checksums) that run when their tokens are configured. - Diagrams in the documentation: the indexing pipeline, how a score is composed, and the parts of a deployment.
server.enable_grpc(defaulttrue) and--no-grpcto run without the gRPC API.- Storage health checks. At startup and before each save the server warns when the filesystem holding the index is out of inodes or short of space, and when a filesystem holding indexed source is out of inodes. A failed save with “No space left on device” says whether bytes or inodes ran out, since an inode-exhausted disk still shows free space.
- One server per index: the server holds a lock on
<index_path>.lockand writes its PID and addresses to<index_path>.lock.owner. A second server given the sameindex_pathlogs which process owns it, loads the index read-only and never saves, so the two can no longer overwrite each other’s index. - The diagnostics page has a Service card: web UI and gRPC status, where
the index is saved (or that it is memory only / read-only), and startup
and storage problems, which also mark the status as degraded.
/api/healthreports the problem count, and the search page shows “Server started with N problems” linking to it. - Regex search help in the web UI: a
?cheat-sheet next to the REGEX toggle with runnable examples and the rules that catch people out; when a search finds nothing, close variants that do find something (flexible spacing, ignore case, the same text without regex, or regex for a plain query that looks like one) are offered as one-click suggestions; an invalid regex shows where it is wrong and offers to search the text literally or escaped. The regex docs gained examples and pitfalls.
Security
- rustls 0.23.36 -> 0.23.45 for RUSTSEC-2026-0285 (TLS 1.3 handshake messages accepted across encryption-level boundaries).
Changed
-
The keyword search results header is shorter: “1,234 RESULTS · 42 ms” instead of “1234 RESULTS FOUND · LATENCY: 42.3ms · FULL (5,678 files)”. The ranking mode and file count moved to tooltips; the header still says when fast ranking searched only some of the files.
-
An invalid regex’s error now carries the parser’s explanation and points at the problem in the pattern as typed (it used to repeat only the pattern).
-
Rust imports through the crate’s own name (
use my_crate::search::Xinmain.rs,src/bin/andtests/) now resolve to the library’s files, so those files have import edges and the library modules they use get the dependency ranking boost. Existing indexes pick this up on rebuild. -
A busy or forbidden port no longer stops the server. If the gRPC or web port cannot be bound, the server logs an error naming the port and how to find what holds it, and keeps indexing and serving on the other API. It exits only when no API could start. Previously a busy gRPC port shut the server down at once, interrupting indexing at 0 files while the web UI stayed up, and the cause appeared only after Ctrl+C.
-
Startup logs where the config came from (or that none was found and defaults are used) and the config warnings; both were previously discarded because they were emitted before logging was set up.
-
An unset
indexer.index_pathis now a warning when indexing starts (“Index persistence OFF … lost when the server stops”), and the ready line showsindex=memory only, not saved. A missingindex_pathdirectory is a warning rather than an error, since the first save creates it. -
--initwrites every config key with its default.index_pathis now set in the generated file, named after it (--init work.tomlgives~/.local/share/fast_code_search/work.fcsidx);cors_origins,max_concurrent_searches,request_timeout_secs,respect_gitignore,include_extensions,exclude_filesandbatch_sizeare no longer missing or commented out. A test keeps the template complete. -
fast_code_search_semanticlogs where its config came from, and a busy gRPC or web port is a clear error that disables only that API, instead of a panic inside a background task that left the process running with nothing listening. -
Documentation brought up to date with the above: the dependency explorer and
/api/graph/*in the in-app docs, regex help in the web UI and query-syntax pages,enable_grpc/--no-grpc, the index lock, storage warnings and the diagnostics Service card in the API reference, configuration cookbook and troubleshooting guide. The deployment guide is rewritten around the published container image, HTTP health and readiness probes, and one index per server (its gRPC examples relied on reflection, which the server does not offer).
Fixed
- Search correctness review: a file with more than 100 hits now reports
the total as unknown and LOAD MORE reaches its later lines (it claimed an
exact count of 100); a regex mentioning
\nkeeps^and$per line (they matched only at the file’s ends), and an escaped backslash such as\\nno longer switches to whole-file matching; symbol search returns files whose only match is their name again and honours-term;file:and-file:words no longer match the indexed root folder’s own name (-file:searchdropped every file of a root namedfast_code_search); a filename-only hit obeyscase:yesandword:yes;fcs search -e -iagainst an index file now ignores case, as it does against a server. - A search that stopped at its match limit returned a different first page on almost every run (threads raced for the budget), and LOAD MORE repeated some results and skipped others. Documents are now taken in a fixed order, and LOAD MORE fetches the whole list again at the bigger size and redraws it (up to 1000 results; past that it pages as before), so nothing repeats or goes missing. Results already shown can move.
- Greek: a query or line with a word-final sigma (
ς, or a capitalΣthat lowercases to it) missed matches written with the other sigma. - A file starting with a UTF-8 byte order mark (an invisible marker some
editors write) kept it as part of line 1, so
^textmissed that line and the result showed a stray character. The mark is now skipped when a file is read, as ripgrep does. Files on disk are untouched. - A regex that can match the empty string (
x*,^\s*$) reported a hit on a line past the end of a file that ends with a newline. - Plain-text search for code containing quotes (
{ "success": True,"key":"value") found nothing: the query parser stripped every", so the term becamesuccess:. A quote now opens a phrase only when it wraps a whole token; any other quote is searched for as written.
0.13.0 - 2026-09-08
Added
- A ranking-quality suite:
examples/ranking_quality.rsruns the labelled queries inbenches/ranking_quality.toml(tokio 1.45.0 + Django 5.2) and reports precision@1 / @5; CI fails a ranking regression. Machine specifications and every figure of the real-corpus benchmark are written as JSON (corpus_bench --json) and published to the site per run. - Rust items wrapped in brace-delimited macro invocations (
cfg_rt! { … }) are extracted as definitions and references. Existing indexes re-extract symbols once on the first start. - The benchmark trend page (
dev/bench/) is now the project’s own: it groups the charts by suite, says what each one measures and what would move it, labels every axis with its unit and “lower is better”, shades each run’s spread, links each point to its commit, and charts the scan-tool comparison: the latest run as one bar chart and each query’s ripgrep / ugrep / fcs times per commit. The README’s benchmark section explains the three measurements and how to read them. scripts/bench/compare.shtimes the same queries through ripgrep, ugrep andfcson the same tree (warm cache, median of seven runs, machine recorded) and refuses FUSE/NFS/SMB mounts, where scan tools are unfairly slow. CI runs it and the documentation site shows the result alongside the latency and ranking-quality figures, with two new book pages.
Changed
- Ranking: single-term lines of a multi-term query are capped per file and
the files holding the phrase are scanned first, so the match budget can
no longer starve the best hits; the whole-word phrase outranks the phrase
as a prefix of a longer identifier; test, example, mock and fixture paths
are demoted at line level; public definitions score higher and the
start-of-line boost ignores visibility modifiers; symbol search scores
from the symbol cache before reading files, weights exact names above
prefixes more strongly, and ranks
implblocks below the type. The suite went from 0.30 to 1.00 precision@1 on the labelled set. - Reference search scores each hit by its file (dependency boost and the test-path penalty) instead of a flat 1.0, and every result ordering breaks ties on the path rather than the file id, so the same query returns the same page on every machine.
- The “Background indexing completed” log line reports the bytes of text indexed rather than the bytes currently memory-mapped.
0.12.1 - 2026-09-08
Changed (dependencies and CI)
toml1.x,actions/upload-artifact7,actions/download-artifact8,actions/setup-node7,codecov/codecov-action7,softprops/action-gh-release3; the Pages deploy has its own concurrency group and pushes without force.
Added
- A documentation site built with mdBook (
docs/book, published under/docs/on GitHub Pages): Start here, Guides, Reference, How it works, Benchmarks and Project sections. The canonical documents are imported at build time with links rewritten; thefcshelp, server help and configuration template pages are generated from the binaries and drift-checked in CI (scripts/docs/). CODE_OF_CONDUCT.md(Contributor Covenant 2.1), issue forms for bugs, performance problems and feature requests (questions go to Discussions), and a fuller security policy with private reporting and response targets.docs/API.md: the REST, gRPC, server command line and configuration reference, moved out of the README.- Release pages carry an install table, checksum instructions and a first-run line alongside the changelog section.
Changed
- README rewritten as a landing page that leads with how results are ranked; benchmark figures are from the latest CI run and link to it.
- The site and web docs no longer state unmeasured claims (“10 GB+”, “600k+ files”); headline figures come from the benchmark run.
- Internal engineering notes (
plans/, the instruction-files blueprint) moved underdocs/internal/.
0.12.0 - 2026-09-07
Added
- A Web UI job in CI runs the JavaScript helper tests (
npm test, Node’s built-in runner overstatic/tests) and fails if the committedtailwind.cssdoes not match the markup.
Changed
- Web UI: the keyword pages use inline SVG icons instead of the 3.9 MB
icon font (which stays only for the semantic pages); result groups are
rendered on demand as the page scrolls; the pure helpers live in
static/lib/keyword-helpers.jsand the query tokenizer now matches the server’s rules exactly; the unused dark highlight stylesheet is gone. - Results interleave files within a score tier: every file’s best hit, then every file’s second hit, and so on, so one file holding a hundred matches no longer fills the page. Phrase and all-term tiers still come first; paging stays deterministic.
Fixed
- Web UI: dependency popover failures are shown inline instead of an
alert(); copying a path works on plain-HTTP origins (textarea fallback) with a visible Copied/Copy failed state; the hover-preview cache is cleared when the index changes, so previews cannot go stale.
0.11.0 - 2026-09-07
Added
fcs, a command-line client (docs/CLI.md): sends queries to the running server over the REST API with the web UI’s syntax and modes (refs,symbols, regex, globs, context), printspath:line:col:textwhen piped or grouped colour output on a terminal,--jsonfor tools, and uses grep’s exit codes. When no server answers it searches the on-disk index in-process (--offline,--index-path).fcs statusreports the server and index.- A guide to running the server at login on Linux (systemd user unit), macOS
(launchd agent) and Windows (Task Scheduler), with ready-made unit files
under
deploy/(docs/RUN-AT-STARTUP.md). - The API’s result types (
SearchResponse,SearchResultJson) are public and deserializable, andreqwestis a regular dependency (the CLI’s HTTP client).
Fixes from the 2026-09-06 engine and UI review
(docs/internal/plans/2026-09-06-keyword-engine-and-ui-review.md).
Changed
- Line-mode regex patterns are matched in one pass per file (compiled with
(?mR), one hit per line, per-line semantics for^,$,\sand CRLF kept), removing the per-line start-up cost that madefn\s+main\(an order of magnitude slower thanmain\(over the same candidates.
Fixed
- Regex candidate generation treated a repetition as one contiguous copy on
both sides, so
fo+bardemanded the literalfobarand never foundfoobar; repetitions are now run boundaries, and a property test asserts the candidate set is a superset of the regex’s matches. Case-insensitive runs no longer break atkands(which previously turned(?i)taskinto a full scan). references=trueapplied index-time columns to the current file content and could panic mid-character on a file edited since indexing (a 500 for every reference query until re-index); positions are verified first.- Rust type mentions are captured as references.
- Multi-term queries rank lines containing more of the terms first and
emit them before the per-file cap, so
fn mainfindsfn main(). - The query syntax no longer swallows
->,-Wall,-1.5,file:or quoted operators;file:src/selects everything undersrc. case=falseis honoured in regex mode; a needle containing\nno longer reports a match range past the end of the line.update_filekept the edges out of a file but dropped every edge into it, zeroing a module’s dependents whenever it was edited; the edges are kept, and a deleted-then-recreated file regains its dependents.- Re-indexing an already indexed path replaced its postings instead of unioning stale trigrams onto them; suffix path lookups match whole path components only, and an empty suffix matches nothing.
- TypeScript call sites (plain and member calls) are captured as
references; the upstream tags query only had type and
newmentions. An index saved by an earlier extractor has its symbols and references re-extracted on load (the fingerprint now carries an extraction schema tag), so this reaches existing indexes without a rebuild. - Multi-term queries prefer the phrase: files whose trigrams contain the terms as written are always opened, and lines holding the phrase rank above lines holding every term, above single-term lines.
- Reload: gitignore matchers are cached per directory during the eligibility pass (it took 14.8 s for 61k files).
- A watcher burst re-indexes modified files with one posting-list pass and a parallel parse instead of a full scan per file under the write lock.
- Removing a file releases its memory map instead of keeping it alive in the tombstoned slot.
- Two concurrent saves (the watcher’s shutdown save racing the indexer’s
final save, a checkpoint racing a watcher save, or two processes sharing
index_path) could delete the index file. Saves are serialized and use unique temp files; the rename fallback never removes the target unless the temp file still exists. offsetabove 10,000 is rejected instead of raising the match budget without bound; the derived budget is capped and uses saturating maths.- Every search runs under an engine deadline just under the request
timeout, so a scan whose response the HTTP layer abandoned stops on its
own. The timeout answers 504 with the JSON error envelope and
Retry-Afterinstead of an empty 408, and the concurrency permit and latency metric follow the search’s real lifetime. - gRPC shares the REST search semaphore (per-connection limits let N
connections run N × limit searches), rejects
Indexwith no paths, runs oneIndexat a time and skipsfinalizewhen nothing was indexed. - A config change (exclude pattern, extension list, size cap,
.gitignorerule) now removes newly ineligible files on reload instead of being logged and ignored. - Discovery no longer descends into excluded directories (
.git/objects,target/,node_modules/), honours.gitignorein trees that are not git repositories (so it agrees with the watcher), and treatsmax_file_size = 0as the default like every other code path. - On Linux the watcher adds one non-recursive watch per kept directory, so excluded trees consume no inotify watches; the change queue is bounded.
- Reload only treats a missing file as removed; a permission or I/O error (an unmounted share at boot) keeps the entry instead of wiping the root.
- Configured roots match files at path-component boundaries and the longest
root wins, so
/x/projno longer claims/x/proj2/.... - Config:
~is expanded, relative paths resolve against the config file’s directory,include_extensionsaccepts".rs", and anFCS_CONFIGpointing at a missing file is an error rather than a silent fallback. save_after_updatescounts files rather than watcher batches; checkpoints followsave_after_build; a second Ctrl+C during a slow shutdown save exits immediately.- REST/gRPC:
references=truecombined withregexorsymbolsis a 400; the invalid-regex message is no longer doubled;max=0means the default page size on both surfaces;/api/contextreturns the same workspace-relative path as search results; an emptyfile=is a 400 instead of resolving to an arbitrary file;/api/readystays ready while a persisted index is reconciled and no longer reports zero files while the write lock is briefly held; the diagnostics self-tests look for the sampled file with a file filter, so a healthy large index is no longer reported as degraded. - Metrics: latency buckets compare fractional seconds (1.9 ms counted as
1 ms); concurrency rejections count in
fcs_search_requests_total. - HTML, scripts and stylesheets are served
no-cachewith ETag revalidation so an upgrade cannot mix old and new bundles; every response carriesX-Content-Type-Options,X-Frame-OptionsandReferrer-Policy;/ws/progresscaps inbound frames. - Web UI: server-side strings on the diagnostics page are escaped (a file
extension could inject markup); match highlighting uses the server’s
match offsets and highlights every query term, honouring
case:and regex mode; URL parameters fully determine the search modes (regex=falseturns regex off; stored settings no longer override shared links) and toggle visuals follow the real state;maxandcontextare validated (no moremax=NaN), with 500 and 1000 available; searches get history entries so Back restores the previous query; results page withoffset(“Load more”), showtotal_matches, mark truncated lines and label filename hits; a REFERENCES toggle; 503 while indexing is retried; the progress socket keeps reconnecting; the compact header search no longer overlaps the stats; the hover preview dismisses on Escape, scroll and outside clicks; dialogs have proper semantics, focus management and a focus trap; results are keyboard-navigable with real focus; contrast meets AA; the file viewer highlights once and windows large files; docs.html matches the API; the “INDEX” header stat is labelled as mapped content.
0.10.1 - 2026-09-06
Fixed
- The index could keep a file whose delete or rename-away event the watcher backend never delivered (FSEvents on macOS drops the source half of a rename), leaving an entry that search could no longer read. After applying a batch of changes the engine now checks the siblings of every changed path and drops entries whose file is gone.
- Import resolution failed when the importing path was not in canonical form (a symlinked temp directory on macOS, a non-verbatim path on Windows); the path is now canonicalized once per importing file.
- CI: failing test names, panic messages and crashes are reported as workflow annotations; the macOS and Windows test jobs pass.
0.10.0 - 2026-09-06
Fixed
- A freshly loaded index reported absolute display paths (and the UI could not round-trip them) until the next finalize, because the configured roots were registered after the per-file metadata pass cached the paths.
- Persisted index corruption after a file removal:
save_indexcompacted the file table over tombstoned ids but wrote trigram bitmaps, symbols and dependency edges keyed by live id, so every file after a removed one was misattributed or lost on reload. Ids are now remapped at save time. - Every match on line 1 of every file received the 3× symbol-definition boost (the synthetic filename symbol at line 0 counted as a definition line).
- Files with a line over 100 KB or extreme nesting were dropped from the whole index; they are now text-searchable and only skip symbol extraction.
- The persisted mtime/size described the on-disk state at save time, so a file edited during a long build was never detected as stale. Both are now captured when the content is read.
- Symbols:
.tsxis parsed with the TSX grammar; JS/TS class methods,const f = () => …, generator functions, abstract classes and namespaces are extracted; symbol line/column now come from the name node (Java@Override, decorators, attributes and multi-line C signatures reported the wrong line). - Directory rename/delete events were silent no-ops in the watcher; every file under the old path stayed indexed. The watcher now also applies the same eligibility rules as the initial build (include extensions, excludes, size).
- A panic on the watcher path poisoned the engine lock and turned every search into a 500 until restart; the watcher path is now panic-guarded, runs on an 8 MB stack, and the REST/gRPC readers recover from a poisoned lock.
/api/contextno longer overflows on a hugecontextvalue and is capped at 200 lines each side; user regexes have an explicit compile-size limit.DependencyIndex::remove_filenever pruned the filename index and every watcher update pushed a duplicate path entry.
Added
- A real-corpus benchmark (
examples/corpus_bench.rs, run in CI over pinned tokio and Django checkouts, nightly over the Rust compiler tree) reporting build throughput, resident memory, query latency percentiles, incremental update cost and index save/load. /api/searchpaging and budgets:offset(deterministic ordering, so pages are stable),timeout_ms, andtotal_matches/truncated_by_budgetin the response;has_moreis now derived from the real total. Regex and symbol searches report ranking info too.- Every result carries
line_match_start/line_match_end(byte offsets into the full line) andmatch_column(0-based character column) on REST and gRPC.
Added
- Symbol references:
/api/search?references=true(gRPCSearchRequest.references) returns the call sites, type mentions and implemented traits of an identifier asSYMBOL_REFERENCEresults, from the grammars’ tags queries (the Rust query is supplemented with path and generic calls). References are stored compactly (12 bytes each, names interned) and persisted. - Multi-line regex: a pattern that mentions a newline (
\n,\r,\x0a) or sets thesflag ((?s)begin.*?end) is matched across lines and reported on the line where each match starts. Other patterns stay line-oriented. - Query syntax for plain-text searches:
"quoted phrases", several AND-ed terms,-term,file:/-file:,lang:/-lang:,case:yes,word:yes(RESTcase/wordparameters and gRPCcase_sensitive/whole_wordoverride the in-query switches). /api/ready(readiness, distinct from/api/healthliveness),/metrics(Prometheus text: search counters by outcome, latency histogram, index gauges) and the standardgrpc.health.v1service.- Config:
server.cors_origins,server.max_concurrent_searches,server.request_timeout_secs,indexer.respect_gitignore;--web-address. - gRPC
SearchRequest.offset/rank/deadline_ms; results carrydependency_count.
Changed
- Build: the semantic engine is behind the
semanticCargo feature (off by default;ml-modelsimplies it), release binaries use thin LTO and are stripped, OpenTelemetry is on 0.32 (one tonic/axum stack),md5andglobare gone (the config fingerprint format changed, so the first start after upgrading rebuilds the index once), andcargo-deny+ Dependabot are wired into CI. - Trigram extraction folds case per byte and dedupes through a bitset instead of lowercasing a copy of every file and hashing every byte.
- Import resolution no longer probes candidate paths through
realpath(17.6 million failingreadlinkcalls while building a 6k-file corpus); candidates are resolved lexically against the indexed path table. Full build of tokio + Django: 25 s -> 3 s. - Updating one file from the watcher went from 55 ms to 4 ms on that corpus:
posting-list removal is parallel with a membership fast path, and compiled
.gitignorematchers are cached per directory (keyed by mtime) instead of rebuilt per event. - Memory diet: each indexed file stores its path once (shared with the path lookup map) and keeps at most 64 bytes of inline state; only files above the mmap threshold allocate mapping state. The evictable fallback cache for unmapped large files is gone (they are read per access), and the all-documents bitmap is borrowed rather than cloned per short or unconstrained query.
- Symbols are extracted with each grammar’s own
tags.scmquery (name-node positions, upstream-maintained coverage), supplemented by the previous walker; new symbol kinds Module, Macro, Field and Property; C++.hheaders are detected; JSON/TOML/YAML/HTML/CSS/Markdown are no longer parsed; parses are capped at 2 s. - Unknown config keys are rejected and the configuration is validated at
startup (addresses, limits, index path directory);
OTEL_SDK_DISABLED=truecan no longer be overridden. - REST requests time out (default 30 s), bodies are capped, and searches beyond
max_concurrent_searchesget 503 + Retry-After instead of queueing; gRPC has a per-request timeout and per-connection concurrency limit. - gRPC
max_results = 0now means the default page of 50 (was clamped to 1); theIndexRPC applies the indexer’s eligibility rules and no longer holds the engine lock for its whole walk. /api/diagnosticsreports the real indexer configuration and caches its extension breakdown per index generation; malformed query parameters return the JSON error envelope; display paths resolve through their root instead of a per-file suffix scan.- Every query’s work is bounded by a match budget (8× the page, minimum 512) and
an optional deadline;
rank=fullno longer materializes every match of every candidate before truncating. - Regex searches are accelerated for
(?i)literals and repeated suffixes (abc+), and compiled patterns are cached; plain-text verification scans the whole buffer withmemchrand builds symbol maps only on the first hit. - Symbol search consults the symbol cache before reading any file, never drops low-scoring files before matching, ranks exact > prefix > substring, and returns one row per line.
- Retrieval no longer reads through a live memory map for files up to 1 MiB:
a searcher touching a mapped file that an editor truncated in place was an
uncatchable SIGBUS that killed the server. Small files (essentially all source
files) are read into an owned buffer per access; only larger files are mapped.
This also keeps the mapping count far below
vm.max_map_count. .gitignore/.ignorefiles under the indexed paths are honoured by the initial build and by the watcher (indexer.respect_gitignore, default true).- Persisted index format v7 (
FCSIDX05): a sectioned layout (header with version, CRC-32 and section lengths; metadata; fixed-width sorted trigram directory; bitmap region). Saving streams the posting lists without an intermediate copy and loading memory-maps the file, validates it before decoding, and deserializes bitmaps in parallel from the mapping. The format also stores unresolved imports (so a checkpoint restore still gains the edge when the target file is indexed later), nanosecond mtimes, byte paths, run-optimised bitmaps and symbol references. Index files written by earlier versions are rebuilt automatically; a golden fixture pins the format. - Import resolution is now per language (Rust crate/module paths, Python
relative and package imports, JS/TS extension and
indexprobing,@/aliases) and no longer guesses a same-named file anywhere in the repo for bare package names; unresolved imports are retried only when a file with a matching name appears instead of after every batch. - Watcher events are gathered for 200 ms and applied under one write lock with a single posting-list pass; ranking metadata and the all-documents cache stay current across incremental updates and after a persisted load.
- Graceful shutdown: SIGINT/SIGTERM now stop both servers, stop the indexer (persisting a checkpoint), save pending watcher updates, and flush telemetry.
- Security defaults: both listeners bind to
127.0.0.1; CORS is off unlessserver.cors_originsis set; the gRPCIndexRPC only accepts paths under the configuredindexer.pathsand no longer follows symlinks; a web-UI bind failure is fatal instead of silently leaving the server without a REST API. - Minimum supported Rust is 1.89 (was documented as 1.70); the toolchain is
pinned via
rust-toolchain.toml;reqwestuses rustls so builds and tests no longer need system OpenSSL. - CI lints all targets and features, checks docs, has an MSRV job, and releases
are gated on tests; the VS Code extension publishes on
ext-v*tags only. Failing test names are surfaced as workflow annotations. - Releases no longer ship the
-debugarchives (an unoptimised binary per platform); release binaries are built with thin LTO and stripped.
Repository
.gitattributesnormalises line endings; the corrupted.gitignoreis repaired;onnxruntime/and thetest_corpusgitlinks are no longer tracked.
0.9.0 - 2026-06-10
Added
- Real incremental index updates: file modifications, deletions, and renames now
update the index in place (
TrigramIndex::remove_document, file-store tombstone / refresh, dependency-edge removal) instead of leaving stale entries until restart. - Watcher now handles rename events (
Modify(Name)), wiring delete + re-index. - Search API returns a
has_moreflag; the UI shows “N+ results (raise MAX RESULTS)”. - Always-visible FILTER toggle so filters can be set before the first search.
- Keyboard navigation in results (
j/k/arrows to move, Enter to open,/to focus). - Per-result copy-path button.
Changed
- API errors are now consistent JSON (
{ "error": … }); 503s includeRetry-After. - Static assets use compile-time content hashes for ETags and honor
If-None-Match(304), eliminating per-request hashing of large embedded assets. - Hover preview uses the lightweight
/api/contextendpoint with a hover-intent delay and per-file cache instead of fetching the whole file. - Configured
max_file_sizeis honored end-to-end (no longer silently capped at 10 MB). - Docs page documents the
contextparam and the file/context/dependencies/diagnostics endpoints, the JSON-error/Retry-After convention, andtotal_results/has_more.
Fixed
- Eliminated memory-map UTF-8 undefined behavior and truncation crashes during
indexing (no more
from_utf8_uncheckedover bytes that can change underneath it; single-file indexing reads an owned buffer instead of a live mmap). - Guarded all indexing paths against panics and recover from poisoned engine locks instead of silently dropping batches.
- Hardened persisted-index loading (magic header + allocation-limited decode) and made index saves atomic (temp file + fsync + rename).
- Fixed silent result misattribution after reload by remapping trigram document IDs onto the compacted IDs assigned when stale/removed files are skipped.
- Regex acceleration no longer drops matches for alternations / optional literals
(e.g.
hello|worldnow returns files containing only one branch). - Watcher/stale-file exclusions use glob semantics (so
**/.git/**no longer excludes.github/.gitignore, and Windows backslash paths match correctly). - Case-insensitive verification is Unicode-aware (e.g.
übermatchesÜBER); empty / whitespace queries return immediately instead of scanning the whole corpus. - Web UI: quote-safe HTML escaping and removal of inline event handlers (XSS hardening);
group-by-file uses the original (correctly-cased) path for display and fetches; search
requests abort in-flight so stale results can’t overwrite newer ones; offline banner
tracks server recovery;
wss://under HTTPS; rebuilt CSS; removed dead styles.
0.8.0 - 2026-03-27
Added
- Dependency popover and tooltip interactions in the web UI to improve code navigation context.
- Shared search history utilities used by both keyword and semantic UIs for more consistent query history behavior.
Changed
- Search result highlighting now uses dynamic, context-aware highlight styles in the keyword UI.
- Reduced duplicated UI styling by removing redundant syntax-highlighting and tooltip rules from keyword-specific CSS and inheriting from shared styles.
Fixed
- Keyword UI tooltip cleanup behavior now hides context tooltips immediately when opening file views/modal overlays, preventing lingering tooltip artifacts.
0.7.5 - 2026-03-24
Fixed
- Web UI regression where loading
/or/semantic.htmlwith?q=...restored the query text but did not execute search automatically on page load.
0.7.4 - 2026-03-20
Changed
- New UI theme for the keyword search interface with improved layout and styling.
0.7.3 - 2026-03-20
Added
- New agent skills: normalize, onboard, optimize, overdrive, quieter, and teach-impeccable for design context.
Changed
- Updated scoring factors and improved score tooltip information in keyword search results.
- Enhanced UI/UX across diagnostics and semantic pages: improved status banner styles, typography, spacing, input focus styles, and semantic score display.
Fixed
- Benchmark CI failure: reset local changes before gh-pages branch switch to prevent dirty
Cargo.lockblocking the workflow (#97).
0.7.2 - 2026-03-18
Added
- Workspace-relative display paths in results (VSCode-style folder name prefix) (#95).
- New agent skills: arrange, audit, bolder, colorize, critique, delight, polish, typeset.
Changed
- UI improvements: wordmark styling, help icon for docs header link, and accessibility enhancements.
- Improved touch target sizes for mobile accessibility.
Fixed
- Linting issues across the codebase (#94).
0.7.1 - 2026-03-17
Fixed
- Fix path handling: canonical storage, root-relative display, and proper glob exclusions (#91).
- Fix semantic web API handlers panicking on a poisoned
RwLock(#85). - Preserve
dev/bench/across GitHub Pages deployments (#83).
Added
- URL state encoding for semantic search UI; refactored shared helpers into
common.js(#93). - VS Code configuration and tasks for Rust development.
Changed
- Refactor code structure for improved readability and maintainability.
0.7.0 - 2026-03-03
Added
- GitHub Pages site (
docs/site/): A static project site deployed togh-pageson every push tomainvia a newpages.ymlworkflow. The site includes a features overview, live CI badge matrix, embedded benchmark trend charts (fromdev/bench/pushed bygithub-action-benchmark), Codecov coverage badge, dynamically-rendered changelog (fromchangelog.jsongenerated at deploy time), and a Getting Started guide. Theme toggle (dark/light) with OS preference detection. The deploy action usesclean-exclude: dev/to preserve benchmark data, and bothpages.ymlandbenchmark.ymlshare agh-pages-deployconcurrency group to prevent simultaneous pushes.
0.6.9 - 2026-02-28
Changed
- Version bump and release.
0.6.8 - 2026-02-28
Added
- Automated performance benchmarks in CI: A new
benchmarkworkflow runs on every push tomain. It executescargo benchfor bothsearch_benchmarkandpersistence_benchmarksuites using the Criterion bencher output format, runs the validator binary in JSON mode for functional performance validation, and uploads results as abenchmark-resultsartifact. Historical trends are tracked in thegh-pagesbranch viabenchmark-action/github-action-benchmark, with automatic alerts on >200% regressions. - Registered
persistence_benchmarkinCargo.toml: Thebenches/persistence_benchmark.rssuite (save/load index, trigram deserialization, file staleness checks) was already present but not declared as a[[bench]]target. It is now runnable viacargo bench --bench persistence_benchmark.
0.6.7 - 2026-02-27
Added
- Code coverage reports in CI: A new
coveragejob runs on every push and pull request usingcargo-llvm-cov. The HTML coverage report is uploaded as acoverage-reportGitHub Actions artifact and LCOV data is uploaded to Codecov. See the Code Coverage section in the copilot instructions for how to generate reports locally.
Fixed
- Website becomes unresponsive during index building: API handlers (
search,stats,dependents,dependencies,diagnostics) previously calledengine.read()which blocks until any pending write lock (held during batch merges) is released. Under concurrent load, this caused blocking threads to pile up and exhaust the Tokio thread pool, leading to memory pressure and system unresponsiveness. All handlers now useengine.try_read()and immediately return503 Service Unavailablewith the message “Index is currently being updated, please try again shortly” when the write lock is held. Thestatusendpoint was already safe and is unchanged.
0.6.6 - 2026-02-27
Fixed
- OOM during indexing of large codebases (Vec/HashMap over-allocation): After each doubling growth, Rust’s
VecandFxHashMapmay carry up to 2× excess capacity. On a 62 000-file corpus this contributes tens to hundreds of MB of wasted heap. Three changes address this:SearchEngine::compact_memory()— a new method that callsshrink_to_fit()onsymbol_cache(both outer Vec and every innerVec<Symbol>),pending_imports, and the trigram HashMap. Called automatically every 50 batches during the initial index build, so memory is reclaimed without waiting forfinalize().SearchEngine::finalize()— additionally shrinksfile_metadataand runs a fullcompact_memory()pass once indexing completes.TrigramIndex::finalize()— now includesshrink_to_fit()on theFxHashMap<Trigram, RoaringBitmap>which can hold ~50% empty bucket slots after bulk insertion.
0.6.5 - 2026-02-27
Fixed
content_fallbackbytes permanently retained in heap (regression since v0.6.2): Files whose mmap failed fell back to aOnceLock<Vec<u8>>cache, which is write-once — once populated, the bytes could never be freed. Replaced withMutex<Option<Vec<u8>>>so the cached bytes are evictable. Resolves the memory-growth issue tracked in v0.6.4 for Linux deployments approaching the mmap limit.
Improved
- Persist symbol caches and import graph in saved index: When saving the trigram index to disk, the per-file symbol caches and resolved dependency edges are now also persisted. On the next startup, symbols and imports are restored directly from the index file instead of being re-extracted from file contents, eliminating the
rebuild_symbols_and_dependenciespass for unchanged files and significantly reducing startup latency for large codebases.- Symbols are only re-extracted for files whose mtime has changed (stale files), which are queued for re-indexing as before.
- Persisted indexes from older versions (format < 3) are automatically rebuilt once and saved in the new format.
- Index persistence format bumped to version 3.
0.6.4 - 2026-02-27
Fixed
- OOM / memory errors during indexing on large codebases (regression since v0.6.0):
BATCH_SIZEwas silently raised from 500 to 2000 in v0.6.0 as part of the Phase-2 parallelisation change (#24). During Phase 1, allBATCH_SIZEfiles are read into memory simultaneously, so peak RAM scales asbatch_size × average_file_size × ~4. On repos with many sizable files (generated code, minified JS, large headers, etc.) this caused 4× higher indexing-time memory consumption and triggered out-of-memory crashes. The default has been restored to 500. A newbatch_sizekey under[indexer]lets operators tune the trade-off between throughput and RAM:[indexer] batch_size = 1000 # raise on high-RAM machines; lower if still OOM content_fallbackVec permanently retains file bytes in heap (Linux, search-time; tracked): Files that fail mmap (Linux: pastvm.max_map_count; any OS: individual mmap failure) have their content cached in aOnceLock<Vec<u8>>on first search access. BecauseOnceLockis write-once, those bytes are never freed. On large repos where most files exceed the mmap soft-limit, every search that returns such a file permanently pins its content in heap memory; after enough searches the process exhausts RAM. A full fix requires refactoring the&[u8]lifetime API inLazyMappedFile; a TODO comment has been added at the relevant site and the fix is tracked. Thebatch_sizefix above is sufficient for Windows users (where the mmap limit is not enforced).
0.6.3 - 2026-02-27
Improved
- Mmap-limit warning emitted only once: Previously, every file registered without mmap (after the OS
vm.max_map_countsafe limit was reached) emitted its ownWARNlog line, flooding logs on large codebases. A single warning is now issued when the limit is first hit; all subsequent files are silently registered under the direct-read fallback. - Mmap-limit error message clarified: The internal error text now states that remaining files will be indexed via direct-read fallback (slower retrieval, but search still works) instead of the previous misleading “Cannot index more files” wording.
- Background indexer logs persistence settings at startup: When
index_pathis configured, the indexer now emits anINFOlog with the activesave_after_build,save_after_updates, andcheckpoint_interval_filesvalues so operators can confirm persistence is active without reading config files. - README and startup warning updated: Messaging around
vm.max_map_countnow accurately describes the graceful fallback behaviour introduced in 0.6.2 rather than implying hard failures.
0.6.2 - 2026-02-27
Fixed
- Files beyond the OS mmap limit were silently dropped from search results (P0 — Correctness): When the Linux
vm.max_map_countsafe limit (85% of system max, typically 55,700 on default kernels) was exceeded during indexing,add_file()bailed out and the remaining files were neither registered in the store nor added to the trigram index. On large codebases (e.g. 629k files) only the first ~55,700 files were searchable. Files are now always registered. For files that cannot be memory-mapped (limit exhausted), content is read viastd::fs::read()at result-retrieval time and the result is cached, so there is no repeated I/O cost. - File count in UI showed mmap-capped number instead of true indexed count: Because the above fix ensures all files are registered,
file_store.len()— which drives the FILES counter in the web UI and/api/status— now reflects the true number of indexed files rather than the OS mmap limit.
0.6.1 - 2026-02-27
Added
- Checkpoint saves during initial indexing: New
checkpoint_interval_filesconfig option under[indexer](default:0= disabled). When set (e.g.20000), the index is written toindex_pathevery N files during the initial build. If the process is killed before completion, the next run loads the checkpoint and re-indexes only the missing files — no compute is lost. Has no effect ifindex_pathis not configured.
0.6.0 - 2026-02-27
Added
- Non-UTF-8 encoding support: Files in legacy encodings (Latin-1/ISO-8859-1, Windows-1252, Shift-JIS, UTF-16 LE/BE, etc.) are now automatically detected and transcoded to UTF-8 during indexing, making them fully searchable. Zero performance impact on UTF-8 files (fast path via
std::str::from_utf8).- New
transcode_non_utf8config option under[indexer](default:true) to disable transcoding if only UTF-8 codebases are indexed. - Transcoding uses
chardetng(Mozilla’s Firefox charset detector) +encoding_rsfor accurate, battle-tested detection. files_transcodedcounter exposed in/api/status,/api/diagnostics, and gRPCIndexResponsefor visibility.- Transcoded files emit an
INFO-level log entry with the detected encoding name.
- New
- Filename-only matches now returned: Searching for a filename that does not appear in the file’s content previously returned zero results (the file was shortlisted by the trigram index but then silently dropped). All three search paths now synthesize a
SearchMatchat line 0 when the query matches the filename stem but no content lines match.
Fixed
- Regex trigram lookup not lowercased (P0 — Correctness):
search_regex()passed raw literal text (e.g."MyClass") to the trigram index which stores only lowercased content. Uppercase literals ≥3 chars returned zero trigram hits, causing missed results or a full scan fallback. The literal is now lowercased before the trigram lookup. - Exact-match score boost used case-insensitive comparison (P0 — Correctness):
calculate_score_inline()applied the 2× exact-match boost using the already-lowercased query, so every match received the boost regardless of case. The boost now uses the original (un-lowered) query for a true case-sensitive comparison. - C++ template declarations produced duplicate symbols (P1 — Correctness): The
template_declarationtree-sitter handler both pushed template children onto the stack and fell through to the generic push, causing inner nodes to be visited twice and symbols to appear twice in results. - Symbol search scanned all documents (P1 — Performance):
search_symbols()calledtrigram_index.all_documents()regardless of query length, checking every indexed file. For queries ≥3 characters the trigram index is now used to narrow candidates first. - Line length penalty was too harsh (P1 — Quality): The previous
1.0 / (1.0 + len * 0.01)factor dropped to 0.50 at 100 chars and 0.09 at 1000 chars, severely penalizing function signatures and long lines. Replaced with a gentler logarithmic curve(1.0 / (1.0 + (len / 100.0).ln_1p())).max(0.3). FAST_RANKING_TOP_Ntoo low (P2 — Quality): Raised from 500 to 2000. The previous limit could cause relevant files with low base scores to be missed in large codebases.- Empty search results allocated unnecessary
Vec(P2 — Performance):search_in_document_scored(),search_in_document(), andsearch_in_document_regex()returnedSome(Vec::new())on no match. They now returnNone, eliminating spurious allocations. - Trigram bleed at filename/content boundary (P2 — Correctness):
format!("{}\n{}", filename_stem, content)generated spurious trigrams spanning the boundary (e.g. the last chars of the filename joined with the first chars of the content). The separator is now three newlines ("\n\n\n") so no meaningful trigram crosses the boundary. - Redundant trigram deduplication (P3 — Code quality):
TrigramIndex::search()ran a three-step dedup (extract →FxHashSetfilter → collect). Replaced with a single call toextract_unique_trigrams(). - Per-query filename stem allocation (P2 — Performance):
filename_matches_query()re-computedfile_stem().to_lowercase()on every file for every query. A pre-computedlowercase_stem: Stringfield is now stored inFileMetadataat index time. FileNamesymbol search returned wrong content:search_symbols_in_document()fetched line 0 of the file content forFileNamesymbols, returning the first line of the file instead of the file path. It now uses the file path as the match content.
0.5.8 - 2026-02-26
Fixed
- Fix panic in
content_safety_checkon multi-byte UTF-8 content:is_binary_content()sliced a&strat a raw byte offset (8192) which panics when that position falls inside a multi-byte UTF-8 character (e.g. CJK text, emoji). The function now operates onas_bytes()directly, avoiding the invalid slice.
0.5.7 - 2026-02-26
Added
- Automatic mmap limit detection: On Linux the server now reads
vm.max_map_countat startup, computes a safe 85 % ceiling, and refuses to map additional files once that ceiling is reached — preventing “cannot allocate memory” crashes on RHEL 7 / CentOS 7 and similar systems with low default limits. - Startup system-limits logging:
SystemLimits::collect()gathersmax_map_count, current map count, open file descriptors, andfile-max, then logs them atINFOlevel on every launch. - Startup warning for low limits: When
vm.max_map_count < 131072the server prints an actionable warning to stderr with both “with sudo” and “without sudo” remediation steps, then pauses 3 seconds before continuing. diagnose_mmap_error()helper: Mmap failures that look like resource-limit errors now produce a rich diagnostic message including current system limits and concrete fix instructions.- Deployment troubleshooting docs: New “Memory Allocation Errors on RHEL7/CentOS7” section in
DEPLOYMENT.mdcovering symptoms, root cause, automatic detection behaviour, and two solution paths (with/without sudo).
Fixed
include_extensionsconfig now honoured during indexing: The field was parsed but never forwarded to file discovery; it now filters files correctly.max_file_sizeconfig now honoured during indexing: The configured value is forwarded toFileDiscoveryConfiginstead of hardcoding 10 MB.watch = truein config now starts the file watcher: A background thread is spawned to detect and apply file-system changes.save_after_updatesnow triggers periodic index saves: The config field was previously ignored; it now saves every N files.- Import extraction extended to all supported variants:
pyi,pyw,mjs,cjs,mts,ctsnow handled. - Uniform
/healthstatus string: Both keyword and semantic REST APIs now return"healthy". - Config template corrected:
index_path,save_after_build,save_after_updates,watchplaced under[indexer];telemetry.enableddefault fixed.
0.5.6 - 2026-02-26
Fixed
- Binary-detection tests use genuine binary bytes: Test helpers in
file_store,lazy_file_store,engine, andintegration_testsnow construct binary payloads from real non-UTF-8 byte sequences instead of synthetic strings, making the tests more robust and accurately representative of real-world binary files.
0.5.5 - 2026-02-26
Changed
- Maintenance release: version/changelog update only; no functional code changes.
0.5.4 - 2026-02-25
Added
exclude_filesconfig option: Explicitly exclude specific file paths from indexing. Use this when a file causes a crash — identify it viafcs_last_processed.txtthen add it:[indexer] exclude_files = ["/repo/src/generated/bad_file.rs"]- Crash probe file
fcs_last_processed.txt: Written to the working directory before each tree-sitter symbol extraction call. After a SIGABRT crash, read this file to identify the exact file being processed at the time of the crash. - Per-file DEBUG logging: Both indexing phases now emit
tracing::debug!with the file path. Run withRUST_LOG=fast_code_search=debugto see a rolling log of files being processed, making the crashing file identifiable from the last log entry.
0.5.3 - 2026-02-25
Added
- Version logged on startup: The binary version is now emitted as a structured log field
(
version=X.Y.Z) alongside the startup address line, making it easy to confirm which build is running. - Debug builds in release artifacts: Each GitHub release now also ships a debug build
(with full symbols) per platform, archived as
fast_code_search-VERSION-TARGET-debug.{tar.gz,zip}. Use these builds when diagnosing crashes —RUST_BACKTRACE=fullwill show real function names.
0.5.2 - 2026-02-25
Fixed
- SIGABRT / heap corruption during parallel batch indexing: Tree-sitter C parsers were being
called concurrently from rayon’s thread pool (
par_iterover a 500-file batch). Tree-sitter’s internal C allocator is not thread-safe under concurrent use, causing intermittent heap corruption manifesting asmemory allocation of N bytes failed+Aborted (core dumped)after ~110 batches (~55K files). Fixed by splittingPreIndexedFile::processinto two phases:PartialIndexedFile::process— pure-Rust trigram extraction, runs in parallel safely.PreIndexedFile::from_partial— tree-sitter symbol/import extraction, runs sequentially. Trigram extraction (the dominant CPU cost) remains parallel; only the tree-sitter FFI calls are serialised.
0.5.1 - 2026-02-25
Fixed
- OOM crash during background indexing:
PreIndexedFilewas retaining a full copy of each file’s content (String, up to 10 MB each) in memory while rayon processed batches in parallel. With a 500-file batch size this could accumulate up to 5 GB of redundant heap on top of the trigram index, causing an allocation failure on large codebases (~60K files). Thecontentfield has been removed fromPreIndexedFile— it was only needed transiently insideprocess()to compute trigrams, symbols, and imports, all of which are now the only data carried intoindex_batch.
0.5.0 - 2026-02-25
Added
- HNSW vector index for semantic search: Replaced linear O(n) search with HNSW O(log n) algorithm
- New
hnsw_rsdependency for fast approximate nearest neighbor search - Configurable HNSW parameters in
[indexer.hnsw]section:m,ef_construction,ef_search - Default values optimized for moderate codebases (m=16, ef_construction=200, ef_search=100)
- Embeddings now persisted alongside HNSW index for proper save/load functionality
- 2-10x faster query performance for large codebases (>10K chunks)
- New
Changed
- Semantic search vector index now uses HNSW instead of linear search
- Updated documentation with HNSW configuration and tuning guide
- Memory overhead increased to ~3x indexed code size (from ~2x) due to HNSW graph structure
0.4.0 - 2026-02-09
Added
-
OpenTelemetry distributed tracing: Both server binaries now export traces via OTLP/gRPC
- New
[telemetry]TOML config section withenabled,otlp_endpoint, andservice_name - Enabled by default — set
enabled = falseorOTEL_SDK_DISABLED=trueto disable - Standard env var overrides:
OTEL_EXPORTER_OTLP_ENDPOINT,OTEL_SERVICE_NAME,FCS_TRACING_ENABLED - New
src/telemetry.rsmodule withinit_telemetry()/shutdown_telemetry()lifecycle - Graceful shutdown flushes all pending spans before process exit
- New
-
Tracing instrumentation across the stack:
#[tracing::instrument]on all public search methods:search,search_ranked,search_with_filter,search_with_filter_ranked,search_regex,search_symbols#[tracing::instrument]on semantic enginesearchmethod#[tracing::instrument]on all gRPC handlers (keyword + semantic)tower_http::trace::TraceLayeron both HTTP/REST routersServer::builder().trace_fn()on both gRPC servers
-
Semantic model download verification: Optional SHA256 verification via
FCS_MODEL_SHA256orFCS_MODEL_SHA256_<MODEL>
Changed
- Tracing subscriber upgraded from simple
FmtSubscriberto layeredRegistry(fmt + OTel) tracing-subscribernow usesregistryfeature alongsideenv-filtertower-httpnow enablestracefeature alongsidecors- Binary startup reordered: config loads before tracing init so TOML telemetry values are available
--initflag exits before tracing init (no subscriber needed for template generation)
Fixed
- Persisted index loads now rebuild symbol and dependency caches to restore symbol scoring boosts and import-based ranking
- gRPC keyword service lock handling now reports lock errors instead of panicking on poisoned locks
Dependencies
- Added
opentelemetry0.27,opentelemetry_sdk0.27 (rt-tokio),opentelemetry-otlp0.27 (tonic),tracing-opentelemetry0.28
0.3.0 - 2026-02-06
Added
-
Expanded language support: Tree-sitter symbol extraction now supports 18 languages (up from 4)
- Programming languages: Rust, Python, JavaScript, TypeScript, Go, C, C++, Java, C#, Ruby, PHP, Bash
- Config & markup: JSON, TOML, YAML, HTML, CSS, Markdown
- Additional file extensions:
.pyi,.pyw,.mjs,.cjs,.mts,.cts,.cc,.cxx,.hpp,.hxx,.hh,.rake,.gemspec,.jsonc,.scss
-
New symbol types extracted per language:
- Go: functions, methods, structs, interfaces, type aliases, constants, variables
- C/C++: functions, structs, unions, enums, classes, namespaces (C++ templates traversed)
- Java: classes, interfaces, enums, records, methods, constructors
- C#: classes, structs, interfaces, enums, records, methods, properties, constructors
- Ruby: classes, modules, methods (including singleton methods)
- PHP: classes, interfaces, traits, functions, methods
- Bash: functions
Changed
- Symbol extractor now handles C/C++ function declarators correctly (name is in
declaratorfield, notname)
0.2.4 - 2026-02-05
Added
-
Two-phase ranking system for large-scale search: Dramatically improves search performance on codebases with 100k+ files
- Fast mode: Ranks candidates by pre-computed file metadata (symbols, imports, path), reads only top 2000 files
- Full mode: Reads all candidate files for complete scoring
- Auto mode (default): Automatically switches to Fast when >5,000 candidates
- File-level scoring factors: symbol density (+4 max), src/lib location (+2), import count (+5 max), test/example penalty (0.7×)
- New
rankAPI parameter:auto,fast, orfull - Response includes
rank_mode,total_candidates, andcandidates_searchedmetadata
-
Ranking mode UI toggle: New dropdown in Advanced Options to select ranking mode
- Results header shows actual mode used and files searched (e.g., “⚡ Fast (2000/100,000 files)”)
-
Documentation page: New
/docs.htmlpage in Web UI explaining the ranking system, API reference, and path filter patterns -
Lazy file store: Memory-mapped file content is now loaded on-demand rather than all at once
- Reduces memory usage during index loading
- ~8x faster index loading via parallel file mapping
Changed
- Search methods now use file metadata ranking instead of early termination for large candidate sets
search_regex()andsearch_symbols()also use fast ranking for consistency
0.2.3 - 2026-02-04
Added
-
Terminal progress bars for index loading: Added animated progress bars when loading a persisted index from disk
- Shows phase-specific indicators: 📖 Reading file, 🔄 Deserializing, 🔍 Checking files, 🧠 Restoring index, 📁 Loading files
- Progress bar shows elapsed time, visual progress, position/total count, and ETA
- Displays summary with file count when loading completes
-
Whitebox validator binary: New
fast_code_search_validatorbinary for comprehensive search engine validation- Generates deterministic synthetic corpus with seeded randomness for reproducible tests
- Multi-language corpus: Rust (40%), Python (25%), TypeScript (20%), JavaScript (15%)
- Varied file sizes and complexity (functions, classes, nested structures)
- Validates index completeness via embedded “needle” markers at known locations
- Tests all query options:
search(),search_with_filter(),search_regex(),search_symbols() - Verifies line number accuracy and symbol extraction
- Optional load testing mode with throughput (queries/sec) and latency percentiles (p50/p95/p99)
- JSON output mode for CI integration
- Run with:
cargo run --release --bin fast_code_search_validator
-
Index persistence: The trigram index can now be saved to disk and loaded on restart for faster startup times
- Configure
index_pathin config to enable persistence save_after_build = true(default) saves after initial indexing completessave_after_updates = Nsaves after N file updates via watcher (disabled by default)- Stores config fingerprint for detecting configuration changes
- File locking ensures safe concurrent access (exclusive writes, shared reads)
- Multiple read-only servers can share the same index file
- Configure
-
Incremental reconciliation: When loading a persisted index, the system reconciles against the current filesystem
- Detects stale files via mtime + size checks and re-indexes them
- Detects new files not in index and indexes them
- Detects removed files and excludes them from results
- Detects config changes (paths, extensions, excludes) and incrementally updates
- Background reconciliation allows searches during the update process
-
New indexing status states: UI shows
loading_indexwhen loading from disk andreconcilingduring background reconciliation -
Symbols-only search mode: New search mode that searches only in discovered symbol names (functions, classes, methods, types, etc.) plus filename matches. Enable via
symbols=truequery parameter in REST API orsymbols_only=truein gRPC SearchRequest. This provides faster, more targeted results when looking for definitions.
Changed
- Updated performance comparison docs: Added comprehensive benchmark comparison with traditional search tools (ripgrep, ag, git grep, grep) in README.md and PRIOR_ART.md. Includes published benchmark data from ripgrep’s official benchmarks, break-even analysis for when indexing pays off, and detailed feature comparison tables.
Fixed
- ML models build on Windows: Fixed ort 2.0 API compatibility issues and CRT linking conflicts
- Updated
ortdependency to useload-dynamicfeature to avoid RuntimeLibrary mismatch withtokenizers - Fixed import paths for
Session,GraphOptimizationLevel, andTensorin ort 2.0 API - Added comprehensive Windows setup documentation in
docs/semantic/SEMANTIC_SEARCH_README.md - Windows users must download ONNX Runtime DLL and set
ORT_DYLIB_PATHenvironment variable
- Updated
- ONNX Runtime version compatibility: Downgraded
ortfrom 2.0.0-rc.11 to 2.0.0-rc.10- rc.11 requires ONNX Runtime >= 1.23.x, but bundled version is 1.22.0
- rc.10 supports ONNX Runtime 1.18.x - 1.22.x
- Added Windows launcher scripts (
scripts/run_semantic_server.ps1and.bat) that automatically setORT_DYLIB_PATH
0.2.2 - 2026-02-01
Changed
- Improved indexing log readability: The background indexing completion message now formats large numbers with underscore separators (e.g.,
89_210), roundsfiles_per_secto whole numbers, and removes redundant raw byte values.
0.2.1 - 2026-02-01
Added
- Indexing completion stats: When background indexing completes, the log now reports the total size of indexed text and the current process memory usage in human-readable format (e.g., “150.00 MB”). Uses the
sysinfocrate for cross-platform memory reporting.
0.2.0 - 2026-02-01
Fixed
- Duplicate search results: Fixed issue where the same file could appear multiple times in search results when discovered via symlinks or different path representations. The
FileStorenow deduplicates files by canonical path.
Added
- Server-side search timing: Search API response now includes
elapsed_msfield showing the actual server-side query time in milliseconds. The Web UI displays this instead of client-side round-trip time.
0.1.0 - 2026-02-01
Initial release of fast_code_search — a high-performance, in-memory code search service built in Rust, designed to handle 10GB+ codebases with sub-millisecond query times.
Core Features
Trigram-Based Indexing
- Roaring bitmap inverted index maps 3-character sequences to document IDs
- O(n) intersection for candidate document selection
- Efficient storage with compressed bitmaps
Memory-Mapped File Storage
- Zero-copy file access via
memmap2crate - Handles 10GB+ codebases without loading everything into RAM
- Files indexed in parallel for fast startup
Symbol-Aware Search
- Tree-sitter parsing for Rust, Python, JavaScript, and TypeScript
- Extracts symbol definitions (functions, classes, methods, types, etc.)
- Import/dependency tracking for enhanced relevance
Intelligent Scoring System
- Symbol definitions: 3x boost for symbol definitions
- Exact matches: 2x boost for case-sensitive matches
- Source directories: 1.5x boost for
src/andlib/paths - Line position: Boost for matches at start of lines
- Dependency awareness: Factors in file connectivity
Parallel Search Engine
- Rayon-powered parallel line-by-line search
- Concurrent processing across all CPU cores
- Sub-millisecond search on large codebases
Server & API
gRPC Streaming API (Port 50051)
SearchRPC with streaming results for real-time UI updatesIndexRPC for adding directories to the search index- Protocol Buffers schema in
proto/search.proto - Match types:
TEXT,SYMBOL_DEFINITION,SYMBOL_REFERENCE
REST/JSON API (Port 8080)
GET /api/search?q=query&max=50— Search with JSON responseGET /api/stats— Index statistics (files, size, trigrams)GET /api/status— Indexing progress and statusGET /api/health— Health check endpoint- CORS-enabled for browser clients
Embedded Web UI
- Static HTML/CSS/JS served from embedded files
- Browser-based search interface at
http://localhost:8080 - Real-time search results display
Configuration
TOML Configuration Files
- Auto-discovers
fast_code_search.tomlorconfig.toml - XDG/platform config directories supported
- CLI argument overrides for all settings
CLI Options
fast_code_search_server [OPTIONS]
Options:
-c, --config <FILE> Path to configuration file
-a, --address <ADDR> Server listen address
-i, --index <PATH> Additional paths to index (repeatable)
--no-auto-index Skip automatic indexing on startup
-v, --verbose Enable verbose logging
--init <FILE> Generate template configuration file
-h, --help Print help
-V, --version Print version
Exclude Patterns
- Glob-based exclusion:
node_modules/,target/,.git/,*.min.js - Configurable via
indexer.exclude_patternsin TOML
Developer Tools
Example Clients
examples/client.rs— Basic gRPC client demonstrating index and searchexamples/benchmark_client.rs— Performance benchmarking tool
Build System
build.rscompiles Protocol Buffers on each build- Cross-platform support: Linux, macOS, Windows
Performance Characteristics
| Metric | Value |
|---|---|
| Indexing speed | ~100MB/s on modern hardware |
| Search latency | Sub-millisecond for most queries |
| Memory usage | Fraction of codebase size (memory-mapped) |
| Max file size | 10MB (larger files skipped) |
Requirements
- Rust: 1.70 or later
- protoc: Protocol Buffers compiler
Installation
# Linux/Debian
sudo apt-get install protobuf-compiler
# macOS
brew install protobuf
# Build
cargo build --release