DeepSeek Harness Hub
← 返回列表

Owen718/snapgrep

DeepSeek Harnessspec-screened在 GitHub 查看 ↗
✓ 可直接安装

一个进程内 trigram 索引,让 Pi 中的代码搜索

自动检查通过:npm 包已发布且 engines 声明满足基线(声明 Node >=22.19.0);该结论来自程序自动检查,未经人工实机验证。 · 最近上游提交 2026/8/15 · 已提供中文文档

一个进程内三元组索引,使 Pi&DSH 中的代码搜索比 ripgrep 快 20-70 倍,结果完全相同,且无需边车进程。

综合分
35
GitHub 分
35
用户评分
★ Stars
10
周下载量
安装插件(需先安装 dsh CLI 引擎:npm install -g @deepseek-ai/dsh)
dsh plugin --profile web add snapgrep
npm 包 snapgrep 已校验归属本仓库,走 npm 安装最省事
数据截至 2026/9/17(元数据每日更新 · 实装验证按队列轮转,单条结论的验证时间见上方)
安装兼容性检查✓ 自动检查通过

以下结论由程序自动检查 npm 包、engines 声明与入口文件得出,未做人工实机验证——能装不等于用着没问题。

npm 包snapgrep @ 0.1.1
Node 引擎要求 >=22.19.0 · 基线 Node 22.19 满足
dsh CLI 依赖未声明 dsh 版本约束
入口文件main/exports/bin 已声明

验证方式:npm registry 存在性 + package.json 静态校验 · 最后验证 2026/9/18 02:58:09

依赖的 DSH / Cordis 模块
@deepseek-ai/cordis@deepseek-ai/dsh-tools@deepseek-ai/schemastery
用户评分
还没有人投票,来当第一个
订阅周报,不错过优质插件更新
每周一封 · 高评分插件 + 新用户活动

README

snapgrep

一个进程内 trigram 索引,让 Pi 中的代码搜索
在热索引上通常比 ripgrep 快 40–70 倍——
并且每个结果都会与 ripgrep 逐字节核对。

没有边车进程。没有守护进程。只有一个 3.2 MB 的原生插件加载在 agent 进程内部。

安装

Pi

npm install -g snapgrep

Pi 内置的 grep 会在每个项目中自动被替换。要确认这一点,让 Pi 搜索一个你知道存在的字符串——工具详情会显示 actualBackend: kernel。

oh-my-pi (omp)

npm install -g snapgrep

同一个包,无需改动:omp 将 @earendil-works/pi-coding-agent 视为别名作用域,其加载器同时接受 .pi 目录和 .omp。

DeepSeek Harness (dsh)

dsh plugin --profile headless add snapgrep

用 dsh --profile headless --dump-config | grep snapgrep 验证。

它同时替换了 grep 和 glob:注册表会直接拒绝重复的工具名,因此内置的搜索行被禁用,由该插件提供两者。glob 运行的是内置版本所用的同一个 ripgrep 调用。

只会下载你的机器能够运行的那个插件——约 1.2 MB,两个包。不会编译任何东西,也不会启动任何守护进程。

为 macOS(Apple Silicon 和 Intel)、Linux(x64 和 arm64,glibc)以及 Windows x64 预构建。Alpine/musl 尚未构建;在不支持的平台上,该扩展会明确指出它找不到的确切文件,而不是静默失败。

其他安装方式——不用 npm、单个项目,或为每台机器准备一份副本

不用 npm,一个脚本会从最新发布版获取匹配的归档并安装到 ~/.pi/agent/extensions:

curl -fsSL https://raw.githubusercontent.com/Owen718/snapgrep/main/install.sh | sh

设置 PI_EXTENSIONS_DIR 可安装到其他位置。若想要一个在任何机器上都能用的目录,同一发布版中的 snapgrep-extension-all-platforms.tar.gz 包含全部五个插件,并在加载时挑选正确的那一个。

安装到单个项目而非全局:

git clone https://github.com/Owen718/snapgrep.git

mkdir -p /path/to/your-project/.pi/extensions
cp -R snapgrep/artifacts/pi-extension/pi-fast-grep /path/to/your-project/.pi/extensions/

cd /path/to/your-project
pi --approve

--approve 只在第一次需要,用于信任项目级扩展。该构件带有一个仅作用于其自身目录的 .gitignore,因此安装它不会让你的仓库变脏。

从源码构建:npm run build:kernel && npm run package:extension。

中文安装说明见 安装说明.md。

为什么会有这个项目

编码智能体需要不断搜索。在大型仓库中,每一次 grep 调用都意味着 ripgrep 要重新读取磁盘上的每一个字节——耗时数百毫秒,每分钟发生数次,永无止境。

基于索引的搜索可以解决这个问题,但通常的方案是使用边车守护进程(Zoekt,以及所有基于它构建的东西):需要启动、监管、保持同步,并在它出现偏差时进行调试的另一个进程。对于一个不断启动和停止的 CLI 智能体来说,这套机制太笨重了。

snapgrep 将整个索引放在智能体自己的进程内。Rust 在 git 快照上构建并查询三元组索引;Node 通过 N-API 调用它。从启动到首次查询大约需要半秒钟,而且索引比它所索引的代码还要小。

实测结果

所有数字均为在 3 次预热后、7 次实测迭代的 P50 值,内核与 ripgrep 在同一次运行中交替执行,使用相同的仓库快照、相同的查询参数和输出限制。

与 ripgrep 对比

| 查询 | snapgrep | ripgrep | 加速比 |
| --- | ---: | ---: | ---: |
| 稀有 token,4,000 个文件中 1 个匹配 | 0.120 ms | 230.8 ms | 1921× |
| 转义正则表达式,1,200 个匹配文件 | 3.188 ms | 220.2 ms | 69× |
| 17 MB 仓库中的 createServer | 2.065 ms | 147.7 ms | 72× |
| defineConfig,186 个候选文件 | 2.574 ms | 142.9 ms | 56× |
| import\.meta,538 个候选文件 | 4.179 ms | 156.5 ms | 37× |
| 路径过滤搜索 | 0.973 ms | 8.0 ms | 8× |

加速比消失的情况

加速来自于不读取文件,因此它取决于 ripgrep 原本需要打开多少个文件。把这个数字推到极限,优势就会消失。来自 Linux 上的一次独立运行,单核,20 MB / 5,001 个文件的仓库:

| 场景 | snapgrep | ripgrep | 加速比 |
| --- | ---: | ---: | ---: |
| 每个文件都匹配(5,001 个文件) | 6.98 ms | 18.5 ms | 2.6× |
| 300 个文件匹配 | 0.22 ms | 14.1 ms | 63.5× |
| 一个文件匹配(稀有 token) | 0.006 ms | 14.2 ms | 2458× |

当每个文件都匹配时,索引没有什么可以跳过的,它最多只能赢得 2.6×。 对于 JavaScript 仓库中像 function 这样的查询,这就是需要围绕其做规划的数值。四位数的数字是另一个极端:ripgrep 必须扫描整棵树才能证明某个 token 只出现一次,而索引可以从倒排列表直接给出答案。

大多数真实的智能体搜索处于中间地带——某个符号名出现在几十到几百个文件中。

与 Zoekt 对比

Zoekt 是参考级的基于索引的引擎。snapgrep 从其索引中服务的每个查询都至少快两倍,这是在同一次运行中与 Zoekt 对比测得的:

| 查询 | snapgrep | Zoekt | 比率 |
| --- | ---: | ---: | ---: |
| Glob 过滤 .yaml,800 个文件 | 1.533 ms | 70.4 ms | 0.022 |
| 不区分大小写,500 个文件 | 3.003 ms | 51.8 ms | 0.058 |
| Glob 过滤 .ts | 3.053 ms | 14.8 ms | 0.206 |
| 不区分大小写 defineConfig | 4.806 ms | 13.8 ms | 0.347 |
Zoekt 的单次查询开销主要由 HTTP 传输、JSON 解码以及跨进程边界的重新验证占据。移除进程边界即可消除这三者。

占用情况

| | 合成 17.0 MB 语料库 | 真实 17.4 MB 仓库 |
| --- | ---: | ---: |
| 索引大小 | 6.5 MB(源文件的 0.38 倍) | 15.9 MB(源文件的 0.91 倍) |
| 冷启动到首次查询 | 662 ms | 508 ms |
| 常驻进程 | 0 | 0 |

为什么要自研内核

不是因为 trigram 算法有多新颖。它并不新颖——Zoekt 在这方面已经做得很好很多年了,而本项目在每一次被接受的变更上都以 Zoekt 为基准来衡量自己。

原因在于,购买索引就意味着购买它的进程边界,而边界的开销比搜索本身更大。以下是 Zoekt 在 vite 语料库上最慢的查询实际花费其 18.32 ms 的地方:

| 阶段 | 时间 | 占比 |
| --- | ---: | ---: |
| Zoekt 自身的搜索 | 1.24 ms | 7% |
| HTTP 传输 + JSON 解码 | 3.58 ms | 20% |
| 验证(主要是 rg 进程启动) | 8.34 ms | 45% |
| 本地合并与分类 | 5.14 ms | 28% |

搜索本身只占账单的 7%。 其余 93% 是索引存放在别处所带来的成本:序列化查询、跨越套接字、解码 JSON,然后启动 ripgrep 重新读取索引本已驻留在内存中的文件。在这台机器上,单次 ripgrep 启动就有约 6 ms 的下限——仅这一项就超过了整个搜索。

拥有内核正是让那 93% 消失的原因:

- 索引被映射进 agent 自己的内存。 没有套接字,没有 JSON,没有序列化。一次查询就是一次函数调用。
- 验证读取的是 mmap 的索引,而不是磁盘。 候选结果在进程内针对已经驻留的字节进行确认,因此没有进程启动,也没有第二次读取。
- 不支持的查询可以失败关闭,而不是近似处理。 这一点不是性能论据,而且它正是通用库不够用的原因。早先的一次尝试使用现成的文件查找器进行候选选择;在 Linux 内核代码树上,它返回了 17,767 个文件,而 ripgrep 找到了 17,772 个。缺少的五个文件来自嵌套的 .gitignore 重新包含规则——而最后一遍 ripgrep 只能移除多余结果,永远无法恢复候选阶段已经丢弃的结果。当召回边界是别人的实现细节时,你既无法证明它,也无法修复它。
- 索引格式针对这种数据形态进行了调优——压缩的内容块、delta-varint 的 gram 元数据——这正是索引大小落在其所索引源文件 0.38–0.91 倍的原因。

这个取舍是真实存在的,值得说明:每个平台需要构建和发布一个 .node,并且每一种 ripgrep 行为都必须重新证明,而不是继承。二十四个基准查询中有二十个如今由索引提供结果;另外四个触及 ripgrep 的二进制文件和 NUL 字节输出语义,这些尚未被复刻,因此它们会回退而不是猜测。

正确性优先
每个结果集都会在同一快照上使用相同标志与 ripgrep 进行核对。标准是 missing = 0 && extra = 0 —— 并且顺序、字节偏移、上下文行和截断行为也完全一致。

全部 24 个一致性查询都与 ripgrep 完全匹配。 其中 20 个由索引直接回答;另外 4 个回退到 ripgrep,且比较覆盖了这两条路径。这一点在每一个被接受的变更上都始终成立。

任何索引无法正确处理的内容都会回退到完整的 ripgrep 搜索,而不是返回部分答案。回退会在结果元数据中附带原因报告,因此不受支持的查询绝不会看起来像空结果。

目前会回退的情况:

- 适用 ripgrep 的 NUL 字节输出语义的二进制文件
- 正则与路径或 glob 过滤器组合
- 短于 3 字节的字面量,或跨换行符的字面量
- 使用非 ASCII 模式的大小写不敏感搜索
- 已证实子集之外的 glob 语法(!、?、[]、{})

这些是正确性边界,而不是缺失的功能。每一种都是尚未证明能够匹配 ripgrep 精确行为的情况,因此索引拒绝猜测。

保持新鲜

索引只有在反映你刚刚编辑的内容时才有用。

在任何可能修改文件的工具运行之前,进行中的搜索会被排空,索引会失效。工具完成后,索引会根据当前工作树重建。因此,搜索只能观察到完整状态——绝不会是编辑中途的撕裂状态。

在一个 300 文件仓库上测得的恢复时间:从编辑到搜索再次在索引上运行,为 41–62 ms。连续两次 edit 调用和一次 bash 调用之后,每次搜索都回到索引并完全匹配 ripgrep。

当重建正在进行时,搜索会回退到 ripgrep。正确,只是没有加速。

工作原理

Pi (Node)                         snapgrep (Rust, same process)
┌────────────────┐   N-API     ┌──────────────────────────────┐
│ grep tool      │ ──────────► │ trigram index over a git     │
│                │ ◄────────── │ snapshot, mmap'd             │
└───────┬────────┘             └──────────────┬───────────────┘
│                                     │ candidates
│ unsupported query,                  ▼
│ or index rebuilding      ┌────────────────────────┐
▼                          │ in-process exact verify│
ripgrep (full search)           └────────────────────────┘

1. 将模式拆分为 trigram,并对其倒排列表求交集,以获得候选文件。
2. 在读取任何文件之前,在候选级别应用隐藏 / 路径 / glob / 大小写过滤器。
3. 在 mmap 的索引上,在进程内进行精确验证——没有子进程,也不从磁盘重新读取。
4. 仅物化请求的限制和上下文实际需要的行。

索引是每个仓库一个文件,基于干净的 git 快照构建,内容块经过压缩,gram 元数据使用 delta-varint 编码。

开发

./scripts/bootstrap.sh      # 固定依赖、构建、运行测试套件
npm run check               # 类型检查
npm test                    # 单元测试和集成测试
npm run test:kernel         # 针对真实 addon 的原生测试
npm run benchmark           # 正确性和性能测试框架

需要 Node 22.19+、Rust,以及 PATH 中的 ripgrep。

最近接受的轮次中的基准测试结果产物位于 artifacts/results/,包括正确性运行、同一轮 A/B 无回归测量,以及上文引用的交付就绪数字。

方法说明

这个项目通过惨痛教训学到的两条测量规则,都值得借鉴:

绝不要与早先运行中记录的 P50 进行比较。 跨会话重新测量同一份未更改的代码时,结果漂移了 −31% 到 +9%——足以制造出并不存在的回归,或掩盖真实存在的回归。每次无回归检查都在同一进程批次中运行两个版本,以 ABAB 交替进行,每个版本都有自己的预热。

对于不做任何工作的查询,5% 阈值毫无意义。 对于零候选文件的搜索,5% 大约是 4 微秒——低于计时噪声。将两个字节完全相同的构建相互运行,清楚显示了每一类查询携带了多少噪声,而阈值正是根据该测量设定的:对近乎零工作的查询使用绝对微秒界限,对其他所有情况使用相对的 5% 界限。那次校准立即捕捉到了一个真实的 5 微秒回归,其原因是一个新增的每实例属性改变了 V8 的对象布局。

许可证

MIT

上游仓库有新提交时邮件通知你(每天最多一封,无更新不打扰),随时一键退订。

💬 加入 DPharness 群聊

插件用法、部署报错、新插件第一时间同步——群里问,比一个人翻文档快。

点击加入 QQ 群
DPharness 群聊二维码,手机 QQ 扫码进群
扫码进群