Module: LangExtract::Core::TokenSimilarity
- Defined in:
- lib/langextract/core/token_similarity.rb
Overview
Per-token similarity gate used by the token-level fuzzy aligner. Prevents near-word substitutions (humane→human, chart→cart) by requiring both a length-ratio floor and a character-similarity floor.
Constant Summary collapse
- MIN_LENGTH_RATIO =
0.85- MIN_CHAR_SIMILARITY =
0.78
Class Method Summary collapse
- .char_similarity(left, right) ⇒ Object
- .edit_distance_at_most_one?(left, right) ⇒ Boolean
- .equal_length_one_mismatch?(left, right) ⇒ Boolean
- .normalize(value) ⇒ Object
- .one_insertion_or_deletion?(left, right) ⇒ Boolean
-
.short_typo?(target_token, source_token) ⇒ Boolean
A one-character edit is intentionally limited to short words.
- .similar?(target_token, source_token) ⇒ Boolean
Class Method Details
.char_similarity(left, right) ⇒ Object
88 89 90 91 92 93 94 |
# File 'lib/langextract/core/token_similarity.rb', line 88 def char_similarity(left, right) return 1.0 if left == right return 0.0 if left.empty? || right.empty? matches = SequenceMatcher.match_count(left.each_char.to_a, right.each_char.to_a) (2.0 * matches) / (left.length + right.length) end |
.edit_distance_at_most_one?(left, right) ⇒ Boolean
52 53 54 55 56 57 58 59 |
# File 'lib/langextract/core/token_similarity.rb', line 52 def edit_distance_at_most_one?(left, right) return true if left == right return false if (left.length - right.length).abs > 1 return equal_length_one_mismatch?(left, right) if left.length == right.length one_insertion_or_deletion?(left, right) end |
.equal_length_one_mismatch?(left, right) ⇒ Boolean
61 62 63 |
# File 'lib/langextract/core/token_similarity.rb', line 61 def equal_length_one_mismatch?(left, right) left.each_char.zip(right.each_char).count { |a, b| a != b } <= 1 end |
.normalize(value) ⇒ Object
33 34 35 36 |
# File 'lib/langextract/core/token_similarity.rb', line 33 def normalize(value) normalized = value.to_s.unicode_normalize(:nfc).downcase normalized.gsub(/[\u0027\u2019]/u, "").gsub(/\s+/, " ").strip end |
.one_insertion_or_deletion?(left, right) ⇒ Boolean
65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 |
# File 'lib/langextract/core/token_similarity.rb', line 65 def one_insertion_or_deletion?(left, right) shorter, longer = left.length < right.length ? [left, right] : [right, left] shorter_chars = shorter.each_char.to_a longer_chars = longer.each_char.to_a short_index = 0 long_index = 0 edits = 0 # Keep this bounded to one edit. The early exits make the routine # linear in the short token length and avoid allocating a matrix. while short_index < shorter.length && long_index < longer.length if shorter_chars[short_index] == longer_chars[long_index] short_index += 1 else edits += 1 return false if edits > 1 end long_index += 1 end edits + (longer.length - long_index) <= 1 end |
.short_typo?(target_token, source_token) ⇒ Boolean
A one-character edit is intentionally limited to short words. Long
near-words (for example human/humane) remain subject to the
stricter ratio and character-similarity gates above.
41 42 43 44 45 46 47 48 49 50 |
# File 'lib/langextract/core/token_similarity.rb', line 41 def short_typo?(target_token, source_token) target = normalize(target_token) source = normalize(source_token) return false if target.empty? || source.empty? return false if target == source return false if target.length < 3 || source.length < 3 return false if [target.length, source.length].max > 5 edit_distance_at_most_one?(target, source) end |
.similar?(target_token, source_token) ⇒ Boolean
14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
# File 'lib/langextract/core/token_similarity.rb', line 14 def similar?(target_token, source_token) target = normalize(target_token) source = normalize(source_token) return true if target == source return false if target.empty? || source.empty? # Short one-edit substitutions are useful when they are anchored by # another exact token in the same extraction. The alignment policy enforces # that contextual anchor; keeping the broader lexical gate here lets # it consider those candidates without weakening single-token # grounding. return true if short_typo?(target, source) ratio = [target.length, source.length].min.to_f / [target.length, source.length].max return false if ratio < MIN_LENGTH_RATIO char_similarity(target, source) >= MIN_CHAR_SIMILARITY end |