☰
fsearch内容索引深入解析:三元组倒排索引与LSM式分段,如何做到搜索结果永不过期
2026/10/10 14:24:35 网站建设 项目流程

【免费下载链接】fsearch

Whole-disk file search for macOS: fuzzy names, typo tolerance, indexed content grep. ~1 ms over 8M files.

项目地址:https://gitcode.com/gh_mirrors/fsea/fsearch
点击查看免费下载

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 同款):

  1. 不可变分段:索引数据按文件写入只读文件seg-000001.fsc、seg-000002.fsc……,启动后直接mmap映射进内存(content.rs#L203-L219)。旧分段永远不改写,新变化只追加新分段。
  2. 删除用墓碑标记:文件被删或改动,不去动旧分段,而是在该分段的dead位图上置一个 bit(content.rs#L127-L135)。
  3. 分层合并:每次增量更新生成的小分段,会按"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,而不是重新扫描:

  1. 名字索引(FSEvents 实时维护,见 src/live.rs)知道每个目录"应该"索引哪些文件;
  2. 内容索引按size + mtime与手头文档比对:变了/没了的打墓碑,缺失的加入重建列表(content.rs#L717-L746 的diff);
  3. 只有 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.

项目地址:https://gitcode.com/gh_mirrors/fsea/fsearch
点击查看免费下载

相关推荐

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询