Wolf Garbe
Wolf Garbe CEO and co-founder of SeekStorm

symspell_rs v7 - 3x faster lookup, 40% less memory consumption, 11x faster Damerau-Levenshtein edit distance

symspell_rs v7 - 3x faster lookup, 40% less memory consumption, 11x faster Damerau-Levenshtein edit distance

symspell_rs v7 introduces both significant implementation changes to SymSpell as well as algorithmic changes to the Damerau-Levenshtein calculation between v6.8.4 vs. v7.0.1.
The resulting performance improvements are measured with the new benchmark suite and discussed below.

Changes

Improved Damerau-Levenshtein calculation, optimal string alignment (OSA) variant

  • Optimized SymSpell v7 damerau_levenshtein_osa by using bit-parallel OSA (Hyyrö 2003), making it 11x faster than SymSpell v6.8.4 damerau_levenshtein_osa.
  • Optimized SymSpell v7 damerau_levenshtein_osa_fallback by using the multi-word block version of the bit-parallel OSA (Hyyrö 2003), to cover any length., making it 7x faster than than SymSpell v6.8.4 damerau_levenshtein_osa for terms > 64 chars.
  • Exposed damerau_levenshtein_osa as a public method for standalone use.

Improved SymSpell implementation: internal storage, allocation free, shared term table, lightweight hasher

  • Internal storage: words now maps terms to ids, and counts live in a new term table. Serialized dictionaries from previous versions (serde feature) are not compatible.
  • The derived PartialEq on SymSpell compares term ids, so dictionaries with the same content but a different insertion order compare unequal.
  • ASCII input now takes an allocation-free fast path: stack-based candidates, borrowed hits, and Suggestion strings created only for the returned results (after sort and max_results). Non-ASCII input uses a char-based path that shares the same verification code.
  • Delete buckets store compact 8-byte entries (term id, length, ASCII flag) that refer to a shared term table, instead of a heap copy of the term per delete. This lowers memory use and removes pointer chasing.
  • The deletes map uses a lightweight hasher, since its keys are already 32-bit hashes.

Verbose Benchmark setup

  • Benchmark harness: divan with a tracking global allocator. Results below are the figures printed to stderr by the benchmark.
  • 81 lookup experiments per version: English frequency dictionaries with 30k, 82k and 500k entries × prefix_length 5, 6, 7 × maximum edit distance 1, 2, 3 × Verbosity Top, Closest, All. The dictionary is rebuilt for each maximum edit distance (maximum dictionary edit distance = maximum edit distance), which gives 27 dictionary builds per version.
  • Average latency = average time per lookup() call. Speedup = latency of v6.8.4 ÷ latency of current (higher is better).
  • Build time = time to build the dictionary. Resident memory = resident memory after the build. Peak memory = maximum memory consumption during the build.
  • Charts use a logarithmic y-axis where values span orders of magnitude (latency, build memory across dictionary sizes).
  • Verbose benchmark based on divan: cargo bench --bench verbose --features gxhash

Basic benchmark setup

  • Lookup latency experiments (162 in total): the 30k, 82k and 500k English frequency dictionaries × prefix_length 5, 6, 7 × maximum edit distance 1, 2, 3 × Verbosity Top, Closest, All × {this version, symspell_rs 6.8.4}.
  • Load dictionary time experiments (27 in total): the 30k, 82k and 500k English frequency dictionaries × prefix_length 5, 6, 7 × maximum edit distance 1, 2, 3
  • Damerau-Levenshtein OSA latency experiments (6 in total): maximum edit distance 1, 2, 3 x bit-parallel OSA, multi-word block bit-parallel OSA
  • Basic benchmark: cargo bench --bench basic --features gxhash

Test data

noisy_query_en_1000.txt

For the query we use the first 1000 unique words from Norvig’s text corpus big.txt.

For each word a random number of edits in the range 0..Min(word.length/2 , 4) is chosen. For each edit a random type of edit (delete, insert random char, replace with random char, switch adjacent chars) is applied at a random position within the word. After the edits no duplicates and words with length<2 are allowed.

frequency_dictionary_en_30_000.txt

These are the 29,159 unique words from Norvig’s text corpus big.txt, together with their frequency in that corpus.

frequency_dictionary_en_82_765.txt

The frequency_dictionary_en_82_765.txt was created by intersecting the two lists mentioned below. By reciprocally filtering only those words which appear in both lists are used. Additional filters were applied and the resulting list truncated to ≈ 80,000 most frequent words.

frequency_dictionary_en_500_000.txt

These are the most frequent 500,000 words from the English One Million list from Google Books Ngram data, together with their frequency.

All three test data files are released on GitHub.

Summary: what improved in the new version

  • Faster Damerau-Levenshtein calculation, (optimal string alignment (OSA) variant). All 3 Damerau-Levenshtein experiments are faster, by a geometric mean of 10.9× (range 10.0× to 12.2×).
    • damerau_levenshtein_osa using bit-parallel OSA (Hyyrö 2003) (length <= 64 chars): 10.9x on average (10.0× to 12.2×)
    • damerau_levenshtein_osa_fallback using the multi-word block version of the bit-parallel algorithm (Hyyrö 2003) (any length): 6.9x faster on average (6.7x to 8.0x)
    • max edit distance 1: 12.2× (OSA), 8.0 (OSA fallback)
    • max edit distance 2: 10.6× (OSA), 6.2 (OSA fallback)
    • max edit distance 3: 10.0× (OSA), 6.7 (OSA fallback)
  • Faster lookups in every experiment. All 81 lookup experiments are faster, by a geometric mean of 2.8× (range 1.5× to 5.9×).
    • Verbosity::Top: 3.0× on average (1.8× to 5.9×)
    • Verbosity::Closest: 2.9× on average (1.5× to 4.2×)
    • Verbosity::All: 2.7× on average (1.6× to 4.8×)
    • max edit distance 1: 2.4× on average (1.5× to 3.7×)
    • max edit distance 2: 2.8× on average (2.1× to 5.2×)
    • max edit distance 3: 3.4× on average (1.8× to 5.9×)
    • 30,000-word dictionary: 2.9× on average (1.5× to 4.8×)
    • 82,765-word dictionary: 3.0× on average (1.8× to 5.9×)
    • 500,000-word dictionary: 2.6× on average (1.6× to 3.5×)
  • Much lower memory use during lookups. Peak heap allocation per lookup is on average only 31% of v6.8.4 (best case 17%, worst case 77%).
    • Verbosity::Top: 18% of v6.8.4 on average (17% to 18%)
    • Verbosity::Closest: 32% of v6.8.4 on average (18% to 73%)
    • Verbosity::All: 52% of v6.8.4 on average (33% to 77%)
  • Roughly 40% less memory for the dictionary. Resident memory after the build is on average 62% of v6.8.4 (best case 48%, worst case 83%). Peak memory during the build is on average 64% of v6.8.4 (48% to 88%).
    • Largest example, 500k dictionary, prefix 7, edit distance 3: resident memory 874 MiB → 436 MiB, peak 874 MiB → 436 MiB, build time 14.20 s → 12.60 s.
  • Build time is on par or slightly better. On average the build is 1.09× as fast as v6.8.4 (range 0.86× to 1.58×). 6 of 27 builds are marginally slower (at most 17%), so the memory savings come at no real build-time cost.

💡 The interesting part:

In the first step, we improved the Damerau-Levenshtein calculation only, while letting the SymSpell implementation unchanged. An 11x faster edit distance gave only a 20% speedup for SymSpell. That told us two things: 1️⃣ SymSpell’s makes spelling correction latency largely independent of raw edit distance performance. The symmetric delete algorithm already avoids most of that work. 2️⃣ Further performance gains had to come from SeekStorm implementation improvements rather than edit distance calculation optimization. We did, and achieved 300% speedup for SymSpell.

Damerau-Levenshtein (OSA) latency

  • strsim v0.11.1 osa_distance uses a vanilla implementation of the Damerau-Levenshtein algorithm.
  • SymSpell v6.8.4 damerau_levenshtein_osa uses a vanilla implementation of the Damerau-Levenshtein algorithm.
  • SymSpell v7.0.1 damerau_levenshtein_osa uses bit-parallel OSA (Hyyrö 2003) (length <= 64 chars)
  • SymSpell v7.0.1 damerau_levenshtein_osa_fallback uses multi-word block version of the bit-parallel OSA (Hyyrö 2003) (any length)
max edit distanceSymSpell v6.8.4 damerau_levenshtein_osaSymSpell v7.0.1 damerau_levenshtein_osaspeedup
1173 ns14 ns12.18×
2170 ns16 ns10.63×
3178 ns17 ns10.04×

damerau_levenshtein_osa latency


max edit distancestrsim v0.11.1 osa_distanceSymSpell v7.0.1 damerau_levenshtein_osaspeedup
1169 ns14 ns11.89×
2180 ns16 ns11.28×
3181 ns17 ns10.22×

damerau_levenshtein_osa latency


max edit distanceSymSpell v6.8.4 damerau_levenshtein_osaSymSpell v7.0.1 damerau_levenshtein_osa_fallbackspeedup
1173 ns21 ns7.99×
2170 ns27 ns6.17×
3178 ns26 ns6.67×

damerau_levenshtein_osa_fallback latency

Using noisy_query_en_1000.txt, calculation the edit distance between misspelled_string and corrected_string, for the given maximum edit distance.

Lookup latency

30,000-word dictionary

30,000 words, Verbosity::Top

max edit distanceprefix lengthv6.8.4currentspeedup
153.41 µs1.27 µs2.69×
162.84 µs1.28 µs2.22×
172.38 µs1.12 µs2.12×
2523.1 µs6.63 µs3.48×
2612.6 µs4.40 µs2.86×
279.67 µs3.17 µs3.05×
3591.7 µs21.3 µs4.31×
3638.3 µs9.54 µs4.01×
3731.9 µs6.70 µs4.76×

Lookup latency, 30,000 words, Top

30,000 words, Verbosity::Closest

max edit distanceprefix lengthv6.8.4currentspeedup
153.17 µs1.22 µs2.60×
163.53 µs1.27 µs2.78×
171.98 µs1.31 µs1.51×
2522.2 µs6.98 µs3.18×
2612.0 µs4.53 µs2.66×
278.50 µs3.70 µs2.30×
3591.1 µs21.8 µs4.18×
3638.3 µs10.2 µs3.75×
3723.2 µs7.05 µs3.29×

Lookup latency, 30,000 words, Closest

30,000 words, Verbosity::All

max edit distanceprefix lengthv6.8.4currentspeedup
155.97 µs2.16 µs2.76×
165.20 µs2.15 µs2.42×
175.07 µs2.03 µs2.50×
2591.3 µs30.0 µs3.04×
2655.2 µs18.6 µs2.96×
2740.3 µs19.1 µs2.11×
35941.9 µs265.5 µs3.55×
36452.4 µs136.7 µs3.31×
37431.0 µs177.1 µs2.43×

Lookup latency, 30,000 words, All

82,765-word dictionary

82,765 words, Verbosity::Top

max edit distanceprefix lengthv6.8.4currentspeedup
158.96 µs3.36 µs2.67×
164.03 µs2.10 µs1.92×
175.32 µs1.42 µs3.75×
2555.3 µs24.4 µs2.27×
2638.3 µs7.40 µs5.17×
2728.1 µs7.39 µs3.80×
35319.4 µs54.6 µs5.85×
36139.1 µs32.0 µs4.35×
3739.4 µs21.6 µs1.83×

Lookup latency, 82,765 words, Top

82,765 words, Verbosity::Closest

max edit distanceprefix lengthv6.8.4currentspeedup
156.28 µs2.40 µs2.62×
165.60 µs1.74 µs3.22×
176.39 µs1.74 µs3.67×
2555.4 µs23.9 µs2.32×
2629.9 µs10.2 µs2.93×
2718.4 µs7.88 µs2.34×
35315.8 µs89.0 µs3.55×
3686.2 µs21.3 µs4.04×
3765.4 µs21.3 µs3.07×

Lookup latency, 82,765 words, Closest

82,765 words, Verbosity::All

max edit distanceprefix lengthv6.8.4currentspeedup
159.64 µs4.47 µs2.16×
167.20 µs3.53 µs2.04×
1710.7 µs3.59 µs2.98×
25335.0 µs86.0 µs3.89×
26121.9 µs52.7 µs2.31×
27106.1 µs45.9 µs2.31×
353.89 ms808.3 µs4.81×
361.66 ms424.8 µs3.91×
371.02 ms421.0 µs2.42×

Lookup latency, 82,765 words, All

500,000-word dictionary

500,000 words, Verbosity::Top

max edit distanceprefix lengthv6.8.4currentspeedup
1515.5 µs6.47 µs2.40×
167.32 µs3.24 µs2.26×
175.74 µs2.85 µs2.01×
25241.7 µs81.1 µs2.98×
26107.6 µs35.3 µs3.05×
2730.8 µs12.6 µs2.44×
351.06 ms316.5 µs3.35×
36307.9 µs87.0 µs3.54×
3780.3 µs29.1 µs2.77×

Lookup latency, 500,000 words, Top

500,000 words, Verbosity::Closest

max edit distanceprefix lengthv6.8.4currentspeedup
1516.4 µs6.79 µs2.42×
1613.1 µs3.77 µs3.49×
175.72 µs3.15 µs1.82×
25243.6 µs90.5 µs2.69×
2666.3 µs25.0 µs2.66×
2736.8 µs13.2 µs2.79×
351.07 ms302.0 µs3.54×
36348.2 µs103.7 µs3.36×
3779.0 µs30.3 µs2.61×

Lookup latency, 500,000 words, Closest

500,000 words, Verbosity::All

max edit distanceprefix lengthv6.8.4currentspeedup
1572.3 µs22.0 µs3.28×
1629.0 µs18.4 µs1.57×
1727.9 µs16.2 µs1.72×
251.84 ms677.9 µs2.71×
261.02 ms415.8 µs2.45×
27990.3 µs406.6 µs2.44×
3525.48 ms9.25 ms2.75×
3610.93 ms3.85 ms2.84×
379.66 ms4.12 ms2.34×

Lookup latency, 500,000 words, All

Dictionary build

Build time

dictionaryprefix lengthmax dictionary edit distancev6.8.4currentchange
30,0005148 ms44 ms1.09× faster
30,00052148 ms125 ms1.18× faster
30,00053217 ms197 ms1.10× faster
30,0006158 ms51 ms1.14× faster
30,00062198 ms190 ms1.04× faster
30,00063339 ms346 ms1.02× slower
30,0007177 ms69 ms1.11× faster
30,00072267 ms246 ms1.08× faster
30,00073781 ms635 ms1.23× faster
82,76551254 ms160 ms1.58× faster
82,76552462 ms499 ms1.08× slower
82,76553736 ms859 ms1.17× slower
82,76561234 ms238 ms1.02× slower
82,76562813 ms843 ms1.04× slower
82,765631.40 s1.20 s1.17× faster
82,76571246 ms212 ms1.16× faster
82,765721.00 s966 ms1.04× faster
82,765732.30 s2.20 s1.05× faster
500,000511.40 s963 ms1.45× faster
500,000523.20 s3.00 s1.07× faster
500,000534.80 s4.70 s1.02× faster
500,000611.70 s1.70 ssame
500,000624.20 s4.30 s1.02× slower
500,000636.70 s6.60 s1.02× faster
500,000712.00 s1.60 s1.25× faster
500,000726.40 s6.10 s1.05× faster
500,0007314.20 s12.60 s1.13× faster

Build time

Resident memory

dictionaryprefix lengthmax dictionary edit distancev6.8.4currentchange
30,000518.6 MiB6.5 MiB-24%
30,0005216.3 MiB9.4 MiB-42%
30,0005323.8 MiB12.2 MiB-49%
30,0006111.2 MiB8.8 MiB-21%
30,0006226.2 MiB16.9 MiB-35%
30,0006340.1 MiB22.1 MiB-45%
30,0007115.8 MiB13.1 MiB-17%
30,0007238.0 MiB26.4 MiB-31%
30,0007359.7 MiB34.3 MiB-43%
82,7655121.8 MiB16.6 MiB-24%
82,7655244.4 MiB24.9 MiB-44%
82,7655366.7 MiB33.2 MiB-50%
82,7656127.8 MiB21.4 MiB-23%
82,7656268.2 MiB41.3 MiB-39%
82,76563109.2 MiB56.3 MiB-48%
82,7657137.5 MiB30.1 MiB-20%
82,7657296.0 MiB61.8 MiB-36%
82,76573161.7 MiB85.4 MiB-47%
500,00051128.3 MiB91.7 MiB-29%
500,00052261.7 MiB141.3 MiB-46%
500,00053390.4 MiB189.1 MiB-52%
500,00061154.8 MiB111.6 MiB-28%
500,00062376.1 MiB214.0 MiB-43%
500,00063611.9 MiB300.5 MiB-51%
500,00071195.5 MiB147.1 MiB-25%
500,00072502.5 MiB300.8 MiB-40%
500,00073874.0 MiB435.7 MiB-50%

Resident memory

Peak memory during build

dictionaryprefix lengthmax dictionary edit distancev6.8.4currentchange
30,000519.3 MiB7.3 MiB-22%
30,0005216.9 MiB10.1 MiB-40%
30,0005324.2 MiB12.9 MiB-47%
30,0006111.9 MiB9.6 MiB-19%
30,0006226.7 MiB18.2 MiB-32%
30,0006340.4 MiB22.6 MiB-44%
30,0007118.2 MiB16.0 MiB-12%
30,0007239.1 MiB31.5 MiB-19%
30,0007359.7 MiB34.8 MiB-42%
82,7655121.8 MiB16.6 MiB-24%
82,7655244.4 MiB24.9 MiB-44%
82,7655366.7 MiB33.2 MiB-50%
82,7656127.8 MiB21.4 MiB-23%
82,7656268.2 MiB41.3 MiB-39%
82,76563109.2 MiB56.3 MiB-48%
82,7657138.0 MiB32.7 MiB-14%
82,7657296.0 MiB65.6 MiB-32%
82,76573161.7 MiB85.4 MiB-47%
500,00051133.6 MiB101.0 MiB-24%
500,00052261.7 MiB146.2 MiB-44%
500,00053390.4 MiB189.1 MiB-52%
500,00061159.1 MiB120.6 MiB-24%
500,00062376.1 MiB225.0 MiB-40%
500,00063611.9 MiB302.4 MiB-51%
500,00071199.2 MiB155.9 MiB-22%
500,00072502.5 MiB305.8 MiB-39%
500,00073874.1 MiB435.7 MiB-50%

Peak memory during build

Peak heap allocation during lookups

Peak heap allocation of a single lookup (tracking global allocator); lower is better. Ratio = current ÷ v6.8.4.

30,000-word dictionary

max edit distanceprefix lengthTop v6.8.4Top currentClosest v6.8.4Closest currentAll v6.8.4All current
155.5 KiB1.0 KiB (17%)8.0 KiB3.8 KiB (48%)8.0 KiB3.9 KiB (49%)
165.5 KiB1.0 KiB (17%)8.0 KiB3.8 KiB (48%)8.0 KiB3.9 KiB (49%)
175.5 KiB1.0 KiB (17%)8.0 KiB3.8 KiB (48%)8.0 KiB3.9 KiB (49%)
2542.1 KiB7.5 KiB (18%)47.2 KiB14.6 KiB (31%)62.9 KiB38.2 KiB (61%)
2621.6 KiB3.8 KiB (18%)31.4 KiB14.6 KiB (47%)63.1 KiB38.2 KiB (61%)
2721.6 KiB3.8 KiB (18%)31.4 KiB14.6 KiB (47%)63.1 KiB38.2 KiB (61%)
35166.7 KiB30.0 KiB (18%)169.0 KiB31.5 KiB (19%)497.1 KiB245.5 KiB (49%)
3686.0 KiB15.0 KiB (17%)88.6 KiB16.5 KiB (19%)416.3 KiB225.5 KiB (54%)
3746.8 KiB8.5 KiB (18%)49.4 KiB14.6 KiB (30%)293.8 KiB225.5 KiB (77%)

82,765-word dictionary

max edit distanceprefix lengthTop v6.8.4Top currentClosest v6.8.4Closest currentAll v6.8.4All current
155.6 KiB1.0 KiB (17%)6.7 KiB3.3 KiB (49%)10.8 KiB4.0 KiB (37%)
165.6 KiB1.0 KiB (17%)6.7 KiB3.3 KiB (49%)8.3 KiB4.0 KiB (48%)
175.6 KiB1.0 KiB (17%)6.7 KiB3.3 KiB (49%)8.3 KiB4.0 KiB (48%)
2583.5 KiB15.0 KiB (18%)86.0 KiB16.5 KiB (19%)125.0 KiB67.8 KiB (54%)
2622.5 KiB3.8 KiB (17%)26.9 KiB10.2 KiB (38%)125.3 KiB67.8 KiB (54%)
2722.0 KiB3.8 KiB (17%)27.0 KiB10.2 KiB (38%)125.3 KiB67.8 KiB (54%)
35332.3 KiB60.0 KiB (18%)334.3 KiB61.5 KiB (18%)1.45 MiB493.5 KiB (33%)
36168.1 KiB30.0 KiB (18%)170.7 KiB31.5 KiB (18%)834.0 KiB453.5 KiB (54%)
3788.0 KiB16.0 KiB (18%)90.2 KiB17.5 KiB (19%)833.0 KiB453.5 KiB (54%)

500,000-word dictionary

max edit distanceprefix lengthTop v6.8.4Top currentClosest v6.8.4Closest currentAll v6.8.4All current
1511.0 KiB1.9 KiB (17%)18.5 KiB13.5 KiB (73%)42.3 KiB15.9 KiB (37%)
1610.9 KiB1.9 KiB (17%)18.5 KiB13.5 KiB (73%)31.3 KiB15.9 KiB (51%)
1710.9 KiB1.9 KiB (17%)18.5 KiB13.5 KiB (73%)31.3 KiB15.9 KiB (51%)
25329.8 KiB60.0 KiB (18%)332.4 KiB61.5 KiB (19%)992.1 KiB484.6 KiB (49%)
2685.4 KiB15.0 KiB (18%)89.1 KiB22.5 KiB (25%)992.1 KiB484.6 KiB (49%)
2783.5 KiB15.0 KiB (18%)94.0 KiB22.5 KiB (24%)992.1 KiB484.6 KiB (49%)
352.58 MiB480.0 KiB (18%)2.58 MiB486.0 KiB (18%)7.76 MiB3.89 MiB (50%)
36666.0 KiB120.0 KiB (18%)668.7 KiB123.0 KiB (18%)6.49 MiB3.57 MiB (55%)
37335.2 KiB61.0 KiB (18%)340.6 KiB64.0 KiB (19%)4.61 MiB3.57 MiB (77%)

Notes

  • Each lookup experiment is a short run, so single cells can be noisy; the trend is consistent across all 81 experiments.

1000x Faster Spelling Correction algorithm
Fast approximate string matching with large edit distances in Big Data
Very fast Data cleaning of product names, company names & street names
Sub-millisecond compound aware automatic spelling correction
SymSpell vs. BK-tree: 100x faster fuzzy string search & spell checking
Fast Word Segmentation for noisy text
The Pruning Radix Trie — a Radix trie on steroids

Application

Possible application fields of the SymSpell algorithm are those of fast approximate dictionary string matching: spell checkers for word processors and search engines, correction systems for optical character recognition, natural language translation based on translation memory, record linkage, de-duplication, matching DNA sequences, fuzzy string searching and fraud detection.

For a single user or for small edit distances other algorithms might just be fine. But for search engines and search-as-a-service search API where you have to serve thousands of concurrent users, while still maintaining a latency of a few milliseconds, and where spelling correction is not even the main procession task, but only one of many components in query preprocessing, you need the fastest spelling correction you can get.

Source code

The Rust implementation of the Symmetric Delete Spelling Correction algorithm is released on GitHub as Open Source under the MIT license.

The C# implementation of the Symmetric Delete Spelling Correction algorithm is released on GitHub as Open Source under the MIT license.

Ports

There are ports in C, C++, Crystal, Go, Haskell, Java, Javascript, Julia, Kotlin, Objective-C, PHP, Python, Ruby, Rust, Scala, Swift and Zig available.

Rating: