Skip to content

Competitive benchmarks

Where does Verbora stand against the Rust ecosystem? This page reports like-for-like, version-pinned measurements — same input, equivalent result, and a public loss whenever another library is faster.

Read by capability, not by one global score. A tokenizer, a stemmer and a string-distance function solve different problems. Each section therefore shows the measured workload, the result and its limits.
CoverageWhat it means
314 timed comparisons13 capabilities with a fair Rust comparison
3 capabilities without a fair peerWordNet, sentence analysis and sentiment are documented on their feature pages instead
1 primary metricCriterion median, single-threaded, on the hardware below

Start with String distance, Tokenizers, Stemmers, or Spellcheck. Exact harnesses and raw data are linked from each section; reproduction instructions are at the end.

Methodology and audit details

Benchmark methodology

CPUIntel(R) Core(TM) i9-14900KF (32 threads)
Memory125 GiB
OSLinux 7.0.11-76070011-generic
rustc1.97.1, --release (opt-level = 3, lto = "thin", codegen-units = 16) — identical [profile.release]/[profile.bench] to the main Verbora workspace, not a tuned profile for this audit
Node.jsv25.9.0 (used only for the three JavaScript-library-only modules linked above, not for the tables on this page)
Verbora commitaf1aee9, crate version 0.1.0
DatasetsShared word/name/pair lists from benches/data/*.json (tools/bench-data/generate.py, one generator read by every implementation); the 13-language, 4-tier UDHR corpus for language-detection accuracy (sourced below)
WarmupCriterion's own warmup phase before every measured sample (400 ms–1 s per group; see below)
SamplesCriterion's default 100 per benchmark, reduced for the most expensive groups: 30 for language-detection-by-length, 20 for spellcheck construction, 15 for POS-tagging cold start (model load)
MetricMedian, per Criterion's own robust-statistics estimate — not mean, per this project's own PRIMARY METRIC policy
Threads1 (single-threaded) for every benchmark on this page — no parallel API is exercised anywhere in this audit; see Thread counts
Sourcebenchmarks/competitive/rust-competitors/benches/*.rs (one file per module), raw Criterion output under benchmarks/competitive/results/raw/, joined into results/results.json
DateResults captured 2026-08-15/17 (results/metadata.json's timestamp: 2026-08-15T23:50:55Z; the Metaphone, unrestricted-Damerau, N-Grams, and the eight byte-exact phonetics groups are the freshest, from 2026-08-17)

Every number on this page is read from Criterion's own saved estimates — joined into that results.json file for most modules, and read directly from the same per-group raw estimates.json files for the freshest groups named in the Date row — none is retyped from memory or rounded inconsistently; the relative-speedup figures are computed from the raw median_ns values at page-generation time. See Reproducing these numbers for the exact commands that regenerate all of it from a clean checkout.

One machine, one run. Per the project's own environment policy (results/metadata.json's own note): "a single dedicated run on one physical machine with no other significant workload active" — CPU affinity pinning, thermal monitoring and containerization were evaluated and deliberately not built, since the spec marks all three optional. Treat exact figures as one machine's numbers and the ratios/orders-of-magnitude as the more portable signal, exactly as String distance results already asks of its own numbers.

Thread counts

No benchmark on this page compares a parallel implementation against a sequential one. Every call measured here — Verbora's and every competitor's — runs on a single thread; where Verbora exposes a parallel-feature batch API (phonetics' par_encode_batch, language detection's par_detect_batch), that comparison is sequential-vs-sequential too, because no competitor in this audit exposes an equivalent batch-parallel API to compare against. Verbora's own sequential-vs-parallel numbers, with thread counts disclosed, live on the Parallelism page instead — a different question from this one.

How to read these tables

  • Time (median) is Criterion's median estimate for one call of the benchmarked operation (or, where noted, one call over a fixed-size batch — the input-size column says which).
  • Throughput is 1 ÷ median time — calls of this exact operation per second at the measured latency. It is a call-rate figure, not a per-item/per-token throughput, because this audit does not have a verified items-per-call count for every operation; where a benchmark already operates over a fixed-size batch (e.g. "1024 words"), throughput is still batches/second, not words/second — read the input-size column together with it.
  • Relative is each row's time divided by the fastest row's time in that same table, labeled slower above 1.00×. Rows are ordered by time, not by library — Verbora is not pinned to the top row, and several tables on this page show it losing.
  • Where a comparison is explicitly not a fair like-for-like ratio (a different dictionary, a different corpus), no Relative column is shown at all, and the table says so — see Spellcheck for the two cases this applies to.

How these numbers were audited

The page's audited core — 290 of the 314 comparisons — was cleared by an independent fairness audit that read every benchmark file and correctness test in this workspace, re-ran the Rust suite and the language-accuracy report itself, and cross-referenced all 133 "Verbora loses" rows in results.json (each (benchmark, competitor) pair where Verbora's median is slower than that competitor's, the same per-row comparison the Relative column below uses) against docs/PERFORMANCE_GAPS.md. Its verdict: every comparison in that set is FAIR — same input, genuinely equivalent (or honestly narrowed and labeled) semantics, black_box on every call's input and output, correctness-before-performance tests that were run and passed, and version pins of =x.y.z on every third-party crate. Two items the audit flagged as borderline rather than unfair are called out inline where they occur: the normalizers' accented-input case (Normalizers) and the WhatlangDetector wrapper-overhead check (Language detection), neither of which is a ranked "X beats Y" comparison in the first place.

The remaining 24 comparisons — the byte-exact encoder table in Phonetics — carry a stronger correctness check in place of that audit's narrowed-semantics review: byte-exact output equality with the competitor itself, asserted in tests/phonetics_correctness.rs and independently re-verified by an adversarial audit that differentially fuzzed 104,114 inputs per encoder against rphonetic with zero mismatches, proved every documented divergence exactly as narrow as each module's own documentation claims, and mutation-tested the correctness suites.

Results by capability

Per this project's OVERALL SCORE policy, there is no combined ranking anywhere on this page. Tokenization, stemming, spell-correction and every other capability below are different workloads solving different problems — mixing their numbers into one score would hide more than it reveals.

Distance

Verbora's edit-distance functions (docs/COMPETITIVE_BENCHMARKS.md§1.8) against strsim 0.11.1 and rapidfuzz 0.5.0 — the Rust ecosystem's de-facto-standard string-similarity crate (strsim: ~990M downloads) and the tightest single-crate algorithm match found. Both are char-indexed like Verbora's UTF-16-unit indexing (BMP-equivalent), and restricted to ASCII input here specifically so that distinction cannot matter. Unlike the heuristic encoders elsewhere on this page, Levenshtein, Damerau-Levenshtein, Hamming, Jaro and Jaro-Winkler are exact, well-specified integer/float functions with a single correct answer per input — their equivalence is established by the matrix's own algorithm research (docs/COMPETITIVE_BENCHMARKS.md §1.8) rather than a dedicated runtime correctness test, the same way the rest of this page uses one wherever a variant could plausibly disagree.

rapidfuzz implements Myers/Hyyrö bit-parallel Levenshtein (O(nm/64)). Verbora matches that algorithmic class with a single-word bit-vector fast path plus a multi-word block extension (Hyyrö's 2003 generalisation, following rapidfuzz's own hyrroe2003_block structure directly rather than re-deriving it — verified independently line-by-line, then adversarially fuzz- and mutation-tested against the trusted scalar DP before being trusted for anything). The kernels' pattern-match (Peq) tables are flat/packed bit tables rather than a hash map, and the single-word gate covers 1–64 units. Bit-parallelism extends beyond plain Levenshtein too: restricted-Damerau/OSA kernels (Hyyrö's 2003 transposition extension of Myers, single-word and multi-word block, gated to unit costs), and Jaro/Jaro-Winkler match-flagging kernels in Verbora's own greedy orientation. Unrestricted Damerau has no bit-vector formulation, so it runs on scalar kernels dispatched by operand length — a table-free stack matrix for tiny inputs, then register- and memory-carried two-rows-plus-per-symbol-snapshot kernels — instead of the full f64 cost+parent matrices. Every kernel is parity-verified by differential tests against the retained scalar implementations, plus an independent adversarial audit with mutation testing, before being trusted; Hamming has no bit-parallel kernel.

Plain Levenshtein beats all five Rust competitors at every size from 4 to 1024 characters (Verbora is 1.09× faster at 1024 characters, 1.77× at 16). Restricted Damerau/OSA beats every competitor at every size, and Jaro/Jaro-Winkler wins every size too. Unrestricted Damerau beats rapidfuzz at every size and strsim at four of five, with one ~2% loss at 1024 characters — see its own section below. See PERFORMANCE_GAPS.md entry 26 for the mechanism and verification story these kernels build on.

Levenshtein

stringmetrics 2.2.2 joins as a fourth full-equivalence (Yes/Yes) competitor here and in Hamming below — char-indexed like strsim/ rapidfuzz, just without a Damerau-Levenshtein implementation at all (its damerau module is commented out of both mod and pub use in the published crate, confirmed by reading stringmetrics-2.2.2's own source — not merely unused, genuinely uncompiled).

LibraryVersionLanguageTime (median, 1024 chars)ThroughputRelative
Verbora0.1.0Rust29.07 µs34.4K/s1.00×
rapidfuzz0.5.0Rust31.72 µs31.5K/s1.09× slower
strsim0.11.1Rust625.21 µs1.6K/s21.51× slower
stringmetrics2.2.2Rust915.47 µs1.1K/s31.49× slower
Input / shapeVerborarapidfuzzstrsimstringmetrics
4 random14.8 ns32.0 ns20.6 ns26.0 ns
16 random41.9 ns74.3 ns271.3 ns169.7 ns
64 random164.7 ns247.8 ns2.86 µs2.91 µs
256 random2.09 µs3.30 µs41.83 µs55.70 µs
1024 random29.07 µs31.72 µs625.21 µs915.47 µs
1024 near (d = 1)306 ns806 ns655 µs580 ns

Verbora is the fastest implementation at every size — against all four char-indexed competitors here and the two byte-level ones below. Against rapidfuzz, the closest competitor and the only other bit-parallel implementation in the table: 2.16× faster at 4 characters, 1.77× at 16, 1.50× at 64, 1.58× at 256, and 1.09× at 1024 — the margin is widest at small sizes, where the flat [u64; 256]/packed-distinct-rows Peq tables and the 1–64-unit single-word gate keep per-call setup cost low, and narrowest at 1024, where both sides run the same class of multi-word block algorithm. Against strsim the win is 1.39× (4 chars) up to 21.5× (1024); against stringmetrics 1.76× up to 31.5× — neither scalar design has a bit-vector formulation to close the gap with.

The random rows are intentionally retained as their own workload. They do not show the common-affix optimization: independent strings have almost no affix to remove. The same competitive harness also measures a 1,024-unit pair with one central substitution (1024-near):

LibraryMedian (1024-near)Relative to Verbora
Verbora306 ns1.00×
editdistancek182 ns1.68× faster
stringmetrics580 ns1.89× slower
rapidfuzz806 ns2.63× slower
triple_accel541 µs1,766× slower
strsim655 µs2,141× slower

These are the current local competitive medians (30 samples, release build, Intel i9-14900KF). The important comparison is within this row: Verbora's previous near-1024 result was about 30.8 µs, so the affix path reduces it to about 306 ns (roughly 101×). editdistancek remains faster on this specific near-match shape because its bounded-edit algorithm is designed for small distances; that is a legitimate workload-specific result, not hidden by the aggregate random table.

Competitive shape suite

The competitive harness also has a dedicated levenshtein_edge_shapes group. It uses the exact same lowercase-ASCII, unit-cost inputs for every implementation, so both char-indexed and byte-indexed competitors remain directly comparable. It keeps the shape-sensitive results separate from the random-size table above:

CaseOperandsWhat it verifies
near/10241,024 vs. 1,024; one central substitutionCommon-prefix/suffix trimming
disjoint/10241,024 vs. 1,024; no character overlapDisjoint-alphabet early exit
late-overlap/65x1000065 vs. 10,000; overlap only at the endThe disjoint probe's worst placement

This is deliberately a separate group, rather than an extra median in the table above: random pairs, near pairs and disjoint alphabets exercise different valid algorithmic shortcuts, and blending them would hide that trade-off. It benchmarks Verbora, strsim, rapidfuzz, stringmetrics, triple_accel and editdistancek; the accompanying correctness test checks that all six return the same distance on every timed shape. Reproduce it with:

bash
cd benchmarks/competitive/rust-competitors
cargo test --test distance_correctness levenshtein_competitors_agree_on_the_timed_edge_shapes
cargo bench --bench distance -- levenshtein_edge_shapes

Damerau–Levenshtein (unrestricted)

LibraryVersionLanguageTime (median, 1024 chars)ThroughputRelative
strsim0.11.1Rust1.87 ms535.8/s1.00×
Verbora0.1.0Rust1.91 ms524.6/s1.02× slower
rapidfuzz0.5.0Rust2.12 ms472.0/s1.14× slower
Input sizeVerborarapidfuzzstrsim
429.5 ns70.6 ns55.5 ns
16396.6 ns530.7 ns441.6 ns
644.65 µs7.51 µs6.95 µs
256116.22 µs133.43 µs120.04 µs
10241.91 ms2.12 ms1.87 ms

Verbora beats rapidfuzz at every size (2.39× at 4 characters, 1.34× at 16, 1.61× at 64, 1.15× at 256, 1.11× at 1024) and beats strsim at four of the five (1.88× at 4, 1.11× at 16, 1.49× at 64, 1.03× at 256). The one remaining loss in this comparison is ~2% to strsim at 1024 characters (1.91 ms vs. 1.87 ms) — shown as a loss, and a structural one rather than an open tuning gap: a probe evaluating nothing but the bare loop-carried min-chain of Verbora's pinned recurrence already costs 1.86–1.88 ms at this size, and the recurrence's divergence from textbook DL (next paragraph) rules out the Zhao–Sahni candidate pruning strsim uses to get under that floor. Distance mode never builds the full f64 cost+parent matrices: the byte path dispatches across three measured tiers — a table-free stack-matrix kernel for operands of at most 8 bytes, a register-carried two-rows-plus-per-symbol-snapshot kernel up to 128 bytes, and a memory-carried variant beyond (integer cells: u16 while the combined input length fits, u32 after) — and UTF-16 input runs the generic snapshot kernel. Every tier evaluates the same recurrence exactly, differentially verified against the retained full-matrix oracle and against the neighbouring tiers on their shared domains.

One structural caveat, documented rather than glossed over: strsim and rapidfuzz implement textbook (Lowrance–Wagner/Zhao–Sahni) unrestricted Damerau-Levenshtein, while Verbora computes its pinned reference recurrence, which is deliberately not textbook DL — it measurably diverges (on 38.6% of random small-alphabet pairs: "bb""abbb" is 1 under Verbora's recurrence, 2 under the textbook algorithm, and the recurrence is not even symmetric; see crates/verbora-distance/src/levenshtein.rs's own doc comment). That divergence is also exactly what structurally forbids Verbora from adopting the competitors' linear-space algorithm or common-affix trimming. Read this table as a same-shape-of-work comparison over the same shared ASCII corpus, not as three implementations of one identical function on all inputs — a weaker equivalence than the Yes/Yes rows above, in the same spirit as the phonetics module's Partial rows.

Damerau–Levenshtein (restricted / OSA)

LibraryVersionLanguageTime (median, 1024 chars)ThroughputRelative
Verbora0.1.0Rust32.47 µs30.8K/s1.00×
rapidfuzz0.5.0Rust45.06 µs22.2K/s1.39× slower
strsim0.11.1Rust2.43 ms411.5/s74.84× slower
Input sizeVerborarapidfuzzstrsim
415.9 ns30.2 ns67.5 ns
1646.2 ns83.6 ns321.4 ns
64179.3 ns264.2 ns4.56 µs
2562.39 µs3.02 µs140.64 µs
102432.47 µs45.06 µs2.43 ms

Verbora is the fastest at every size. Restricted Damerau's one-transposition-back reach needs more state than plain Levenshtein's two-row shape, so it gets its own bit-parallel kernels implementing Hyyrö's 2003 transposition extension of Myers' algorithm — a single-word kernel plus a multi-word block generalisation, gated to unit-cost options, with the scalar three-row DP retained for every non-unit-cost call and as the differential-test oracle. Against rapidfuzz, the only other bit-parallel OSA here: 1.90× faster at 4 characters, 1.81× at 16, 1.47× at 64, 1.26× at 256, 1.39× at 1024. Against strsim's scalar implementation the margin runs 4.25× (4 chars) up to 74.8× (1024). triple_accel's byte-level rdamerau is covered in the byte-level subsection below.

Hamming

LibraryVersionLanguageTime (median, 1024 chars)ThroughputRelative
Verbora0.1.0Rust272.9 ns3.7M/s1.00×
strsim0.11.1Rust552.2 ns1.8M/s2.02× slower
stringmetrics2.2.2Rust567.1 ns1.8M/s2.08× slower
rapidfuzz0.5.0Rust603.9 ns1.7M/s2.21× slower

Verbora wins Hamming against every char-indexed competitor here from 16 characters up — a fixed per-call setup cost (still small in absolute terms, nanoseconds) makes Verbora the slowest of the four specifically at 4 characters, the one exception, before it pulls ahead and stays ahead. No bit-parallel state to build on either side, so this stays an apples-to-apples scalar-vs-scalar race outside that one small-input crossover. (triple_accel's genuinely SIMD-accelerated Hamming is a different story — see the byte-level subsection below, where Verbora loses decisively instead.)

Input sizeVerborarapidfuzzstrsimstringmetrics
46.9 ns6.3 ns4.8 ns2.4 ns
169.2 ns17.7 ns14.0 ns15.1 ns
6421.0 ns53.0 ns43.4 ns43.9 ns
25670.1 ns165.8 ns149.8 ns145.5 ns
1024272.9 ns603.9 ns552.2 ns567.1 ns

Jaro / Jaro–Winkler

LibraryVersionLanguageTime (median, 1024 chars)ThroughputRelative
Verbora0.1.0Rust10.32 µs96.9K/s1.00×
rapidfuzz0.5.0Rust13.00 µs76.9K/s1.26× slower
strsim0.11.1Rust330.91 µs3.0K/s32.06× slower

Verbora beats both competitors at every size. rapidfuzz's Jaro/Jaro-Winkler is bit-parallelized (rapidfuzz-0.5.0/src/distance/jaro.rs); Verbora matches that with its own bit-parallel match-flagging kernels (word-sized plus multi-word block) in its own greedy match orientation, with the scalar loop retained for inputs of at most 16 units and as the differential-test oracle, and the fractional-transposition semantics preserved exactly. Against rapidfuzz: 3.42× faster at 4 characters, 1.10× at 16, 2.06× at 64, 1.13× at 256, 1.26× at 1024.

Input sizeVerborarapidfuzzstrsim
410.6 ns36.3 ns27.8 ns
1674.9 ns82.7 ns165.6 ns
64124.7 ns257.3 ns1.52 µs
2561.82 µs2.06 µs22.92 µs
102410.32 µs13.00 µs330.91 µs

Sørensen–Dice against a Rust crate, and plain Jaro against a JavaScript library, are not benchmarked (plain Jaro against the Rust competitors above is — see the table just before this paragraph) — the matrix records both as narrowed/no-fair-competitor per docs/COMPETITIVE_BENCHMARKS.md §1.8. See benches/distance.rs's own module doc comment for the full accounting of every row this module benchmarks and every row it deliberately does not.

triple_accel and editdistancek — byte-level, ASCII-only-fair

Kept separate from every table above rather than merged into them: triple_accel and editdistancek operate on raw &[u8] bytes, not chars/UTF-16 code units like Verbora/strsim/rapidfuzz/stringmetrics — numerically identical to the char-indexed approach on the ASCII-only corpus this whole module shares, but genuinely different on non-ASCII input, which the research matrix marks Partial/Selected cases rather than the full Yes/Yes equivalence every row above carries. triple_accel is genuinely SIMD-accelerated (AVX2/SSE4.1); editdistancek is a Myers-style banded/diagonal algorithm over isize buffers. Byte-identical correctness against Verbora verified in tests/distance_correctness.rs before any number below was trusted — including a check that this benchmark calls triple_accel::rdamerau, not the crate's separate rdamerau_exp function, which carries a real, independently-confirmed upstream bug (see Upstream bugs found) unrelated to the plain rdamerau used here.

Levenshtein — the same bit-vector kernels above win here too, by wider margins than against rapidfuzz/strsim:

LibraryVersionTime (median, 1024 chars)Relative
Verbora0.1.029.07 µs1.00×
triple_accel0.4.0497.94 µs17.13× slower
editdistancek1.0.21.06 ms36.46× slower
Input sizeVerboratriple_acceleditdistancek
414.8 ns66.3 ns44.0 ns
1641.9 ns229.1 ns373.2 ns
64164.7 ns1.63 µs4.79 µs
2562.09 µs36.67 µs69.80 µs
102429.07 µs497.94 µs1.06 ms

Verbora wins at every size, outright — a clean 5.5× win at 16 characters (41.9 ns vs. 229.1 ns), the flat-table Peq setup and 1–64-unit single-word gate keeping per-call overhead low.

Restricted Damerau-Levenshtein — Verbora's OSA bit-parallel kernels win at every size, by a margin that widens with input: 4.80× faster at 4 characters, 5.64× at 16, 11.6× at 64, 21.1× at 256, and 22.7× at 1024 (32.47 µs vs. triple_accel's 737.06 µs).

Hamming — the widest gap in this whole module: triple_accel's Hamming is a vectorized XOR-and-popcount over the whole string with no data-dependent branching, versus Verbora's scalar per-position comparison loop — 18.6× faster at 1024 characters (14.7 ns vs. Verbora's 272.9 ns), the gap widening steadily from a modest 2.3× at 4 characters.


Tokenizers

Verbora's RegexpTokenizer(\s+) and WordTokenizer (docs/COMPETITIVE_BENCHMARKS.md§1.1) against tantivy 0.26.1's WhitespaceTokenizer/SimpleTokenizer and Hugging Face tokenizers 0.23.1's WhitespaceSplit/Whitespace pre-tokenizers, called in isolation (never through HF's full BPE pipeline). Narrowed to punctuation-free ASCII text, where all three character classes are provably identical — boundary-exact agreement (not just token-count agreement) proven in tests/tokenizers_correctness.rs before any number below was trusted.

Whitespace tokenization

LibraryVersionLanguageTime (median, 77,684 B)ThroughputRelative
Verbora0.1.0Rust49.82 µs20.1K/s1.00×
tantivy0.26.1Rust98.31 µs10.2K/s1.97× slower
tokenizers (Hugging Face)0.23.1Rust1.65 ms606.1/s33.12× slower
Input size (bytes)Verboratantivytokenizers (HF)
123101.3 ns112.3 ns1.90 µs
1,187567.6 ns962.9 ns15.99 µs
9,7094.27 µs10.06 µs133.89 µs
77,68449.82 µs98.31 µs1.65 ms
Verbora beats tantivy at every size (1.11× at 123 B, 1.70× at 1,187 B, 2.36× at 9,709 B, 1.97× at 77,684 B). See PERFORMANCE_GAPS.md entry 3 for the mechanism: RegexpTokenizer drives capture-free patterns through find_iter instead of the general regex crate's capture machinery, and the exact \s+ pattern gets a dedicated ASCII-first SWAR whitespace scanner — proven identical to the regex engine's \s by an exhaustive test over every Unicode scalar value (~1.1M values, checking regex's \s against char::is_whitespace). The win comes while matching full Unicode whitespace, where tantivy's WhitespaceTokenizer tests only c.is_ascii_whitespace() — a strictly more general character class winning anyway. Verbora also beats Hugging Face's pre-tokenizer at every size in the same table (18.8×–33.1×).

Word tokenization

LibraryVersionLanguageTime (median, 77,684 B)ThroughputRelative
Verbora0.1.0Rust115.12 µs8.7K/s1.00×
tantivy0.26.1Rust124.91 µs8.0K/s1.09× slower
tokenizers (Hugging Face)0.23.1Rust1.85 ms541.1/s16.05× slower
Input size (bytes)Verboratantivytokenizers (HF)
123182.9 ns136.6 ns2.22 µs
1,1871.28 µs1.15 µs19.08 µs
9,7099.45 µs11.66 µs166.89 µs
77,684115.12 µs124.91 µs1.85 ms

A genuine crossover, reported in both directions per this project's no-cherry-picking rule: tantivy's hand-written SimpleTokenizer is slightly ahead at the two smallest sizes, Verbora's hand-written WordRuns scanner pulls ahead at the two largest — see PERFORMANCE_GAPS.md entry 4. Both scanners do the same shape of work here — the same hand-written-scanner-vs-hand-written-scanner matchup as the whitespace row above — so every ratio in this table stays under 1.5×.

Sentence tokenization

SentenceTokenizer against unicode-segmentation 1.13.3's unicode_sentences()/split_sentence_bounds(), on the narrowed plain-declarative-sentence domain both sides provably agree on (no abbreviations/URIs/digits/quotes/brackets — the boundary-exact agreement is proven in tests/tokenizers_correctness.rs before any number below was trusted).

LibraryVersionLanguageTime (median, 118,588 B)ThroughputRelative
Verbora0.1.0Rust1.17 ms854.7/s1.00×
unicode-sentences1.13.3Rust1.36 ms735.3/s1.16× slower
unicode-bounds1.13.3Rust1.35 ms740.7/s1.15× slower
Input size (bytes)Verboraunicode-sentencesunicode-bounds
2002.05 µs2.37 µs2.37 µs
1,83617.44 µs21.02 µs20.99 µs
14,806146.16 µs172.11 µs174.95 µs
118,5881.17 ms1.36 ms1.35 ms

Verbora wins at every size, by a flat 1.15×–1.21×, and the gap does not widen with document size: 2048 sentences is 8× the document size of 256 sentences and roughly 8× the wall-clock too — the linear scaling a single-pass algorithm should have, with no O(sentences²) term in the placeholder-restoration pass. See PERFORMANCE_GAPS.md entry 23 for the placeholder-restoration mechanism in full. The residual ~1.15×–1.21× is a flat, size-independent constant factor consistent with unicode-segmentation's single forward-scan design still being a genuinely leaner data path than Verbora's placeholder-mask-and-restore one — closing it further would need a different, non-placeholder-based algorithm, a much larger change not attempted here.


N-Grams

Character n-gram generation with frequency counting (docs/COMPETITIVE_BENCHMARKS.md§1.2) against ngrammatic 0.7.0's Ngram/NgramBuilder — the character n-gram + frequency-count generator its headline Corpus/search fuzzy-matching feature is itself built on. Only that generator is benchmarked here: Corpus/search solves a different problem (fuzzy corpus search) with no Verbora equivalent, and is not compared. Both sides pad with arity - 1 copies of the same character and slide an identical window over every word in the shared 20,000-word list; byte-identical (gram, count) output is proven at arity 2 and arity 3 in tests/ngrams_correctness.rs before any number below was trusted.

A genuine split by arity, not a clean sweep. Verbora wins bigram generation on every one of 3 independent runs (~1.07×–1.16× faster) and loses trigram generation on every one of the same 3 runs, by a smaller but equally consistent margin (ngrammatic ~1.03×–1.08× faster). See PERFORMANCE_GAPS.md entry 38 for the full 3-run reading and the profiling-backed theory for why the two arities diverge.
LibraryVersionLanguageTime (median, bigrams, 20,000 words)ThroughputRelative
Verbora0.1.0Rust8.50 ms2.35M/s1.00×
ngrammatic0.7.0Rust9.88 ms2.02M/s1.16× slower
LibraryVersionLanguageTime (median, trigrams, 20,000 words)ThroughputRelative
ngrammatic0.7.0Rust11.66 ms1.72M/s1.00×
Verbora0.1.0Rust11.98 ms1.67M/s1.03× slower

Both implementations do the same conceptual work — pad, slide a window, fold into a (gram, count) map — over the same input, so the residual is plausibly a small-string-accumulation difference rather than an algorithmic one: ngrammatic accumulates directly into a HashMap<SmolStr, usize>, whose small-string optimization skips a heap allocation for any gram that fits inline (every bigram and trigram over this word list does), while Verbora's benchmarked path builds each gram through its generic ngrams() engine and folds the result into a HashMap<String, usize> with one String allocation per unique gram. That difference plausibly narrows (and eventually reverses) as grams get longer relative to the inline-capacity boundary — a source-read hypothesis, not yet confirmed with a profiler; see PERFORMANCE_GAPS.md entry 38 above for the full accounting.


Stemmers

Nine canonical Snowball-algorithm languages (de es fr it nl no pt ru sv) against two independent Snowball-to-Rust ports: rust-stemmers 1.2.0 (the official Snowball compiler's own output, and the Rust ecosystem's de-facto Snowball crate) and snowball_stemmers_rs 1.0.1 (a second, independently-generated port from the same compiler, published by the original SymSpell author). English is benchmarked separately against nltk-porter 0.1.0 and porter-stemmer 0.1.2: both competitors' "English" is a documented different algorithm from rust-stemmers' Snowball Porter2 and matches Verbora's original-1980 Porter instead (docs/COMPETITIVE_BENCHMARKS.md§1.3). Byte-exact agreement on every benchmarked word verified in tests/stemmers_correctness.rs — including several real, narrow divergences found and excluded from the benchmarked domain rather than hidden (Russian ёе folding, Dutch's sticky cross-call state, porter-stemmer's single isolated "sky""ski" bug).

The least flattering comparison on this page. Both Snowball competitors beat Verbora on seven of the nine shared languages, at every batch size measured; German is a clean win and Dutch is a genuine split (Verbora beats rust-stemmers, loses to snowball_stemmers_rs). Both competitors compile their suffix rules to a binary-search find_among via the official Snowball compiler; Verbora's stemmer does a literal linear scan through the same rule tables, guarded by a cheap last-code-unit reject in the shared suffix-matching helper (ends_with) before it commits to a full comparison. See PERFORMANCE_GAPS.md entry 34 for the full mechanism and the alternative approaches considered.
LanguageVerbora (1024-word batch)rust-stemmers (1024-word batch)Faster
de136.97 µs141.95 µsVerbora, 1.04×
nl128.99 µs217.56 µsVerbora, 1.69×
es640.01 µs125.94 µsrust-stemmers, 5.08×
fr481.95 µs188.55 µsrust-stemmers, 2.56×
it532.07 µs229.00 µsrust-stemmers, 2.32×
no249.77 µs50.06 µsrust-stemmers, 4.99×
pt614.10 µs155.92 µsrust-stemmers, 3.94×
ru588.00 µs89.87 µsrust-stemmers, 6.54×
sv261.10 µs56.53 µsrust-stemmers, 4.62×

Scaling from 4 to 1024 words per batch (Verbora ÷ rust-stemmers speedup; bold = Verbora ahead):

Language416642561024
de0.96×1.02×1.02×1.03×1.04×
nl1.65×1.75×1.61×1.61×1.69×
es0.18×0.19×0.20×0.19×0.20×
fr0.32×0.38×0.40×0.42×0.39×
it0.41×0.40×0.44×0.42×0.43×
no0.18×0.20×0.19×0.20×0.20×
pt0.21×0.25×0.26×0.25×0.25×
ru0.17×0.15×0.15×0.15×0.15×
sv0.21×0.21×0.22×0.21×0.22×

snowball_stemmers_rs — a second, independently-generated Snowball port

Languages are never averaged together, so this is a second, independent data point on the same compiler-generated-binary-search-vs.-Verbora's-linear-scan question above, not a repeat of it. Agreement is even stronger than the rust-stemmers row in one case: snowball_stemmers_rs's russian.sbl carries the same ё→е fold Verbora's stemmer does, so Russian agrees 100% byte-exact includingёлкаrust-stemmers does not. Dutch needs Algorithm::DutchPorter specifically; the crate's plainly-named Algorithm::Dutch is actually Kraaij–Pohlmann, a different, non-canonical stemmer, confirmed by reading the crate's own algorithm list rather than assumed from the name.

LanguageVerbora (1024-word batch)snowball_stemmers_rs (1024-word batch)Faster
de136.97 µs185.61 µsVerbora, 1.36×
es640.01 µs94.54 µssnowball_stemmers_rs, 6.77×
fr481.95 µs154.71 µssnowball_stemmers_rs, 3.12×
it532.07 µs177.07 µssnowball_stemmers_rs, 3.00×
nl128.99 µs95.27 µssnowball_stemmers_rs, 1.35×
no249.77 µs33.67 µssnowball_stemmers_rs, 7.42×
pt614.10 µs88.64 µssnowball_stemmers_rs, 6.93×
ru588.00 µs86.06 µssnowball_stemmers_rs, 6.83×
sv261.10 µs38.73 µssnowball_stemmers_rs, 6.74×

Scaling from 4 to 1024 words per batch (Verbora ÷ snowball_stemmers_rs speedup; bold = Verbora ahead) — Dutch is the one language where the two Snowball competitors split, since it beats rust-stemmers above but loses here:

Language416642561024
de1.34×1.38×1.34×1.28×1.36×
es0.13×0.14×0.14×0.14×0.15×
fr0.27×0.30×0.31×0.33×0.32×
it0.34×0.32×0.35×0.32×0.33×
nl0.72×0.77×0.71×0.71×0.74×
no0.12×0.13×0.13×0.13×0.13×
pt0.15×0.14×0.14×0.14×0.14×
ru0.17×0.15×0.15×0.15×0.15×
sv0.15×0.15×0.15×0.15×0.15×

English — nltk-porter and porter-stemmer

Two independent original-1980-Porter ports, since rust-stemmers' own "English" is Snowball Porter2, a different algorithm (excluded from the Snowball tables above).

LibraryVersionTime (median, 1024-word batch)ThroughputRelative
Verbora0.1.0522.48 µs1.9K/s1.00×
nltk-porter0.1.01.92 ms520.9/s3.67× slower
porter-stemmer0.1.2310.13 µs3.2K/s1.68× faster

The nltk-porter win is the one clean, consistent Verbora win in this module — narrowing gently with batch size. porter-stemmer is the opposite: a real Verbora loss, operating on grapheme clusters rather than code units, an architecture question that turns out not to matter on this plain-ASCII corpus (63 of 64 benchmarked words agree byte-exact, the one mismatch a real, isolated porter-stemmer bug — "sky""ski" — unrelated to graphemes and excluded from the sample).

Input sizeVerboranltk-porterporter-stemmer
41.53 µs6.85 µs (4.49× faster)881.33 ns (1.73× slower)
167.15 µs28.56 µs (4.00× faster)3.76 µs (1.90× slower)
6432.54 µs115.31 µs (3.54× faster)20.11 µs (1.62× slower)
256138.33 µs498.36 µs (3.60× faster)77.32 µs (1.79× slower)
1024522.48 µs1.92 ms (3.67× faster)310.13 µs (1.68× slower)

Japanese — lindera-analysis

Verbora's StemmerJa (trailing katakana U+30FC drop, minimum 4 code units) against lindera-analysis's JapaneseKatakanaStemTokenFilter, min = 3 (the filter's own default) — verified to reproduce Verbora's >= 4-unit threshold exactly on the shared word list before any number below was trusted.

Input sizeVerboralindera-analysisFaster
434.75 ns456.58 nsVerbora, 13.14×
16160.62 ns2.11 µsVerbora, 13.14×
64597.05 ns8.99 µsVerbora, 15.06×
2562.30 µs35.27 µsVerbora, 15.33×
10249.38 µs140.42 µsVerbora, 14.97×

A clean, decisive win on both time and allocations — 0 vs. 6 over the 7-word correctness list. Verbora's algorithm borrows and allocates nothing; lindera-analysis's Vec<Token>-batch filter API always allocates at least the Vec, on top of running through a full dictionary-backed tokenizer pipeline Verbora's purpose-built stemmer doesn't need.

Indonesian — sastrawi

Verbora's StemmerId (Sastrawi/Nazief–Adriani) against sastrawi (iDevoid/rust-sastrawi) 0.1.1 — genuine shared lineage, not a coincidence: both implement the same published PHP Sastrawi algorithm, and both dictionaries hold exactly 29,932 root words, confirmed directly. Real correctness gaps found in sastrawi during verification, excluded from the benchmarked sample rather than hidden: no hyphenated-reduplication/ compound-plural handling at all, and only a single (not iterated-up-to-3×) prefix-stripping pass — 13 of 16 benchmarked words still agree byte-exact.

Input sizeVerborasastrawiFaster
43.53 µs969.23 nssastrawi, 3.64×
1654.63 µs8.59 µssastrawi, 6.36×
64243.09 µs35.87 µssastrawi, 6.78×
256954.89 µs152.38 µssastrawi, 6.27×
10243.97 ms583.38 µssastrawi, 6.80×
The one clear loss in this module. `sastrawi`'s own one-time Dictionary::new() + Stemmer::new() construction cost (~47K allocations, ~21 MB) is real but paid once; Verbora's StemmerId::new() is a zero-sized unit struct backed entirely by compiled-in static data, needing no runtime construction at all — a trade-off the per-word numbers above don't capture.

CarryStemmerFr (French, Carry variant — a distinct 3-pass suffix-table algorithm, not standard Snowball French) has no fair Rust competitor: no crate in the ecosystem implements it, confirmed by checking that rust-stemmers' Algorithm::French is standard Snowball French rather than Carry.


Normalizers

remove_diacritics (docs/COMPETITIVE_BENCHMARKS.md§1.4) against diacritics 0.2.2, chosen over five other Rust candidates as the closest semantic match — case-preserving, non-decomposing table lookup, not NFD-based like unaccent and not forced-lowercasing like secular. Byte-exact agreement on ASCII, precomposed accented Latin, Cyrillic rejection and the shared ß//ſ/İ/ı/œ table quirks verified in tests/normalizers_correctness.rs before any timing was trusted — one real divergence found and deliberately excluded from the benchmarked domain: diacritics silently strips standalone Unicode combining marks, which remove_diacritics never decomposes and so leaves untouched.

Pure-ASCII input

LibraryVersionLanguageTime (median, 1024 B)ThroughputRelative
Verbora0.1.0Rust40.1 ns24.95M/s1.00×
diacritics0.2.2Rust10.75 µs93.0K/s268.28× slower
Input sizeVerboradiacriticsVerbora vs. diacritics
43.1 ns182.4 ns59.75× faster
1610.6 ns178.8 ns16.85× faster
6410.7 ns740.9 ns69.55× faster
25634.6 ns2.72 µs78.85× faster
102440.1 ns10.75 µs268.28× faster

Verbora's one-line s.is_ascii() fast path returns Cow::Borrowed immediately — no scan, no allocation — while diacritics::remove_diacritics folds through its char match unconditionally, even on input it will not change.

Accented (working) input

Inconclusive — reported as such, not forced to a verdict. Two independent reruns of this specific case disagreed on direction at the largest size (1024 B): diacritics is slightly faster (1–13%) at four of five sizes, and one of the two reruns flipped at 1024 B. The magnitude is small enough, and the run-to-run variance real enough, that the fairness audit judged forcing a numeric verdict here would misrepresent the evidence — see the numbers below from this page's own canonical run, and treat them as one data point rather than a settled ranking.
Input sizeVerboradiacriticsVerbora vs. diacritics
4532.2 ns491.7 ns1.08× slower
162.09 µs1.91 µs1.09× slower
6410.20 µs9.03 µs1.13× slower
25633.50 µs30.90 µs1.08× slower
1024134.42 µs125.01 µs1.08× slower

No PERFORMANCE_GAPS.md entry follows from this case, consistent with that file's own policy of recording settled findings, not noise near the measurement floor.


Inflectors

CountInflector::nth — English ordinal suffixing (1st/2nd/3rd/…) — against ordinal 0.4.0 (docs/COMPETITIVE_BENCHMARKS.md§1.5), the single full-equivalence (Yes) row in the whole Inflectors group and the only one benchmarked here. Two real divergences were found and excluded from the benchmarked domain before any timing was trusted, in tests/inflectors_correctness.rs: negative integers (different rounding conventions), and a real bug in ordinal 0.4.0 itself: its teens exception uses n % 20 where it needs n % 100, misformatting 12% of non-negative integers (31.to_ordinal_string() returns "31th", not "31st"). The benchmarked domain verifiably avoids every affected value.

LibraryVersionLanguageTime (median, 1024-int batch)ThroughputRelative
Verbora0.1.0Rust14.94 µs67.0K/s1.00×
ordinal0.4.0Rust27.53 µs36.3K/s1.84× slower
Input sizeVerboraordinalVerbora vs. ordinal
458.1 ns100.5 ns1.73× faster
16226.5 ns433.5 ns1.91× faster
64916.3 ns1.70 µs1.85× faster
2563.61 µs6.69 µs1.85× faster
102414.94 µs27.53 µs1.84× faster

A flat ~1.8×–1.9× regardless of batch size: both sides are O(1) per integer, so this is a genuine constant-factor difference — ordinal formats through Display/format!, Verbora writes digits directly into a pre-sized buffer.


Trie

Generic prefix-search throughput (never output/ordering equivalence — no Rust crate replicates Verbora's UTF-16-code-unit keying or its own numeric-keys-first child enumeration order) against trie-rs 0.4.2 (highest download count of any competitor in the whole audit: 5.9M) and qp-trie 0.8.2 (docs/COMPETITIVE_BENCHMARKS.md§1.18). Order-blind set-equality of every operation's result proven in tests/trie_correctness.rs before any timing was trusted; "build" is timed as push-then-compile for trie-rs's LOUDS architecture, matching how that crate is actually used.

LibraryVersionLanguageTime (median, 20,000-word build, random)ThroughputRelative
Verbora0.1.0Rust1.65 ms607.5/s1.00×
qp-trie0.8.2Rust3.04 ms328.8/s1.85× slower
trie-rs0.4.2Rust11.58 ms86.3/s7.04× slower

Verbora wins build against both competitors (1.85×–7.04× at random keys; 2.11×–2.64× at prefix-heavy keys) and wins every operation against trie-rs by 99×–492×. Against qp-trie specifically, the read path inverts:

OperationVerboraqp-trieVerdict
contains (hit, 20K words)1.22 ms869.48 µsqp-trie 1.41× faster
contains (miss, 20K words)1.22 ms796.63 µsqp-trie 1.54× faster
common_prefix_search256.34 µs— (not implemented)
predictive_search (1-char prefix)1.17 ms123.72 µsqp-trie 9.47× faster
predictive_search (empty prefix, all 20K)1.77 ms121.88 µsqp-trie 14.56× faster
A real, disclosed loss on the read path. See PERFORMANCE_GAPS.md entry 2: qp-trie is a crit-bit/PATRICIA-style radix trie with path compression (lookup depth bounded by distinguishing nybbles between stored keys, not key length) and stores each key whole at its leaf (no reconstruction cost on enumeration); Verbora's arena trie has neither — one hop per UTF-16 code unit, and every enumerated word is rebuilt one unit at a time. The same two properties invert for build, which is why Verbora wins there instead.

common_prefix_search has no qp-trie competitor: that crate implements no "stored words that are prefixes of a query" operation at all.

fast_radix_trie — a path-compressed radix map, and Verbora's own FrozenTrie answer

fast_radix_trie 1.2.0 is a path-compressed radix map created 2025-10-30. It uses unsafe internally (dynamically-sized nodes via raw pointers, miri-tested per its own docs); Verbora's own trie has zero unsafe anywhere.

OperationVerborafast_radix_trieVerdict
build (random)1.567 ms2.415 msVerbora 1.54× faster
contains (hit)1.136 ms1.340 msVerbora 1.18× faster
contains (miss)1.249 ms1.335 msVerbora 1.07× faster
predictive_search (1-char prefix)1.145 ms696.8 µsfast_radix_trie 1.64× faster
predictive_search (empty prefix, all 20K)1.453 ms663.4 µsfast_radix_trie 2.19× faster

A genuine split, not a one-sided result: Verbora wins build and contains, fast_radix_trie's path compression wins prefix enumeration — fewer node-hops per query, the exact property path compression buys.

Verbora's own answer: FrozenTrie. Given this real evidence, Trie::freeze() now exists — a safe-Rust (zero unsafe), path-compressed, read-only representation built once from a Trie. It closes most of the gap and overtakesfast_radix_trie on the realistic autocomplete shape:
OperationFrozenTrie vs. arena TrieFrozenTrie vs. fast_radix_trie
predictive_search (1-char prefix)1.89× faster1.06× faster — overtakes
predictive_search (empty prefix, all 20K)1.49× faster1.45× slower — narrowed, not closed
contains (hit/miss)1.5×–1.7× sloweralso loses — arena Trie wins this one

An honest trade-off, not a clean win: FrozenTrie closes most of the predictive_search gap and beats fast_radix_trie outright on single-letter prefixes (the realistic autocomplete shape), but still trails on full-corpus enumeration, and its own contains genuinely regresses relative to the plain arena. Neither representation replaces the other — Trie for lookup-heavy code, FrozenTrie (frozen once after bulk load) for enumeration-heavy code. See PERFORMANCE_GAPS.md entry 32 for the full numbers and reasoning.

fst — a frozen finite-state transducer

fst 0.4.7 (Andrew Gallant's) is architecturally nothing like a trie: a finite-state transducer built once from sorted input, queried via a Streamer, never mutated again. Two separate comparisons:

ComparisonResult
build/contains/predictive_search (vs. plain Trie)Verbora wins every operation, 1.21×–4.49×
Levenshtein-automaton fuzzy lookup (vs. FuzzyIndex)a double crossover — see below

fst's own Levenshtein automaton (via its levenshtein feature) answers the same fuzzy-candidate question as verbora_spellcheck::FuzzyIndex — a genuine double crossover on both construction and query:

WordsConstruction: FuzzyIndexConstruction: fstQuery: FuzzyIndexQuery: fst
10031.6 µs59.4 µs563.7 µs30.6 ms
1,000778.3 µs473.8 µs10.44 ms68.4 ms
10,00012.30 ms3.96 ms96.89 ms102.3 ms
20,00025.90 ms7.08 ms187.4 ms128.7 ms

FuzzyIndex wins construction small, fst wins from 1,000 words up; query runs the opposite direction — FuzzyIndex wins small and mid-size corpora (dramatically at 100 words), fst overtakes only at the largest size tested. fst is also one of three crates in this audit found to carry a real, independently-confirmed upstream defect — see Upstream bugs found.


Phonetics

Eleven encoder types (docs/COMPETITIVE_BENCHMARKS.md§1.6) against rphonetic 3.0.6 — the one actively-maintained Rust crate covering the same phonetic-algorithm families, in the Apache commons-codec lineage, in a single crate. The comparison runs in two regimes with two different equivalence claims, both verified in tests/phonetics_correctness.rs before any number was trusted:

  • Four variant encoders — throughput-only (Partial, never Yes).SoundEx, Metaphone, DoubleMetaphone and SoundExDM implement Verbora's own documented variants (condense-before-drop Soundex, a documented Metaphone stage-ordering quirk, single-branch Daitch–Mokotoff), rphonetic the textbook originals — byte-exact output is never asserted, only that both sides do the same shape of work.
  • Seven byte-exact encoders — full output equivalence (Yes).Cologne, Nysiis, Caverphone1/Caverphone2, Phonex, RefinedSoundex, MatchRatingApproach and the branching DaitchMokotoff are Verbora-native extensions whose output is byte-identical to rphonetic's on every input rphonetic handles without panicking — a stronger claim than any Partial row on this page carries.

The four variant encoders — throughput only

rphonetic's Metaphone/Double Metaphone default to a 4-character max code length; both are reconfigured to Some(32) here to match Verbora's real default of 32 — independently verified by test to actually change rphonetic's output length, not silently still capped at 4.

Algorithm1 name10,000 names100,000 names
Soundex2.79× faster2.17× faster2.08× faster
Metaphone1.43× faster1.03× faster1.10× faster
Double Metaphone3.27× faster2.18× faster4.22× faster
SoundExDM (Daitch–Mokotoff)7.52× faster9.55× faster5.12× faster

Verbora wins all four algorithms at every benchmarked size. Metaphone is the closest of the four:

LibraryVersionLanguageTime (median, 100,000 names)ThroughputRelative
Verbora0.1.0Rust6.89 ms145.1/s1.00×
rphonetic3.0.6Rust7.57 ms132.2/s1.10× slower
Metaphone — a clean sweep at every size. Verbora's Metaphone runs as a single skip-gated driver, fused from the original 21 ordered whole-string rewrite stages, over per-thread pooled scratch: letter-mask gates decide which rules can possibly fire on a given word, window edits plus fused rules replace whole-string rewrites, and the pipeline's two scratch buffers are reused across calls — an ASCII token folds lowercase directly into pooled scratch, so a steady-state call's only allocation is the returned code. The original 21-stage implementation is retained internally as the differential-test oracle it is checked against, over a ~900K-comparison corpus. rphonetic's Metaphone is a single indexed forward scan (O(n)); Verbora wins anyway: 1.43× at a single name (51.5 ns vs. 73.5 ns), 1.03× at 10,000 names (711.91 vs. 735.77 µs), 1.10× at 100,000 (6.89 vs. 7.57 ms). See PERFORMANCE_GAPS.md entry 6 for the full mechanism.

Full per-algorithm, per-size data for these four groups: results/results.json (module "phonetics").

The seven byte-exact encoders

Verification for this table goes beyond the shape-parity check above, because here byte-exact equality is the claim: tests/phonetics_correctness.rs asserts identical output over the shared 653-name corpus plus per-algorithm extras for every encoder; for MatchRatingApproach it additionally checks the real MRA match decision (compare, not just the code) over every ordered pair of corpus names (~426K pairs); and Daitch–Mokotoff is checked three ways at once — the pipe-joined process string, the codes vector, and the first-branch code against rphonetic's non-branching encode. An independent adversarial audit then differentially fuzzed 104,114 inputs per encoder against rphonetic with zero mismatches, proved every documented divergence exactly as narrow as claimed, and mutation-tested the suites. The only divergences are inputs on which rphonetic itself panics — see Upstream bugs found; those input shapes are excluded from the benchmark domain per this page's fairness pattern, and the ASCII-only shared corpus never reaches them anyway. Configuration is identical on both sides: Nysiis runs strict (the commons-codec default), Phonex at its default max code length of 4.

Verbora is faster in all 24 cells (Verbora vs. rphonetic, Criterion medians):

Encoder1 name10,000 names100,000 names
Cologne17.4 ns vs. 72.2 ns (4.16×)314.99 µs vs. 738.89 µs (2.35×)3.250 ms vs. 7.321 ms (2.25×)
NYSIIS29.7 ns vs. 224.5 ns (7.56×)266.53 µs vs. 1.889 ms (7.09×)2.688 ms vs. 20.517 ms (7.63×)
Caverphone 1.0176.4 ns vs. 914.3 ns (5.18×)1.907 ms vs. 10.317 ms (5.41×)19.013 ms vs. 99.617 ms (5.24×)
Caverphone 2.0153.9 ns vs. 811.8 ns (5.27×)1.770 ms vs. 9.262 ms (5.23×)17.748 ms vs. 88.009 ms (4.96×)
Phonex41.3 ns vs. 145.7 ns (3.53×)483.95 µs vs. 1.358 ms (2.81×)4.608 ms vs. 12.933 ms (2.81×)
Refined Soundex14.8 ns vs. 114.2 ns (7.73×)129.36 µs vs. 972.06 µs (7.51×)1.298 ms vs. 9.630 ms (7.42×)
Match Rating Approach31.9 ns vs. 489.3 ns (15.32×)323.12 µs vs. 5.344 ms (16.54×)3.059 ms vs. 53.249 ms (17.41×)
Daitch–Mokotoff (branching)154.9 ns vs. 363.4 ns (2.35×)1.788 ms vs. 3.720 ms (2.08×)16.834 ms vs. 37.170 ms (2.21×)

The mechanism is consistent across all eight groups rather than one trick: Verbora's encoders run single-pass scans over one reused buffer, with one heap allocation per call for the returned code (the branching Daitch–Mokotoff adds a small branch list), against static compiled-in rule tables. rphonetic's implementations allocate intermediate Strings as they go (Caverphone's rewrite cascade is one freshly allocated String per step there) and, for Daitch–Mokotoff, parse the rules text with a nom grammar at builder time and walk a BTreeMap per lookup where Verbora indexes a pre-sorted static array. The margins range from 2.08× (Daitch–Mokotoff at 10,000 names — the one algorithm where both sides spend most of their time in the same branching walk) to 17.41× (Match Rating at 100,000).

The Daitch–Mokotoff row compares Verbora's branching DaitchMokotoff::process against rphonetic's own pipe-joined soundex() — output-format identical, unlike the SoundExDM row above, which is Verbora's separate single-branch type benchmarked against rphonetic's non-branching encode(). Four rphonetic Daitch–Mokotoff behavioral quirks are reproduced deliberately for byte-parity and documented in crates/verbora-phonetics/src/daitch_mokotoff.rs's own module documentation. Raw Criterion estimates for these eight groups live in the same Criterion tree cargo bench writes — see Reproducing these numbers.

A second Double Metaphone implementation — C++, not Rust

Every other competitor on this page is a Rust crate. pixelglow/double_metaphone is different: a header-only C++11 implementation of Lawrence Philips' Double Metaphone algorithm, vendored into the workspace and compiled by build.rs, then called through a thin extern "C" shim — the only non-Cargo, non-Rust competitor benchmarked anywhere on this page. Every measured call crosses the Rust/C++ boundary once (one CString construction, one FFI call, two bounded buffer copies, one UTF-8 validation on the way back), and that cost is measured as part of the number, not subtracted out.

Correctness here is Partial, not byte-exact: 584 of 653 real English surnames (89.4%) produce identical primary and secondary keys on both sides. The one confirmed, dominant rule difference: Verbora silences a trailing S whenever it is preceded by A or I, while the C++ library only silences a trailing S in the narrower pattern where I or Y is immediately followed by S then Lisland, isle, carlisle. Both sides agree Isle should lose its S; they disagree on names like Davis, which keeps its S on the C++ side but loses it on Verbora's, encoding the same way Isle does. Neither reading is more correct; Double Metaphone's own published algorithm write-up never fully disambiguates this case.

LibraryVersionLanguageTime (median, 653 names)ThroughputRelative
Verbora0.1.0Rust47.23 µs21.2K/s1.00×
pixelglow/double_metaphone79dd226 (2014)C++1185.30 µs11.7K/s1.81× slower

Verbora is 1.81× faster (medians, full Criterion defaults). This is a result about one specific, vendored C++ implementation, benchmarked once and disclosed honestly — not a claim about C++ Double Metaphone implementations in general.


Language detection

Statistical language detection over free text (docs/COMPETITIVE_BENCHMARKS.md§1.9) — Verbora's WhatlangDetector against lingua 1.8.0 (built with from_languages(), restricted to the 21-language overlap with Verbora, never its default 75) and whichlang 0.1.1 (13-language overlap, and — disclosed explicitly, not folded silently into the accuracy numbers — it cannot abstain: detect_language always returns a guess). A widely-used JavaScript NLP library has no general statistical language-detection module (verified from source, not assumed), so it does not appear here.

Speed alone is not enough for this capability — this project's own policy requires accuracy alongside speed wherever correctness has a statistical dimension. See Accuracy below before reading the speed table as the whole story.

Speed, by input length (English)

TierVerboralinguawhichlang
short word (~6 B)38.09 µs49.95 µs92.6 ns
short phrase (~30 B)36.97 µs449.73 µs290.8 ns
sentence (~140 B)32.12 µs331.66 µs757.1 ns
paragraph (~500 B)103.87 µs650.09 µs6.53 µs
LibraryVersionLanguageTime (median, paragraph)ThroughputRelative
whichlang0.1.1Rust6.53 µs153.2K/s1.00×
Verbora0.1.0Rust103.87 µs9.6K/s15.91× slower
lingua1.8.0Rust650.09 µs1.5K/s99.59× slower
The largest gap on this page — 16×–411× slower than whichlang, reported in full. See PERFORMANCE_GAPS.md entry 7: whichlang is a zero-allocation, single-pass hashed linear model over 16 languages; the whatlang engine WhatlangDetector wraps runs a combined alphabet-filter *and* trigram-frequency pass (measured 25 heap allocations per call) over 22 languages. Verbora beats lingua consistently (6.3×–12.2×) in the same table. The gap narrows as input grows, because whatlang's ~25-allocation overhead is largely length-independent and gets amortized, while whichlang's per-feature cost scales with length. See the accuracy table below for what that speed buys — or does not.

Speed, by language (sentence tier)

LanguageVerboralinguawhichlangVerbora vs. whichlang
German56.89 µs234.51 µs816.1 ns69.7× slower
English30.97 µs333.48 µs567.2 ns54.6× slower
Spanish41.39 µs188.17 µs1.85 µs22.4× slower
French32.11 µs304.65 µs620.6 ns51.7× slower
Hindi9.64 µs10.62 µs571.3 ns16.9× slower
Italian33.33 µs251.00 µs634.7 ns52.5× slower
Japanese292.6 ns9.37 µs330.6 ns1.1× faster
Dutch31.18 µs427.23 µs609.3 ns51.2× slower
Portuguese31.55 µs280.65 µs1.41 µs22.4× slower
Russian8.44 µs118.54 µs855.7 ns9.9× slower
Swedish31.93 µs271.52 µs552.2 ns57.8× slower
Vietnamese30.49 µs310.10 µs615.1 ns49.6× slower
Chinese166.9 ns3.43 µs159.7 ns~tied

Japanese and Chinese are the one real exception: whatlang's alphabet pre-filter recognizes CJK codepoints immediately and short-circuits before the far more expensive trigram pass, so those two rows barely pay the 25-allocation cost that dominates every Latin-script row.

Accuracy

13 languages (the triple overlap all three detectors can be scored on identically) × 4 length tiers, sourced from the OHCHR UDHR Translation Project (public-domain UN text; full sourcing and per-tier extraction rule in datasets/README.md). Reproduced with cargo run --release --example language_accuracy.

Detectorshort wordshort phrasesentenceparagraphOverall
lingua (21-language restricted)92.3% (12/13)100%100%100%98.1% (51/52)
Verbora (WhatlangDetector)76.9% (10/13, 1 abstained)100%100%100%94.2% (49/52)
whichlang (13-language, cannot abstain)69.2% (9/13)100%100%100%92.3% (48/52)

At phrase length and longer, this 13-language test set does not distinguish the three detectors at all — every one is perfect. The entire accuracy gap lives at the single-word tier, where lingua's dedicated short-input model has a real edge, Verbora abstains once rather than guess wrong (scored as incorrect here, per this report's own coverage-vs-accuracy distinction), and whichlang — which cannot abstain — has the lowest raw accuracy at exactly this tier, the same tier where it is 400×+ faster. Read alongside the speed table above: whichlang's speed is real, and its accuracy cost at the hardest tier is real too — neither is quoted here without the other. 13 languages is a small test set (one extra mistake swings the short-word percentage by ~8 points) — see that dataset's own README for this caveat stated in full.

WhatlangDetector wrapper overhead — not a ranked comparison

Isolates the cost of Verbora's own wrapper around whatlang::Detector — it is not "Verbora vs. whatlang," because WhatlangDetector literally constructs a whatlang::Detector and calls .detect() on it. Numbers below are this page's own single run. The noisy ratios include a tier where the wrapper measured faster than the bare call it makes, which is structurally impossible as a real effect. Read as noise from a shared benchmark machine, not a finding.
TierVerbora (WhatlangDetector)whatlang (raw crate)Ratio
short word39.15 µs26.99 µs1.45×
short phrase53.57 µs52.34 µs1.02×
sentence37.03 µs32.21 µs1.15×
paragraph102.01 µs146.49 µs0.70×

Script detection

Verbora's detect_script against whatlang::detect_script 0.18.0 (docs/COMPETITIVE_BENCHMARKS.md§1.10) — a real, public, standalone function doing the same conceptual work (per-codepoint Unicode-range classification, majority vote), just over a wider set (25 scripts vs. Verbora's 10). A widely-used JavaScript NLP library has no script-detection module at all (verified from source).

TierVerborawhatlangVerbora advantage
short word9.7 ns55.1 ns5.7×
short phrase28.4 ns59.1 ns2.1×
sentence70.4 ns114.3 ns1.6×
paragraph786.0 ns1.05 µs1.3×
LibraryVersionLanguageTime (median, paragraph)ThroughputRelative
Verbora0.1.0Rust786.0 ns1.27M/s1.00×
whatlang0.18.0Rust1.05 µs951.8K/s1.34× slower

Verbora wins at every length (narrowing as input grows, since both are O(chars × scripts) with different constants). By language (sentence tier), Verbora is faster or tied in 12 of 13; the one exception is honestly reported rather than dropped:

LanguageVerborawhatlangFaster
German69.4 ns118.3 nsVerbora
English70.6 ns111.3 nsVerbora
Spanish183.6 ns342.9 nsVerbora
French80.9 ns127.4 nsVerbora
Hindi180.7 ns180.4 ns~tied
Italian74.9 ns121.0 nsVerbora
Japanese231.6 ns276.5 nsVerbora
Dutch83.1 ns122.9 nsVerbora
Portuguese115.5 ns124.1 nsVerbora
Russian163.9 ns126.3 nswhatlang, 1.3×
Swedish97.6 ns202.7 nsVerbora
Vietnamese105.6 ns147.3 nsVerbora
Chinese115.6 ns147.6 nsVerbora

Russian's small loss is disclosed rather than dropped; no source-level cause that would predict Cyrillic specifically was found, and it is plausibly this run's reduced-sample noise floor.


Transliteration

Japanese kana→romaji, throughput only — against wana_kana 5.0.0 (docs/COMPETITIVE_BENCHMARKS.md§1.11), the only Rust kana↔romaji crate with real current adoption/maintenance found — every alternative investigated is scope-mismatched or effectively abandoned. Never an output-correctness comparison: wana_kana uses a doubled-vowel convention ("スーパー""suupaa") while Verbora uses modified Hepburn with macrons ("tōkyō") — a real, executed divergence proven in tests/transliteration_convention_diff.rs, not merely asserted.

RepeatsVerborawana_kanaVerbora advantage
724.6 ns1.00 µs1.4×
16×5.59 µs12.55 µs2.2×
256×88.51 µs206.78 µs2.3×
LibraryVersionLanguageTime (median, 256×)ThroughputRelative
Verbora0.1.0Rust88.51 µs11.3K/s1.00×
wana_kana5.0.0Rust206.78 µs4.8K/s2.34× slower

POS tagging

BrillPosTagger (transformation-based, English) against postagger 0.0.3 (a pretrained averaged-perceptron model, NLTK weights) and rust-bert 0.23.0's POSModel (a MobileBERT transformer pipeline) — the most widely adopted general Rust NLP crate found in this whole audit (254K downloads, 3,077 stars) (docs/COMPETITIVE_BENCHMARKS.md§1.16). Both are genuinely different algorithm classes — a trained classifier and a transformer forward pass, not rival implementations of the same rule table — so this is reported as a technique comparison, with cold start and steady state kept strictly separate.

Cold start — everything needed before tagging one sentence

LibraryVersionLanguageTime (median)Relative
Verbora (Lexicon + RuleSet + BrillPosTagger)0.1.0Rust7.47 µs1.00×
postagger (parses a 5.6 MB weights file)0.0.3Rust121.61 ms16,283× slower
rust-bert (loads a ~94 MB MobileBERT checkpoint)0.23.0Rust1.281 s171,501× slower

Expected and by design: a fixed rule table needs no deserialization step; a pretrained model must load its weights first.

Steady state — per-call latency, tagger already constructed

LibraryVersionLanguageTime (median, 9 tokens)Time (median, 20 tokens)Time (median, batch of 8×9-tok)
Verbora0.1.0Rust1.75 µs4.23 µs14.08 µs
postagger0.0.3Rust62.40 µs (35.6× slower)80.46 µs (19.0× slower)549.50 µs (39.0× slower)
rust-bert0.23.0Rust17.26 ms (9,854× slower)10.93 ms (2,583× slower)24.62 ms (1,749× slower)

Verbora wins every steady-state row — the expected result once the technique gap is named, not a surprising one: postagger still evaluates a feature-weighted vote per token, and rust-bert's MobileBERT pass is a full transformer forward pass, categorically more arithmetic than a deterministic rule lookup. This is not an accuracy claim for either technique — see docs/COMPETITIVE_BENCHMARKS.md §1.16 for why both rows are Partial, not Yes; this audit makes no tagging-quality comparison for POS tagging.


Spellcheck

Spellcheck::get_corrections and ::is_correct against three genuinely different algorithms (docs/COMPETITIVE_BENCHMARKS.md§1.17): symspell 0.5.2 (precomputed deletion dictionary), harper-core 2.8.0 (FST + Levenshtein automaton; by far the most widely adopted standalone spellchecking crate found, 14,470 GitHub stars on its parent repo), and spellbook 0.4.2 (Hunspell affix-rule morphology).

symspell and harper-core — same corpus as Verbora

Both loaded with the identical words.json corpus and per-word frequencies Verbora uses.

GroupCorpusVerborasymspellharper-core
construction (new)10014.05 µs402.42 µs (28.6× slower)77.97 µs (5.6× slower)
construction (new)20,0003.58 ms115.42 ms (32.3× slower)10.95 ms (3.1× slower)
is_correct (hit)20,000226.16 µs310.15 µs (1.4× slower)271.89 µs (1.2× slower)
get_corrections, distance 110022.00 µs921 ns (23.9× faster)5.02 µs (4.4× faster)
get_corrections, distance 120,00022.95 µs857 ns (26.8× faster)36.87 µs (1.6× slower)
get_corrections, distance 21,0004.96 ms2.31 µs (2,152× faster)34.79 µs (142.7× faster)
get_corrections, distance 220,0005.80 ms3.50 µs (1,657× faster)333.83 µs (17.4× faster)

Verbora wins construction and membership-testing at every size against both competitors. symspell and, at the largest corpus, harper-core win correction generation — by enormous margins at distance 2.

Two real, disclosed losses, with the matching cost trade-off shown alongside each. See PERFORMANCE_GAPS.md entry 8: symspell's query speed is bought with 29×–32× more expensive construction — the delete-dictionary is precomputed once at load instead of generated per query. harper-core (entry 8b) is a genuine crossover, not a one-sided loss: it wins at small corpora and at distance 2 throughout, but Verbora wins distance-1 correction once the corpus reaches 10,000–20,000 words.

spellbook — matched-workload timing only, not a fair ratio

Hunspell's .aff/.dic format has no concept of a flat frequency corpus — spellbook cannot load Verbora's corpus, and Verbora cannot load a Hunspell dictionary. This is a timing comparison of two different dictionaries doing conceptually the same job, never presented as a ratio. No Relative column below.
OperationLibraryVersionDictionaryTime (median)
check / is_correct, hitspellbook0.4.2real en_US Hunspell372.5 ns
check / is_correct, near-miss typospellbook0.4.2real en_US Hunspell3.25 µs
is_correct, hitVerbora0.1.0own 20,000-word corpus41.71 µs
suggest / get_corrections, one typo (4 probes)spellbook0.4.2real en_US Hunspell5.28–9.05 ms
get_corrections, one typo (typo8)Verbora0.1.0own 20,000-word corpus23.83 µs

spellbook's check is a curated, bundled FST/hash lookup — sub-microsecond as expected for a fixed, pre-built dictionary — while its full affix-aware suggest costs milliseconds, the opposite trade-off from its own check.

fast_symspell — a second deletion-index crate, and Verbora's own answer to it

fast_symspell 0.1.10 is a second, independent SymSpell-family implementation. Its published metadata carries no linked repository, but its source is real and readable via crates.io's own tarball — a near-verbatim (confirmed line-for-line) fork of symspell 0.5.2 with three real deltas: ahash hashing, a triple_accel-backed verification pass (which carries its own real, independently-confirmed bug — see Upstream bugs found), and an rkyv zero-copy archived-load path. Loaded with Verbora's own corpus, same discipline as symspell above.

GroupCorpusVerborafast_symspell
construction10012.8 µs356.8 µs (27.8× slower)
construction20,0003.60 ms122.3 ms (34.0× slower)
get_corrections, distance 110021.19 µs766 ns (27.7× faster)
get_corrections, distance 120,00023.15 µs896 ns (25.9× faster)
get_corrections, distance 21,0005.97 ms2.16 µs (2,769× faster)
get_corrections, distance 220,0005.54 ms3.29 µs (1,686× faster)

The same shape as symspell above, more extreme at distance 2 — a delete-precomputation index trades expensive, size-scaling construction for near-flat query cost, and Verbora's own combinatorial edit generation is the side paying a large, roughly size-independent cost per call once distance reaches 2.

Verbora's own answer: DeletionIndex. Given this real, repeated evidence that a deletion index wins query speed by a widening margin, verbora_spellcheck::DeletionIndex now exists — a SymSpell-style index built in-house, offered alongside the existing FuzzyIndex BK-tree rather than replacing it. See FuzzyIndex vs. DeletionIndex below for the real, measured trade-off between them.

FuzzyIndex vs. DeletionIndex — two Verbora-native structures

Neither of these has a reference counterpart — both are Verbora-native extensions answering the same question (which stored words are within edit distance k of this query?) via different mechanisms. FuzzyIndex is a BK-tree, max_distance chosen per query. DeletionIndex is a SymSpell-style deletion index, max_distance fixed once at construction — built after real evidence (the fast_symspell comparison above) showed the query-speed trade-off was worth having available, not built speculatively ahead of that evidence.

WordsConstruction: FuzzyIndexConstruction: DeletionIndexQuery: FuzzyIndexQuery: DeletionIndex
10038.7 µs977.6 µs (25.3× slower)589.9 µs1.018 ms (1.73× slower)
1,000779.9 µs11.83 ms (15.2× slower)10.93 ms2.23 ms (4.9× faster)
10,00012.39 ms162.6 ms (13.1× slower)93.28 ms2.64 ms (35.3× faster)
20,00026.97 ms407.0 ms (15.1× slower)174.1 ms3.21 ms (54.3× faster)

A genuine crossover on the query side: FuzzyIndex is faster at the smallest corpus tested (100 words), where a shallow BK-tree beats a deletion index's fixed per-query overhead — but DeletionIndex wins from 1,000 words up, by a rapidly widening margin. Construction is the honest cost throughout: DeletionIndex is 13×–25× slower to build at every size. Neither structure replaces the other — FuzzyIndex stays the default (cheaper, more predictable, no build-time distance ceiling); reach for DeletionIndex when the dictionary is large, max_distance is known ahead of time, and query volume is high enough to amortize the steep one-time build.


TF-IDF

Corpus build/ingestion and query/scoring (docs/COMPETITIVE_BENCHMARKS.md§1.12) against tfidf (afshinm) 0.3.0 (a stateful add()/idf()/tfidf() struct — the architecturally closest Rust match found, but a genuinely different, unsmoothed weighting formula) and rust-tfidf 1.1.1 (query/scoring only — it has no ingestion step, nothing to time as "build"). Both comparisons are explicitly build/query speed only — neither crate's output values are compared against Verbora's, since the weighting formulas differ by design.

LibraryVersionLanguageTime (median, build, 256 docs)ThroughputRelative
tfidf (afshinm)0.3.0Rust75.19 ms13.3/s1.00×
Verbora0.1.0Rust522.49 ms1.9/s6.95× slower
DocsVerboratfidf (afshinm)
48.73 ms1.10 ms
1633.99 ms4.54 ms
64134.79 ms18.76 ms
256522.49 ms75.19 ms
A real, disclosed ingestion loss — with the matching query-time win it buys, shown right below. See PERFORMANCE_GAPS.md entry 13: tfidf's add() is a single space-split pass with zero allocation, no lowercasing, no real tokenizer, no stop-word filtering. Verbora's add_document runs its own full pipeline — lowercasing, real word-boundary tokenization, stop-word filtering, interning — because that is what its own behaviour contract and its own O(1) query-time payoff (below) require.
LibraryVersionLanguageTime (median, tfidf() query, 256 docs)ThroughputRelative
Verbora0.1.0Rust64.7 ns15.46M/s1.00×
rust-tfidf1.1.1Rust1.03 ms971.9/s15,904× slower
tfidf (afshinm)0.3.0Rust235.99 ms4.2/s~3.65M× slower
DocsVerboratfidf (afshinm)rust-tfidf
465.1 ns4.33 ms12.47 µs
1665.0 ns15.79 ms41.86 µs
6463.8 ns59.39 ms131.99 µs
25664.7 ns235.99 ms1.03 ms

Verbora's query cost is flat regardless of corpus size (the interned, incrementally-maintained document-frequency table this crate's own build cost pays for); both competitors rescan the whole corpus on every query, so their cost grows linearly with it. idf() shows the same pattern (module "tfidf", group "idf", in results.json). This is the same trade-off in both directions: expensive-but-thorough ingestion buying near-free, corpus-size-independent queries — not a one-sided result either way.


Classifiers

BayesClassifier training and prediction against smartcore 0.6.5's MultinomialNB (by far the most downloaded classifier candidate found, 476K downloads, actively maintained) and linfa-bayes 0.8.1's MultinomialNb (docs/COMPETITIVE_BENCHMARKS.md§1.13). Both operate on a pre-built dense count matrix, not raw text — vocabulary construction is placed inside the timed closure on both sides specifically to match Verbora's own text-in/model-out boundary, per this module's own bench doc comment.

LibraryVersionLanguageTime (median, train, 1024 docs)ThroughputRelative
smartcore0.6.5Rust1.52 ms658.3/s1.00×
linfa-bayes0.8.1Rust2.89 ms346.5/s1.90× slower
Verbora0.1.0Rust8.90 ms112.4/s5.86× slower
DocsVerborasmartcorelinfa-bayes
432.07 µs5.35 µs422.85 µs
16136.63 µs24.62 µs1.06 ms
64583.80 µs89.92 µs1.76 ms
2562.30 ms313.92 µs2.04 ms
10248.90 ms1.52 ms2.89 ms
LibraryVersionLanguageTime (median, predict)ThroughputRelative
linfa-bayes0.8.1Rust1.02 µs979.9K/s1.00×
smartcore0.6.5Rust3.71 µs269.3K/s3.64× slower
Verbora0.1.0Rust9.31 µs107.4K/s9.12× slower
A real, disclosed loss — a clean one against smartcore, a genuine crossover against linfa-bayes. See PERFORMANCE_GAPS.md entry 15: Verbora is 6.0×–6.9× slower than smartcore at every size, but faster than linfa-bayes below 256 docs (13× at 4 docs) and slower above it. Verbora's per-document cost includes real, specified tokenization, Porter stemming and stop-word filtering; neither competitor's benchmark adapter does any of that — a whitespace split and a lowercase is the whole preprocessing step on both competing sides.

Accuracy: is the slower classifier at least more correct?

A separate, signal-bearing corpus (four non-overlapping topical vocabularies, generated by tools/bench-data/generate.py) was built specifically because the training corpus above is shape-only random data — useless for accuracy. cargo test --test classifiers_accuracy trains all three implementations at each size and scores them against a fixed, disjoint 128-document test set:

Train sizeVerborasmartcorelinfa-bayes
498.4%93.0%93.0%
16100.0%100.0%100.0%
64100.0%100.0%100.0%
256100.0%100.0%100.0%
1024100.0%100.0%100.0%

All three converge to a perfect score by 16 training documents. Read alongside the speed table above: this is neither "Verbora is slower but more correct" nor "faster but less correct" — accuracy is statistically indistinguishable between the three at every size that matters, so the speed numbers stand as measured, not offset by a quality difference that is not actually there on this test set. (This corpus's four vocabularies share no words at all — an easy separation problem, good for catching a broken implementation, not sharp enough to meaningfully discriminate real-world accuracy past the smallest size.)


No Rust competitor exists: WordNet, analyzers, sentiment

Three of the workspace's 19 audited modules have no fair Rust competitor at all — every candidate found was investigated and rejected on maintenance, adoption or scope grounds (WordNet: the one candidate is abandoned since 2017; analyzers and sentiment: no Rust crate performs the specific composed task). Per this project's NO FAIR COMPETITOR FOUND policy, none is forced. A widely-used JavaScript NLP library remains the available baseline for those modules. Reproduce or publish any comparison using the method on this site; do not infer a Rust ranking where no equivalent Rust implementation exists.

N-grams has a real Rust competitor for character n-gram generation — see N-Grams above. Its separate comparison against the JavaScript library is a separate result set from the Rust comparison documented here.

Full reasoning for every rejected candidate: docs/COMPETITIVE_BENCHMARKS.md § 3.

Phonetic Index / Phonetic Neighbors (PhoneticIndex) has zero competitors of any kind — a Verbora-native extension with no upstream equivalent to compare against. Its own internal build/query benchmark suite lives on the Phonetic neighbors feature page instead of here.

Upstream bugs found

Two verification disciplines surfaced real, reproducible defects in third-party dependencies — none in Verbora's own code: re-verifying crates flagged as stale or abandoned before trusting their numbers (this audit's own "do not trust marketing benchmarks — reproduce locally" rule), and the differential fuzzing behind the byte-exact phonetics table (Phonetics). Disclosed here, not filed upstream without separate confirmation.

  • triple_accel 0.4.0rdamerau_exp("tac", "tatc") returns 2; the correct restricted-Damerau-Levenshtein distance is 1. Confirmed against strsim::damerau_levenshtein and triple_accel's own plain levenshtein_exp (both return 1). Real impact: fast_symspell uses this exact function as its post-lookup verification pass, so it can silently miss or misrank a correction on an ordinary doubled-letter typo.
  • fst 0.4.7 — its Levenshtein automaton silently returns incomplete results for same-byte-length multi-byte UTF-8 substitutions (e.g. Cyrillic characters one substitution apart). Matches a still-open upstream issue, BurntSushi/fst#38, opened 2017. The ASCII-only corpus this page's own fst comparisons use never exercises it.
  • eddie 0.4.2 — its internal buffer code violates a slice::get_unchecked_mut safety precondition on ordinary input (the textbook Wikipedia Jaro example, not an edge case), aborting any debug-profile build on a modern Rust toolchain. Does not reproduce in --release; the numbers on this page (a --release audit throughout) are unaffected. Real, if latent, evidence for the "abandoned since 2020" caveat this page already carries for that crate.
  • rphonetic 3.0.6 — several encoders panic on realistic non-ASCII input, all reproduced against 3.0.6 release builds during the differential fuzzing above: Nysiis in strict mode byte-slices its code at offset 6 with no character-boundary check, panicking whenever a longer code's byte 6 splits a multi-byte character (4,233 of the 104,114 fuzzed inputs); Caverphone1/Caverphone2 panic the same way at their fixed 6-/10-byte code cut; RefinedSoundex indexes a 26-entry mapping table out of bounds for any alphabetic character whose uppercase form leaves AZ (é, Cyrillic, CJK, the Kelvin sign); and MatchRatingApproach can panic on both its encode path (mid-character truncation) and its compare path (an empty-encoding underflow: ("ab", "..") panics where ("..", "ab") returns false). The ASCII-only shared corpus never exercises any of these; on every one of those inputs Verbora's own seven byte-exact encoders return a documented substitute output instead of panicking — the one place they deliberately do not match rphonetic.

Library coverage summary

What each library actually covers, not what its scope implies. ✓ = genuine, broadly-equivalent coverage exists; P = coverage exists but only for a narrowed input domain, a reconfigured competitor, or part of the module; = no fair competitor was found in that ecosystem for this capability at all. This does not claim other libraries are trying to be all-in-one — it shows honestly that none of them are, without implying Verbora's breadth makes it better at any one of these than a specialist crate necessarily is.

CapabilityVerboraJS libraryRust ecosystem
TokenizersP
N-gramsP
StemmersP
NormalizersP
InflectorsP
PhoneticsP
Phonetic index / neighbors
Distances
Language detectionP
Script detectionP
TransliterationP
TF-IDFP
Classifiers (Bayes / logistic / MaxEnt)P
Sentiment
WordNet
POS taggingP
SpellcheckP
TrieP
Analyzers

19 of 19 — Verbora. 16 of 19 — the JS library (missing language detection, script detection, and phonetic indexing, which it never implemented). 0 of 19 — any single Rust crate at full, unqualified equivalence across a whole module; the Rust ecosystem's real strength shows up inside individual algorithms instead — strsim/rapidfuzz are genuine Yes-equivalence competitors for most of Distances, rust-stemmers for 9 of 17 stemmer variants — not as one library matching Verbora's combined scope. That fragmentation is the whole reason this audit went module-by-module rather than searching for one all-in-one Rust rival: no such rival exists, and claiming one would misrepresent the comparison.

Competitors — attribution

Every library compared anywhere on this page, with its official repository, package registry page, and documentation.

LibraryLanguageVersionLicenseRepositoryPackageDocs
JavaScript NLP libraryJavaScript8.1.1MITrepository README
ngrammaticRust0.7.0MITGitHubcrates.iodocs.rs
strsimRust0.11.1MITGitHubcrates.iodocs.rs
rapidfuzzRust0.5.0MITGitHubcrates.iodocs.rs
triple_accelRust0.4.0MITGitHubcrates.iodocs.rs
eddieRust0.4.2MITGitHubcrates.iodocs.rs
tantivyRust0.26.1MITGitHubcrates.iodocs.rs
tokenizers (Hugging Face)Rust0.23.1Apache-2.0GitHubcrates.iodocs.rs
rust-stemmersRust1.2.0MIT / BSD-3-ClauseGitHubcrates.iodocs.rs
snowball_stemmers_rsRust1.0.1MITGitHubcrates.iodocs.rs
nltk-porterRust0.1.0Apache-2.0GitHubcrates.iodocs.rs
porter-stemmerRust0.1.2MPL-2.0GitHubcrates.iodocs.rs
lindera-analysisRust5.2.0MITGitHubcrates.iodocs.rs
sastrawiRust0.1.1MITGitHubcrates.iodocs.rs
diacriticsRust0.2.2GPL-3.0GitHubcrates.iodocs.rs
ordinalRust0.4.0MPL-2.0GitHubcrates.iodocs.rs
trie-rsRust0.4.2MIT OR Apache-2.0GitHubcrates.iodocs.rs
qp-trieRust0.8.2MPL-2.0GitHubcrates.iodocs.rs
fast_radix_trieRust1.2.0MITGitHubcrates.iodocs.rs
fstRust0.4.7MIT OR UnlicenseGitHubcrates.iodocs.rs
rphoneticRust3.0.6Apache-2.0GitHubcrates.iodocs.rs
whatlangRust0.18.0MITGitHubcrates.iodocs.rs
linguaRust1.8.0Apache-2.0GitHubcrates.iodocs.rs
whichlangRust0.1.1MITGitHubcrates.iodocs.rs
wana_kanaRust5.0.0MITGitHubcrates.iodocs.rs
postaggerRust0.0.3Apache-2.0GitHubcrates.iodocs.rs
rust-bertRust0.23.0Apache-2.0GitHubcrates.iodocs.rs
symspellRust0.5.2MITGitHubcrates.iodocs.rs
harper-coreRust2.8.0Apache-2.0GitHubcrates.iodocs.rs
spellbookRust0.4.2MPL-2.0GitHubcrates.iodocs.rs
fast_symspellRust0.1.10MITno repository URL in its published metadata — re-verified via cargo's registry-cache tarball rather than taken on trust, see the Spellcheck section abovecrates.iodocs.rs
tfidf (afshinm)Rust0.3.0MITGitHubcrates.iodocs.rs
rust-tfidfRust1.1.1MIT OR Apache-2.0GitHubcrates.iodocs.rs
smartcoreRust0.6.5Apache-2.0GitHubcrates.iodocs.rs
linfa-bayesRust0.8.1MIT OR Apache-2.0GitHubcrates.iodocs.rs

Full research dossier for every candidate considered — including every crate investigated and not selected, and why — lives in docs/COMPETITIVE_BENCHMARKS.md.

Reproducing these numbers

Everything on this page regenerates from a clean checkout:

bash
# Shared inputs both sides read (run once)
python3 tools/bench-data/generate.py

cd benchmarks/competitive

# Third-party model/dictionary assets for POS tagging and spellcheck
./scripts/fetch-models.sh

# Every module's Criterion benchmarks (this page's numbers)
cargo bench --release

# Machine metadata (results/metadata.json)
./scripts/machine-metadata.sh

# Join Criterion's raw output into results/results.json + results/raw/
python3 scripts/collect-results.py distance levenshtein:verbora,strsim,rapidfuzz ...
# (see benchmarks/competitive/README.md for the full per-module command list)

# Language-detection accuracy report
cargo run --release --example language_accuracy

../../scripts/competitive-benchmarks.sh (repo root) drives all of the above in one command. Full detail, including the exact collect-results.py invocation for every module, is in benchmarks/competitive/README.md and in each module's own dossier in docs/COMPETITIVE_BENCHMARKS.md.

  • String distance results — the JavaScript-library baseline this page's Distance section extends with real Rust competitors.
  • Benchmark method — warmup, sample-count and regression-tracking conventions this page inherits.
  • Parallelism — Verbora's own sequential-vs-parallel numbers, thread counts disclosed, for the APIs this page's competitors have no equivalent to compare against.
  • docs/PERFORMANCE_GAPS.md — every real loss on this page, with its investigated likely cause and — where one exists — a flagged, not-yet-implemented optimization opportunity.
  • docs/COMPETITIVE_BENCHMARKS.md — the full research matrix: every competitor considered, selected or rejected, and why.

Released under the MIT License.