tgrep:trigram 索引 + 常驻服务做仓库正则搜索,以及三个会让文件被静默剔除的坑
tgrep 是一个带客户端/服务端架构的 trigram 索引 grep,目标是在大代码仓库里做快速正则搜索。它已经被集成进 GitHub Copilot CLI,用来支撑大仓库上的 grep 搜索。
- 仓库:
- 给 AI 编码工具用:见 AGENTS.md
- 完整 benchmark:BENCHMARKS.md
为什么需要它
grep、ripgrep 这类工具的每次查询都要扫全部文件,复杂度是 O(总字节数)。在 10 万文件以上的 monorepo 里这个开销很难受。tgrep 预先构建 trigram 索引,查询只触碰可能命中的那一小部分文件。服务起一次,之后查询就是即时的。
tgrep index . # build the trigram index
tgrep serve . # start server (watches for file changes)
tgrep "fn main" . # instant — auto-connects to running server
Benchmark
平均单次查询延迟,索引已预先构建:
| Repo | Files | Platform | ripgrep | tgrep | Speedup |
|---|---|---|---|---|---|
| gecko-dev | 388K | macOS arm64 | 33402ms | 643ms | 51.9x |
| gecko-dev | 388K | Windows | 17841ms | 463ms | 38.6x |
| gecko-dev | 388K | Linux | 1195ms | 162ms | 7.36x |
| chromium | 504K | macOS arm64 | 41806ms | 2643ms | 15.8x |
| chromium | 504K | Windows | 24576ms | 1396ms | 17.6x |
| chromium | 504K | Linux | 2404ms | 631ms | 3.81x |
| go | 16K | Windows | 592ms | 79ms | 7.53x |
| rust | 62K | Windows | 1489ms | 194ms | 7.69x |
| kubernetes | 31K | Windows | 1342ms | 190ms | 7.08x |
| linux | 96K | macOS arm64 | 5390ms | 256ms | 21.0x |
| linux | 96K | Windows | 3280ms | 94ms | 34.8x |
| linux | 96K | Linux | 427ms | 46ms | 9.38x |
测得的 18 个格子里 tgrep 赢下 17 个,唯一的例外是 Linux 上的 Kubernetes,0.93x 接近平手。差距大小取决于仓库规模,也取决于查询返回多少匹配——返回数万条匹配的搜索,花在“把结果送出去”上的时间会超过索引省下的查找时间。
架构
tgrep
---TCP---> tgrep serve (multi-client)
HybridIndex
├── IndexReader (mmap disk)
└── LiveIndex (in-memory overlay)
Periodic Flush
File Watcher (notify)
Background Indexer (rayon parallel)
- IndexReader — mmap 映射的磁盘索引,零拷贝,在有序的 trigram 查找表上做二分。
- LiveIndex — 内存覆盖层,存放服务启动后被修改的文件,以及后台索引器正在构建的文件。
- HybridIndex — 合并两层,覆盖层优先。
- Background Indexer — 以每批 1,024 个文件并行构建索引;冷启动期间先以空索引对外服务,直到第一批构建发布;恢复部分索引时按 500 文件为一批处理。
- Periodic Flush — 每 50K 文件或每 5 分钟把内存索引刷到磁盘并切换 reader,让内存占用保持有界。
- 自动刷新 — 原生 notify 订阅实时更新 LiveIndex;预算不足或注册失败时切换到轮询。
- Filename Index —
--files会把内容索引里的路径与一个紧凑 sidecar 求并集。 - TCP Server — 换行分隔 TCP 上的 JSON-RPC 2.0;每个连接一个线程,支持多客户端同时连接。
- File Cache — 50K 条目的内容缓存,用 RwLock 做无锁读。
性能相关的设计
- 并行搜索 — 候选文件用 rayon 并行搜索。
- 快速查询规划 — 有序 posting list 直接求交/求并,不做重新排序。
- 智能文件遍历 — 基于扩展名拒绝二进制文件(50+ 种格式)加 8KB 内容检查;索引和搜索两侧都有 64 MiB 的单文件大小上限(
--no-max-filesize可去掉)。 - 无锁读 —
RwLock缓存。 - 热服务 — 后台建索引期间查询立即可用。
用法
构建索引
tgrep index .
tgrep index /path/to/repo
tgrep index . --index-path /tmp/idx
tgrep index . --exclude vendor --exclude third_party
index 会报告耗时与峰值内存。注意峰值是 private/committed 字节,不是 resident set:索引单个 2 GiB 文件时保持 77.8 MiB 占用,而工作集达到 1.99 GiB。
没有 .git 目录的仓库
.gitignore 只在 git 仓库内生效(与 ripgrep 行为一致)。这点经常坑到索引 Perforce、Source Depot 或普通目录 enlistment 的人:根目录的 .gitignore 不会被任何东西读取,症状是索引比预期大得多。tgrep 会给出警告:
warning: /src/enlistment has a .gitignore but is not a git repository, so it is not applied (this matches ripgrep). Pass --no-require-git to apply it.
--no-require-git 会照常应用规则,在 index、serve、search 上行为一致。
大小写不敏感的仓库
在 Windows 上每次 clone,git 都会设置 core.ignorecase,匹配忽略规则时不再区分大小写。一条写成 QLogs 的规则会隐藏名为 qlogs 的目录。包括 ripgrep 在内的大多数工具始终按大小写敏感匹配忽略规则,于是那个目录照常被遍历/读取/索引,尽管 git status 从不提它。在一份 Windows enlistment 上,那是一个 13.4 GiB 的构建产物,占语料 71%,每次查询多花约 16 秒。tgrep 会读取 core.ignorecase,按仓库自身的方式匹配,git 跟踪的文件豁免在外。在这份 enlistment 上,遍历结果从比 git ls-files --cached --others --exclude-standard 多列一个文件,变成完全一致,代价是 293k 文件的遍历多花约 0.4 秒。--no-ignore 可以关掉这个行为。
index 与 serve 的筛选 flag 必须一致
决定哪些文件属于索引的 flag——--no-require-git、--no-ignore、--max-filesize、--exclude——在 tgrep index 与 tgrep serve 之间必须一致。服务在启动时会把索引与文件系统比对,一个在索引里但文件系统上看不到的文件会被当成已删除。所以拿一个没有大小上限的索引去配 --max-filesize 8M 的 serve,会把该索引里所有超过 8 MiB 的文件永久剔除。两侧默认都是 64 MiB,因此只有一侧显式写了限制时才会踩到,记得把同样的 flag 传给两边。
超大仓库的内存占用
构建默认走 --index-strategy=external,用外部归并排序把峰值内存压住。在 Linux kernel 上(94,634 个文件,990 MiB 索引):
- external(默认,64 MiB arena):31 个 spill segment,峰值工作集 160.1 MiB,22.6 s
- external
--index-buffer 16:122 个 segment,109.6 MiB,约 23 s memory:2.20–3.76 GiB,23–32 s
--index-strategy=memory 是给只读或索引卷足够大时的逃生口。tgrep serve 用的是同一个有界构建器(在 Linux kernel 上是 148.6 MiB 而不是 1.6 GiB,快 2.6 倍)。首次构建期间,服务从空索引回答查询。
启动服务
tgrep serve .
tgrep serve . --index-path /tmp/idx
tgrep serve . --watch-mode poll --poll-interval 60
tgrep serve . --watch-budget 4096
tgrep serve . --no-watch
tgrep serve . --exclude node_modules
首次构建期间查询会由空索引回答,也就是什么都不返回;tgrep status 会显示正在建索引。
相关 flag:
--max-memory默认 50% 内存(512 MB–16 GB)--max-cpu默认 50--auto-save-mutations默认 5000--watcher-queue-cap默认 16384
与文件系统保持同步
--watch-mode默认auto--poll-interval默认 120,范围 1–86400--watch-budget默认 8192--no-watch关闭
auto 模式下超出 watch 预算会把整个进程切到轮询,且重启前一直保持。默认预算 8192 是保守上限,不是对空闲容量的估算;Linux 上 inotify 配额与同用户的其它进程共享。原生安全对账大约每小时一次。--no-watch 仍然允许初始构建和启动时的对账。
搜索
tgrep "pattern" .
常用参数:
-i忽略大小写,-Ssmart-case,-F字面量-l只列文件名,-c计数,-o只输出匹配部分-w全词匹配,-v反向匹配-m 5每个文件最多 5 条-g "*.rs"glob,-t rust按类型,-e多条模式-A/-B/-C上下文--json输出 ripgrep 兼容格式,--vimgrep,--stats--no-index走暴力搜索,-U多行,-q静默--files-without-match、--files、--type-list
查看状态
tgrep status .
输出包括 PID、Port、Files、Trigrams、Cache、Watcher、Watch mode、Watch budget、Poll interval、Reconcile、Last successful reconcile、Indexing。
统计文件数
tgrep count-files .
计数打到 stdout(方便脚本处理),细节打到 stderr。
CLI 参数(部分)
-i、-s、-S、-F、-w、-v、-o、-e、-f、-U 等。