5 个版本 (3 个重大更改)
0.3.1 | 2024年2月20日 |
---|---|
0.3.0 | 2023年12月22日 |
0.2.0 | 2023年8月29日 |
0.1.0 | 2023年8月11日 |
0.0.0 | 2023年8月6日 |
#297 in 文本处理
每月下载量 10,322
在 17 个 crate 中使用 (直接使用 4 个)
205KB
4K SLoC
Nucleo
nucleo
是一个用 Rust 编写的性能极高的模糊匹配器。它的目标是填补与 fzf
和 skim
相同的使用场景。与 fzf
相比,nucleo
具有显著更快的匹配算法。这主要在匹配大量项目上具有低选择性时才有所体现。以下基准测试部分显示了(非科学的)比较。
注意:如果您正在寻找
fuzzy-matcher
crate 的替代品,而不是一个完全管理的模糊选择器,则应使用nucleo-matcher
crate。
nucleo
使用与 fzf 完全相同的评分系统。这意味着您应该获得与您在 fzf 中习惯的相同(或更好)的排名质量。然而,nucleo
对 Smith-Waterman 算法的实现更为忠实,该算法通常用于 DNA 序列比对(见 https://www.cs.cmu.edu/~ckingsf/bioinfo-lectures/gaps.pdf),它使用两个单独的矩阵(而不是像 fzf 中的一个)。这意味着 nucleo
更频繁地找到最佳匹配。例如,如果您在 xf foo
中匹配 foo
,nucleo
将匹配 x__foo
,而 fzf
将匹配 xf_oo
(您可以增加单词长度,结果将保持不变)。前者是更直观的匹配,并且根据 nucleo
和 fzf 的排名系统具有更高的分数。
与 skim
(以及 fuzzy-matcher
包)相比,nucleo
具有更大的性能优势,通常比前者快约 六倍(见下文基准测试)。此外,nucleo 和 fzf 所使用的奖励系统(在我看来)更为一致/优越。nucleo
还能更好地处理非ASCII文本。(skim
的奖励系统和不区分大小写的功能仅适用于ASCII。)
Nucleo 还能更正确地处理 Unicode 图形符号。 Fzf
和 skim
都在 Unicode 代码点上操作(字符)。这意味着多代码点图形符号可能会产生奇怪的效果(多次匹配、分数异常变化等)。nucleo
将始终使用图形符号的第一个代码点进行匹配(并报告图形符号索引,以便正确突出显示)。
状态
Nucleo 被用于 helix-editor,因此拥有庞大的用户群,并经过了大量的实际测试。核心匹配器实现被认为是完整的,不太可能看到重大变化。《code>nucleo-matcher 包已经完成,准备广泛使用,重大更改应该非常罕见(1.0 版本应该不远了)。
虽然高层 nucleo
包也运行良好(并且也被用于 helix),但未来还将添加更多功能。高层包还需要更好的文档,并且未来可能会看到一些 API 变更。
基准测试
正在进行中,目前更多的是演示而非全面的基准测试套件,特别是与
fzf
的科学比较仍然缺失(因为无法作为库调用,很痛苦)
匹配器微基准测试
比较各种模式与 Linux 内核源代码中所有文件的运行时。在您的系统上重复执行以下操作:BENCHMARK_DIR=<path_to_linux> cargo run -p benches --release
(您可以指定空目录,内核会自动克隆)。
方法 | 平均值 | 样本 |
---|---|---|
nucleo "never_matches" | 2.30 毫秒 | 2,493/2,500 |
skim "never_matches" | 17.44 毫秒 | 574/574 |
nucleo "copying" | 2.12 毫秒 | 2,496/2,500 |
skim "copying" | 16.85 毫秒 | 593/594 |
nucleo "/doc/kernel" | 2.59 毫秒 | 2,499/2,500 |
skim "/doc/kernel" | 18.32 毫秒 | 546/546 |
nucleo "//.h" | 9.53 毫秒 | 1,049/1,049 |
skim "//.h" | 35.46 毫秒 | 282/282 |
与 fzf 的比较
例如,在以下两个屏幕录制中,模式 ///.
被粘贴到 fzf
和 nucleo
中(两者都大约打开了 300 万个项目)。
fzf
过滤文本需要一段时间(大约 1 秒),而 nucleo
几乎没有任何可感知的延迟(屏幕录制中的单帧,所以大约 1/30 秒)。这个比较是在一个非常强大的 CPU(Ryzen 5950x)上进行的,所以在较慢的系统上,差异可能更大。
未来工作
- 将集成到 helix
- 构建一个独立的 CLI 应用程序
- 达到与
fzf
的功能对等(主要是--no-sort
和--tac
) - 添加列匹配功能
- 达到与
- 暴露 C API,以便高层 API 和匹配算法本身都可以在其他应用程序中使用(如各种 nvim 插件)
命名
名称 nucleo
是基于 Smith-Waterman
算法(它基于此)最初是为匹配 DNA/RNA 序列而开发的。被匹配的 DNA/RNA 元素称为 核苷酸,这里简称为 nucleo
。
该名称还表明它与 螺旋 编辑器有密切关系(继续 DNA 主题)。
实现细节
这仅针对感兴趣的人,对大多数人来说并不相关。我打算在有时间的时候将其写成博客文章。
模糊匹配算法基于 Smith-Waterman
(带有平移间隙)算法,如 https://www.cs.cmu.edu/~ckingsf/bioinfo-lectures/gaps.pdf 中所述(待解释)。Nucleo
忠实地实现了此算法,因此有两个单独的矩阵。然而,通过预计算下一个 m-matrix
行,我们可以避免存储 p-matrix,而是只需在迭代行时将值存储在变量中。
nucleo
实际上从未真正存储 m-matrix
,我们只存储当前行(同时作为下一行)。然而,在索引计算期间,需要完整的矩阵来回溯实际匹配的索引。这里只存储两个 bool 值(以指示我们在矩阵中的位置)。
相比之下,skim
存储了完整的 p 和 m 矩阵。而 fzf
总是分配完整的 mn
矩阵(即使在匹配期间!)。
nucleo
的矩阵宽度仅为 n-m+1
,而不是宽度 n
。这源于观察到的 p
字符需要 p-1
个字符在其之前,以及 m-p
个字符在其之后,因此总共有 p-1 + m-p = m+1
个字符无法匹配当前字符。这种方法特别适用于只使用单行,因为第一个相关的字符始终位于同一位置,尽管在技术上更靠右。这是特别令人高兴的,因为我们预计算了 m-matrix 行。m-matrix 从对角线元素计算得出,因此预计算值保持在同一矩阵单元格中。
与 skim
相比,nucleo 还执行了一些更简单(但可以说是更有影响力的)优化。
- 预分割 Unicode:Unicode 分割相对较慢,匹配器会频繁过滤相同的元素,因此只需进行一次就很好。它还可以防止一个非常常见的错误来源(我们在这里使用的字符索引和 utf8 索引的混合),从而使得代码变得更加简单。fzf 也这样做。
- 积极预过滤:特别是对于 ASCII,这效果非常好,但我们也在一定程度上对 Unicode 进行了这种操作。这确保我们尽快拒绝不匹配的靶子。通常,当模糊匹配大型列表时,大多数靶子通常不会匹配,因此对于这种情况有一个快速路径是一个巨大的胜利。
- 特殊处理 ASCII:90% 的实际文本是 ASCII。ASCII 可以作为字节而不是
chars
存储,这极大地提高了缓存局部性,并且我们可以使用memchar
进行超级快速的预过滤(甚至可以通过这种方式进行不区分大小写的预过滤)。 - 非常长匹配的回退:为了避免大型匹配时的
O(mn)
爆炸,我们回退到贪婪匹配器,该匹配器的时间复杂度为O(N)
(空间复杂度为O(1)
)。这是fzfs的旧算法,结果相当不错(但并非非常出色)。
依赖关系
~0.6–0.8MB
~11K SLoC