<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Victor Shepelev</title>
    <description>The latest articles on DEV Community by Victor Shepelev (@zverok).</description>
    <link>https://dev.to/zverok</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F324515%2F4a64be3c-ba8a-4e5a-8c4f-0f209f0cdde0.png</url>
      <title>DEV Community: Victor Shepelev</title>
      <link>https://dev.to/zverok</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/zverok"/>
    <language>en</language>
    <item>
      <title>Rebuilding the spellchecker, pt.4: Introduction to suggest algorithm</title>
      <dc:creator>Victor Shepelev</dc:creator>
      <pubDate>Fri, 22 Jan 2021 10:49:12 +0000</pubDate>
      <link>https://dev.to/zverok/rebuilding-the-spellchecker-pt-4-introduction-to-suggest-algorithm-814</link>
      <guid>https://dev.to/zverok/rebuilding-the-spellchecker-pt-4-introduction-to-suggest-algorithm-814</guid>
      <description>&lt;p&gt;&lt;strong&gt;&lt;em&gt;This is the fourth part of the "Rebuilding the spellchecker" series, dedicated to explaining how the world's most popular spellchecker Hunspell works.&lt;/em&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Today's topic is &lt;strong&gt;suggest&lt;/strong&gt;!&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Quick recap&lt;/strong&gt;:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;In the &lt;strong&gt;&lt;a href="//2021-01-05-spellchecker-1.html"&gt;first part&lt;/a&gt;&lt;/strong&gt;, I've described what Hunspell is; and why I decided to rewrite it in Python. It is an &lt;strong&gt;explanatory rewrite&lt;/strong&gt; dedicated to uncovering the knowledge behind the Hunspell by "translating" it into a high-level language, with a lot of comments.&lt;/li&gt;
&lt;li&gt;In the &lt;strong&gt;&lt;a href="//2021-01-09-spellchecker-2.html"&gt;second part&lt;/a&gt;&lt;/strong&gt;, I've covered the basics of the &lt;strong&gt;lookup&lt;/strong&gt; (word correctness check through the dictionary) algorithm, including &lt;em&gt;affix compression&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;In the &lt;strong&gt;&lt;a href="//2021-01-14-spellchecker-3.html"&gt;third part&lt;/a&gt;&lt;/strong&gt;, the rest of the lookup is explained: compounding, word breaking, and text case.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;And now, we'll switch to the juiciest part of the spellchecking problem: guessing the corrections for the misspelled word, called &lt;em&gt;suggest&lt;/em&gt; in Hunspell. This post only draws the big picture of suggestion algorithms in general and the Hunspell's particular flavor. Even more &lt;del&gt;nasty&lt;/del&gt; amazingly curious details would be covered in the next issue (or, rather, issues).&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem with suggest
&lt;/h2&gt;

&lt;p&gt;The question "how the suggest works?" was what drew me initially to the project. The lookup part seemed trivial. And even if, as I understood later, it is not that trivial, the lookup is still a task with a &lt;em&gt;known answer&lt;/em&gt;. The word is either correct or not; the spellchecker, however it is implemented and however it stores its data, should just say whether it is correct. All the complexity of lookup implementation is only a set of optimizations, because it is hard or impossible to just store a list of "all correct words".&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;But suggest is a different beast altogether.&lt;/strong&gt; There are many ways to misspell a word, due to mis&lt;i&gt;typing&lt;/i&gt;, genuine error, or OCR glitch; and going back from the misspelled word to the correct one is no easy task. Frequently, only the text's author can say for sure what is right: was "throught" meant to be "through", "thought", or maybe "throughout"?.. What about "restraunt": "restraint" or "restaurant"? Ideally, there should be exactly one guess (then we can even apply auto-correct to the user's text), but that's rarely the case.&lt;/p&gt;

&lt;p&gt;Even when the human can guess "what word was misspelled here", it is not always obvious what is an algorithmic way to deduce the correct word from the misspelled one, such that its results &lt;em&gt;felt correct&lt;/em&gt; for the human. Moreover, the algorithm found for one case or set of cases may produce an irrelevant result in others, and it is hard to find the objective measure of whether your suggester is "good".&lt;/p&gt;

&lt;p&gt;So, while lookup approaches vary only by their performance, the smallest tweaks in the suggestion algorithm might produce dramatically different results.&lt;/p&gt;

&lt;h2&gt;
  
  
  How it can be done
&lt;/h2&gt;

&lt;p&gt;The famous article by Peter Norvig "&lt;a href="https://norvig.com/spell-correct.html"&gt;How to Write a Spelling Corrector&lt;/a&gt;" describes the possible algorithm in these steps:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;generate multiple "edits" of the word (insert one letter, remove one letter, swap two adjacent letters, etc.)&lt;/li&gt;
&lt;li&gt;from all edits, select the words that are present in the dictionary;&lt;/li&gt;
&lt;li&gt;rank them by word's commonness (using a source dictionary with weights, or a big source text which is summarized to "word → how often it is used");&lt;/li&gt;
&lt;li&gt;take the first one as a singular good suggestion.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The entire algorithm implementation in Python takes less lines than most of the core methods of Spylls.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Note that Norvig's article is an awesome, concise, and friendly explanation of the basic &lt;em&gt;idea&lt;/em&gt; of how spellchecking &lt;em&gt;might&lt;/em&gt; work, intended to create the intuition about the process. But it is by no means enough to build a good spellchecker. Unfortunately, quite a few libraries exist that claim to be production-ready spellchecking solution implementing "the famous Norvig's algorithm". They ignore both "The full details of an industrial-strength spell corrector are quite complex..." at the very beginning of the article and a large section "Future Work" in the end. In real life, the results are typically less than satisfying. Much less.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Some of the modern approaches to spellchecking still take this road: for example, &lt;a href="https://github.com/wolfgarbe/SymSpell"&gt;SymSpell&lt;/a&gt; algorithm (claiming to be "1 million times faster") is at its core just a brilliant idea for a novel storage format for a flat word list, that allows optimizing the calculation of edit distance significantly.&lt;/p&gt;

&lt;p&gt;Most of the "industrial-strength spell correctors" (using Norvig's definition), though, are multi-stage. They produce possible corrections with several different algorithms and, most frequently, return several suggestions, not relying on the algorithm's ability to guess the very best one.&lt;/p&gt;

&lt;p&gt;For example, &lt;a href="http://aspell.net/"&gt;Aspell&lt;/a&gt;, one of the Hunspell's "uncles"&lt;sup id="fnref1"&gt;1&lt;/sup&gt;  (still considered by some to have better suggestion quality &lt;em&gt;for English&lt;/em&gt;), has quite &lt;a href="http://aspell.net/man-html/Aspell-Suggestion-Strategy.html"&gt;succinct description&lt;/a&gt; of its suggestion strategy, and even exposes command-line options for the user to control &lt;a href="http://aspell.net/0.50-doc/man-html/4_Customizing.html#suggestion"&gt;some parameters&lt;/a&gt; of this strategy.&lt;/p&gt;

&lt;p&gt;Hunspell's approach is much more complicated, not to say "cumbersome". From what I can guess—I didn't dive deep into history and reasoning behind all the decisions—it grew organically with Hunspell's popularity, resulting from a multitude of cases and requirements from users of a variety of languages. There is no single "complex algorithm" that can be extracted and explained on the whiteboard, but rather a sequence of simpler algorithms. They are guided by a ton of settings that can be present in aff-files and kept together by lots of tests.&lt;/p&gt;

&lt;h2&gt;
  
  
  How Hunspell does it
&lt;/h2&gt;

&lt;p&gt;Hunspell does the search for a correction in the following stages:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Generate a list of edits and check their correctness with the lookup, but

&lt;ul&gt;
&lt;li&gt;there are many more of them than the classic insert-delete-swap-replace; in fact, more than dozen, depending on the particular language meta-information provided by aff-file;&lt;/li&gt;
&lt;li&gt;there is no ranking/reordering of edits (neither by word popularity nor by closeness to the original word); the order of their calculation &lt;em&gt;is&lt;/em&gt; the order they will be returned: it is assumed that Hunspell's code already applies edits in the highest-probability-first order.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;If there were no results on the edit stage, or they weren't considered very good (more on this later), the search through the entire dictionary is performed:

&lt;ul&gt;
&lt;li&gt;the similarity of the misspelled word and each dictionary stem is calculated with rough and quick formula;&lt;/li&gt;
&lt;li&gt;for top-100 similar stems, all of their affix forms are produced, and similarity to them is calculated with another rough and quick formula;&lt;/li&gt;
&lt;li&gt;for top-200 of similar affixed forms, a very complicated and precise formula is used to choose only the best suggestions.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;There &lt;em&gt;might&lt;/em&gt; be an optional third stage: metaphone (pronunciation) based suggestions... Although, it depends on the existence of the metaphone encoding data in dictionary's aff-file, and there is a &lt;em&gt;very&lt;/em&gt; small number of such dictionaries in the wild (namely, one). We'll touch on this curious topic briefly in the future.&lt;/li&gt;
&lt;li&gt;Finally, some post-processing is performed on the suggestion, like converting it to the same character case as an initial misspelling (&lt;em&gt;unless&lt;/em&gt; it is a prohibited case for this word!) or replacing some characters with "output conversion" rules.&lt;/li&gt;
&lt;/ol&gt;

&lt;blockquote&gt;
&lt;p&gt;For the impatient: we'll cover the details of the implementation of each stage in the future posts, but you can begin reading the docs and the code right now, starting from the &lt;a href="https://spylls.readthedocs.io/en/latest/hunspell/algo_suggest.html"&gt;&lt;code&gt;algo.suggest&lt;/code&gt;&lt;/a&gt; module.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Quality estimation
&lt;/h2&gt;

&lt;p&gt;Is Hunspell's suggestion algorithm good? And &lt;em&gt;how&lt;/em&gt; good is it?&lt;/p&gt;

&lt;p&gt;Those questions are open ones—and even the way they can be answered is unclear. Intuitively, Hunspell's suggestions are quite decent—otherwise, it wouldn't be the most widespread spellchecker, after all. A fair amount of "unhappy customers" can be easily found, too, in &lt;a href="https://github.com/hunspell/hunspell/issues"&gt;hunspell's repo issues&lt;/a&gt;. At the same time, one should distinguish between different reasons for the sub-par suggestion quality. It might be due to the algorithm itself, or due to the source data quality: the literal absence of the desired suggestion in the dictionary, or lack of aff-file settings that could've guided Hunspell to finding it.&lt;/p&gt;

&lt;p&gt;Hunspell's development process, to the best of my knowledge, doesn't use any realistic text corpora to evaluate suggestion algorithm—only feature-by-feature synthetic tests.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;In contrast, Aspell's site &lt;a href="http://aspell.net/test/cur/"&gt;provides an evaluation dataset&lt;/a&gt; for English, including comparison with Hunspell (Aspell wins, by a large margin). Hunspell's repo actually &lt;a href="https://github.com/hunspell/hunspell/tree/master/tests/suggestiontest"&gt;contains&lt;/a&gt; something similar: script to evaluate Hunspell vs. Aspell based on Wikipedia's &lt;a href="https://en.wikipedia.org/wiki/Wikipedia:Lists_of_common_misspellings"&gt;List of common misspellings&lt;/a&gt; (Hunspell wins), but mostly for informational purposes: the results are neither promoted nor used as a reference point for further development.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The current Hunspell's development consensus "what's the best suggestion algorithm" is maintained by a multitude of synthetic &lt;a href="https://github.com/hunspell/hunspell/tree/master/tests"&gt;test dictionaries&lt;/a&gt;, validating that one of the suggestion features, or set of them, works (and frequently indirectly validating other features). This situation is both a blessing and a curse: synthetic tests provide stable enough environment to refactor Hunspell (or to rewrite it in a different language, IYKWIM); on the other hand, there is no direct way to test the &lt;em&gt;quality&lt;/em&gt;—the tests only confirm that &lt;em&gt;features work in expected order&lt;/em&gt;. So, there is no way to prove that some big redesign, or some alternative spellchecker passes the quality check at least &lt;em&gt;as good as Hunspell&lt;/em&gt; and improves over this baseline.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;There is, for example, a curious &lt;a href="https://github.com/bakwc/JamSpell#benchmarks"&gt;evaluation table&lt;/a&gt; provided by a modern ML-based spellchecker JamSpell. According to it, JamSpell is awesome—while Hunspell is a mere 0.03% better than dummy ("fix nothing") spellchecker... Which doesn't ring true, somehow!&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;My initial assumption for the Spylls project was that understanding the current implementation in full would be a precondition for public experimentation to improve it significantly. Or—as I dreamed—we'll be able to mix-and-match approaches of several spellcheckers (at least Hunspell and Aspell, considering, say, &lt;a href="https://battlepenguin.com/tech/aspell-and-hunspell-a-tale-of-two-spell-checkers/"&gt;the popular article&lt;/a&gt; demonstrating the cases where the latter beats the former). What I uncovered, though, makes me suspect that relying on feature-by-feature tests and strict ordering of simple algorithms makes Hunspell too rigid for a breakthrough quality improvement... But more on this later.&lt;/p&gt;

&lt;p&gt;For now—however we estimate the quality, &lt;em&gt;practically, it works&lt;/em&gt;. &lt;strong&gt;In the next part,&lt;/strong&gt; we'll look closely at all the hoops Hunspell jumps through in order to provide meaningful &lt;strong&gt;edit-based suggestions&lt;/strong&gt;.  Follow me &lt;a href="https://twitter.com/zverok"&gt;on Twitter&lt;/a&gt; or &lt;a href="///subscribe.html"&gt;subscribe to my mailing list&lt;/a&gt; if you don't want to miss the follow-up!&lt;/p&gt;

&lt;p&gt;PS: Huge thanks to &lt;a href="https://twitter.com/squadette"&gt;@squadette&lt;/a&gt;, my faithful editor. Without his precious help, the text would be even more convoluted!&lt;/p&gt;




&lt;ol&gt;

&lt;li id="fn1"&gt;
&lt;p&gt;Aspell is older than Hunspell, but it is not its direct ancestor. There was once an old &lt;a href="https://en.wikipedia.org/wiki/Ispell"&gt;Ispell&lt;/a&gt;, then &lt;a href="https://en.wikipedia.org/wiki/GNU_Aspell"&gt;Aspell&lt;/a&gt; and &lt;a href="https://en.wikipedia.org/wiki/MySpell"&gt;MySpell&lt;/a&gt; were created independently to replace it, then Hunspell superseded MySpell (and also Aspell took some features from MySpell too, namely affix compression). It's complicated. "Uncle" would be the most appropriate family relation. ↩&lt;/p&gt;
&lt;/li&gt;

&lt;/ol&gt;

</description>
      <category>spellcheck</category>
      <category>python</category>
      <category>textprocessing</category>
      <category>showdev</category>
    </item>
    <item>
      <title>Rebuilding the spellchecker, pt.3: Lookup—compounds and solutions</title>
      <dc:creator>Victor Shepelev</dc:creator>
      <pubDate>Fri, 15 Jan 2021 10:11:32 +0000</pubDate>
      <link>https://dev.to/zverok/rebuilding-the-spellchecker-pt-3-lookup-compounds-and-solutions-2a0j</link>
      <guid>https://dev.to/zverok/rebuilding-the-spellchecker-pt-3-lookup-compounds-and-solutions-2a0j</guid>
      <description>&lt;p&gt;&lt;strong&gt;&lt;em&gt;This is the third part of the "Rebuilding the spellchecker" series, dedicated to the explanation of how the world's most popular spellchecker Hunspell works.&lt;/em&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Quick recap&lt;/strong&gt;:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;In the &lt;strong&gt;&lt;a href="https://dev.to/zverok/rebuilding-the-most-popular-spellchecker-part-1-25e4"&gt;first part&lt;/a&gt;&lt;/strong&gt;, I've described what Hunspell is; and why I decided to rewrite it in Python. It is an &lt;strong&gt;explanatory rewrite&lt;/strong&gt; dedicated to uncovering the knowledge behind the Hunspell by "translating" it into a high-level language, with a lot of comments.&lt;/li&gt;
&lt;li&gt;In the &lt;strong&gt;&lt;a href="https://dev.to/zverok/rebuilding-the-spellchecker-pt-2-just-look-in-the-dictionary-they-said-gmb"&gt;second part&lt;/a&gt;&lt;/strong&gt; I've covered the basics of the &lt;strong&gt;lookup&lt;/strong&gt; (word correctness check through the dictionary) algorithm, including &lt;em&gt;affix compression&lt;/em&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This part is a carry-over of &lt;strong&gt;lookup algorithm explanation&lt;/strong&gt;, dedicated to &lt;strong&gt;word compounding&lt;/strong&gt; and some less complicated but nonetheless important concerns: word case and word-breaking. To understand this part, reading &lt;a href="https://dev.to/zverok/rebuilding-the-spellchecker-pt-2-just-look-in-the-dictionary-they-said-gmb"&gt;the previous one&lt;/a&gt; is strongly suggested. At very least you should remember that there are &lt;em&gt;stems&lt;/em&gt; with &lt;em&gt;flags&lt;/em&gt;, specified by &lt;code&gt;.dic&lt;/code&gt;-file, with the meaning of flags defined in &lt;code&gt;.aff&lt;/code&gt;-file.&lt;/p&gt;

&lt;h2&gt;
  
  
  Word compounding
&lt;/h2&gt;

&lt;p&gt;Many languages, like German, Dutch, or Norvegian, have &lt;em&gt;word compounding&lt;/em&gt;: two stems can be joined together, producing the new word. To check the spelling of the word in the language with compounding, the spellchecker needs to break it into all possible parts, and check if there exists a combination of parts such that all parts would be correct words that are allowed inside compound words.&lt;/p&gt;

&lt;p&gt;Hunspell has two independent mechanisms to specify the compounding logic of a language in aff-file: &lt;strong&gt;per-stem flags&lt;/strong&gt;, and &lt;strong&gt;regexp-like rules&lt;/strong&gt;. Sometimes both mechanisms are used in the same dictionary.&lt;/p&gt;

&lt;h3&gt;
  
  
  Per-stem flag checks
&lt;/h3&gt;

&lt;p&gt;There is a generic &lt;code&gt;COMPOUNDFLAG&lt;/code&gt; directive to specify a flag, which, when attached to a stem, means "this stem can be anywhere in a compound" (examples from LibreOffice's Norvegian dictionary):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# In nb_NO.aff:
...
# Directive defines: any word with "z" flag is allowed to be in a compound
COMPOUNDFLAG z

# In nb_NO.dic:
...
fritt/CEGKVz
...
røyk/AEGKVWz
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both &lt;code&gt;fritt&lt;/code&gt; ("free") and &lt;code&gt;røyk&lt;/code&gt; ("smoke") have &lt;code&gt;z&lt;/code&gt; flag, which means they could be in any place in compound word, and thus, "røykfritt" ("smoke-free" = "non-smoking") is a valid one—and "frittrøyk" too&lt;sup id="fnref1"&gt;1&lt;/sup&gt;.&lt;/p&gt;

&lt;p&gt;There are also more precise &lt;code&gt;COMPOUNDBEGIN&lt;/code&gt;/&lt;code&gt;COMPOUNDMIDDLE&lt;/code&gt;/&lt;code&gt;COMPOUNDEND&lt;/code&gt; directives, setting the flags for stems which can be only at a certain place in compounds. Flags designated by those directives could be freely mixed: a compound can consist of a part marked with generic &lt;code&gt;COMPOUNDFLAG&lt;/code&gt;, and another part marked with &lt;code&gt;COMPOUNDEND&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;To check the compound word for correctness, Hunspell needs to chop off the beginning of the word, of every possible length, and check if it is a valid stem which is allowed at the beginning of the compound. If so, the algorithm recursively chops the next parts, till the whole word is split into compound parts (or no suitable parts found).&lt;/p&gt;

&lt;p&gt;Note that depending on the word's length, and on how many dictionary words are allowed to be in compounds, the loop can take quite some time: the process we described in the &lt;a href="https://dev.to/zverok/rebuilding-the-spellchecker-pt-2-just-look-in-the-dictionary-they-said-gmb"&gt;previous part&lt;/a&gt; (affix-based search of the correct form) can be repeated dozens of times for various "part candidates".&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Let &lt;a href="https://spylls.readthedocs.io/en/latest/hunspell/algo_lookup.html#spylls.hunspell.algo.lookup.Lookup.compounds_by_flags"&gt;&lt;code&gt;Lookup.compound_by_flags&lt;/code&gt;&lt;/a&gt; in Spylls documentation be your guide!&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Defining compounds as regexp-like rules
&lt;/h3&gt;

&lt;p&gt;There is another way to specify compounding logic. It is implemented by &lt;code&gt;COMPOUNDRULE&lt;/code&gt; directive, with statements like &lt;code&gt;A*B?C&lt;/code&gt; (meaning, "correct compound consists of any number of words with the flag &lt;code&gt;A&lt;/code&gt;, then one or zero words with the flag &lt;code&gt;B&lt;/code&gt;, then a mandatory word with the flag &lt;code&gt;C&lt;/code&gt;"). The most common use of it is specifying suffixes in numerals. For example, in the &lt;code&gt;en_US&lt;/code&gt; dictionary:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# en_US.aff
COMPOUNDRULE 2     # we have 2 compound rules listed below
COMPOUNDRULE n*1t  # rule 1: any number (*) of "n"-marked stems, then "1"-marked stem, then "t"-marked stem
COMPOUNDRULE n*mp  # rule 2: any number (*) of "n"-marked stems, then "m"-marked stem, then "p"-marked stem

# en_US.dic
# ...defines numbers as "stems" for this rule:
0/nm
1/n1
2/nm
3/nm
4/nm
5/nm
6/nm
7/nm
8/nm
9/nm
# ...and numerical suffixes as stems, with different flags, too!
0th/pt
1st/p
1th/tc
2nd/p
2th/tc
3rd/p
3th/tc
4th/pt
5th/pt
6th/pt
7th/pt
8th/pt
9th/pt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This leads to Hunspell being able to say that "1201st" is correct (the rule &lt;code&gt;n*mp&lt;/code&gt; matched: "1" and "2" with "n" flags, "0" with "m", and "1st" with "p"), and "1211th" is correct (another rule in action: &lt;code&gt;n*1t&lt;/code&gt;), but "1211st" is not.&lt;/p&gt;

&lt;p&gt;Handling the word correctness check in a presence of the &lt;code&gt;COMPOUNDRULE&lt;/code&gt; requires to again recursively split word into possible parts—but this time already found parts should be checked for a partial match against known rules.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;a href="https://spylls.readthedocs.io/en/latest/hunspell/algo_lookup.html#spylls.hunspell.algo.lookup.Lookup.compounds_by_rules"&gt;Lookup.compound_by_rules&lt;/a&gt; implements this in a complicated, yet concise way.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  But wait, there is more!
&lt;/h3&gt;

&lt;p&gt;&lt;del&gt;To make things more complicated&lt;/del&gt; To match the complexity of real life, both algorithms of compound words checking need to consider:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Numeric &lt;strong&gt;limitations&lt;/strong&gt;: some dictionaries might limit the minimum size of a part of the compound, or the maximum number of parts.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Affixes:&lt;/strong&gt; By default, any prefix is allowed at the beginning of the compound word, and any suffix is allowed at the end; and yet, some affixes might have flags saying "it should never be in any compound", and some others might have flags saying "it is allowed &lt;em&gt;in the middle&lt;/em&gt; of the compound" (e.g. prefix to non-first or suffix to a non-last part).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Several rules&lt;/strong&gt; that, being present in aff-file, reject some compound words with seemingly correct parts as incorrect: for example, if the letter at the boundary of the compound is tripled (&lt;code&gt;fall+lucka&lt;/code&gt;); if some parts of the compound are repeated (&lt;code&gt;dubb+bon+bon&lt;/code&gt;); if the non-first part of the compound is capitalized, or "this regexp-like pattern is prohibited at the boundary of the compound parts", and so on.&lt;/li&gt;
&lt;li&gt;Some of those settings might lead to a whole &lt;strong&gt;new word checking loop&lt;/strong&gt; in the middle of compound checking: for example, &lt;code&gt;CHECKCOMPOUNDREP&lt;/code&gt; setting tells the algorithm: use the &lt;code&gt;REP&lt;/code&gt;-table specified in aff-file (typical misspelled sequences of letters, like "f=&amp;gt;ph", usually used on suggest) to check if some part of the compound, with replacement applied, is the valid word. If yes, then it is an incorrect compound! E.g. "badabum" split into parts "ba", "da", "bum", but then, if we apply the replacement "u=&amp;gt;oo", turns out "daboom" is a correct non-compound word... Then we should consider "badabum" a misspelling, and the "ba daboom" is most probably what was misspelled.&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;Are you thrilled? Then follow the &lt;a href="https://spylls.readthedocs.io/en/latest/hunspell/algo_lookup.html#spylls.hunspell.algo.lookup.Lookup.compound_forms"&gt;&lt;code&gt;Lookup.compound_forms&lt;/code&gt;&lt;/a&gt; docs to uncover even more dirty details.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  ...and other complications
&lt;/h2&gt;

&lt;p&gt;Affix check and (de)compounding are the main parts of the algorithm, yet there is more! Just a brief overview to give you some taste:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Words case: "kitten" can be spelled "Kitten" or "KITTEN", but "Paris" can't be spelled "paris"

&lt;ul&gt;
&lt;li&gt;...but, the word might have a flag defined as &lt;code&gt;KEEPCASE&lt;/code&gt; in aff-file, meaning it should be ONLY in the exact case as in the dictionary;&lt;/li&gt;
&lt;li&gt;...and there are complications with the German language: "SS" can be downcased as "ss" or "ß" ("sharp s"), and both should be checked through the dictionary, and also, when the word is uppercased, it is allowed to have "ß": "STRAßE";&lt;/li&gt;
&lt;li&gt;...and in Turkic languages casing rules for "i" are different: "i=&amp;gt;İ" and "I=&amp;gt;ı";&lt;/li&gt;
&lt;li&gt;...and the ending part of the compound might have a flag saying "this compound should be titlecased": in Swedish dictionary, there are special words like "afrika", which are allowed only at the end of compounds, and require the whole compound to be in titlecase: "Sydafrika" (South Afrika);&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;
&lt;li&gt;Word breaking: "foo-bar" should be checked as the whole word, and also as two separate words "foo" and "bar"

&lt;ul&gt;
&lt;li&gt;...unless aff-file redefines this, by prohibiting word-breaking, or changing by which patterns words should be broken.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;
&lt;li&gt;Some words might be present in the dictionary with a flag defined as &lt;code&gt;FORBIDDENWORD&lt;/code&gt;: it is used to disallow words that are logically possible (allowed stem with allowed suffix), but this specific combination is incorrect in the language.&lt;/li&gt;
&lt;li&gt;There might be an &lt;code&gt;ICONV&lt;/code&gt; ("input conversions") directive defined in aff-file, saying which chars convert before the spellchecking: for example, replacement of several kinds of typographic apostrophes with simple &lt;code&gt;'&lt;/code&gt; to simplify the dictionary, or unpacking the ligatures (&lt;code&gt;ﬁ&lt;/code&gt; → &lt;code&gt;fi&lt;/code&gt;).

&lt;ul&gt;
&lt;li&gt;But this feature can be used not only for handling fancy typography: for example, the Dutch dictionary uses it for enforcing proper case of "ij": In Dutch, it is considered a single entity and both letters should always have the same case. It is achieved by &lt;code&gt;ICONV&lt;/code&gt;-ing to ligatures: &lt;code&gt;ij&lt;/code&gt;→&lt;code&gt;ĳ&lt;/code&gt; and &lt;code&gt;IJ&lt;/code&gt;→&lt;code&gt;Ĳ&lt;/code&gt; (but &lt;code&gt;Ij&lt;/code&gt; wouldn't be converted, and wouldn't be found in a dictionary, as all dictionary words also contain ligatures).&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;
&lt;li&gt;An &lt;code&gt;IGNORE&lt;/code&gt; directive, defined in aff-file, says which characters to drop before spellchecking (in Arabic and Semitic languages, where vowels may be present but should be ignored).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That's mostly the size of it!&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;To go for the full ride, start reading the Spylls docs from the &lt;a href="https://spylls.readthedocs.io/en/latest/hunspell/algo_lookup.html#spylls.hunspell.algo.lookup.Lookup.__call__"&gt;&lt;code&gt;Lookup.__call__&lt;/code&gt;&lt;/a&gt; method. You won't be disappointed!&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Lookup: takeout
&lt;/h2&gt;

&lt;p&gt;To reiterate on everything said above: There are good and useful dictionaries for spellchecking of many languages, freely available in Hunspell's format, and one might be tempted to reuse them in own code. But the process of going from the not-that-complicated input format to a full reliable spellchecking includes at least:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;em&gt;Reading of aff-files (consisting of multiple directive "types", with reading logic depending on particular directive) and dic-files (words with flags)—we'll talk about this interesting task in later installments&lt;sup id="fnref2"&gt;2&lt;/sup&gt;;&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;Affix analysis: either on-the-fly (how Hunspell and Spylls do), or once: "unpack" the list of stems with flags into words with affixes;&lt;/li&gt;
&lt;li&gt;Compounding analysis—unless you just want to omit the support for languages with compounding (this apparently &lt;em&gt;can't&lt;/em&gt; be solved with a pre-generated list of "all correct compound words");&lt;/li&gt;
&lt;li&gt;Handling of complications with word breaking, text case, special characters, and whatnot.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Some of those tasks are directly related to how Hunspell is built and could've been done differently. But mostly, this chapter tries to show that &lt;strong&gt;"check if the word spelled correctly", especially in languages other than English, should be seen as a not-that-trivial task&lt;/strong&gt;, to say the least.&lt;/p&gt;

&lt;p&gt;And we haven't even started on &lt;strong&gt;corrections suggestion yet, which we'll gladly do in the next part&lt;/strong&gt;.  Follow me &lt;a href="https://twitter.com/zverok"&gt;on Twitter&lt;/a&gt; or &lt;a href="///subscribe.html"&gt;subscribe to my mailing list&lt;/a&gt; if you don't want to miss the follow-up!&lt;/p&gt;

&lt;p&gt;PS: Huge thanks to &lt;a href="https://twitter.com/squadette"&gt;@squadette&lt;/a&gt;, my faithful editor. Without his precious help, the text would be even more convoluted!&lt;/p&gt;




&lt;ol&gt;

&lt;li id="fn1"&gt;
&lt;p&gt;The second one can't be found in Google—this compound form is &lt;em&gt;grammatically correct&lt;/em&gt;, but doesn't make sense. ↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn2"&gt;
&lt;p&gt;I admit that starting with the proper format description would be &lt;em&gt;logically correct&lt;/em&gt;, but I wanted to tell the juiciest stuff first. There would be a post on file formats and how to read them. For the impatient, Spylls docs cover &lt;a href="https://spylls.readthedocs.io/en/latest/hunspell/data_aff.html"&gt;aff&lt;/a&gt; and &lt;a href="https://spylls.readthedocs.io/en/latest/hunspell/data_aff.html"&gt;dic&lt;/a&gt; files quite comprehensively. ↩&lt;/p&gt;
&lt;/li&gt;

&lt;/ol&gt;

</description>
    </item>
    <item>
      <title>Rebuilding the spellchecker, pt.2: Just look in the dictionary, they said!</title>
      <dc:creator>Victor Shepelev</dc:creator>
      <pubDate>Sun, 10 Jan 2021 13:10:14 +0000</pubDate>
      <link>https://dev.to/zverok/rebuilding-the-spellchecker-pt-2-just-look-in-the-dictionary-they-said-gmb</link>
      <guid>https://dev.to/zverok/rebuilding-the-spellchecker-pt-2-just-look-in-the-dictionary-they-said-gmb</guid>
      <description>&lt;p&gt;&lt;strong&gt;&lt;em&gt;This is the second part of the "Rebuilding the spellchecker" series, dedicated to the explanation of how the world's most popular spellchecker Hunspell works.&lt;/em&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Quick recap:&lt;/strong&gt; &lt;a href="https://dev.to/zverok/rebuilding-the-most-popular-spellchecker-part-1-25e4"&gt;In the first part&lt;/a&gt;, I've described what Hunspell is; and why I decided to rewrite it in Python. It is an &lt;strong&gt;explanatory rewrite&lt;/strong&gt; dedicated to uncovering the knowledge behind the Hunspell by "translating" it into a high-level language, with a lot of comments.&lt;/p&gt;

&lt;p&gt;Now, let's dive into how the stuff really works!&lt;/p&gt;

&lt;p&gt;There are two main parts of word-by-word spellchecker algorithms:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Check if a word is correct: &lt;strong&gt;"lookup"&lt;/strong&gt; part&lt;/li&gt;
&lt;li&gt;Propose the correction for incorrect words: &lt;strong&gt;"suggest"&lt;/strong&gt; part&lt;/li&gt;
&lt;/ol&gt;

&lt;blockquote&gt;
&lt;p&gt;Hunspell also implements several other algorithms to be useful as standalone software. It can extract plain text from numerous formats, like HTML or TeX, split it into words (tokenize), correctly handling punctuation—but at the end of the day, a word-by-word correctness check is applied. I excused myself from implementing "wrapper" algorithms: text extraction and tokenization are thoroughly investigated topics, and there are numerous libraries in any language solving it with decent speed and quality.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Hunspell works on a word-by-word basis (no context is taken into account). Each word is just &lt;strong&gt;looked up&lt;/strong&gt; in the &lt;strong&gt;dictionary&lt;/strong&gt; loaded from the plaintext  &lt;code&gt;&amp;lt;langname&amp;gt;.dic&lt;/code&gt; file in Hunspell-specific format. If it is not considered correct by dictionary lookup (which, as we'll see soon, is more complex than "is it present in the dictionary"), several algorithms of &lt;strong&gt;suggest&lt;/strong&gt; are applied sequentially, trying to find correct words similar to the given one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Hunspell's lookup algorithm, or, Just look in the dictionary, they said!
&lt;/h2&gt;

&lt;p&gt;When coming from English-only spellchecking, the developers tend to perceive the "lookup" part as trivial (e.g., the famous &lt;a href="https://norvig.com/spell-correct.html"&gt;Peter Norvig's article&lt;/a&gt; starts from the assumption that only the correction—"suggest"—part deserves some explanations). But that's not quite so.&lt;/p&gt;

&lt;p&gt;The first and most straightforward idea&lt;sup id="fnref1"&gt;1&lt;/sup&gt; for the lookup would be: we'll just take the dictionary (presumably, the flat list of all correct words) and look for our candidate word in this list: if it is there, it is correct. End of story.&lt;/p&gt;

&lt;p&gt;Now, Hunspell's dictionaries exist for many languages and have a plaintext format, which, at the first sight is quite close to a plain word list—so, probably it would be easy to reuse them? Let's take a look into the &lt;a href="https://github.com/LibreOffice/dictionaries/blob/master/en/en_US.dic"&gt;&lt;code&gt;en_US.dic&lt;/code&gt;&lt;/a&gt; in the LibreOffice dictionary repository. You'll see a list of words in the following format:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;...
acetyl
acetylene/M
ache/DSMG
achene/MS
achievable/U
achieve/BLZGDRS
achievement/SM
...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The line &lt;code&gt;ache/DSMG&lt;/code&gt; specifies the &lt;em&gt;stem&lt;/em&gt; &lt;code&gt;ache&lt;/code&gt;, having &lt;code&gt;D&lt;/code&gt;, &lt;code&gt;S&lt;/code&gt;, &lt;code&gt;M&lt;/code&gt;, &lt;code&gt;G&lt;/code&gt; &lt;em&gt;flags&lt;/em&gt; associated with it. The meaning of flags is defined by &lt;a href="https://github.com/LibreOffice/dictionaries/blob/master/en/en_US.aff"&gt;&lt;code&gt;en_US.aff&lt;/code&gt;&lt;/a&gt; (called "affix file", or just aff-file; every Hunspell dictionary is distributed as a pair of &lt;code&gt;.dic&lt;/code&gt; and &lt;code&gt;.aff&lt;/code&gt; files).&lt;/p&gt;

&lt;h3&gt;
  
  
  Affix compression
&lt;/h3&gt;

&lt;p&gt;In this particular case, all four flags are associated with word &lt;em&gt;suffixes&lt;/em&gt;. Here's the definition of &lt;code&gt;D&lt;/code&gt; suffix:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;SFX D Y 4                    # Suffix header: suffix (SFX), with flag D, combinable with prefixes (Y), 4 entries:
SFX D   0  d    e            # * if the stem ends is "e", strip nothing (0), and add "d"
SFX D   y  ied  [^aeiou]y    # * if the stem ends with "y", preceded by non-vowel, strip "y" and add "ied"
SFX D   0  ed   [^ey]        # * if the stem ends with not "e", and not "y", strip nothing, add "ed"
SFX D   0  ed   [aeiou]y     # * "y" with preceding vowel: strip nothing, add "ed"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For our &lt;code&gt;ache&lt;/code&gt; stem, this definition says that form &lt;code&gt;ached&lt;/code&gt; exists. In the similar fashion, &lt;code&gt;S&lt;/code&gt; flag defines that word &lt;code&gt;aches&lt;/code&gt; exists, &lt;code&gt;G&lt;/code&gt; flag defines &lt;code&gt;aching&lt;/code&gt;, &lt;code&gt;M&lt;/code&gt; flag defines &lt;code&gt;ache's&lt;/code&gt;. So, the &lt;code&gt;ache/DSMG&lt;/code&gt; line in &lt;code&gt;.dic&lt;/code&gt; file specifies 5 correct words: "ache", "ached", "aches", "aching", "ache's".&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Note: the fact that flags are similar to the suffixes they define (&lt;code&gt;S&lt;/code&gt; flag defines suffix &lt;code&gt;-s&lt;/code&gt; and so on) is just a convention. It is rather a handy mnemonics that creators of &lt;code&gt;en_US&lt;/code&gt; dictionary used, and there could be any other symbol.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;In a similar fashion, word prefixes might be defined (specified with flags in dic-file, and described with &lt;code&gt;PFX&lt;/code&gt; directive in aff-file). Say, this definition:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;advantage/GMEDS
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;...defines these forms: &lt;em&gt;advantage, advantage's, advantaging, advantaged, advantages, disadvantage's, disadvantaging, disadvantaged, disadvantages, disadvantage&lt;/em&gt; (all combinations of four suffixes and a prefix "dis-").&lt;/p&gt;

&lt;p&gt;This technique of "packing" dictionaries is called &lt;strong&gt;affix compression&lt;/strong&gt;, and its primary goal is to optimize dictionary size: on disk and in memory. It becomes extremely important for languages with rich inflection. For example, in English one stem might produce no more than ten forms, but in Ukrainian it could easily be dozens or even hundreds, so the amount of possible correct forms quickly grows to tens of millions. "Just a flat list of all known words" &lt;em&gt;might&lt;/em&gt; become impractical (not on today's top MacBook, probably, but still), and that's where the affix compression comes in handy.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;This is not the only possible approach to make dictionary storage more effective: for example, &lt;a href="https://github.com/morfologik/morfologik-stemming"&gt;morfologik&lt;/a&gt; (the default internal spellchecker of the most widely used open-source proofreading software &lt;a href="https://languagetool.org"&gt;LanguageTool&lt;/a&gt;) codes &lt;em&gt;all&lt;/em&gt; possible forms in binary files, using finite-state automata. And this approach is very efficient by speed and memory, but very hard for humans to edit and review, and thus, to keep dictionary up-to-date.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Another benefit of splitting words into stems and affixes: when Hunspell's user wants to add a new word to their personal dictionary, Hunspell allows to just specify "it is inflected the same way as (some other word)", thus sparing the user of teaching the dictionary "'monad' is a word, and 'monads' too, as well as 'monad's'...".&lt;/p&gt;

&lt;p&gt;Note, though, that "suffixes" and "prefixes" specified in some language's dictionary not necessarily correspond to &lt;em&gt;grammatical&lt;/em&gt; suffixes/prefixes of the language. The splitting into stems and affixes is deduced automatically by dictionary authoring tools from flat word lists, so it is up to probabilities whether "common endings" deduced have any grammatical meaning. Actually, Hunspell's format has a rich &lt;a href="https://manpages.debian.org/experimental/libhunspell-dev/hunspell.5.en.html#Optional_data_fields"&gt;sub-language to specify grammatical information&lt;/a&gt;, but of all LibreOffice and Firefox dictionaries I've checked, only a few (Latvian, Slovak, Galician, Breton) made use of this feature.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;One important factor to mention is Hunspell's limitation for the amount of suffixes/prefixes a word may have. Currently, the software understands no more than 2 suffixes and 2 prefixes in a word, which is lower than the common number of grammatical suffixes a lot of languages allow. Most of the dictionaries solve this by "linearizing" the word list: Ukrainian word "громадянство" (citizenship) grammatically consists of the stem "громад-" and suffixes "-ян-", "-ств-", "-о" (the latter is called an ending in grammatically correct terms), but Hunspell's Ukrainian dictionary includes the full word "громадянство". For other languages, the suffix number limitation makes Hunspell totally unusable, so to spellcheck the Finnish, you need to install &lt;a href="https://voikko.puimula.org/"&gt;Voikko&lt;/a&gt; spellchecker.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Affix compression comes with a price&lt;/strong&gt; paid in lookup algorithm complexity and performance. Instead of just a quick lookup through a hashtable or other lookup-optimized structure, we now have to:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Check if the whole word is in the list of stems. If yes, it is correct,

&lt;ul&gt;
&lt;li&gt;...unless it has a flag corresponding to the aff-file directive "this stem &lt;em&gt;requires&lt;/em&gt; prefixes or suffixes".&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;If no, check if the word has some of the known suffixes; and if so, whether the stem without one of those suffixes is in the stem list, &lt;em&gt;and&lt;/em&gt; has a flag corresponding to this suffix.&lt;/li&gt;
&lt;li&gt;If no, check if one more suffix can be found in the stem (then we'll have a stem and two suffixes, and need to check the compatibility of their flags).&lt;/li&gt;
&lt;li&gt;Repeat with prefixes (up to two), and with all possible suffix-prefix combination (taking into account whether suffix and prefix both have "cross-product" allowed).&lt;/li&gt;
&lt;li&gt;Consider that suffixes and prefixes can have flags of their own, specifying "suffixes with this flag might be attached after me", or "if used, this suffix requires that at least one other affix would be present" and ... many other things.&lt;/li&gt;
&lt;li&gt;And there are funny cases like &lt;code&gt;CIRCUMFIX&lt;/code&gt; flag: if the suffix has it, this means that this suffix is only allowed in words having a prefix with the same flag.&lt;/li&gt;
&lt;/ol&gt;

&lt;blockquote&gt;
&lt;p&gt;It is &lt;em&gt;still&lt;/em&gt; a simplified description. To follow the algorithm in full, you can read Spylls docs starting from &lt;a href="https://spylls.readthedocs.io/en/latest/hunspell/algo_lookup.html#spylls.hunspell.algo.lookup.Lookup.affix_forms"&gt;&lt;code&gt;Lookup.affix_forms&lt;/code&gt;&lt;/a&gt;, follow the links to methods it invokes, and read inline comments under "Show code".&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Performance-wise, for each word correctness check, there could be many lookups through known suffixes and prefixes lists, and quite a few dictionary lookups (as consecutive chopping off of suffixes and prefixes produces new stems we need to check).&lt;/p&gt;

&lt;p&gt;And once you tackle the affixes problem, it is all uphill from there!&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Stay tuned for the next installment about the Hunspell lookup, where we'll cover word compounding problem, and some other important edge cases of the word correctness check.&lt;/strong&gt; &lt;/p&gt;




&lt;ol&gt;

&lt;li id="fn1"&gt;
&lt;p&gt;&lt;a href="https://prog21.dadgum.com/29.html"&gt;This blog entry&lt;/a&gt; circa 2008, recently &lt;a href="https://news.ycombinator.com/item?id=25296900"&gt;seen on HackerNews&lt;/a&gt;, even uses the spellchecking as an example of how much we've progressed with our tools, making the spellchecker implementation as trivial as hashmap lookup. ↩&lt;/p&gt;
&lt;/li&gt;

&lt;/ol&gt;

</description>
      <category>spellcheck</category>
      <category>python</category>
      <category>textprocessing</category>
      <category>showdev</category>
    </item>
    <item>
      <title>Rebuilding the most popular spellchecker. Part 1</title>
      <dc:creator>Victor Shepelev</dc:creator>
      <pubDate>Wed, 06 Jan 2021 12:25:19 +0000</pubDate>
      <link>https://dev.to/zverok/rebuilding-the-most-popular-spellchecker-part-1-25e4</link>
      <guid>https://dev.to/zverok/rebuilding-the-most-popular-spellchecker-part-1-25e4</guid>
      <description>&lt;h2&gt;
  
  
  How I decided to write a spellchecker and almost died trying
&lt;/h2&gt;

&lt;p&gt;A few years ago I had a fun idea for a "weekend project": pure-Ruby spellchecker. Ruby is my language of choice, and no-dependencies spellchecker seemed a small useful tool for the CI environment: for example, to check comments/docs spelling without installing any third-party software. I actually &lt;em&gt;could've&lt;/em&gt; pulled out the project in its limited scope (only English, only spot misspelled words without fixing, limited dictionary) with just a flat list of known words, but that's not what happened.&lt;/p&gt;

&lt;p&gt;Back then, I decided to make a moderately generic tool, at least able to work with multiple languages. Fortunately (or so I believed!), there were many already existing and freely available spellchecking dictionaries distributed as LibreOffice and Firefox extensions. All of those dictionaries are in the format defined by the &lt;strong&gt;&lt;a href="http://hunspell.github.io/"&gt;Hunspell&lt;/a&gt;&lt;/strong&gt; tool/library—which is an open-source library that is used for spellchecking in Libre/OpenOffice, Mozilla products (Firefox, Thunderbird), but also Google Chrome/Chromium, macOS, several Adobe products, and so on.&lt;/p&gt;

&lt;p&gt;The dictionaries looked like easy to reuse text files with some ("insignificant" as it seemed) metadata, and the whole "use Hunspell dictionaries from pure Ruby spellchecker" project &lt;em&gt;still&lt;/em&gt; felt like a "weekend-long" one, for the first few weekends. Only gradually the underwater complexity of the multilanguage word-by-word spellchecking uncovered. Eventually, I was distracted from the project and abandoned it, but I still had the fascination with the seemingly-simple, actually-mind-blowingly-complicated Hunspell, the software everybody used daily and hardly ever notice.&lt;/p&gt;

&lt;p&gt;The idea to dig deeper into it, to &lt;em&gt;understand&lt;/em&gt; it and &lt;em&gt;explain&lt;/em&gt;, grew on me and bothered me for quite some time. And what is a better way to understand something, if not to retell it in your own words? After several lazy and not very far-progressed attempts to write something Hunspell-alike (twice in Ruby, once in Rust, once in Python), eventually, in February 2020, the task I settled down to solve is: "explanatory rewrite" of the Hunspell into high-level language with a lot of comments. I achieved this goal by December 2020, with the first release of the &lt;a href="https://github.com/zverok/spylls"&gt;Spylls&lt;/a&gt; project: &lt;strong&gt;the port of Hunspell's core algorithms into modern, well-documented, well-structured Python&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;And now I want to share some insights of what I uncovered on the road: about spellchecking in general and Hunspell in particular.&lt;/p&gt;

&lt;p&gt;In the ongoing article series, I'll cover these topics:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;What is Hunspell, why is it significant, and why try to "explain" it (current article)&lt;/li&gt;
&lt;li&gt;Base spellchecking concepts: lookup and suggest, as seen by Hunspell&lt;/li&gt;
&lt;li&gt;How lookup (checking if the word is correct) works, and why it could be much more complicated than "just look in the list of the known words"&lt;/li&gt;
&lt;li&gt;How suggest (proposed fix for the incorrect word) works, and how hard it is to estimate its quality&lt;/li&gt;
&lt;li&gt;A closer look into Hunspell's dictionary format. It is the most widespread open dictionary format in the world, and we'll see what linguistic and algorithmic information it &lt;em&gt;potentially&lt;/em&gt; can carry, and what part of it is actually used in existing dictionaries&lt;/li&gt;
&lt;li&gt;Some details on Spylls implementation process and results&lt;/li&gt;
&lt;li&gt;Closing thoughts on the big picture of word-by-word spellchecker problem, and Hunspell's approach to it&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  What is Hunspell?
&lt;/h3&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; The information on Hunspell's origins and history is mostly my guesses, following partial and incomplete sources everywhere.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Hunspell (&lt;small&gt;&lt;a href="https://en.wikipedia.org/wiki/Hunspell"&gt;Wikpedia article&lt;/a&gt;&lt;/small&gt;), initially &lt;strong&gt;Hun&lt;/strong&gt;garian spellchecker, emerged as an alternative for previously existing aspell/ispell/myspell somewhere in 2002 (I guess?). It was created by László Németh, in a need of supporting languages with complicated suffixing/prefixing rules and word compounding (such as Hungarian). Hunspell's design seemingly proved itself to be flexible enough to support most of the world's languages, and in a few years, it became the most used spellchecker in the world. You have most probably used it even if you've never heard the name before today: Hunspell is the default spellchecking engine in Chrome and Firefox, Libre/OpenOffice, Adobe products, and macOS (not an exhaustive list). Dictionaries in Hunspell format exist for almost all actively used languages for which the concept of word-by-word spellchecking makes sense&lt;sup id="fnref1"&gt;1&lt;/sup&gt;.&lt;/p&gt;

&lt;p&gt;Currently, Hunspell is maintained &lt;a href="https://github.com/hunspell/hunspell"&gt;on GitHub&lt;/a&gt; (repo has only around 1k stars, will you believe it?). It seems that maintenance is not that easy if you'll weight the number of open issues and PRs, and the latest commits timeline: at the time of writing it (Jan 2021), the last commit to master was of May 2020, and the last release was 1.7 on Dec 2018. Hunspell's codebase is mostly "old-school" C++. It is being slowly modernized and it has very few comments; there are thousands of two-branch &lt;code&gt;if&lt;/code&gt;s to handle non-Unicode and Unicode text separately. There is also an attempt to rewrite Hunspell from scratch in a modern C++, which at some point was developed under the &lt;code&gt;hunspell&lt;/code&gt; GitHub organization. Now it is independent and called &lt;a href="https://github.com/nuspell/nuspell"&gt;nuspell&lt;/a&gt; (and, while not yet supporting all of the Hunspell features, already "achieved" version 4.2.0).&lt;/p&gt;

&lt;p&gt;Obviously, there are open-source spellcheckers other than Hunspell. GNU aspell (that at one point was superseded by Hunspell, but still holds its ground in English suggestion quality), to name one of the older ones; but also there are novel approaches, like &lt;a href="https://github.com/wolfgarbe/SymSpell"&gt;SymSpell&lt;/a&gt;, claiming to be "1 million times faster" or ML-based &lt;a href="https://github.com/bakwc/JamSpell"&gt;JamSpell&lt;/a&gt;, claiming to be much more accurate.&lt;/p&gt;

&lt;p&gt;And yet, what makes Hunspell stand out is its coverage of the world's languages. It is not ideal, but the amount of dictionaries ready to use immediately, and amount of &lt;em&gt;experience&lt;/em&gt; of dealing with typical problems and corner cases, coded into the codebase, is hard to beat.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why rewrite it?
&lt;/h3&gt;

&lt;p&gt;As I've already stated above, the goal of the Spylls project was to create an &lt;em&gt;explanatory&lt;/em&gt; rewrite: E.g., the "retelling" of how Hunspell works in a way that is easy to follow and to play with.&lt;/p&gt;

&lt;p&gt;The necessity of this approach came to me from three facts:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Hunspell is used almost everywhere and is taken for granted;&lt;/li&gt;
&lt;li&gt;It is much more complicated than one might naively expect;&lt;/li&gt;
&lt;li&gt;This complexity—and years of human work that was spent growing the project—is notoriously hard to follow through the Hunspell's codebase and grasp in full.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;In other words, I wanted to &lt;strong&gt;make the knowledge behind Hunspell more open&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The way I have chosen was not, of course, the only one possible. I could've just read through the original code and write a series of articles (or, rather, a book?) on how it works. I could've thoroughly commented and republished the original source code. But I felt that &lt;em&gt;reimplementing&lt;/em&gt; is the only way of understanding what's and why's of the algorithms (at least for somebody not being a Hunspell's core developer); and that implementation in a high-level language will allow focusing on words and language-related algorithms, not memory management or fighting with Unicode.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Note that there are also a few "pragmatic" ports of Hunspell into other languages (in order to use it in environments where C++ dependency is undesireable), namely &lt;a href="https://github.com/aarondandy/WeCantSpell.Hunspell"&gt;WeCantSpell.Hunspell&lt;/a&gt; in C# and &lt;a href="https://github.com/wooorm/nspell"&gt;nspell&lt;/a&gt; in JS (very incomplete); and aforementioned &lt;a href="https://github.com/nuspell/nuspell"&gt;nuspell&lt;/a&gt; can also be considered a "port" (from legacy C++ to a modern one).&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Why Python?
&lt;/h3&gt;

&lt;p&gt;My language of choice is Ruby. It was also the first language that I've tried to port Hunspell into. I'd be happy to proceed with Ruby if my goal has been just a "pragmatic" library. And yet, when I decided that my goal is to make the knowledge of Hunspell's algorithms accessible to a wide audience, I understood that Ruby is not the best choice: language reputation (slightly esoteric and mostly-for-web) would make my project lest noticeable; and my preferred coding style (mix of OO and functional, with lots of small immutable domain objects and fluent chains of iterators), while allowing me to be very effective, would make the code less accessible to other languages users.&lt;/p&gt;

&lt;p&gt;What I needed was a high-level language, with as low boilerplate as possible; as mainstream as possible; as easy to experiment with and prototype as possible. Without diving into too much argument here, Python and modern JavaScript seemed to be the most suitable options, and, to be honest, Python was just closer to my soul. So, here we are!&lt;/p&gt;

&lt;p&gt;The code style is mostly imperative (as it corresponds to how Hunspell is structured), with large-ish, but clearly structured methods, and a small number of classes/objects (mostly they are either "whole algorithm as a class" or almost-passive "structs" -- or, in Python, dataclasses). I tried to limit myself in the usage of complex Python-specific features (like functools or itertools), but have a decent use of "list comprehensions" (as they are quite readable and Pythonic) and generators (lazy lists). Overall, I wanted the code to be good Python, but not too smart. Whether I succeeded, is up to you to decide.&lt;/p&gt;

&lt;p&gt;Currently, &lt;a href="https://github.com/zverok/spylls"&gt;Spylls&lt;/a&gt; has &lt;strong&gt;≈1.5k lines of library code&lt;/strong&gt; in 14 files. It conforms (with &lt;a href="https://spylls.readthedocs.io/en/latest/#completeness"&gt;some reservations&lt;/a&gt;) to all Hunspell's integrational tests. Those tests look like a set of files each, consisting of "test dictionary + what words should be considered good, what words should be considered bad, what should be suggested instead of the bad words", and there are &lt;strong&gt;127 of such sets to pass&lt;/strong&gt;. There are &lt;strong&gt;2 thousand comment lines&lt;/strong&gt; in the code, explaining thoroughly every detail of the algorithm and rendered at the &lt;a href="https://spylls.readthedocs.io/en/latest/hunspell.html"&gt;Spylls documentation site&lt;/a&gt;; note that besides docstrings at the beginning of each class and method, there are also inline comments in code—that's why the documentation site uses custom theme with inline "Show code" feature.&lt;/p&gt;




&lt;p&gt;With this being said, I am wrapping up the introductory post.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;In the next series: An introduction to Hunspell's "lookup" and "suggest" concepts; and deeper dive into the lookup.&lt;/strong&gt;&lt;/p&gt;




&lt;ol&gt;

&lt;li id="fn1"&gt;
&lt;p&gt;Word-by-word spellchecking makes less sense for hieroghliphic languages like Chinese and Japanese; it is also problematic for languages where words aren't separated by whitespaces, like Lao or Thai. ↩&lt;/p&gt;
&lt;/li&gt;

&lt;/ol&gt;

</description>
      <category>spellcheck</category>
      <category>python</category>
      <category>news</category>
      <category>textprocessing</category>
    </item>
  </channel>
</rss>
