fff
针对AI agents和开发者设计的极速文件搜索工具包,提供MCP server、Neovim插件和多种语言SDK。核心优势在于保持常驻内存索引,避免多次搜索时的进程开销,比ripgrep快几个数量级,支持freency排名、git状态感知、模糊匹配和纠错。适合构建需要频繁文件搜索的AI工具或编辑器插件。
README

面向人类和 AI 智能体的文件搜索工具包。速度极快。
容错的路径和内容搜索、基于 frecency(频率+近期)排序的文件访问、后台文件监视器,以及轻量级的内存内容索引。在任何需要重复搜索的长期运行进程中,速度远超 ripgrep 和 fzf 等 CLI 工具。
最初作为用户喜爱的 Neovim 插件 诞生,但后来发现,大量 AI 框架和代码编辑器都需要同样的东西:一个作为库使用的精准、快速文件搜索。这正是 fff 的定位。
选择你感兴趣的部分:
MCP 服务器
可与 Claude Code、Codex、OpenCode、Cursor、Cline 以及任何兼容 MCP 的客户端配合使用。更少的 grep 往返,更少的上下文浪费,更快的回答。

一行安装
Linux / macOS:
curl -L https://dmtrkovalenko.dev/install-fff-mcp.sh | bash
Windows (PowerShell):
irm https://raw.githubusercontent.com/dmtrKovalenko/fff.nvim/main/install-mcp.ps1 | iex
脚本位于 install-mcp.sh 和 install-mcp.ps1,你可以先阅读它们。
它会打印出客户端所需的确切连接指令。服务器连接后,指示智能体“使用 fff”,它就会调用 ffgrep、fffind 和 fff-multi-grep 工具。
推荐的智能体提示词
将下面内容放入项目的 CLAUDE.md 或等效文件中:
对于当前 git 索引目录下的任何文件搜索或 grep,请使用 fff 工具。
带来的变化
- Frecency 记忆。你实际打开过的文件下次排名会更高。自动从 git 触摸历史中预热。
- 定义优先提示。类似代码定义的行会在 Rust 端进行分类,无需在提示词中开销正则表达式。
- 智能大小写与自动模糊回退。
IsOffTheRecord能匹配到蛇形写法变体;零匹配查询会自动改用模糊搜索,并显示最佳近似结果。 - Git 感知注解。修改、未跟踪和暂存的文件会被标记,以便智能体优先访问你正在更改的文件。
源码:crates/fff-mcp/。
MCP 服务器为任何智能体提供比内置工具更快、token 更高效的文件搜索能力。
Pi 智能体扩展
安装
pi 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。内容搜索。支持path、exclude(逗号、空格或数组,前导!可选)、caseSensitive、context以及分页游标。自动检测正则表达式,零精确匹配时回退为模糊搜索,并预先拒绝.*类型的纯通配符模式。fffind。路径和文件名搜索。匹配仓库相对路径,而不仅仅是文件名。支持 frecency。弱匹配检测器会在散乱的模糊噪声淹没智能体上下文之前将其标记出来。
命令
/fff-mode [tools-and-ui | tools-only | override]。显示或切换模式。/fff-health。查看选择器、frecency 和 git 集成状态。/fff-rescan。强制重新扫描。
源码:packages/pi-fff/。
Pi 扩展将原生工具替换为 FFF 实现,并利用 frecency 排序的索引为交互式编辑器的 @ 提及自动补全提供数据。
fff.nvim
在 Linux 内核仓库(10 万文件,8GB)上的演示:
https://github.com/user-attachments/assets/5d0e1ce9-642c-4c44-aa88-01b05bb86abb
安装
lazy.nvim
{
'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 = 'FF 查找文件' },
{ "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("<cword>") }) end,
desc = '搜索当前单词',
},
},
}
vim.pack
vim.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.kind
if name == 'fff.nvim' and (kind == 'install' or kind == 'update') then
if not ev.data.active then vim.cmd.packadd('fff.nvim') end
require('fff.download').download_or_build_binary()
end
end,
})
vim.g.fff = {
lazy_sync = true,
debug = { enabled = true, show_scores = true },
}
vim.keymap.set('n', 'ff', function() require('fff').find_files() end, { desc = 'FF 查找文件' })
公共 API
require('fff').find_files() -- 在当前仓库中查找文件
require('fff').live_grep() -- 实时内容 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) -- 更改根目录
-- 程序化搜索(无 UI)。适用于插件集成。
require('fff').file_search(query, opts) -- 模糊搜索文件/目录/混合
require('fff').content_search(query, opts) -- 程序化 grep
file_search(query, opts)
返回结构化结果 { items, scores, total_matched, total_files?, total_dirs?, location? }。每个 item 包含 type 字段("file" 或 "directory")以及 name / relative_path。文件项还暴露 size、modified、git_status、is_binary 和 frecency 分数。
local r = require('fff').file_search('button', {
mode = 'mixed', -- 'files'(默认)| 'directories' | 'mixed'
max_results = 50,
page = 0, -- 基于 0 的分页
current_file = nil, -- 用于距离排序时降低优先级
max_threads = 4,
cwd = nil, -- 切换到不同的索引根目录(见下文)
wait_for_index_ms = nil, -- 覆盖默认的扫描等待超时
})
for _, item in ipairs(r.items) do
print(item.type, item.relative_path)
end
content_search(query, opts)
返回 GrepResult 类型 { items, total_matched, total_files_searched, total_files, filtered_file_count, next_file_offset, regex_fallback_error? }。每个匹配项包含 relative_path、name、line_number、col、line_content、match_ranges,以及与 file_search 相同的文件元数据。
local r = require('fff').content_search('TODO', {
mode = 'plain', -- 'plain'(默认)| 'regex' | 'fuzzy'
max_file_size = 10 * 1024 * 1024,
max_matches_per_file = 100,
smart_case = true,
page_size = 50,
file_offset = 0,
time_budget_ms = 0,
trim_whitespace = false,
cwd = nil, -- 切换到不同的索引根目录
wait_for_index_ms = nil, -- 覆盖默认的扫描等待超时
})
for _, m in ipairs(r.items) do
print(string.format('%s:%d %s', m.relative_path, m.line_number, m.line_content))
end
两个函数都支持与 UI 选择器相同的约束语法(例如 git:modified、*.rs、!test/、glob 模式)。
cwd 与索引
file_search 和 content_search 都支持可选的 cwd 字段。第一次调用任一函数时,会在 config.base_path(默认是 Neovim 的当前工作目录)处懒初始化选择器。
- 如果
cwd与当前索引根目录匹配,则立即返回现有索引的结果。 - 如果
cwd不同,则选择器会在新根目录重新索引,并且调用会阻塞(默认最多 10 秒),直到新选择器安装完毕且初始扫描完成——因此调用者始终能从正确的目录树获得结果。 - 如果在调用
change_indexing_directory后索引仍在预热中,你可以传入wait_for_index_ms = N来最多阻塞 N 毫秒,无论cwd是否触发了切换。传0则完全不等待(适用于可接受部分结果的“即发即忘”调用)。 - 无效或不存在的
cwd路径会返回空结果,并通过vim.notify发出错误。
命令
:FFFScan。重新扫描文件。:FFFRefreshGit。刷新 git 状态。:FFFClearCache [all|frecency|files]。清除缓存。:FFFHealth。健康检查。:FFFDebug [on|off|toggle]。切换评分显示。:FFFOpenLog。打开~/.local/state/nvim/log/fff.log。
配置
默认配置已足够合理。只需覆盖你关心的部分。
require('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', -- 或 'top'
preview_position = 'right', -- 'left' | 'right' | 'top' | 'bottom'
preview_size = 0.5,
flex = { size = 130, wrap = 'top' },
min_list_height = 10, -- 低于此阈值时不显示除列表外的任何内容
show_scrollbar = true,
path_shorten_strategy = 'middle_number', -- 'middle_number' | 'middle' | 'end' | 'start'
anchor = '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 = '<Esc>',
select = '<CR>',
select_split = '<C-s>',
select_vsplit = '<C-v>',
select_tab = '<C-t>',
move_up = { '<Up>', '<C-p>' },
move_down = { '<Down>', '<C-n>' },
preview_scroll_up = '<C-u>',
preview_scroll_down = '<C-d>',
toggle_debug = '<F2>',
cycle_grep_modes = '<S-Tab>',
cycle_previous_query = '<C-Up>',
toggle_select = '<Tab>',
send_to_quickfix = '<C-q>',
focus_list = '<leader>l',
focus_preview = '<leader>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,
},
debug = {
enabled = false, -- 在预览旁边显示文件信息面板
show_scores = false, -- 在文件列表中内联显示分数
-- 文件信息面板的每个区域开关。接受布尔值简写
-- (`show_file_info = true|false`) 来同时切换所有区域。面板
-- 会根据宽度自适应:窄布局垂直排列,宽布局显示为两列网格。
-- 禁用某个区域也会缩小面板。
show_file_info = {
file_info = true, -- 大小、类型、git 状态、frecency
score_breakdown = true, -- 总分 + 匹配类型、加分、修饰、惩罚
-- 修改 + 访问时间戳;传入表格可隐藏特定行:
-- timings = { modified = false, accessed = true }
timings = true,
full_path = true, -- 底部的相对路径(过长时会换行)
},
},
logging = {
enabled = true,
log_file = vim.fn.stdpath('log') .. '/fff.log',
log_level = 'info',
},
})
实时 grep 模式
<S-Tab> 在 plain、regex 和 fuzzy 之间循环。模式列表可通过 grep.modes 配置,单模式设置会隐藏切换指示器。
单次调用覆盖:
require('fff').live_grep({ grep = { modes = { 'fuzzy', 'plain' } } })
require('fff').live_grep({ query = 'search term' }) -- 预填充
约束语法
查找和 grep 都支持以下 token 来细化查询:
git:modified。可选值:modified、staged、deleted、renamed、untracked、ignored。test/。匹配test/下的所有深层子项。!something、!test/、!git:modified。排除。./**/*.{rs,lua}。任何有效的 glob,由 zlob 支持。
仅 grep 支持:
*.md、*.{c,h}。扩展名过滤。src/main.rs。在单个文件中 grep。
自由组合:git:modified src/**/*.rs !src/**/mod.rs user controller。
多选与 quickfix
<Tab>。切换选择(在符号列显示粗▊)。<C-q>。将选中的文件发送到 quickfix 列表并关闭选择器。
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 构建的默认值。
-- 对所有选择器窗口应用相同的 winhighlight
hl = { 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_section |
Title |
区域标题标签 |
file_info_separator |
FloatBorder |
作为区域边框的虚线 |
file_info_label |
Comment |
行标签(大小、类型、Git...) |
file_info_value |
Normal 前景色 |
普通值 |
file_info_value_dim |
NonText |
暗淡值,行内分隔符 |
file_info_size |
Number |
文件大小值 |
file_info_type |
Type |
文件类型值 |
file_info_path |
Directory |
完整路径 |
file_info_total_score |
粗体 + Number |
总分(粗体) |
file_info_match_type |
粗体 + Special |
匹配类型(粗体) |
file_info_score_pos |
DiagnosticOk |
正向分数成分 |
file_info_score_neg |
DiagnosticError |
负向分数成分 |
文件过滤
FFF 遵循 .gitignore。如果需要选择器特有的忽略(不影响 git),可添加一个同级文件 .ignore:
*.md
docs/archive/**/*.md
运行 :FFFScan 强制重新扫描。
故障排除
:FFFHealth验证选择器初始化、可选依赖和数据库连接。:FFFOpenLog打开日志文件。
Neovim 上最好的文件搜索选择器。没有之一。更快速、更直观的查询,frecency 排序,定义分类等等。
Node & Bun SDK
npm install @ff-labs/fff-node
# 或
bun add @ff-labs/fff-node
import { 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<T>({ ok: true, value } | { ok: false, error })。完整类型参考:packages/fff-node/src/types.ts。
针对 Node.js 和 Bun 的 C 语言库 TypeScript 封装。可用于在 FFF 之上构建自定义智能体工具、CLI 或 IDE 集成。
Rust crate
添加依赖
FFF 用 Rust 编写,因此这是开销最小的使用方式。
[dependencies]
fff-search = "0.6"
完整的 API 文档:docs.rs/fff-search。
执行所有搜索的原生 Rust crate。稳定且文档完善。
C 语言库
构建
# 仅构建 C cdylib(最快):
make build-c-lib
# 或直接用 cargo:
cargo build --release -p fff-c --features zlob
输出为 cdylib(libfff_c.so / libfff_c.dylib / fff_c.dll)。头文件位于 crates/fff-c/include/fff.h。
每个版本(包括 main 上的每次提交)的预编译二进制文件可在 releases 页面 找到。相同的二进制文件也包含在 @ff-labs/fff-bin-* npm 包中。
安装
# 系统级(需要 sudo):
sudo make install
# 用户本地,无需 sudo:
make install PREFIX=$HOME/.local
# 打包人员可分阶段安装:
make install DESTDIR=/tmp/pkgroot PREFIX=/usr
将 libfff_c.{so,dylib,dll} 放入 $(PREFIX)/lib,头文件放入 $(PREFIX)/include/fff.h。使用 make uninstall 移除,该命令会遵循同样的 PREFIX 和 DESTDIR。
安装后链接:
cc my_app.c -lfff_c -o my_app
确保 $(PREFIX)/lib 在运行时库搜索路径中(Linux 上为 LD_LIBRARY_PATH,macOS 上为 DYLD_LIBRARY_PATH,或在 /etc/ld.so.conf.d/ 中添加条目)。
最小示例
#include <fff.h>
#include <stdio.h>
int main(void) {
FffResult *res = fff_create_instance(
".", // base_path
"", // frecency_db_path(空 = 默认)
"", // history_db_path
false, // use_unsafe_no_lock
true, // enable_mmap_cache
true, // enable_content_indexing
true, // watch
false // ai_mode
);
if (!res->success) {
fprintf(stderr, "init failed: %s\n", 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释放。
源码:crates/fff-c/。
稳定的 C ABI。可从 C/C++、Zig、Go(通过 cgo)、Python(通过 ctypes)或任何支持 C FFI 的语言绑定。
什么是 FFF,为何要使用它而不是 ripgrep 或 fzf?
FFF 是一个文件搜索库,而不是 CLI。Ripgrep 和 fzf 是优秀的工具,但它们是命令行程序:每次调用都会 fork 新进程,重新读取 .gitignore,重新 stat 目录,并在内存中重建所需的状态,然后才能给出答案。当你在 shell 中只 grep 一次时,这没问题。但当编辑器或 AI 智能体希望在同一个会话中运行数百次搜索时,问题就大了。
FFF 在一个长期运行的进程中保持索引和文件缓存驻留,并通过四个薄层暴露相同的 Rust 核心:原生 crate(fff-search)、C 库(libfff_c)、Node/Bun SDK(@ff-labs/fff-node)和 MCP 服务器。你只需调用一次 FileFinder.create(),之后每次搜索都会命中热内存。在包含 50 万文件的 Chromium 存储库中,这意味着每次 ripgrep 生成需要 3-9 秒,而 FFF 查询只需不到 10 毫秒。
模糊匹配算法比 fzf 的算法更全面——它具有容错能力,并提供带有额外约束解析的查询语言以进行预过滤,例如 *.rs !test/ shcema 对 fff 来说是一个完全有效的查询,但 fzf 甚至对 "shcema" 中的单个拼写错误都找不到结果。
为什么程序化 API 很重要
- 无需进程生成。每次调用都保持在进程内,避免了支配短时
rg调用的 fork、exec、argv 解析和 stdout 管道设置。 - 一次文件系统遍历、元数据收集和
.gitignore解析。忽略步行器只在扫描时运行一次,结果会被每次搜索复用。 - 结果以类型化对象返回,而非需要重新解析的文本。SDK 直接提供
{ relativePath, lineNumber, lineContent, gitStatus, totalFrecencyScore, isDefinition, ... }。 - 支持跨调用持久的分页游标。Ripgrep 没有“这些匹配的第二页”的概念;FFF 有。
- 长期运行的进程带来一次性 CLI 无法应用的优化:热缓存、增量重新索引、跨查询 frecency 以及共享 SIMD 状态。
核心功能
- 基于 frecency 排序的模糊匹配。每个索引文件都带有访问分数和修改分数。搜索会将你最近和经常打开的文件排在冷结果之前。这与 VS Code 的最近打开列表思路相同,但应用于每个搜索结果,而不仅仅是侧边栏。
- 针对路径和内容的容错匹配。Smith-Waterman 模糊评分可用于 grep 路径;路径搜索使用 SIMD 加速的模糊匹配(通过 frizbee 衍生的核心),能容忍字符遗漏和重排。
- 三种内容 grep 模式。纯字面量(SIMD memmem)、正则(Rust
regexcrate)和模糊(逐行 Smith-Waterman)。自动从模式中检测使用哪种模式,当纯搜索返回零结果时回退到模糊。 - 多模式 OR 搜索。SIMD Aho-Corasick 用于“一次找到这 20 个标识符中的任何一个”,比正则交替更快,比运行 20 次独立的 ripgrep 快得多。
- 后台文件监视器。文件变化时索引自动更新。热路径上永远不需要支付重新扫描的开销。
- Git 状态感知。已修改、暂存、未跟踪和忽略的状态被缓存,并随每次结果返回,因此调用者可以在不调用 git 的情况下进行排序或过滤。监视器直接与 libgit2 通信,而不是生成
gitCLI。 - 定义分类器。Rust 端的字节级扫描器标记以
struct、fn、class、def、impl等开头的行。
重要的性能选择
- 高效的内存分配器和分配策略(见下一段)。默认使用
mimaloc。 - 不受到编排逻辑干扰的并行多线程搜索管道。
- 所有内容优先使用 SIMD 算法。高效且无分配的排序。
- 特定平台的文件系统优化(getdents64、Windows 上的 NTFS API 等)。
- 轻量级实时内容索引,支持容错的 grep。
- 内存映射内容缓存。将部分文件存储在虚拟内存中(数量有限)。
- 单一连续 arena 存储字符串块。显著减少内存使用量,并大幅提高 CPU 缓存命中率。
内存分配
是的,FFF 本质上需要比调用单个子进程更多的内存。这是速度提升的主要来源。在实践中,作为 Neovim 最受欢迎的文件搜索选择器之一,FFF 最终使用的 RAM 比 burst 式的 ripgrep 调用要少。
FFF 还维护一个内容索引,每个索引文件大约 360 字节,因此一个 10 万文件的仓库大约需要 36 MB。并非所有文件都会被索引——二进制文件、过大文件以及任何不适合 grep 的文件都会被跳过。如果即使这个开销也过高,索引可以改用内存映射文件而不是匿名 RAM。
这在实际中意味着什么
如果你正在构建一个智能体、IDE 扩展、预提交检查或任何需要多次搜索同一存储库的长期运行工具,将 FFF 作为库调用比调用 ripgrep 的子进程要便宜得多。代价是真实的内存:FFF 将索引保留在 RAM 中并预热内容缓存。对于一个 1.4 万文件的仓库,这大约需要 26 MB 常驻内存。对于像 Chromium 这样 50 万文件的仓库,预计需要几百 MB。作为交换,每次搜索都附带 git 状态、frecency 排序、文件元数据、上次访问和编辑的时间戳等信息。
如果你只是在终端运行一次 grep,rg 仍然是正确的工具。如果你在同一进程内运行数十次 grep,从第二次调用开始 FFF 就会收回成本。如果你从事 AI 智能体工作,FFF 会在你的智能体有机会调用它之前完成所有准备工作。
对比
- ripgrep:FFF 使用相同的底层正则引擎和更先进的纯文本匹配算法。存储内容索引和文件树。主要优势在于重复搜索的工作负载。在“从 bash grep 一次然后退出”的场景中不如 ripgrep。
- fzf:FFF 的路径搜索像 fzf 一样是模糊的,但还具备 frecency 感知和 git 感知,并采用更容错的算法。fzf 是纯匹配和过滤工具;FFF 根据你实际打开文件的频率对结果排序。
- Telescope / fzf-lua / snacks.picker:FFF 自带自己的 Neovim 选择器,使用与 MCP 服务器和 SDK 相同的排序。选择器是可选的,核心是相同的。
- Tantivy 或其他全文搜索引擎:不同类型的工具。Tantivy 对文档进行索引以实现大规模查询时评分。FFF 作用于单个仓库,优化目标是低于 10 毫秒的响应。它不在磁盘上持久化倒排索引。
仓库布局
crates/fff-search、crates/fff-grep、crates/fff-query-parser- Rust 核心。crates/fff-c- 用于所有语言绑定的 C FFI。crates/fff-nvim- Neovim 插件的 Lua/mlua 绑定。crates/fff-mcp- MCP 服务器二进制文件。packages/fff-node- Node.js SDK(@ff-labs/fff-node)。packages/fff-bun- Bun