grep and ripgrep vs Semlith
Where grep and ripgrep beat an index, where they miss, and what changes for a coding agent. Runs on Flask and the SWE-bench Verified numbers.
Checked on 29 October 2026 with ripgrep 15.2.0 and semlith 0.50.0, on Flask 3.1.3 on an M1 MacBook Air.
What grep is good at
ripgrep needs no index and nothing running in the background. It is one binary, it reads the files as they are right now, and it answers in milliseconds. It is also how Claude Code finds code today (Claude Code doesn’t index your codebase).
When you know the name you want, nothing beats it.
git clone --depth 1 --branch 3.1.3 https://github.com/pallets/flask && cd flask
rg -n max_content_length srcsrc/flask/wrappers.py:55: _max_content_length: int | None = None
src/flask/wrappers.py:60: def max_content_length(self) -> int | None:
src/flask/wrappers.py:80: if self._max_content_length is not None:
src/flask/wrappers.py:81: return self._max_content_length
src/flask/wrappers.py:84: return super().max_content_length
src/flask/wrappers.py:88: @max_content_length.setter
src/flask/wrappers.py:89: def max_content_length(self, value: int | None) -> None:
src/flask/wrappers.py:90: self._max_content_length = valueEvery line is right. If your agent already knows the name, keep using rg.
Where it misses
Now ask for the same thing the way a person would, without the name. An agent turns the question into words and greps for them.
rg -n -i "upload|big" srcsrc/flask/cli.py:297: a bigger role. Typically it's created automatically by the
src/flask/helpers.py:542: @app.route("/uploads/<path:name>")
src/flask/helpers.py:545: app.config['UPLOAD_FOLDER'], name, as_attachment=True
src/flask/helpers.py:549: files or uploads. Uses :func:`~werkzeug.security.safe_join` toNone of these is the answer, because src/flask/wrappers.py never says “upload” or “big”. The agent tries more words and opens more files, and that is where the time and the tokens go. An index ranks by meaning as well as by words.
semlith index .indexed 211 files (2778 chunks, 5 images) in 84.3s — 0 already indexed, 4 skipped (4 empty), 0 removedsemlith search -k 5 "limit how big an uploaded request body can be"1. 0.087 vf src/flask/wrappers.py:59-71
2. 0.083 vf src/flask/wrappers.py:91-106
3. 0.077 v docs/web-security.rst:26-38
4. 0.076 vf docs/patterns/fileuploads.rst:135-153
5. 0.072 vf src/flask/wrappers.py:105-123
5 hits in 360.027667msThe first hit is the max_content_length property. Grep answered in no time and Semlith took about a third of a second, after an index that took 84 seconds to build the first time.
Where grep still wins here
Not every question goes Semlith’s way. Ask where Flask decides which error handler runs for an exception, and grep’s words land in the code that does it.
rg -n -i "error handler" srcsrc/flask/app.py:748: registered error handlers and fall back to returning the
src/flask/app.py:812: """Handle an exception that did not have an error handler
src/flask/app.py:813: associated with it, or that was raised from an error handler.
src/flask/app.py:823: If an error handler is registered for ``InternalServerError`` or
src/flask/app.py:881: return value of the view or error handler. This does not have to
src/flask/app.py:930: request dispatching as well as error handlers.
src/flask/sansio/scaffold.py:110: #: A data structure of registered error handlers, in the format
src/flask/sansio/scaffold.py:118: #: To register an error handler, use the :meth:`errorhandler`
src/flask/sansio/app.py:826: """Return a registered error handler for an exception in this order:
src/flask/sansio/app.py:855: If it returns ``True`` for any exception the error handler for thisLine 826 of src/flask/sansio/app.py is the docstring of the function that picks the handler. Semlith’s top five for the same question were all in the docs.
semlith search -k 5 "where does Flask decide which error handler runs for an exception"1. 0.090 vf docs/errorhandling.rst:157-169
2. 0.087 vf docs/errorhandling.rst:223-244
3. 0.079 vf docs/errorhandling.rst:243-263
4. 0.067 vf docs/lifecycle.rst:143-153
5. 0.062 v docs/errorhandling.rst:132-148Those pages explain the order Flask uses, so they answer the question, but they are not the code. When the question already uses the words the code uses, grep is hard to beat.
Measured on SWE-bench Verified
One question in each direction proves nothing, so we measured it. On SWE-bench Verified, 500 real GitHub issues, the issue text is the question and the answer is every file the fix edits. It measures retrieval only, not whether an agent then fixes the bug.
The file the fix edits is in Semlith’s top 10 for 76.8% of the issues, against 47.0% for grep then read. Grep then read is what an agent does without an index. It ranks the files one ripgrep pass hits and reads them whole, in that order. Before it reaches the first right file it goes through a median of 15,491 tokens, and Semlith 224.
In the scorecard’s tool comparison, every tool got the same checkout and issue text on 20 issues from SWE-bench Lite. There rg ran the issue’s words and names together and ranked files by how many lines matched. It had the right file in its top 5 for 15.0% of them, and Semlith for 55.0% of the same issues.
Grep can’t read inside PDF, Word or Excel files at all. On 127 questions about passages in 53 public documents, rg found 7 of 127 within 4k tokens, rga with document adapters found 34, and semlith_search found 116 of 127.
With an agent in the loop
Finding the file is not the whole job. On 50 questions about a codebase it had never seen, headless Claude Code on Opus answered 92.0% correctly with grep alone, and 92.0% with Semlith installed beside it. A strong agent with grep gets there on one repository. Where an index earns its place is a question asked in other words, many repositories at once, and documents grep can’t open.
Every figure here is on the benchmarks page, and the scorecard has the harness and the command behind each one.
Use both
Semlith doesn’t replace grep, and it isn’t meant to. Claude Code keeps its own grep and gains semlith_search beside it. Keep rg for names you know, and ask the index when you only know what the code does. Connect Claude Code shows the setup.
All comparisons
Semlith beside grep and ripgrep, Serena and claude-context. What each tool does well, runs you can repeat on Flask, and the published benchmark numbers.
Serena
Serena gives agents IDE-style symbol tools over language servers. Semlith finds code from a question. What each does, run on Flask and SWE-bench Lite.