140 万条索引毫秒级出结果:FSearch 如何把 Linux 文件搜索变成一场“查表“
140 万条索引毫秒级出结果:FSearch 如何把 Linux 文件搜索变成一场"查表"
【免费下载链接】fsearchA fast file search utility for Unix-like systems based on GTK3项目地址: https://gitcode.com/gh_mirrors/fs/fsearch
如果你在一个 Linux 桌面上打开 FSearch,会先被状态栏右下角那个数字震到——1,408,753 Items。这是它已经吃进内存的文件索引总量:/usr/share 下的图标、/home 里的下载、/var 下的日志,一百四十万个条目。而你在搜索框敲下第一个字符的瞬间,匹配结果几乎是同步刷出来的。
这不是"快一点"的差别,而是机制的根本不同。传统find是每次搜索都去磁盘上跑一遍目录树,FSearch 则是先把整个文件系统"背诵"进内存,搜索时只做查表。这篇文章就沿着"它凭什么这么快"这条线索往下拆,每一层都会对应src/里的真实代码。
第一层拆解:搜索不是"去找",而是"去查"
先说清一个反直觉的事实:FSearch 从不扫描磁盘。它运行的是索引数据库,而这个数据库在内存里以十种维度组织。打开src/fsearch_database_index.h,能看到这份清单:
typedef enum { DATABASE_INDEX_TYPE_NAME, DATABASE_INDEX_TYPE_PATH, DATABASE_INDEX_TYPE_SIZE, DATABASE_INDEX_TYPE_MODIFICATION_TIME, DATABASE_INDEX_TYPE_ACCESS_TIME, DATABASE_INDEX_TYPE_CREATION_TIME, DATABASE_INDEX_TYPE_STATUS_CHANGE_TIME, DATABASE_INDEX_TYPE_FILETYPE, DATABASE_INDEX_TYPE_EXTENSION, NUM_DATABASE_INDEX_TYPES, } FsearchDatabaseIndexType;这段枚举定义了九种可检索的属性。搜索size:>1gb时它直接比对"大小"列,搜索ext:png时走"扩展名"列,而不是像find那样把每个文件重新 stat 一遍。整个设计思路与 Windows 上 Everything Search Engine 一脉相承——FSearch 正是它的 Linux 移植哲学:用一次性的全量扫描成本,换取此后每一次搜索的瞬时响应。
配套的还有FsearchDatabaseIndexFlags位掩码(1 << 0到1 << 6),用于标记哪些属性已建索引,避免数据库体积失控。
第二层拆解:把 140 万条记录压进紧凑内存
140 万条记录放进内存,听起来是内存杀手,FSearch 却用两招把开销压了下来。
第一招是内存池。看src/fsearch_memory_pool.c,它按"块"预分配内存:
static void fsearch_memory_pool_new_block(FsearchMemoryPool *pool) { FsearchMemoryPoolBlock *block = calloc(1, sizeof(FsearchMemoryPoolBlock)); block->items = calloc(pool->block_size + 1, pool->item_size); pool->blocks = g_list_prepend(pool->blocks, block); }每个数据库条目从池里按固定大小切出,释放时只是把指针挂回空闲链表(pool->freed_items),下次申请直接复用。这避免了为百万级条目反复malloc/free带来的碎片和系统调用开销。
第二招是自研的动态数组。src/fsearch_array.h里那个DynamicArray不是普通链表,它支持二分查找(darray_binary_search_with_data)和多线程排序(darray_sort_multi_threaded)。搜索命中后,结果排序可以按文件名、路径、大小、修改时间任一维度进行,靠的是fsearch_database_entry.h里一组比较函数,比如:
int db_entry_compare_entries_by_size(FsearchDatabaseEntry **a, FsearchDatabaseEntry **b); int db_entry_compare_entries_by_name(FsearchDatabaseEntry **a, FsearchDatabaseEntry **b);顺带一提,条目本身也做了压缩:fsearch_database_entry.h区分了文件夹条目和文件条目两种结构,文件夹条目还会缓存子文件/子文件夹数量(db_entry_folder_get_num_children),这让childcount:1、empty:这类搜索不用现场数孩子。
第三层拆解:1000 条,并行搜索的启动闸门
索引再多,匹配也得逐条做。FSearch 的答案是多线程切分。src/fsearch_database_search.c里有一行几乎决定了整个搜索体验的常量:
#define THRESHOLD_FOR_PARALLEL_SEARCH 1000当索引条目少于 1000 时,单线程顺序扫过就够快;超过这个阈值,才把数组切成 N 段交给线程池并行处理,每个 worker 只管自己start_pos到end_pos的一段:
static void db_search_worker(void *data) { DatabaseSearchWorkerContext *ctx = data; for (uint32_t i = start; i <= end; i++) { if (G_UNLIKELY(g_cancellable_is_cancelled(ctx->cancellable))) { break; } FsearchDatabaseEntry *entry = darray_get_item(entries, i); fsearch_query_match_data_set_entry(match_data, entry); if (fsearch_query_match(query, match_data)) { results[num_results++] = entry; } } }注意每次循环开头的g_cancellable_is_cancelled:你每敲一个字符,上一次搜索的取消信号就到达这些线程,它们立刻中断,把 CPU 让给新查询。这就是"边打字边出结果"不卡顿的机制——不是搜索够快,而是旧搜索退得够快。
第四层拆解:让 "TEST" 匹配 "test" 的 Unicode 工程
大小写不敏感的搜索是 FSearch 的默认行为,但中文、德文、土耳其文用户会问:这玩意儿只做了个tolower吗?答案藏在src/fsearch_utf.h:
#include <unicode/ucasemap.h> #include <unicode/unorm2.h> typedef struct FsearchUtfBuilder { UCaseMap *case_map; const UNormalizer2 *normalizer; char *string_utf8_folded; UChar *string_folded; UChar *string_normalized_folded; ... } FsearchUtfBuilder;它直接用 ICU 做Unicode 大小写折叠(case folding)加归一化(normalization)。这意味着straße能匹配STRASSE,带重音的café能匹配cafe,而不会因为 UTF-8 多字节编码导致逐字节比较出错。每个字符串只折叠一次并缓存结果(string_utf8_is_folded标志),搜索期间零重复计算。对多语言系统来说,这层处理比"快"更关键——它决定了结果对不对。
实战场景一:运维在磁盘告警之夜
服务器监控弹了告警,/var分区使用率 91%。你需要在几百 GB 日志里找出"最近一个月内、超过 500MB 的大文件",只删日志不动别的。
在 FSearch 搜索框输入:
file:size:>500mb AND dm:last30days AND path:/var/log各段含义:file:只匹配文件(folder:则反过来),size:>500mb是带单位的大小比较,dm:是datemodified:的缩写,last30days这类日期常量由help/C/search_syntax_functions.page里的语法解析器直接支持。按下回车前结果已经刷出来了,按大小排序,逐个右键"Move to Trash",问题定位全程不超过两分钟。同样的任务用find /var/log -size +500M -mtime -30也不是不行,但每次都要等磁盘遍历。
实战场景二:开发者在陌生代码库里找函数
接手一个没人写文档的 C 项目,你想知道fsearch_query_match这个函数到底在哪些地方被调用过,以及调用点附近有没有可疑的参数传递:
path:/home/dev/fsearch src ext:c regex:"fsearch_query_match\("regex:开启 PCRE2 正则匹配,.之前都要加反斜杠是正则的常规要求;ext:c限定只查 C 源文件;path:限制搜索范围。整个表达式用双引号包住正则,避免 FSearch 自己的语法符号((、))被误解析——这一点search_syntax_modifiers.page里专门提醒过。搜索结果列出来,配合预览面板直接看上下文,比在 IDE 里逐个目录 grep 快一个量级。
诚实的代价:FSearch 的三处"不完美"
把性能做到极致,是要付账的。README 里的 "Current Limitations" 写得很坦白:
- 按 Type 排序很慢。文件类型(MIME)信息没有被索引,排序时要现场采集,结果多了就卡;而且只要视图按 Type 排序,新的搜索会把排序重置回 Name。
- 移到回收站的文件不会从索引里消失。
Move to Trash不会同步更新数据库,被删的文件仍会出现在结果里,直到下次重建索引。 contenttype:搜索是昂贵的。MIME 类型判定是重活,文档明确建议先用path:之类的条件把候选集缩小再使用。
另外,索引更新是手动或定时触发的,不是实时监控文件系统事件。如果你经常增删大量文件,记得在"数据库"菜单里手动刷新。
从源码到你的桌面:三条命令上手
想把这份索引搬进自己的机器,最直接的路是源码编译。依赖项在 README 里列得很清楚:GTK 3.18、GLib 2.50、PCRE2、ICU 3.8。然后:
git clone https://gitcode.com/gh_mirrors/fs/fsearch cd fsearch meson build cd build ninja sudo ninja install装好后在首选项里把索引范围圈定在/home、/var这类高频目录,再补上.git、node_modules的排除规则。重启 FSearch,等首次全量索引跑完——它会把十几分钟前的那个 1,408,753 变成属于你自己的数字。
回到开头的那个状态栏:140 万条索引,本质上是把一次全盘扫描换成了常驻内存的九张"检索卡片"。真正的价值不在数字本身,而在它意味着从输入到结果之间,不再有"等待"这个环节。下一次你想在 Linux 上找一个文件却记不清名字时,打开 FSearch 敲第一个字符,就是这篇文章全部内容的验证时刻。🚀
附注:搜索语法的完整参考位于项目help/C/目录下的search_syntax_functions.page与search_syntax_modifiers.page,编译安装后可在帮助文档中查看。
图:FSearch 标题栏模式(Headerbar),搜索框与窗口控制按钮融为一体,结果列表即时刷新。
图:FSearch 菜单栏模式,保留 File/Edit/View/Search/Help 传统菜单,状态栏右下角可见总索引条目数(此处为 1,408,753)。
【免费下载链接】fsearchA fast file search utility for Unix-like systems based on GTK3项目地址: https://gitcode.com/gh_mirrors/fs/fsearch
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
