Dev.to AI 🤖 Ai 👁 0 📖 9 min read

How a Coding Agent Finds Code: Grep Versus an Embedding Index

Engineer's view. This is the site search that returns nothing for "refund" because the code says "reverse charge". A coding agent searches your repo the same way. Grep needs the exact word, and each miss costs the agent

How a Coding Agent Finds Code: Grep Versus an Embedding Index

Engineer's view. This is the site search that returns nothing for "refund" because the code says "reverse charge". A coding agent searches your repo the same way. Grep needs the exact word, and each miss costs the agent another turn. An embedding index matches by meaning, so it catches the plain-English requests grep misses.

TL;DR. Your coding agent finds code by guessing words, and plain-English requests make it guess wrong more often. Give it the names your code uses, or a second search that matches by meaning.

Explain Like I'm 5

You ask a helper to fetch a book from a huge library. The helper can only read the words on the spines. So they guess a word and pull every book with that word on it.

If your word is on the spine, they find it fast. If you said "the one about sad dogs" and the spine says "Old Yeller", they guess again, and again. Each guess is another trip down the aisles.

A second helper has read every book. They walk straight to the one that is about what you meant. Sometimes they bring a book that only sounds right, so the best team uses both.

The Problem

You have shipped this bug before, in a system with no model in it.

Your store's search box returned nothing for "refund". The page existed. It was titled "Reverse a charge", because that is what the billing team called it. The search worked exactly as written. It matched words, and your customers did not use your words. You fixed it with a list of synonyms.

A coding agent searching your repo is that search box, run by a model. You type "refunding twice when the customer retries". Most terminal coding agents search with grep, the command that lists files containing a word. So the model has to guess a word that is in the file. If your code says reverse_charge, its first guess misses. It tries another word, and gets back 40 files that mention payments. It narrows, and tries again.

Every one of those guesses is a search round: one search, then the model reads the result. Each round resends the whole conversation, so a search that takes four rounds costs four times the reading. The worse case is quiet: the agent stops at a file that looks right, edits it, and you review the wrong change.

I replayed 400 real Django commits as requests to a scripted grep searcher. When the request named the code, grep found the file within three rounds 75% of the time. When it was plain English, 45%.

The fix: give the agent the names your code uses, and give it a second search that matches by meaning, then measure how many search rounds it spends before its first edit.

The Fix: Bridge the Words People Say to the Names in the Code

Why does grep miss a file that is right there?

Grep is exact. It answers "which files contain this string", and it never answers "which files are about this".

In Django, a request like "Made select_for_update() raise after combined queries" names the function. Grep on select_for_update returns a short list with the right file in it. One round.

A request like "Fixed time zone validation when the zone directory is missing" names nothing. The longest words, "validation" and "directory", appear in dozens of files. The scripted searcher needed 2.4 rounds on average when it succeeded. Within six rounds it still missed 45% of these requests.

The repo's size makes it worse. With three quarters of Django's files removed, grep found 59% of plain-English requests within three rounds. With all 873, 45%. More files means more files that share your words.

What does an embedding index do differently?

An embedding is a list of numbers that places a piece of text by its meaning. Texts about similar things get similar numbers. An embedding index splits every file into chunks, stores each chunk's embedding, and returns the chunks closest to the request. It needs no shared word. Cursor builds one when you open a project. Its own tests found agents answered 12.5% more questions correctly with it.

One request, two searches

So is the index simply better?

No. On the same 400 requests, a small general-purpose embedding model (all-MiniLM-L6-v2, top 10 files) found 87% of named-code requests and 85% of plain ones. That beats grep on both. But they miss different files. Together they found 94% and 87%.

Django, 400 requests: file found (%)

What This Means for You

When this matters. You use Claude Code, Codex, Cursor or Gemini CLI on a repo with hundreds of files or more. It matters most when the names in the code differ from the words your team uses: old names, abbreviations, a domain the code calls something else. On a 50-file project, grep finds almost everything and none of this is worth your time.

How it affects you. You see it as an agent that runs five searches and reads ten files before it changes anything. You pay for that in time and tokens. You also see it as a confident edit in the wrong file, because the agent stopped at the first file that looked right. Requests that name a function or paste an error string are the easy case. Your plain-English feature requests are the hard one.

What to do about it. First, with no setup: name the code in your request when you know it. "Fix the double refund in reverse_charge" is a one-round search. "Fix the double refund" may take four.

Next, write down the names your team uses for things in the file your agent reads at the start of every session. A line mapping "refund" to reverse_charge turns a missed search into a first-round hit.

Then measure it. Count the search calls your agent makes before its first edit, and add a meaning-based search where the count stays high. The code for all three steps is in ## Implementing It.

New to embeddings? Start with Embeddings. Why each extra search costs more than it looks: How Agent Context Gets Re-Read Every Turn.

Implementing It

The change.

Three roles change: you, measuring your own sessions; the repo, carrying a glossary; and the tool, getting a second search.

Role 1, you: count the search rounds. Claude Code saves each session as a JSON-lines file under ~/.claude/projects/<project>/. Each tool call is a tool_use block inside an assistant message. Count the searches before the first edit:

import json, sys
SEARCH = {"Grep", "Glob"}                  # plus shell grep, rg, find
EDIT = {"Edit", "Write", "MultiEdit"}

def searches_before_first_edit(path):
    n = 0
    for line in open(path):
        msg = json.loads(line).get("message") or {}
        for b in msg.get("content") if isinstance(msg.get("content"), list) else []:
            if b.get("type") != "tool_use":
                continue
            cmd = str(b.get("input", {}).get("command", ""))
            if b["name"] in EDIT:
                return n
            if b["name"] in SEARCH or cmd.startswith(("grep", "rg ", "find ", "git grep")):
                n += 1
    return n

print(searches_before_first_edit(sys.argv[1]))

Run it over last week's sessions and sort. Sessions above five are your misses. Read their first user message. It is usually a plain-English request about a part of the code with unusual names.

Role 2, the repo: a glossary the agent reads. Claude Code reads CLAUDE.md at the start of every session. Codex and Cursor read AGENTS.md. Add the words your team says and the names the code uses:

## Where things live (what we say -> what the code says)
- refund -> `reverse_charge` in billing/charges.py
- seat, license -> `Entitlement` in accounts/entitlements.py
- the nightly job -> `reconcile_ledger` in jobs/reconcile.py

Keep it to the words that miss. Every line costs tokens on every turn. Do not list what grep already finds.

Role 3, the tool: add a search by meaning. Cursor already has one. For Claude Code, an MCP server can add one. Zilliz's open-source claude-context gives the agent a search_code tool that runs keyword and embedding search together:

claude mcp add claude-context \
  -e OPENAI_API_KEY=sk-... -e MILVUS_TOKEN=... \
  -- npx @zilliz/claude-context-mcp@latest

If you build your own search tool, merge the two result lists by rank rather than by score. This is called rank fusion. Grep has no score, and an embedding's distance does not compare with a keyword count. So each file earns points for how high it sits in each list:

from collections import defaultdict

def merge_results(grep_files, index_files, k=60):
    points = defaultdict(float)
    for files in (grep_files, index_files):
        for position, path in enumerate(files, start=1):
            points[path] += 1 / (k + position)
    return sorted(points, key=points.get, reverse=True)[:10]   # what the agent opens

The constant k keeps one list's first place from drowning out everything else. Sixty is the usual default. Return ten files, not fifty. A long list is just another round of reading.

code_example.py replays your own repo's git history against a scripted grep searcher, with and without a glossary.

How you know it worked.

The median of searches_before_first_edit should fall after the glossary lands. Rerun code_example.py with REPO set to your checkout. The plain-words "within 3 rounds" rate should rise for the requests your glossary covers. If it does not move, the glossary names words nobody types. Use the words from your slow sessions instead.

When an Embedding Index Is the Wrong Tool

When the repo is small. Under a few hundred files, grep finds most things in a round or two. An index adds a sync job and a dependency for little gain.

When your code cannot leave the machine. claude-context sends chunks to OpenAI for embedding and stores the vectors in Zilliz Cloud. Cursor computes embeddings on its servers. Check that against your security policy before you turn either on.

When the request already names the code. A stack trace, an error string or a function name is grep's best case. Grep is exact and always current, and an index can lag your last edit.

When the index is stale. A search that returns last week's version of a file is worse than no result. Cursor syncs changed files with a hash tree. A tool you run yourself needs its own re-index step.

Three questions before you add one:

  • How many search calls do your agent's sessions make before the first edit?
  • Do your team's words for things match the names in the code?
  • May your source code be sent to an embedding provider?

Glossary

  • grep — the command that lists every file containing an exact word or pattern.
  • embedding — a list of numbers that places text by its meaning, so similar texts sit close together.
  • embedding index — every file's chunks stored as embeddings, searched for the ones closest to a request.
  • search round — one search call and the agent reading its result; each round resends the conversation.
  • glossary — lines in CLAUDE.md or AGENTS.md mapping your team's words to the names in the code.
  • rank fusion — merging result lists by each item's position rather than by incomparable scores.

Further reading

Originally published at theaicommit.com, a daily AI engineering lab for software engineers. The original has an interactive version you can play with, and the full runnable code is on GitHub.

📰 Read the original article on Dev.to AI

Originally published by Dev.to AI. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.