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 文本处理

Download history 1851/week @ 2024-04-08 2142/week @ 2024-04-15 1896/week @ 2024-04-22 1969/week @ 2024-04-29 1810/week @ 2024-05-06 2121/week @ 2024-05-13 2088/week @ 2024-05-20 2097/week @ 2024-05-27 2026/week @ 2024-06-03 1828/week @ 2024-06-10 1757/week @ 2024-06-17 1795/week @ 2024-06-24 1768/week @ 2024-07-01 2181/week @ 2024-07-08 3839/week @ 2024-07-15 2303/week @ 2024-07-22

每月下载量 10,322
17 个 crate 中使用 (直接使用 4 个)

MPL-2.0 许可证

205KB
4K SLoC

Nucleo

nucleo 是一个用 Rust 编写的性能极高的模糊匹配器。它的目标是填补与 fzfskim 相同的使用场景。与 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 中匹配 foonucleo 将匹配 x__foo,而 fzf 将匹配 xf_oo(您可以增加单词长度,结果将保持不变)。前者是更直观的匹配,并且根据 nucleo 和 fzf 的排名系统具有更高的分数。

skim(以及 fuzzy-matcher 包)相比,nucleo 具有更大的性能优势,通常比前者快约 六倍(见下文基准测试)。此外,nucleo 和 fzf 所使用的奖励系统(在我看来)更为一致/优越。nucleo 还能更好地处理非ASCII文本。(skim 的奖励系统和不区分大小写的功能仅适用于ASCII。)

Nucleo 还能更正确地处理 Unicode 图形符号。 Fzfskim 都在 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 的比较

例如,在以下两个屏幕录制中,模式 ///. 被粘贴到 fzfnucleo 中(两者都大约打开了 300 万个项目)。

fzf 过滤文本需要一段时间(大约 1 秒),而 nucleo 几乎没有任何可感知的延迟(屏幕录制中的单帧,所以大约 1/30 秒)。这个比较是在一个非常强大的 CPU(Ryzen 5950x)上进行的,所以在较慢的系统上,差异可能更大。

asciicast asciicast

未来工作

  • 将集成到 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