Full-Text Search
Reflex’s core search mode uses trigram indexing — the same technique behind Google Code Search and Zoekt. Every query finds every matching occurrence, not just definitions or references.
How trigram search works
Section titled “How trigram search works”A trigram is a sequence of three consecutive characters. When Reflex indexes your code, it extracts every trigram from every line of every file and builds an inverted index mapping each trigram to the lines (grouped by file) that contain it.
For a query like handleRequest:
- Reflex extracts trigrams:
han,and,ndl,dle,leR,eRe,Req,equ,que,ues,est - Looks up each trigram’s posting list in the memory-mapped index
- Intersects the lists (AND operation) — only lines containing all trigrams survive as candidates
- Verifies just those candidate lines, in parallel, against the actual query
Because only candidate lines are ever compared against the query, most searches finish in single-digit milliseconds regardless of how much code doesn’t match.
Basic queries
Section titled “Basic queries”# Simple text searchrfx query "handleRequest"
# Case-sensitive by defaultrfx query "HandleRequest" # different from above
# Ignore case (like rg -i)rfx query "handlerequest" -i
# Phrases with spacesrfx query "fn hash_content"Whole identifiers by default
Section titled “Whole identifiers by default”A plain query matches whole identifiers: Error finds Error but not NetworkError, and test finds test but not test_helper. Use --contains for substring matching:
rfx query "hash_conten" # 0 results — not a whole identifierrfx query "hash_conten" --contains # matches hash_content, ...When a whole-identifier search finds nothing but substring matches exist, Reflex tells you:
Hint: 0 whole-identifier matches; 9 substring matches — pass contains:true (--contains on the CLI) to see them. Reflex matches whole identifiers by default, so "hash_conten" does not match longer names that merely contain it.No results found (searched in 6ms).-i / --ignore-case works with the default whole-identifier search, with --contains and with --regex. The literals are still looked up in the trigram index under every case variant, so a case-insensitive query costs about the same as a case-sensitive one.
A pattern containing brackets (such as unwrap()) can never match as a whole identifier, so Reflex searches it as an escaped regex (substring semantics) and prints a warning on stderr. Patterns that start with - need --pattern or --:
rfx query --pattern "-> Result<"rfx query -- "-> Result<"What gets searched
Section titled “What gets searched”By default Reflex indexes the same files ripgrep would search: everything not excluded by .gitignore, .ignore or .rgignore, skipping binary files and dot-directories. Each file lands in one of these tiers:
| Tier | Examples | Searched by default |
|---|---|---|
| Code | .rs, .ts, .py, … (15 parsed languages) | Yes |
| Text | Markdown, YAML, TOML, JSON, Makefile, Dockerfile, … | Yes |
| Lock | Cargo.lock, package-lock.json, *.lock, go.sum | No — pass --include-locks |
| Generated | *.pb.go, *.min.js, *.map, *_generated.* | No — pass --include-generated |
# Only documentation and config filesrfx query "timeout" --lang text
# Which lockfile pins this package?rfx query "serde" --include-locksrfx query "serde" --lang lock # lock files onlyIf a query misses only because the matches live in lock or generated files, Reflex prints a hint pointing at these flags.
Filtering results
Section titled “Filtering results”By language
Section titled “By language”rfx query "import" --lang typescriptrfx query "use" --lang rustBy file path
Section titled “By file path”--file does simple substring matching on the path:
rfx query "TODO" --file src/cli/--glob and --exclude follow gitignore rules (like rg -g) and can be repeated:
rfx query "TODO" --glob "src/**/*.rs" # .rs files anywhere under the root src/rfx query "TODO" --glob "src/*.rs" # only files directly in src/rfx query "TODO" --glob "**/src/**/*.rs" # under any src/ directoryrfx query "TODO" --exclude "tests/**"- A pattern containing
/is anchored at the index root, sosrc/**/*.rsdoes not matchvendor/src/x.rs. Write**/src/**/*.rsfor that. - A bare name (
*.rs,Makefile) matches at any depth. *never crosses/;**matches recursively.
Limit and paginate
Section titled “Limit and paginate”rfx query "error" --limit 20rfx query "error" --limit 20 --offset 20rfx query "error" --count # exact total only, no resultsWith a limit, Reflex stops verifying once the page is full and estimates the total:
Found 20 results (~383 total, estimated) in 6msUse --limit/--offset to paginate, or --count for the exact totalThe default limit is 100; --all removes it. Use -p / --paths to list only the unique matching file paths.
Timeout
Section titled “Timeout”Long-running queries can be capped (0 disables the timeout):
rfx query "TODO" --timeout 5The default timeout is 30 seconds.
Understanding results
Section titled “Understanding results”Results are grouped by file, sorted by path and then line number. Each match shows its line number followed by the matching line:
src/indexer.rs (6 matches) 635 let current_hash = self.hash_content(&bytes); ──────────────────────────────────────── 911 let hash = self.hash_content(&bytes); ──────────────────────────────────────── 2547 fn hash_content(&self, content: &[u8]) -> String {
Found 8 results in 6msIn a terminal the file header gets a 📁 icon and syntax highlighting; --plain turns both off. Long previews are truncated to about 100 characters — --no-truncate shows full lines. For scripts and agents, --json returns the same results with pagination and index freshness (status, can_trust_results); -C 3 adds three lines of context before and after each match to the JSON.
Performance characteristics
Section titled “Performance characteristics”On the Reflex repository itself (~300 files), a typical query takes 6–8 ms end to end. Published benchmarks:
| Measurement | Result |
|---|---|
Zero-hit search (30 MB corpus, median via rfx mcp) | 0.09 ms |
| Common word, first page (30 MB corpus) | ~3 ms |
Regex fn (get|set)_\w+ (30 MB corpus) | ~12 ms |
(?i)kubernetes --count on Kubernetes (27,448 files, 115,539 matches) | 55 ms engine time |
To be honest about the trade-off: on a warm mid-size repository, ripgrep still wins plain one-off scans by 6–10x. Reflex is built for what a linear scan can’t do — symbol and dependency queries, and “every occurrence” on very large trees.
Performance comes from:
- Trigram indexing — only candidate lines are ever verified, in parallel
- Memory-mapped I/O — the index opens in well under a millisecond and relies on the OS page cache
- Early stop — with a limit, verification stops once the page is full
- Content fingerprints — files are tracked by size, modification time and blake3 hash, so
rfx indexdetects an unchanged tree instantly and every query can report whether its results are fresh
Use --timing to see where the time goes (printed to stderr; with --json it becomes a timings object that also reports whether the index was used, index_path: "trigram", or every line was scanned, "scan"):
timing: open 0.35ms | candidates 0.03ms | verify 1.10ms | status wait 5.05ms (compute 6.14ms) | group 0.00ms | engine 6.53ms | wall 6.59msWhen to use full-text vs. other modes
Section titled “When to use full-text vs. other modes”| Use case | Mode |
|---|---|
| Find all occurrences of a string | Full-text search (this page) |
| Find only definitions (functions, classes) | Symbol search |
| Pattern matching with wildcards | Regex search |
| Structural code patterns | AST patterns |
Next steps
Section titled “Next steps”- Symbol Search — filter results to symbol definitions
- Regex & AST Patterns — pattern matching and structural queries
- CLI Commands — full
rfx queryreference