三亩地 三亩地SAN MU DI · CODE DIARY
ARTICLE DETAIL

日记详情

真实记录编程学习的某一天,欢迎挑你感兴趣的翻一翻。

140 万条索引毫秒级出结果:FSearch 如何把 Linux 文件搜索变成一场“查表“

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 << 01 << 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:1empty:这类搜索不用现场数孩子。

第三层拆解:1000 条,并行搜索的启动闸门

索引再多,匹配也得逐条做。FSearch 的答案是多线程切分src/fsearch_database_search.c里有一行几乎决定了整个搜索体验的常量:

#define THRESHOLD_FOR_PARALLEL_SEARCH 1000

当索引条目少于 1000 时,单线程顺序扫过就够快;超过这个阈值,才把数组切成 N 段交给线程池并行处理,每个 worker 只管自己start_posend_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" 写得很坦白:

  1. 按 Type 排序很慢。文件类型(MIME)信息没有被索引,排序时要现场采集,结果多了就卡;而且只要视图按 Type 排序,新的搜索会把排序重置回 Name。
  2. 移到回收站的文件不会从索引里消失Move to Trash不会同步更新数据库,被删的文件仍会出现在结果里,直到下次重建索引。
  3. 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这类高频目录,再补上.gitnode_modules的排除规则。重启 FSearch,等首次全量索引跑完——它会把十几分钟前的那个 1,408,753 变成属于你自己的数字。

回到开头的那个状态栏:140 万条索引,本质上是把一次全盘扫描换成了常驻内存的九张"检索卡片"。真正的价值不在数字本身,而在它意味着从输入到结果之间,不再有"等待"这个环节。下一次你想在 Linux 上找一个文件却记不清名字时,打开 FSearch 敲第一个字符,就是这篇文章全部内容的验证时刻。🚀


附注:搜索语法的完整参考位于项目help/C/目录下的search_syntax_functions.pagesearch_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),仅供参考

← 返回列表