Mac Anderson

Part 4: Deterministic retrieval

Why grep is the wrong retrieval engine

Searching is not understanding, and code already carries the structure you need.

Mac Anderson4 min read995 words2 sources cited
View markdown

You ask your agent to change a function signature, and it starts grepping. Grep answers one question: which lines contain this string? The questions the change actually turns on are structural. Who calls this function? What implements this interface? What breaks if I change the signature? Where does this value come from? Text search approximates those answers over many noisy round trips. A code graph returns them in one query.

Code is already a graph, so stop flattening it#

Compilers have modeled code as structure for decades: ASTs, symbol tables, call graphs, dependency graphs, cross-reference indexes. Your IDE resolves "go to definition" in milliseconds with that machinery, and no model is involved. The default agent loop discards all of it and hands the model a shell, which forces it to rebuild structural knowledge out of string matches:

# The grep loop (typical agent run, abbreviated):
grep -rn "process_refund" .        # 47 matches, 31 in tests and comments
cat billing/processors.py           # 800 lines into context
grep -rn "RefundProcessor" .        # which of these are real call sites?
cat api/handlers/refunds.py         # another 600 lines
# ... more calls, and the call graph is still not in context

# The graph query:
g.callers("process_refund", depth=2)         # ranked call sites, 0 tokens
g.slice(symbol="process_refund", budget=6000) # minimal closed subgraph

One hundred greps approximate what one graph traversal computes. The grep version is not only slower. Each round trip is a model call that re-sends the conversation history, and most of the lines it returns are irrelevant to the change, which part 2 showed is the kind of noise that costs accuracy rather than just tokens. It also gives the model one more chance to take a wrong turn. The graph version is deterministic: the same query against the same index returns the same answer.

The index stack#

  • AST layer (tree-sitter): language-aware parsing, with definitions, references, and signatures for most mainstream languages.
  • Symbol index: every definition and every reference site, linked in both directions, which is what the Language Server Protocol (LSP) and the SCIP Code Intelligence Protocol (SCIP) already standardize.
  • Dependency graph: imports, package boundaries, build targets.
  • Call graph: who invokes whom, with edge weights from reference counts or runtime traces.
  • Centrality ranking: PageRank over the reference graph tells you which symbols matter most for understanding any region of the code.
Evidence · Graph-ranked context in production tools

Aider's repo map is the clearest proof this works at tool scale. It parses the whole repository with tree-sitter into definitions and references, builds a graph where files are nodes and symbol references are edges, then runs a PageRank-style algorithm to pick the most-referenced identifiers that fit a fixed token budget (around 1k tokens for an entire repository by default). The model receives a compressed structural map, with key signatures and the most-referenced symbols, instead of raw file dumps. Aider's own benchmarking found that this improves code-editing performance on large repositories.12

AutoCodeRover (Zhang et al., NUS) made the same bet in research. It replaced plain text retrieval with AST-based search APIs (search-class, search-method, search-code-in-file), letting the model query program structure instead of strings, and reported improved issue-resolution efficacy on SWE-bench against text-based navigation.13

Aider, "Building a better repository map with tree-sitter" · Zhang et al., ISSTA 2024

Filesystem traversal against graph traversal#

The reason graphs win is precision per token. A filesystem is organized for humans and build tools, by directory rather than by meaning. The code relevant to one change is scattered across handlers, models, migrations, and tests, and filesystem traversal makes the agent page through that scatter linearly. Graph traversal follows edges of actual relationship, and a two-hop neighborhood around a symbol is close to the minimal sufficient context for editing it. You are not searching a haystack. You are following a wire.

Query the graph when

The question names a symbol and asks about relationships: callers, implementers, overrides, the blast radius of a signature change, or which tests reach this line.

Search text when

The target is a string rather than a symbol: a log message, an error copy string, a feature flag name, a config key, a TODO, or a value in a file no parser understands.

Do this week
  1. Index the repository with tree-sitter. Parse every file into definitions and references, and write them to a table keyed by symbol. The artifact is an index you can rebuild from a clean checkout in one command.
  2. Build the reference graph and rank it. Files or symbols are nodes, references are edges. Run PageRank over it and emit a repository map that fits a fixed token budget, then put that map in the cached prompt prefix.
  3. Expose three queries as tools. callers(symbol, depth), implementers(interface), and slice(symbol, budget). Each returns a ranked, budgeted block, not a file.
  4. Keep text search, and scope it. Leave a search tool for string literals and config values, and say so in its description, so the model reaches for the graph when the question is structural.
  5. Reindex on change. Update the index from the files a commit touched rather than rebuilding it, and record how old the index was at query time.
Measure it
  • index_staleness_seconds: age of the index at query time, at p99. Down is good. A stale index returns confident answers about code that moved.
  • context_precision: lines in the assembled context that the final diff touched, divided by lines sent. Up is good, and it is the number that tells you whether the two-hop neighborhood is the right radius.
  • graph_query_share: graph queries divided by all retrieval calls. Up is good while context_precision holds, and a fall usually means a query the graph cannot answer yet.
  • tokens_to_first_edit: input tokens spent between the prompt and the first diff. Down is good.
Takeaway

Grep ×100 approximates what one graph query computes. Build the graph once, deterministically, and let the model spend its tokens on the change instead of on reconstructing the compiler's knowledge by string matching.

Cite this

Anderson, M. (2026). Why grep is the wrong retrieval engine. In Engineering Deterministic AI Coding Agents (2nd ed., Part 4). Oxagen Inc. https://macanderson.com/manual/why-grep-is-the-wrong-retrieval-engine

BibTeX
@incollection{anderson2026whygrepis,
  author    = {Anderson, Mac},
  title     = {Why grep is the wrong retrieval engine},
  booktitle = {Engineering Deterministic AI Coding Agents},
  edition   = {Second},
  chapter   = {4},
  publisher = {Oxagen Inc.},
  address   = {Los Angeles, CA},
  year      = {2026},
  url       = {https://macanderson.com/manual/why-grep-is-the-wrong-retrieval-engine}
}

Updates by email

Get the next edition of the field manual and new research when it is published.

No spam. Unsubscribe any time. Read the privacy note.