Class: Kotoshu::Analyzers::SemanticAnalyzer
- Inherits:
-
Object
- Object
- Kotoshu::Analyzers::SemanticAnalyzer
- Defined in:
- lib/kotoshu/analyzers/semantic_analyzer.rb
Overview
Unified semantic error analyzer.
Uses word embeddings for context-aware error detection and suggestions. Provides unified semantic analysis without artificial spelling/grammar split.
Constant Summary collapse
- HIGH_CONFIDENCE_THRESHOLD =
Similarity threshold for high-confidence suggestions
0.85- MEDIUM_CONFIDENCE_THRESHOLD =
Similarity threshold for medium-confidence suggestions
0.70- MIN_SIMILARITY =
Minimum similarity for suggestions
0.50- DEFAULT_MAX_SUGGESTIONS =
Default number of suggestions to generate
5
Instance Attribute Summary collapse
-
#max_suggestions ⇒ Object
readonly
Returns the value of attribute max_suggestions.
-
#model ⇒ Object
readonly
Returns the value of attribute model.
Instance Method Summary collapse
-
#analyze(document) ⇒ Array<Models::SemanticError>
Analyze a Documents::Document for semantic errors.
-
#calculate_confidence(suggestions) ⇒ Float
Calculate confidence score for suggestions.
-
#detect_error(word:, source_range: nil, context: nil) ⇒ Models::SemanticError?
Detect semantic error for a single word.
-
#initialize(model, max_suggestions: DEFAULT_MAX_SUGGESTIONS, min_similarity: MIN_SIMILARITY) ⇒ SemanticAnalyzer
constructor
Create a new semantic analyzer.
-
#suggest_corrections(word, context: nil) ⇒ Array<Models::Suggestion>
Suggest corrections for a word.
-
#valid_word?(word) ⇒ Boolean
Check if a word is valid (exists in vocabulary).
Constructor Details
#initialize(model, max_suggestions: DEFAULT_MAX_SUGGESTIONS, min_similarity: MIN_SIMILARITY) ⇒ SemanticAnalyzer
Create a new semantic analyzer.
37 38 39 40 41 42 43 |
# File 'lib/kotoshu/analyzers/semantic_analyzer.rb', line 37 def initialize(model, max_suggestions: DEFAULT_MAX_SUGGESTIONS, min_similarity: MIN_SIMILARITY) raise ArgumentError, "Model must be an EmbeddingModel" unless model.is_a?(Models::EmbeddingModel) @model = model @max_suggestions = max_suggestions @min_similarity = min_similarity end |
Instance Attribute Details
#max_suggestions ⇒ Object (readonly)
Returns the value of attribute max_suggestions.
30 31 32 |
# File 'lib/kotoshu/analyzers/semantic_analyzer.rb', line 30 def max_suggestions @max_suggestions end |
#model ⇒ Object (readonly)
Returns the value of attribute model.
30 31 32 |
# File 'lib/kotoshu/analyzers/semantic_analyzer.rb', line 30 def model @model end |
Instance Method Details
#analyze(document) ⇒ Array<Models::SemanticError>
Analyze a Documents::Document for semantic errors.
Walks every Documents::TextNode, tokenizes its text, and for
each invalid word resolves a Documents::SourceRange via
document.source_range_for so the emitted Models::SemanticError
points at the original markup-bearing source rather than the
flattened text. Context for ranking is built from the
surrounding flattened text.
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 |
# File 'lib/kotoshu/analyzers/semantic_analyzer.rb', line 56 def analyze(document) unless document.is_a?(Kotoshu::Documents::Document) raise ArgumentError, "document must be a Kotoshu::Documents::Document" end errors = [] flattened = document.flattened_text document.text_nodes.each do |text_node| tokenize_with_offsets(text_node.text).each do |word, offset_in_node| next if valid_word?(word) flattened_start = text_node.flattened_offset + offset_in_node flattened_end = flattened_start + word.length source_range = document.source_range_for(flattened_start, flattened_end) context = build_context(flattened, flattened_start, flattened_end) error = detect_error(word: word, source_range: source_range, context: context) errors << error if error end end errors.sort end |
#calculate_confidence(suggestions) ⇒ Float
Calculate confidence score for suggestions.
175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 |
# File 'lib/kotoshu/analyzers/semantic_analyzer.rb', line 175 def calculate_confidence(suggestions) return 0.0 unless suggestions&.any? # Confidence is based on top suggestion quality top = suggestions.first # High confidence: top suggestion > 0.85 similarity return 1.0 if top.confidence > HIGH_CONFIDENCE_THRESHOLD # Medium confidence: top suggestion > 0.70 similarity return 0.7 if top.confidence > MEDIUM_CONFIDENCE_THRESHOLD # Low confidence: top suggestion < 0.70 0.5 end |
#detect_error(word:, source_range: nil, context: nil) ⇒ Models::SemanticError?
Detect semantic error for a single word.
90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 |
# File 'lib/kotoshu/analyzers/semantic_analyzer.rb', line 90 def detect_error(word:, source_range: nil, context: nil) return nil if valid_word?(word) suggestions = suggest_corrections(word, context: context) error_type = classify_error(word, suggestions, context) confidence = calculate_confidence(suggestions) Models::SemanticError.new( id: generate_error_id(word, source_range), source_range: source_range, original: word, suggestions: suggestions, error_type: error_type, confidence: confidence, context: context ) rescue Models::EmptySuggestionsError # Word is genuinely unknown — no close matches. Skip silently # rather than crashing on the suggestions-cannot-be-empty # invariant. nil end |
#suggest_corrections(word, context: nil) ⇒ Array<Models::Suggestion>
Suggest corrections for a word.
For in-vocabulary words the embedding model returns the
nearest neighbors. For OOV words (the typical "misspelling"
case the analyzer exists to catch) the embedding model returns
[] because it has no vector for the input. We fall back to
an edit-distance walk over the model's vocabulary so the OOV
case still produces useful candidates that the rest of the
pipeline (confidence scoring, context ranking) can refine.
126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 |
# File 'lib/kotoshu/analyzers/semantic_analyzer.rb', line 126 def suggest_corrections(word, context: nil) return [] if word.nil? || word.empty? neighbors = @model.nearest_neighbors(word, k: @max_suggestions * 3) neighbors = edit_distance_fallback(word) if neighbors.empty? # Filter by minimum similarity neighbors = neighbors.select { |n| n.similarity >= @min_similarity } # If we have context, rank by contextual relevance if context.is_a?(Kotoshu::Models::Context) neighbors = rank_by_context(neighbors, context) end # Convert to Suggestions neighbors.first(@max_suggestions).map do |neighbor| Models::Suggestion.new( neighbor.word, confidence: neighbor.similarity, source: :semantic, metadata: { distance: neighbor.distance, similarity: neighbor.similarity } ) end end |
#valid_word?(word) ⇒ Boolean
Check if a word is valid (exists in vocabulary).
158 159 160 161 162 163 164 165 166 167 168 169 |
# File 'lib/kotoshu/analyzers/semantic_analyzer.rb', line 158 def valid_word?(word) return false if word.nil? || word.empty? # Skip numbers return true if /^\d+$/.match?(word) # Skip single characters (likely abbreviations) return true if word.length == 1 # Check if word exists in model vocabulary @model.has_word?(word) end |