跳至主要内容
ripgrep 的取舍:不做索引,不给回溯

ripgrep 的取舍:不做索引,不给回溯

同事借我的终端跑了一句 rg TODO,回来问我:你们仓库怎么只有三个 TODO?

我说你加个 -u 再看看。他敲完,屏幕滚了两屏。

这不是 bug,也不是他没学会用工具。这是 ripgrep 明确做的一次权衡:默认把「搜不到」看得比「搜得全」更重要。

默认值替大多数人挡掉了噪音

ripgrep 启动时会读 .gitignore、.ignore、.rgignore,跳过隐藏文件,遇到二进制文件就停。三重过滤叠起来,一个正常的前端仓库能少扫掉九成以上的文件。

代价是真实的:第一次用的人会以为搜索坏了。我见过有人在 issue 里问为什么搜不到 .env,自己也踩过一次——排查构建问题时搜了半天没找到配置文件,因为它以点开头。

但这个取舍我认为是对的。搜索的信噪比不取决于工具,取决于仓库里垃圾文件的数量,而后者的量级通常是业务代码的一百倍。让工具默认站在干净那一侧,等于把「我要看脏数据」变成需要主动表达的动作。

我喜欢的是它的让步方式:作为可叠加的三层,每一层分量都不同,比一个 --all 开关更讲究。

BASH4 行
1
2
3
4
rg TODO            # 尊重 ignore、跳过隐藏、跳过二进制
rg -u TODO # 忽略 ignore 规则
rg -uu TODO # 再加上隐藏文件
rg -uuu TODO # 再加上二进制

每多敲一个 u,成本就多一分,收益也只多一层。这种梯度比一键全开的设计诚实:它逼我想清楚自己到底要找什么。找 .env 我只需要 -uu,把二进制文件也拖进来纯属浪费。

它砍掉了回溯正则,换一个能写进 SLA 的上界

ripgrep 用的正则引擎是 Rust 的 regex crate,核心保证是线性时间:匹配耗时与输入长度成正比,与模式复杂度无关。

这个保证不是白给的。它不支持反向引用和环视,(foo)\1 这种写法会直接报错,而不是悄悄跑得很慢。要用这些特性得加 -P 切到 PCRE2 引擎,同时把最坏情况的保证交出去。

在代码搜索这个场景里,我认为这笔交易非常划算。因为搜索的输入不是你精心构造的字符串,是别人仓库里的任意文本——压缩过的 JS、base64、一坨 minified 代码、几万行没换行的 JSON。回溯引擎在这类输入上不是慢一点,而是直接卡死。我在 CI 里跑过一次带 -P 的搜索,撞上打包产物,任务跑很久最后超时;具体多久记不清了,但那次之后我对回溯正则就有点戒备。

需要 -P 的时候也有,主要是解析日志:想用 (?<=...) 跳过前缀,或者用 \K。这种场景我会先把候选文件缩到几个,再开 PCRE2,不会拿它扫整个仓库。

不建索引,也不默认 mmap

这一条是我最欣赏的。

ctags、cscope、zoekt 走的是另一条路:先建索引,再查索引。快是真快,但索引是有状态的,你改了代码它不知道。于是「为什么搜不到」就有了第二个原因——第一个是 ignore 规则,第二个是索引过期。两个原因叠在一起,排查时得先分清是哪种。

ripgrep 选择每次从文件系统现读。冷启动慢一点,大仓库全量扫一遍会把 CPU 吃满,但「搜不到」只剩一种解释。我处理问题时很看重这一点:能少一个变量就少一个。

关于 mmap 也是个有意思的取舍。不少搜索工具默认用 mmap,看起来省掉一次数据拷贝。ripgrep 默认不开,原因大致是 mmap 把磁盘 I/O 变成了缺页中断,控制权交给内核,而文件如果在搜索过程中被其他进程截断,进程会直接收到 SIGBUS。另外在文件多而小的仓库里,mmap 的收益并不明显,反而多一层系统调用开销。

这一点我没做过严格测量,只是接受作者的判断——他在这类底层细节上的测量比我认真得多。真想试的话,--mmap 开着对比一下自己仓库里的耗时就行。

还有一条隐性的取舍:它不做交互界面。翻结果、预览、跳转,这些留给 fzf、telescope 或者编辑器插件去做。它只负责把匹配输出到 stdout,然后退出。这让我在管道里用它时几乎不用想格式问题。

我因此改掉的几个习惯

用久了之后,有些动作被它替换掉了:

列文件不再用 find,直接 rg --files -g '*.ts'。它同样尊重 ignore 规则,输出的就是「工具眼里的仓库」——做覆盖率统计或者批量脚本时特别有用,不用再手写排除 node_modules 的逻辑。

按类型搜比按 glob 搜顺手。rg -tpy 只搜 Python,rg -tjs 只搜 JS,不用记后缀的完整写法。要加自定义类型就 --type-add 'conf:*.{yaml,toml,ini}'。

字面量搜索加 -F。搜 foo( 这种带特殊字符的串时,-F 省掉一堆转义,也绕开了正则引擎。

在脚本里需要稳定输出时加 --sort path。默认是并行搜索,多个文件的结果顺序不确定——这是为吞吐量做的让步,人眼看无所谓,但要做 diff 或者写进测试快照就得排序,代价是丢掉并行。

CI 里我会用 rg --no-ignore --hidden -c 扫一遍,专门抓那些被 ignore 规则藏起来、但确实被提交了的密钥文件。这个用法有点反直觉:平时它帮我忽略,这里我让它别忽略。

下次在一个陌生仓库里 rg 不到东西,先别怀疑工具。敲一下 rg --files | wc -l,看看它眼里的世界有多大,再决定要不要加那个 u。

关于本文

由 Lu's Blog 撰写,采用 CC BY-NC 4.0 许可协议。

#代码搜索 #开源工具 #设计取舍

评论