Module: Kotoshu::Algorithms::PhonetSuggest
- Defined in:
- lib/kotoshu/algorithms/phonet_suggest.rb
Overview
Phonetic suggestion algorithm provides suggestions based on phonetical (pronunciation) similarity.
Ported from Spylls (Python) phonet_suggest.py
Requires .aff file to define PHONE table (extremely rare in known dictionaries).
Internally:
- Selects words from dictionary similarly to ngram_suggest (and reuses its root_score)
- Scores their phonetic representations (calculated with metaphone) with phonetic representation of misspelling
- Chooses the most similar ones with final_score (ngram-based comparison)
Constant Summary collapse
- MAX_ROOTS =
100
Class Method Summary collapse
-
.final_score(word1, word2) ⇒ Float
Calculate score of suggestion against misspelling.
-
.match_rule(rule, word, pos) ⇒ Integer?
Check if a rule matches at the given position.
-
.metaphone(table, word) ⇒ String
Metaphone calculation.
-
.suggest(misspelling, dictionary_words:, table:) {|String| ... } ⇒ Object
Main entry point for phonetic suggestions.
Class Method Details
.final_score(word1, word2) ⇒ Float
Calculate score of suggestion against misspelling.
101 102 103 104 105 |
# File 'lib/kotoshu/algorithms/phonet_suggest.rb', line 101 def final_score(word1, word2) (2 * StringMetrics.lcslen(word1, word2)) - (word1.length - word2.length).abs + StringMetrics.leftcommonsubstring(word1, word2) end |
.match_rule(rule, word, pos) ⇒ Integer?
Check if a rule matches at the given position. Thin delegate over PhonetTable::Rule#match_length so the matching logic lives in one place — the rule value object — rather than being duplicated here.
156 157 158 |
# File 'lib/kotoshu/algorithms/phonet_suggest.rb', line 156 def match_rule(rule, word, pos) rule.match_length(word, pos) end |
.metaphone(table, word) ⇒ String
Metaphone calculation.
Production in Kotoshu is currently implemented naively as just "search and replace" for rules. To see what potentially should be done, look at aspell's original description: http://aspell.net/man-html/Phonetic-Code.html
117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 |
# File 'lib/kotoshu/algorithms/phonet_suggest.rb', line 117 def (table, word) return word if table.nil? || table.empty? rules = table.rules pos = 0 word_upper = word.upcase result = +'' while pos < word_upper.length char = word_upper[pos] matched = false # Get rules for this character char_rules = rules[char] || [] char_rules.each do |rule| match_result = match_rule(rule, word_upper, pos) next unless match_result result += rule[:replacement] pos += match_result matched = true break end pos += 1 unless matched end result end |
.suggest(misspelling, dictionary_words:, table:) {|String| ... } ⇒ Object
Main entry point for phonetic suggestions.
Note that both this method and NgramSuggest.suggest iterate through the whole dictionary. Hunspell optimizes by doing it all in one loop. Spylls (and Kotoshu) splits them for clarity.
The table structure should have:
- :rules => Hash mapping first character to array of rule hashes Each rule has: :search (Regexp), :replacement (String), :start (Boolean), :end (Boolean)
37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 |
# File 'lib/kotoshu/algorithms/phonet_suggest.rb', line 37 def suggest(misspelling, dictionary_words:, table:, &block) misspelling_lower = misspelling.downcase misspelling_ph = (table, misspelling_lower) scores = [] # First, select words from dictionary whose stems are similar to misspelling # This cycle is exactly the same as the first cycle in ngram_suggest dictionary_words.each do |word| stem = word[:stem] || word # Skip words with length difference > 3 next if (stem.length - misspelling.length).abs > 3 # First, calculate "regular" similarity score, just like in ngram_suggest nscore = NgramSuggest.root_score(misspelling_lower, stem) # Check alternative spellings if available if word[:alt_spellings] word[:alt_spellings].each do |variant| nscore = [nscore, NgramSuggest.root_score(misspelling_lower, variant)].max end end next if nscore <= 2 # Calculate metaphone score word_ph = (table, stem.downcase) score = 2 * StringMetrics.ngram(3, misspelling_ph, word_ph, longer_worse: true) # Use heap-like behavior: keep only MAX_ROOTS best results if (scores.size >= MAX_ROOTS) && scores.first && scores.first[0] < score # Remove the worst score if we're at capacity scores.sort!.shift end scores << [score, stem] if scores.size < MAX_ROOTS || scores.empty? || score > scores.first[0] end # Sort by (score, stem) tuple descending. Python's heap-based # nlargest uses full-tuple comparison, so ties on score are broken # by descending stem — matching that here is what reproduces # Spylls/Hunspell's phonet suggestion order. guesses = scores.sort.reverse # Final pass: re-score with the precise metric. The second sort # must be stable by score only — Python's sorted(key=..., reverse=True) # preserves the order from the previous sort for ties, which is # load-bearing for the phone.sug fixture. guesses2 = guesses.map do |score, word| final_scr = final_score(misspelling_lower, word.downcase) [score + final_scr, word] end.sort_by { |score, _| -score } guesses2.each do |_, sug| yield sug end end |