编程 tgrep:trigram 索引 + 常驻服务做仓库正则搜索,以及三个会让文件被静默剔除的坑

2026-09-11 23:48:38

tgrep:trigram 索引 + 常驻服务做仓库正则搜索,以及三个会让文件被静默剔除的坑

tgrep 是一个带客户端/服务端架构的 trigram 索引 grep,目标是在大代码仓库里做快速正则搜索。它已经被集成进 GitHub Copilot CLI,用来支撑大仓库上的 grep 搜索。

为什么需要它

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

平均单次查询延迟,索引已预先构建:

RepoFilesPlatformripgreptgrepSpeedup
gecko-dev388KmacOS arm6433402ms643ms51.9x
gecko-dev388KWindows17841ms463ms38.6x
gecko-dev388KLinux1195ms162ms7.36x
chromium504KmacOS arm6441806ms2643ms15.8x
chromium504KWindows24576ms1396ms17.6x
chromium504KLinux2404ms631ms3.81x
go16KWindows592ms79ms7.53x
rust62KWindows1489ms194ms7.69x
kubernetes31KWindows1342ms190ms7.08x
linux96KmacOS arm645390ms256ms21.0x
linux96KWindows3280ms94ms34.8x
linux96KLinux427ms46ms9.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 会照常应用规则,在 indexservesearch 上行为一致。

大小写不敏感的仓库

在 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 indextgrep 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 忽略大小写,-S smart-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 等。

推荐文章

程序员茄子在线接单