【免费下载链接】fsearch
Whole-disk file search for macOS: fuzzy names, typo tolerance, indexed content grep. ~1 ms over 8M files.
fsearch 是一款面向 macOS 的全盘文件搜索工具:在 770 万个文件里按名字找文件只需约 1.3 毫秒,而它的核心难点——文件内容索引(全文搜索)——p50 也只要 9 毫秒。本文带你读懂 fsearch 内容索引的三大设计:三元组倒排索引、LSM 式不可变分段,以及"索引只挑候选、匹配现读磁盘"的机制,这正是它的内容搜索结果永不过期的秘密。
一、内容搜索为什么总是"又慢又旧"?🐌
传统方案只有两条路,各有硬伤:
| 方案 | 优点 | 硬伤 |
|---|---|---|
| 每次现爬磁盘 grep | 结果永远新鲜 | 太慢,百万文件搜一次几秒起步 |
| 缓存文件内容 | 查询快 | 文件一改,缓存就过期 |
fsearch 的取舍很聪明:索引里不存任何正文,只记录"哪个文件包含哪些字符三元组"。查询时先用倒排索引在几毫秒内圈定候选文件,再让候选文件从磁盘读最新内容做真实匹配。慢的部分(读文件)被并行化和时间预算控制,快部分(缩小范围)交给索引。
二、三元组倒排索引:把全文搜索变成查字典 🔍
2.1 什么是"三元组"(trigram)
把文本切成所有连续的 3 个字符(自动做大小写折叠,如Apply与apply等价)。任何出现在文件里的字符串,必然由文件中存在的三元组组成——所以"包含apply_dir"这个条件,等价于"三元组app、pp、pl…… 全都存在"。
2.2 倒排表如何工作
fsearch 为每段文本文件维护一张倒排表:三元组 → 包含它的文件列表(posting list)。三元组是 3 字符 × 26 字母折叠,整个空间只有 24 bit(约 1670 万个槽位),小到初建时可以直接做计数排序(见 content.rs#L470-L491 的 counting sort 分支)。
查询grep:apply_dir时,query 计划 把模式拆成一串AND连接的三元组查询,在各分段的 posting 列表上做交集——磁盘上 80% 以上与关键词无关的文件在这一步就被排除了,一个字节正文都没读。
fsearch 'ext:rs grep:apply_dir' # 在 .rs 文件内容里搜关键词 fsearch 'sym:apply_dir' # 只找"定义"它的地方几个值得了解的小设计:
- 符号索引复用同一张键表:
sym:把fn/def/class等定义关键词后跟的标识符哈希成高位键(content.rs#L292-L295),与三元组排在同一张倒排表里,一次检索同时覆盖文本搜索和"找定义"。 - 正则也能拆:正则表达式被解析成 AST,提取其中必然出现的字面片段和字符类,转成三元组的
AND/OR计划(content.rs#L1135-L1208),而不是放弃索引去全量扫描。 - posting 压缩:文件 id 列表用 delta-varint 存储;当某个三元组出现在超过 1/8 的文档里时,自动切换成 bitset(content.rs#L548-L574),两种编码空间都不浪费。
三、LSM 式分段:增量更新如何不打架 🧩
内容索引面对的现实是:你每保存一个文件,索引都要更新,但不能因此把整库重建一遍。fsearch 借用了数据库里 LSM 树的经典思路(LevelDB、RocksDB 同款):
- 不可变分段:索引数据按文件写入只读文件
seg-000001.fsc、seg-000002.fsc……,启动后直接mmap映射进内存(content.rs#L203-L219)。旧分段永远不改写,新变化只追加新分段。 - 删除用墓碑标记:文件被删或改动,不去动旧分段,而是在该分段的
dead位图上置一个 bit(content.rs#L127-L135)。 - 分层合并:每次增量更新生成的小分段,会按"posting 大小"的
log4层级归组——同一层攒满 8 个就合并成 1 个,且合并的瞬时内存被限制在 96MB 内(content.rs#L783-L799 的merge_plan,engine.rs#L457-L473 的合并循环)。这样增量更新永远不会堆积出几千个碎分段。
这套机制的副产品是天然安全:分段写完先落在.tmp再原子rename,进程中途崩溃最多丢一个没登记的分段,下次启动按 manifest 清理即可(content.rs#L684-L696)。
四、为什么搜索结果永不过期?⚡
这是全文里最反直觉的一点:倒排索引允许短暂"落后",但最终结果绝不落后。
你的查询 → 倒排索引圈出候选文件(可能落后最近几秒) → 对候选文件并行 open + 读取"此刻"的最新内容 → 用真正的正则/关键词匹配,按相关度返回索引只负责"别漏、别多",负责"内容对不对"的永远是磁盘上的文件本身。你在索引更新前的那一秒改了文件?没关系——匹配阶段读到的就是最新正文(content.rs 文件头注释 与 verify 函数 讲清了这一契约)。
两个工程细节让它又快又不失控:
- 候选按"你的文件 > 点目录 > 日志"的档位排序,最可能想要的先读;
- 默认 250ms 时间预算,读满
limit个文件或时间到就停(content.rs#L914-L932),所以偶发的慢结果也是"先给你最像的",而不是卡死。
五、增量同步:保存的文件约 2 秒后即可搜到 🕒
内容索引如何跟上磁盘变化?靠的是一次廉价的 diff,而不是重新扫描:
- 名字索引(FSEvents 实时维护,见 src/live.rs)知道每个目录"应该"索引哪些文件;
- 内容索引按
size + mtime与手头文档比对:变了/没了的打墓碑,缺失的加入重建列表(content.rs#L717-L746 的diff); - 只有 diff 出来的文件才读内容、建三元组。
节奏由防抖控制(engine.rs#L385-L479 的content_loop):一个目录最后一次改动后 2 秒才处理;而像state、*.log这种每秒都被应用改写的目录,兜底到5 分钟重建一次,而不是每个事件都索引一遍。所以体验上:你按Cmd+S保存的文件,约 2 秒后就能被grep:搜到;新文件在名字搜索里则约 0.1 秒出现。
六、实测效果与资源开销 📊
官方在 M4 Max、770 万文件/文件夹的磁盘上的数据(README.md):
| 指标 | 数值 |
|---|---|
| 按名字找文件(全盘) | p501.3 ms |
| 文件内容搜索 | p509 ms |
| 新增/改名/删除的文件可搜到 | ~0.1 s |
| 首次全盘爬取 | ~20 s(一次性) |
| 常驻内存 | 30–135 MB |
内容索引还主动做了瘦身:只收文本扩展名清单里的文件(TEXT_EXTS)、单文件不超过 1 MiB,并整体跳过node_modules、build、DerivedData、.git等生成物目录(SKIP_DIRS)——你的索引里都是"你自己的文件"。
七、如何上手与延伸阅读 🚀
构建与安装只需两步(macOS 下建议授予 Full Disk Access 以获得完整覆盖):
cargo build --release && ./target/release/fsearch install fsearch 'readme in:~/Developer' # 按名字找 fsearch 'type:image size:>5mb' # 按类型/大小过滤 fsearch 'ext:rs regex:fn\s+\w+_dir' # 内容正则搜索想继续深入源码,建议按这条路线读(均为仓库内相对路径):
- 倒排索引核心:src/content.rs —— 分段构建、合并、候选验证都在这个文件
- 引擎主循环与内容 worker:src/engine.rs
- 实时名字索引(基线 + 墓碑 + 覆盖层):src/live.rs
- 名字索引的扁平布局:src/index.rs
- 模糊匹配与查询解析:src/query.rs
- 基准测试方法与对比数据:demo/vs_fff.py、README.md
一句话总结:三元组倒排索引负责快,LSM 式分段负责更新不塌方,现读磁盘负责永不过期——三者叠加,fsearch 才敢在 770 万个文件上承诺 9 毫秒的内容搜索。
【免费下载链接】fsearch
Whole-disk file search for macOS: fuzzy names, typo tolerance, indexed content grep. ~1 ms over 8M files.
相关推荐
ice 索引段文件格式深度解析:从 footer 到倒排索引的 Bluge 磁盘布局
ice 索引段文件格式深度解析:从 footer 到倒排索引的 Bluge 磁盘布局 导读 ice 是 Bluge 全文搜索引擎的索引段(segment)文件格
后端即时通讯社交游戏开发Orama索引结构深度解析:从倒排索引到向量索引的完整存储设计指南
Orama索引结构深度解析:从倒排索引到向量索引的完整存储设计指南 Orama是一个强大的开源搜索引擎,支持全文搜索、向量搜索和混合搜索,其独特的索引结构设计使
向量数据库RAG如何用Julia构建高效搜索引擎:倒排索引与全文检索完整指南
如何用Julia构建高效搜索引擎:倒排索引与全文检索完整指南 Julia是一种高性能的编程语言,非常适合构建高效的搜索引擎。本文将为你详细介绍如何使用Julia
编程语言编译器语言运行时标准库JIT编译
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考