FFF:比 ripgrep 和 fzf 更快的文件搜索工具包,多场景性能优势显著!
FFF面向人类与 AI 代理的文件搜索工具包FFF 是一款专为人类和 AI 代理设计的文件搜索工具包搜索速度极快。它支持具备抗拼写错误能力的路径和内容搜索基于访问频率排序的文件访问拥有后台监控器以及轻量级的内存内容索引。在任何需要多次搜索的长期运行进程中FFF 的速度都远超 ripgrep 和 fzf 等 CLI 工具。1. 起源与用途FFF 最初是一个深受用户喜爱的 Neovim 插件后来发现许多 AI 工具和代码编辑器都需要一个准确、快速的文件搜索库FFF 便应运而生。它可以作为 MCP 服务器与 Claude Code、Codex、OpenCode、Cursor、Cline 等任何支持 MCP 的客户端配合使用减少 grep 的往返次数减少无用上下文从而更快地给出答案。2. 安装方法Linux / macOSbashcurl -L https://dmtrkovalenko.dev/install-fff-mcp.sh | bashWindows (PowerShell)powershellirm https://raw.githubusercontent.com/dmtrKovalenko/fff.nvim/main/install-mcp.ps1 | iex如果你想先查看脚本内容脚本位于 install-mcp.sh 和 install-mcp.ps1。安装脚本会打印出客户端的具体配置说明。服务器连接成功后你可以让代理“使用 FFF”它就能调用 ffgrep、fffind 和 fff-multi-grep 工具。3. 推荐的代理提示将以下内容添加到项目的 CLAUDE.md 或类似文件中在当前 Git 索引目录中进行任何文件搜索或 grep 操作时请使用 FFF 工具。4. 主要特性访问频率记忆实际打开过的文件下次搜索时排名会更高。同时它会自动根据 Git 的文件操作历史进行预热。定义优先提示在 Rust 端对看起来像代码定义的行进行分类避免在搜索提示中使用正则表达式带来的开销。智能大小写与自动模糊回退例如IsOffTheRecord 能找到 snake_case 变体当精确匹配为零时查询会自动以模糊模式重试找出最佳近似匹配结果。Git 感知注释标记修改、未跟踪和暂存的文件方便代理快速定位正在修改的文件。5. MCP 服务器MCP 服务器为任何代理提供了一个比内置工具更快、更节省令牌的文件搜索工具。6. Pi 代理扩展安装bashpi install npm:ff-labs/pi-fff三种操作模式可在运行时使用 /fff-mode 切换tools-and-ui默认添加 ffgrep 和 fffind 工具用 FFF 替换 提及的自动补全功能。tools-only仅注入工具保留 Pi 的原生编辑器自动补全功能。override用 FFF 的实现替换 Pi 内置的 grep、find 和 multi_grep 工具。环境变量和标志支持的环境变量有 PI_FFF_MODE、FFF_FRECENCY_DB、FFF_HISTORY_DB支持的标志有 --fff-mode、--fff-frecency-db、--fff-history-db。面向代理的工具ffgrep内容搜索。支持指定路径、排除项用逗号、空格或数组表示可加前置 !、大小写敏感、上下文以及光标分页。能自动检测正则表达式精确匹配为零时自动回退到模糊匹配会直接拒绝仅含 .* 样式通配符的模式。fffind路径和文件名搜索。匹配整个相对于仓库的路径而非仅文件名。具备访问频率感知功能弱匹配检测器会在模糊噪声淹没代理上下文之前进行标记。命令/fff-mode [tools-and-ui | tools-only | override]显示或切换模式。/fff-health查看选择器、访问频率和 Git 集成状态。/fff-rescan强制重新扫描。Pi 扩展会用 FFF 的实现替换 Pi 的原生工具并根据访问频率排序的索引为交互式编辑器的 提及自动补全提供数据。7. fff.nvim演示在 Linux 内核仓库100k 文件8GB上的演示视频fff_nvim_demo.mp4安装lazy.nvimlua{dmtrKovalenko/fff.nvim,build function()-- 下载预构建的二进制文件若失败则使用 cargo 构建require(fff.download).download_or_build_binary()end,-- 对于 NixOS-- build nix run .#release,opts {debug {enabled true,show_scores true,},},lazy false, -- 插件会自动懒初始化keys {{ ff, function() require(fff).find_files() end, desc FFFind files },{ fg, function() require(fff).live_grep() end, desc LiFFFe grep },{ fz, function() require(fff).live_grep({ grep { modes { fuzzy, plain } } }) end, desc Live fffuzy grep },{ fc, function() require(fff).live_grep({ query vim.fn.expand() }) end, desc Search current word },},}vim.packluavim.pack.add({ https://github.com/dmtrKovalenko/fff.nvim })vim.api.nvim_create_autocmd(PackChanged, {callback function(ev)local name, kind ev.data.spec.name, ev.data.kindif name fff.nvim and (kind install or kind update) thenif not ev.data.active thenvim.cmd.packadd(fff.nvim)endrequire(fff.download).download_or_build_binary()endend,})vim.g.fff {lazy_sync true,debug {enabled true,show_scores true,},}vim.keymap.set(n, ff, function() require(fff).find_files() end, { desc FFFind files })公共 APIrequire(fff).find_files()在当前仓库中查找文件。require(fff).live_grep()实时内容搜索。require(fff).scan_files()强制重新扫描。require(fff).refresh_git_status()刷新 Git 状态。require(fff).find_files_in_dir(path)在指定目录中查找文件。require(fff).change_indexing_directory(new_path)更改索引根目录。require(fff).file_search(query, opts)模糊搜索文件、目录或混合搜索。返回结构化结果 { items, scores, total_matched, total_files?, total_dirs?, location? }。require(fff).content_search(query, opts)程序化 grep 搜索。返回 GrepResult { items, total_matched, total_files_searched, total_files, filtered_file_count, next_file_offset, regex_fallback_error? }。配置luarequire(fff).setup({base_path vim.fn.getcwd(),prompt ,title FFFiles,max_results 100,max_threads 4,lazy_sync true,prompt_vim_mode false,layout {height 0.8,width 0.8,prompt_position bottom, -- 或 toppreview_position right, -- left | right | top | bottompreview_size 0.5,flex { size 130, wrap top },min_list_height 10,show_scrollbar true,path_shorten_strategy middle_number, -- middle_number | middle | end | startanchor center,},preview {enabled true,max_size 10 * 1024 * 1024,chunk_size 8192,binary_file_threshold 1024,imagemagick_info_format_str %m: %wx%h, %[colorspace], %q-bit,line_numbers false,cursorlineopt both,wrap_lines false,filetypes {svg { wrap_lines true },markdown { wrap_lines true },text { wrap_lines true },},},keymaps {close ,select ,select_split ,select_vsplit ,select_tab ,move_up { , },move_down { , },preview_scroll_up ,preview_scroll_down ,toggle_debug ,cycle_grep_modes ,cycle_previous_query ,toggle_select ,send_to_quickfix ,focus_list l,focus_preview p,},frecency {enabled true,db_path vim.fn.stdpath(cache) .. /fff_nvim,},history {enabled true,db_path vim.fn.stdpath(data) .. /fff_queries,min_combo_count 3,combo_boost_score_multiplier 100,},git {status_text_color false, -- 若为 true则根据 Git 状态为文件名着色},grep {max_file_size 10 * 1024 * 1024,max_matches_per_file 100,smart_case true,time_budget_ms 150,modes { plain, regex, fuzzy },trim_whitespace false,location_format :%d:%d, -- grep 结果中行:列前缀的 printf 格式例如 :%d 仅显示行号},debug {enabled false, -- 在预览旁边显示文件信息面板show_scores false, -- 在文件列表中显示内联分数show_file_info {file_info true, -- 显示文件大小、类型、Git 状态、访问频率分数score_breakdown true, -- 显示总分数、匹配类型、奖励、修饰符、惩罚timings true, -- 显示修改和访问时间戳full_path true, -- 在底部显示相对路径过长时会换行},},logging {enabled true,log_file vim.fn.stdpath(log) .. /fff.log,log_level info,},})实时搜索模式支持在普通、正则和模糊模式之间循环切换。可通过 grep.modes 配置搜索模式列表单模式设置会完全隐藏模式指示器。也可在每次调用时覆盖默认模式luarequire(fff).live_grep({ grep { modes { fuzzy, plain } } })require(fff).live_grep({ query search term }) -- 预填充搜索词约束条件搜索和 grep 操作都支持以下约束条件来细化查询git:modified可指定 modified、staged、deleted、renamed、untracked、ignored 等状态。test/匹配 test/ 目录下的所有子文件。!something、!test/、!git:modified排除指定内容。./**/*.{rs,lua}任何有效的 glob 模式由 zlob 提供支持。*.md、*.{c,h}仅适用于 grep 的扩展名过滤。src/main.rs在单个文件中进行 grep 搜索。多选和快速修复切换选择在符号列显示粗体 ▊。将选中的文件发送到快速修复列表并关闭选择器。Git 状态高亮符号列指示器默认开启。若要根据 Git 状态为文件名文本着色可设置 git.status_text_color true 并调整 hl.git_* 组。具体可查看 :help fff.nvim。浮动窗口颜色选择器将浮动内容映射到 NormalFloat通过 hl.normal边框映射到 FloatBorder。默认情况下FloatBorder 链接到 NormalFloat因此边框和内容共享背景选择器看起来像一个单独的弹出窗口。可通过覆盖 hl.normal Normal 使选择器与编辑器融合。若需要更精细的控制可设置 hl.winhl 来覆盖每个窗口的 winhighlight它可以是一个应用于所有选择器窗口的字符串也可以是一个包含 prompt、list、preview 和 file_info 可选键的表。缺失的键将使用由 hl.normal、hl.border 和 hl.title 构建的默认值。lua-- 对所有选择器窗口应用相同的 winhighlighthl { winhl Normal:NormalFloat,FloatBorder:FloatBorder,FloatTitle:Title }-- 或仅覆盖特定窗口hl {winhl {prompt Normal:Pmenu,FloatBorder:FloatBorder,list Normal:NormalFloat,FloatBorder:FloatBorder,preview Normal:NormalFloat,FloatBorder:FloatBorder,},}文件信息面板启用 debug.enabled true 可显示文件信息面板。该面板位于预览上方显示文件元数据、分数分解、时间戳和完整绝对路径。它会根据面板宽度自适应显示窄宽度时各部分垂直堆叠宽宽度时各部分以两列网格形式显示。可通过 debug.show_file_info 单独禁用每个部分。可通过 hl 自定义面板键默认值用途file_info_sectionTitle部分标题标签file_info_separatorFloatBorder作为部分分隔线的破折号file_info_labelComment行标签大小、类型、Git 等file_info_valueNormal普通值file_info_value_dimNonText暗淡值行内分隔符file_info_sizeNumber文件大小值file_info_typeType文件类型值file_info_pathDirectory完整路径file_info_total_scorebold Number总分数粗体file_info_match_typebold Special匹配类型粗体file_info_score_posDiagnosticOk正分数组件file_info_score_negDiagnosticError负分数组件文件过滤FFF 会遵循 .gitignore 文件。若要仅在选择器中忽略某些文件而不影响 Git可添加一个同级的 .ignore 文件例如plaintext*.mddocs/archive/**/*.md运行 :FFFScan 可强制重新扫描。故障排除:FFFHealth验证选择器初始化、可选依赖项和数据库连接。:FFFOpenLog打开日志文件。FFF 是 Neovim 中最好的文件搜索选择器具有更快、更直观的查询方式支持访问频率排序、定义分类等诸多功能。8. Node Bun SDKbashnpm install ff-labs/fff-node # 或 bun add ff-labs/fff-nodejavascriptimport { FileFinder } from ff-labs/fff-node;const finder FileFinder.create({ basePath: process.cwd(), aiMode: true });if (!finder.ok) throw new Error(finder.error);await finder.value.waitForScan(10_000);const files finder.value.fileSearch(incognito profile, { pageSize: 20 });const hits finder.value.grep(GetOffTheRecordProfile, { mode: plain, smartCase: true, beforeContext: 1, afterContext: 1, classifyDefinitions: true });finder.value.destroy();每个方法都返回一个 Result ({ ok: true, value } | { ok: false, error })。完整的类型参考可查看 packages/fff-node/src/types.ts。这是一个基于 C 库的 TypeScript 包装器可用于构建自定义代理工具、CLI 或 IDE 集成。9. Rust crate在 Cargo.toml 中添加依赖toml[dependencies]fff-search 0.6完整的 API 文档可查看 [docs.rs/fff-search](https://docs.rs/fff-search)。这是一个原生的 Rust crate负责所有的搜索操作稳定且文档完善。10. C 库构建bash# 仅构建 C cdylib最快make build-c-lib# 或直接使用 cargo 构建cargo build --release -p fff-c --features zlob输出为一个 cdyliblibfff_c.so / libfff_c.dylib / fff_c.dll头文件位于 crates/fff-c/include/fff.h。每个版本包括主分支上的每次提交的预构建二进制文件都可在发布页面找到同样的二进制文件也包含在 ff-labs/fff-bin-* npm 包中。安装系统级安装需要 sudobashsudo make install用户级安装无需 sudobashmake install PREFIX$HOME/.local打包安装bashmake install DESTDIR/tmp/pkgroot PREFIX/usr安装后libfff_c.{so,dylib,dll} 会被放置在 $(PREFIX)/lib 目录头文件会被放置在 $(PREFIX)/include/fff.h。使用 make uninstall 可移除安装它会遵循相同的 PREFIX 和 DESTDIR。安装后可通过以下方式链接bashcc my_app.c -lfff_c -o my_app确保 $(PREFIX)/lib 在运行时库搜索路径中Linux 上为 LD_LIBRARY_PATHmacOS 上为 DYLD_LIBRARY_PATH或在 /etc/ld.so.conf.d/ 中添加条目。最小示例c#include#include fff.h;int main(void) {FffResult *res fff_create_instance(., // base_path, // frecency_db_path (empty default), // history_db_pathfalse, // use_unsafe_no_locktrue, // enable_mmap_cachetrue, // enable_content_indexingtrue, // watchfalse // ai_mode);if (!res-success) {fprintf(stderr, init failed: %s , res-error);fff_free_result(res);return 1;}void *handle res-handle;fff_free_result(res);// 搜索FffResult *search fff_search(handle, main.rs, , 0, 0, 20, 100, 3);// ... 从 search-handle 读取 FffSearchResult然后调用 fff_free_search_result()fff_destroy(handle);return 0;}注意每个返回 FffResult* 的函数都使用 Rust 的 Box 进行内存分配需使用 fff_free_result 释放不要使用 malloc 的 free。有效负载搜索结果、grep 结果、扫描进度有各自独立的释放函数列在头文件中。handle 字段中返回的 C 字符串例如 fff_get_base_path 返回的需使用 fff_free_string 释放。11. 为什么选择 FFF 而非 ripgrep 或 fzfFFF 是一个文件搜索库而非 CLI 工具。ripgrep 和 fzf 是优秀的命令行程序但每次调用都会派生一个新进程重新读取 .gitignore重新统计目录信息并在内存中重建所需状态后才能给出结果。在 shell 中进行单次 grep 操作时这没问题但当编辑器或 AI 代理在一个会话中需要进行数百次搜索时就会出现性能问题。FFF 将索引和文件缓存驻留在一个长期运行的进程中并通过四层接口暴露相同的 Rust 核心原生 cratefff-search、C 库libfff_c、Node/Bun SDKff-labs/fff-node和 MCP 服务器。只需调用一次 FileFinder.create()后续的每次搜索都能命中热内存。在一个包含 500k 文件的 Chromium 仓库中ripgrep 每次启动需要 3 - 9 秒而 FFF 每次查询只需不到 10 毫秒。FFF 的模糊匹配算法比 fzf 更全面具备抗拼写错误能力还提供了带有额外约束解析的查询语言用于预过滤。例如*.rs !test/ shcema 对于 FFF 是一个有效的查询但 fzf 即使在 shcema 有一个拼写错误的情况下也无法找到任何结果。12. 程序化 API 的优势无需进程派生每次调用都在进程内完成避免了 rg 短时间调用时占主导的 fork、exec、argv 解析和 stdout 管道设置。一次文件系统遍历在扫描时只进行一次 .gitignore 的读取、元数据收集和解析结果可用于后续的每次搜索。结构化结果结果以类型化对象形式返回无需重新解析文本。SDK 直接提供 { relativePath, lineNumber, lineContent, gitStatus, totalFrecencyScore, isDefinition, ... } 等信息。游标分页支持跨调用的游标分页而 ripgrep 没有“匹配结果的第 2 页”的概念。长期运行进程的优化长期运行的进程可以进行一些一次性 CLI 无法实现的优化如热缓存、增量重新索引、跨查询的访问频率排序和共享 SIMD 状态。13. FFF 的核心功能基于访问频率的模糊匹配每个索引文件都有访问分数和修改分数搜索结果会将近期频繁打开的文件排在前面类似于 VS Code 的最近打开列表但应用于所有搜索结果而非仅侧边栏。抗拼写错误的路径和内容匹配grep 路径支持 Smith-Waterman 模糊评分路径搜索使用基于 SIMD 加速的模糊匹配通过 frizbee 派生的核心能处理字符缺失和重排序的情况。三种内容搜索模式普通文本SIMD memmem、正则表达式Rust 正则表达式 crate和模糊匹配每行使用 Smith-Waterman 算法。能根据模式自动检测使用哪种模式普通搜索无结果时会回退到模糊匹配。多模式 OR 搜索使用 SIMD Aho-Corasick 算法实现“同时查找 20 个标识符中的任何一个”比正则表达式交替匹配更快比 20 次单独的 ripgrep 运行快得多。后台文件监控器文件更改时索引会自动更新无需在搜索时进行重新扫描。Git 状态感知缓存修改、暂存、未跟踪和忽略的状态并在每个结果中返回调用者无需调用 git 命令即可对结果进行排序或过滤。监控器直接与 libgit2 通信而非派生 git CLI。定义分类器Rust 端的字节级扫描器会标记以 struct、fn、class、def、impl 等开头的行。14. 性能优化高效的内存分配器和策略默认使用 mimaloc。并行多线程搜索管道不受编排逻辑的影响。优先使用 SIMD 算法所有操作都采用高效的 SIMD 算法。高效且无分配的排序减少内存开销。特定平台的文件系统优化如 Linux 上的 getdents64、Windows 上的 NTFS API 等。轻量级实时内容索引支持抗拼写错误的 grep。内存映射内容缓存将部分文件存储在虚拟内存中数量有限。连续的字符串块存储显著减少内存使用提高 CPU 缓存命中率。15. 内存使用FFF 确实比调用单个子进程需要更多的内存这也是其速度提升的主要原因。实际上与 Neovim 中最流行的文件搜索选择器之一相比FFF 使用的 RAM 比多次调用 ripgrep 更少。FFF 还维护一个内容索引每个索引文件约 360 字节因此对于一个 100k 文件的仓库大约需要 36 MB。并非所有文件都会被索引二进制文件、超大文件和不适合 grep 的文件会被跳过。如果内存占用仍然过高索引可以使用内存映射文件而非匿名 RAM 来存储。16. 实际应用场景如果正在构建代理、IDE 扩展、预提交检查或任何需要多次搜索同一仓库的长期运行工具将 FFF 作为库调用比调用 ripgrep 更高效。代价是需要占用一定的内存FFF 将索引存储在 RAM 中并预热内容缓存。对于一个 14k 文件的仓库大约需要 26 MB 内存对于像 Chromium 这样的 500k 文件仓库预计需要几百 MB 内存。作为回报每次搜索都会包含 Git 状态、访问频率排序、文件元数据、最后访问和编辑时间戳等信息。如果只是在终端中进行一次 grep 操作rg 仍然是合适的工具但如果在同一进程中进行多次搜索从第二次调用开始FFF 的优势就会显现出来。如果使用 AI 代理FFF 会在 AI 调用之前完成准备工作。17. 与其他工具的比较ripgrepFFF 使用相同的底层正则表达式引擎和更高级的纯文本匹配算法存储内容索引和文件树在重复搜索工作负载中表现出色但在“从 bash 中进行一次 grep 然后退出”的场景中不如 ripgrep。fzfFFF 的路径搜索与 fzf 一样支持模糊匹配但还具备访问频率感知和 Git 感知功能并且算法更能容忍拼写错误。fzf 是一个纯粹的匹配和过滤工具而 FFF 会根据文件的实际打开频率对结果进行排序。Telescope / fzf-lua / snacks.pickerFFF 有自己的 Neovim 选择器使用与 MCP 服务器和 SDK 相同的排序方式选择器是可选的核心功能保持一致。Tantivy 或其他全文搜索引擎它们属于不同类型的工具。Tantivy 用于大规模文档的查询时间评分索引而 FFF 针对单个仓库进行优化响应时间在 10 毫秒以内且不会在磁盘上持久化倒排索引。18. 仓库布局crates/fff-search、crates/fff-grep、crates/fff-query-parserRust 核心代码。crates/fff-c用于所有语言绑定的 C FFI。crates/fff-nvimNeovim 插件的 Lua/mlua 绑定。crates/fff-mcpMCP 服务器二进制文件。packages/fff-nodeNode.js SDKff-labs/fff-node。packages/fff-bunBun SDKff-labs/fff-node。packages/pi-fffPi 扩展ff-labs/pi-fff。lua/Neovim 端的插件代码。19. 贡献欢迎提交 bug 报告和拉取请求。可以使用自动化编码工具但必须经过人工审核。20. 许可证FFF 采用 MIT 许可证永远开源。