
别让 fsearch 扫全盘选择性索引三招索引更快内存更省【免费下载链接】fsearchWhole-disk file search for macOS: fuzzy names, typo tolerance, indexed content grep. ~1 ms over 8M files.项目地址: https://gitcode.com/gh_mirrors/fsea/fsearch一个号称全盘搜索、毫秒响应的工具默认行为却不是把整块磁盘上的每一个文件都纳入索引。fsearchmacOS 上的模糊文件名搜索 内容 grep基准数据见 README.md在 M4 Max、磁盘上有 770 万文件时按名称搜一个文件 p50 只需 1.3ms常驻内存 30–135MB。这个成绩一部分来自算法另一部分来自一个反直觉的设计索引范围本身就是可裁剪的。本文直接读仓库源码拆解 fsearch 让索引更少、更快、更省的三招——排除目录、白名单路径、定时更新窗口并给出可复制的场景配置模板与可验证的效果指标。为什么全盘索引不是默认最优fsearch 有两条索引线名称索引覆盖整块磁盘遍历 / 下所有条目内容索引trigram 文本索引则从设计上就只覆盖用户目录 $HOME 内的文本文件。全盘 ≠ 全部内容这个区分是第一层选择性。先看全盘方案的真实成本。名称索引首次建库要getattrlistbulk全盘爬一遍README 给出的实测是约 20 秒仅一次常驻守护进程内存 30–135MB7.7M 文件。内容索引是另一笔账它只收录文本文件超过 1MB 的文件直接拒收MAX_FILE 1 20见 src/content.rs没有扩展名的文件只有 ≤256KB 才收。即便如此重建内容索引时构建段、合并段都会产生瞬时内存峰值——src/main.rs 里甚至为此实现了自定义全局分配器超过 1MB 的大块分配直接走 mmap/munmap注释里写着macOS 的 malloc 会把已释放的大块保留为 dirty 映射一次构建后守护进程空占 ~1GB 而实际存活只有 ~2MB。隐私同样是一笔账。fsearch 判断是否具备全盘访问权靠的是能否打开系统的 TCC 数据库/Library/Application Support/com.apple.TCC/TCC.db没有 Full Disk Access 时它会主动跳过系统用授权弹窗保护的那批目录而不是挨个弹窗骚扰用户。具体名单见 src/engine.rs 的gated()const GATED_IN_HOME: [str] [ Desktop, Documents, Downloads, Library/Mobile Documents, Library/Containers, Library/Group Containers, Library/CloudStorage, Pictures/Photos Library.photoslibrary, ]; pub fn gated(home: str) - VecVecu8 { GATED_IN_HOME.iter().map(|d| format!({home}/{d}).into_bytes()).chain([b/Volumes.to_vec()]).collect() }另外无论是否全盘fsearch 都会在进程/线程层面设置 IOPOLno_materialize()src/engine.rs保证打开或列出 iCloud 的 dataless 文件时快速失败绝不触发占位文件下载——索引一切不能以把云端文件拉回本地为代价。所以全盘索引默认就不是最优它要付建库时间、常驻内存、隐私授权三份成本而这三份成本本都可以按需裁剪。第一招排除目录把永不需要的目录挡在扫描之外fsearch 的排除不是事后过滤而是连目录都不会打开。全局跳过表在 src/walk.rspub static SKIP: std::sync::OnceLockVecVecu8 std::sync::OnceLock::new(); pub fn blocked(path: [u8]) - bool { SKIP.get().is_some_and(|v| v.iter().any(|s| path.starts_with(s) (path.len() s.len() || path[s.len()] b/))) }blocked()做的是前缀 边界匹配/a会命中/a和/a/b但不会命中/abc且这一判断发生在所有 IO 之前——src/live.rs 的fetch()注释明确写在不许触碰的目录里连一次 lstat 都不做。触发排除有三种方式没有 Full Disk AccessEngine::start自动走gated()名单跳过上面那些弹窗目录和/Volumes。有 FDA 但显式收窄设置环境变量FSEARCH_RESTRICT即使具备全盘权限也只索引受保护目录之外的部分src/engine.rs 第 156 行None if has_full_disk_access() std::env::var_os(FSEARCH_RESTRICT).is_none() Vec::new()。内容索引的内置排除内容索引维护了三张跳过表src/content.rs——SKIP_DIRSnode_modules、.git、target、DerivedData、pycache、.venv、venv、site-packages、Pods、.next、.turbo、.cache、dist、build、vendor、.cargo、coverage、.Trash 等生成/厂商/缓存目录、SKIP_UNDER_HOMEgo/pkg、.cursor/extensions、.vscode/extensions、.local/share 等依赖与应用数据、SKIP_SUFFIXES.app、.photoslibrary、.library、.bundle、.framework 等包体。这些目录永远不进内容索引。效果是双向的爬得快少打开几百万个目录walk 实测 openclose 每目录约 19µs、内存少名称索引按目录块布局少一片子树就少一块 mmap 区间。而查询侧依旧完整排除的只是内容node_modules里的文件若按名字找依然能命中只是不再被 grep 全文索引覆盖。第二招白名单路径把查询压进目录范围排除目录管建库白名单管查询。fsearch 的名称索引有一个关键布局技巧src/index.rs条目按目录块 DFS 顺序排放每个目录的子节点连续、每个目录的整棵子树是单一区间dir_start..dir_end。于是in:过滤器在 src/query.rs 里被实现成区间裁剪而不是过滤pub fn scope_range(self, q: Query) - Option(usize, usize) { let idx self.live.base; let Some(scope) q.scope else { return Some((1, idx.n)) }; let e idx.lookup(scope)?; let d idx.dir_of(e)? as usize; Some((idx.dir_start()[d] as usize, idx.dir_end()[d] as usize)) }查询时把范围压到目标文件夹的子树区间扫描量从全盘条目数降到该区间内的条目数引擎注释里给出换算基准每次搜索扫描整个 overlay约每 10 万条目 1ms。索引条目越少、范围越窄扫描越快。典型用法fsearch readme in:~/Developer # 只搜 ~/Developer 子树 fsearch ext:rs in:~/code grep:apply_dir # 名字 内容双重收窄 fsearch type:image size:5mb mtime:7d # 媒体库按类型、体积、时间白名单内容侧同理in_scope()src/content.rs对 $HOME 做前缀判断内容索引只收 $HOME 内的文本而grep in:/etc这类索引覆盖不到的范围会退化为从名称索引挑候选文件、读盘验证的扫描路径scan_paths同样受in:范围约束、且只读 ≤1MB 的常规文件。白名单让全盘索引在每次查询时实际退化为目录范围索引。第三招定时更新窗口把实时改成够用索引不可能一直全量重建fsearch 用三条时间策略把更新成本压到最低增量跟随全盘只爬一次之后靠 FSEvents 以目录粒度增量更新重启时只回放自上次落盘以来变化的事件README.md、src/fsevents.rs。新建/重命名/删除的文件约 0.1 秒可见。内容索引的防抖窗口内容索引的同步循环src/engine.rscontent_loop对每个文件夹做防抖——安静 2 秒后处理或者一个文件夹 5 分钟不消停就强制处理一次你保存的文件约 2 秒内可搜每秒都在重写的应用状态、日志每 5 分钟才重索引一次而不是每个事件批次都索引。落盘节流名称索引的 overlay 累计超过 5 万条 pending或距上次落盘超过 12 小时才 compact 一次COMPACT_PENDING/COMPACT_EVERY约 1 秒 CPU ~280MB 写入繁忙磁盘上大约每小时一次。FSEvents 历史在重启时本来就要回放落盘太频繁反而浪费。这三条窗口共同构成定时更新的完整语义搜索永远打在不重扫的索引上新鲜度由事件流 防抖保证磁盘写入被批量节流。这也解释了 README 里那组数字改名后约 0.1 秒可见内容搜索 p50 9ms——实时性不是靠频繁全量重建而是靠精准的增量窗口。针对代码项目与媒体库的索引策略模板代码项目内容索引的SKIP_DIRS已经替你干掉了 node_modules/.git/target/build/dist/vendor/pycache/.venv/Pods 这类噪声源这也是与 fff 对比时 fsearch 内容搜索覆盖文件数反而少 9% 的原因——那些大多是依赖与构建产物。再配合白名单查询ext:rs in:~/code、sym:apply_dir符号索引日常定位符号与文件就在几百 KB 的区间内完成。若同时开多个语言项目且不想互相串扰可在启动守护进程时设置FSEARCH_RESTRICT收窄受保护目录配合in:按项目隔离。媒体库名称索引保留全部媒体条目类型表见 src/query.rsTYPES覆盖 image/video/audio 的全部常见扩展名但内容索引天然不收二进制。查询type:image size:5mb mtime:7d直接用大小与时间白名单圈定近期新增的大图索引体积与扫描量都只和媒体条目相关与磁盘上其他内容无关。隐私敏感机不给~/.local/bin/fsearch授权 Full Disk Accessfsearch 会自动跳过 Desktop/Documents/Downloads/Photos 等弹窗保护目录与 /Volumes配合FSEARCH_RESTRICT可进一步收窄。搜索结果里不会出现受保护目录也不会触发授权弹窗。效果验证选择性索引的收益如何量化仓库自带两组可复现的对比数据。其一是 README.md 的基准表M4 Max磁盘 7.7M 文件按名搜索 p50 1.3ms、内容搜索 p50 9ms、首次全盘爬取约 20 秒、常驻内存 30–135MB。其二是demo/下的实测在 Chromium 仓库509k 文件与 fff 对比完整过程可看演示视频 demo/fsearch-vs-fff.mp4、复现脚本 demo/vs_fff.py核心数据指标fsearchfff按名查找1.1 ms13.8 ms内容搜索5.6 ms53 ms就绪耗时50 ms2.5 s内存50 MB全盘358 MB单文件夹这组对比最能说明选择性索引的价值fff 只索引一个文件夹内存却是 fsearch 全盘索引的 7 倍——省内存靠的不是少索引而是索引结构本身名字驻留7.5M 条目共享约 2M 个不同名字每个名字只存一份并打上字符掩码查询先对名字打分再映射条目src/index.rs而 fsearch 因为跳过 build/vendor 等目录内容搜索需要读取的文件还更少。反之想要更快的扫描就把in:区间和排除表用起来——条目每少 10 万一次全扫描就少约 1ms 的基线开销见 src/engine.rs 注释。验证落地也简单fsearch status直接返回entries、index_bytes、content_docs、content_bytes、content_pending等指标src/engine.rsStatus配合fsearch bench query...src/main.rs对已落盘索引做 20 次中位耗时测试收窄前后各跑一次即可量化索引范围对查询耗时的真实影响——排除的目录越多、白名单越窄扫描的条目越少毫秒里抠出来的都是实打实的收益。把三招合起来看fsearch 的全盘搜索其实是一个分层的选择性系统名称索引全盘覆盖但按目录块组织让in:白名单变成 O(1) 的区间裁剪内容索引默认只碰 $HOME 的文本文件还内置排除生成/缓存/包体目录更新靠 FSEvents 增量 防抖 节流落盘三道窗口。别让它扫全盘——让它扫得更少它才更快、更省。【免费下载链接】fsearchWhole-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),仅供参考