Skip to content

feat: add Arinae algorithm#990

Merged
LoricAndre merged 80 commits intomasterfrom
skim-v3
Mar 1, 2026
Merged

feat: add Arinae algorithm#990
LoricAndre merged 80 commits intomasterfrom
skim-v3

Conversation

@LoricAndre
Copy link
Contributor

@LoricAndre LoricAndre commented Feb 23, 2026

Description

Arinae is designed to become skim's default algorithm in the future.

Technically, it uses Smith-Waterman and a modified Levenshtein distance with affine gaps for scoring, as well as multiple optimizations (the main ones being a loose prefilter and checks for early dismissal of paths that cannot lead to the best match). It also forbids typos on the first char of the query.

In practice, it should feel close to FZY's scoring with typos disabled, but with a more natural behavior regarding typos as Frizbee or other algorithms.

These other algorithms usually work by allowing a set number of typos using 3D matrices for computations, the max-typos value being set based on the length of the query. In practice, that meant that tes will match exactly, but test will allow one typo, meaning that typing a single character will change the filtered items completely. This algorithm will instead penalize typos, not block them completely.

This algorithm does not aim to revolution anything, but it aims at making typo-resistant fuzzy matching feel more like an actual alternative to the current options (namely FZF and FZY), while maintaining per-item performance at least as good as the current algorithms.

Checklist

  • The title of my PR follows conventional commits
  • I have updated the documentation (README.md, comments, src/manpage.rs and/or src/options.rs if applicable)
  • I have added unit tests
  • I have added integration tests
  • I have linked all related issues or PRs

Note: codecov runs on the PR on this repo, but feel free to ignore it.

Benches (w/ bench.sh: less precise but shows rss usage & compatible with FZF)

Skim V2 (current default)

image

Frizbee (no typos)

image

Arinae (no typos)

image

FZF for comparison

image

Frizbee (typos)

image

Arinae (typos) - note the difference in the number of results compared to frizbee

image

Benches (w/ criterion: more precise, hooks directly into the code)

cargo bench --bench read_and_match
   Compiling skim v3.5.0 (/home/loric/src/skim)
    Finished `bench` profile [optimized] target(s) in 1m 21s
     Running benches/read_and_match.rs (target/release/deps/read_and_match-7be3557b13dbf6c1)
Gnuplot not found, using plotters backend
Benchmarking default: Warming up for 3.0000 s
Warning: Unable to complete 10 samples in 5.0s. You may wish to increase target time to 37.9s.
default                 time:   [3.6986 s 3.7315 s 3.7645 s]
                        change: [−2.0247% −0.7908% +0.4336%] (p = 0.25 > 0.05)
                        No change in performance detected.
Found 1 outliers among 10 measurements (10.00%)
  1 (10.00%) high mild

Benchmarking query: Warming up for 3.0000 s
Warning: Unable to complete 10 samples in 5.0s. You may wish to increase target time to 43.9s.
query                   time:   [4.5603 s 4.7987 s 5.0747 s]
                        change: [+1.0711% +7.2752% +14.625%] (p = 0.05 > 0.05)
                        No change in performance detected.
Found 3 outliers among 10 measurements (30.00%)
  1 (10.00%) low severe
  1 (10.00%) low mild
  1 (10.00%) high severe

Benchmarking query_frizbee: Warming up for 3.0000 s
Warning: Unable to complete 10 samples in 5.0s. You may wish to increase target time to 44.3s.
query_frizbee           time:   [4.6213 s 4.8387 s 5.1942 s]
                        change: [−10.587% +0.9013% +13.415%] (p = 0.90 > 0.05)
                        No change in performance detected.
Found 2 outliers among 10 measurements (20.00%)
  1 (10.00%) high mild
  1 (10.00%) high severe

Benchmarking query_ari: Warming up for 3.0000 s
Warning: Unable to complete 10 samples in 5.0s. You may wish to increase target time to 49.4s.
query_ari               time:   [4.0759 s 4.4534 s 5.0057 s]
                        change: [−13.815% +1.4402% +30.916%] (p = 0.91 > 0.05)
                        No change in performance detected.
Found 1 outliers among 10 measurements (10.00%)
  1 (10.00%) high severe

Benchmarking query_frizbee_typos: Warming up for 3.0000 s
Warning: Unable to complete 10 samples in 5.0s. You may wish to increase target time to 44.6s.
query_frizbee_typos     time:   [4.2940 s 4.3626 s 4.4337 s]

Benchmarking query_ari_typos: Warming up for 3.0000 s
Warning: Unable to complete 10 samples in 5.0s. You may wish to increase target time to 43.3s.
query_ari_typos         time:   [4.2142 s 4.4391 s 4.6898 s]
Found 2 outliers among 10 measurements (20.00%)
  1 (10.00%) low severe
  1 (10.00%) high severe

@codecov
Copy link

codecov bot commented Feb 23, 2026

@LoricAndre LoricAndre changed the title feat: add SkimV3 algorithm feat: add Arinae algorithm Feb 26, 2026
@LoricAndre LoricAndre marked this pull request as ready for review February 26, 2026 21:30
@LoricAndre
Copy link
Contributor Author

@saghen, @ibhagwan, sorry for the ping but we lack a better comm channel and I think you might be interested in this ! Feel free to ignore me if I'm wrong.

@saghen
Copy link

saghen commented Feb 26, 2026

This looks really neat! I'll definitely read through your implementation soon. One thing though, I noticed that your frizbee usage might be hurting the performance a bit.

https://github.com/skim-rs/skim/pull/990/changes#diff-98065f4e9dfd493f60cd03421c7288370b7765c98685c1dd23270c0eb6195aacL34

You'll want to use the Matcher API rather than SmithWatermanMatcher so that we perform prefiltering, which should lead to quite the speed-up.

https://github.com/skim-rs/skim/pull/990/changes#diff-668cf417d0fb5e51ead9cf7eb691ab2246e71e1c4b6ccc86bfb1eed4ccc66834R34-R57

Your micro benches use the fuzzy_indices API which will be a lot slower than fuzzy_match for frizbee as well. In general, it might be worth trying to pass the whole list into the Matcher::match_list API to see how the performance compares, as that's the intended entrypoint.

Also, you might want to try using the parallel implementation in frizbee, as it scales much better than rayon in my testing

@ibhagwan
Copy link

Very interesting @LoricAndre, will try!

@LoricAndre
Copy link
Contributor Author

This looks really neat! I'll definitely read through your implementation soon. One thing though, I noticed that your frizbee usage might be hurting the performance a bit.

https://github.com/skim-rs/skim/pull/990/changes#diff-98065f4e9dfd493f60cd03421c7288370b7765c98685c1dd23270c0eb6195aacL34

You'll want to use the Matcher API rather than SmithWatermanMatcher so that we perform prefiltering, which should lead to quite the speed-up.

https://github.com/skim-rs/skim/pull/990/changes#diff-668cf417d0fb5e51ead9cf7eb691ab2246e71e1c4b6ccc86bfb1eed4ccc66834R34-R57

Your micro benches use the fuzzy_indices API which will be a lot slower than fuzzy_match for frizbee as well. In general, it might be worth trying to pass the whole list into the Matcher::match_list API to see how the performance compares, as that's the intended entrypoint.

Also, you might want to try using the parallel implementation in frizbee, as it scales much better than rayon in my testing

And here I was, thinking I finally optimized it enough to catch up to frizbee's performance 😂
More seriously, I'll take a look at Matcher, and finding something better than rayon is next on the to-do list as it seems it's one of the biggest bottlenecks right now (and part of the reason I sit at 500-600% CPU usage and not close to 2400% as I would expect on my system). I'll take a look at what you did then.

@LoricAndre
Copy link
Contributor Author

I took a look at Frizbee's Matcher API but it seems slower in my use (since I run it on a per-item base):

image

The average performance got up by 45+%, but frizbee's only got up by 35+%

@saghen
Copy link

saghen commented Feb 27, 2026

I wish I had the time to investigate this more, but I'm slammed right now. I did a quick test by replacing the frizbee code with the code below, and I've included the results below as well. I realize now the main performance issue is instantiating the Matcher on every match_* call, as we have to allocate the score matrix every time. Ideally you'd instantiate it once and re-use it, but that'd be a non-trivial code change it looks like, as the FuzzyMatcher trait would need to have a set_choice(&mut self, choice: &str) function to instantiate the Matcher and then fuzzy_* would need to be &mut self.

c.bench_function("micro_frizbee", |b| {
    let mut matcher = frizbee::Matcher::new("test", &Default::default());
    b.iter(|| {
        let mut count = 0u64;
        for _ in matcher.match_iter(&lines) {
            count += 1;
        }
        count
    });
});
c.bench_function("micro_typos_frizbee", |b| {
    let config = frizbee::Config {
        max_typos: Some(1),
        ..Default::default()
    };
    let mut matcher = frizbee::Matcher::new("test", &config);
    b.iter(|| {
        let mut count = 0u64;
        for _ in matcher.match_iter(&lines) {
            count += 1;
        }
        count
    });
});
c.bench_function("micro_frizbee_parallel", |b| {
    b.iter(|| {
        let mut count = 0u64;
        for _ in frizbee::match_list_parallel("test", &lines, &Default::default(), 16) {
            count += 1;
        }
        count
    });
});
micro_skim_v2           time:   [352.28 ms 352.98 ms 353.74 ms]

micro_frizbee           time:   [73.265 ms 73.400 ms 73.554 ms]
micro_typos_frizbee     time:   [109.55 ms 109.87 ms 110.21 ms]
micro_frizbee_parallel  time:   [6.1193 ms 6.1390 ms 6.1607 ms]

micro_arinae            time:   [230.56 ms 230.88 ms 231.23 ms]
micro_arinae_range      time:   [124.95 ms 125.13 ms 125.33 ms]
micro_arinae_score      time:   [221.41 ms 221.68 ms 222.00 ms]
micro_typos_arinae      time:   [391.23 ms 391.63 ms 392.06 ms]

@saghen
Copy link

saghen commented Feb 27, 2026

Btw you might want to explore incremental matching in your arinae implementation as well! saghen/frizbee#65

@LoricAndre
Copy link
Contributor Author

Yeah recreating the matcher at each iteration is stupid of me. I'll spend some more time looking into this, thanks
And the incremental matching is also somewhere later in the to-do list

@LoricAndre
Copy link
Contributor Author

Yeah recreating the matcher at each iteration is stupid of me. I'll spend some more time looking into this, thanks And the incremental matching is also somewhere later in the to-do list

I checked a bit more, and the current architecture does not allow me to reuse matchers, even using some ThreadLocal magic. I'll look into this further as I add ways to send entire chunks at a time to the FuzzyMatchers later.

Copy link
Contributor Author

@LoricAndre LoricAndre left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Initial review

@LoricAndre LoricAndre merged commit c652744 into master Mar 1, 2026
14 checks passed
@LoricAndre LoricAndre deleted the skim-v3 branch March 1, 2026 17:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants