Class: Kotoshu::Analyzers::SemanticAnalyzer

Inherits:
Object
  • Object
show all
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.

Examples:

Analyzing a document

model = FastTextModel.from_github('en')
analyzer = SemanticAnalyzer.new(model)
errors = analyzer.analyze(document)

Checking a single word

suggestions = analyzer.suggest_corrections('helo', context_words: ['hello', 'world'])

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

Instance Method Summary collapse

Constructor Details

#initialize(model, max_suggestions: DEFAULT_MAX_SUGGESTIONS, min_similarity: MIN_SIMILARITY) ⇒ SemanticAnalyzer

Create a new semantic analyzer.

Parameters:

  • model (EmbeddingModel)

    The embedding model to use

  • max_suggestions (Integer) (defaults to: DEFAULT_MAX_SUGGESTIONS)

    Maximum suggestions per error

  • min_similarity (Float) (defaults to: MIN_SIMILARITY)

    Minimum similarity threshold

Raises:

  • (ArgumentError)


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_suggestionsObject (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

#modelObject (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.

Parameters:

Returns:



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.

Parameters:

Returns:

  • (Float)

    Confidence score (0.0 to 1.0)



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.

Parameters:

  • word (String)

    The word to check

  • source_range (Kotoshu::Documents::SourceRange, nil) (defaults to: nil)

    Where the word lives in the original source. May be nil for word-level checks that aren't tied to a document.

  • context (Models::Context, nil) (defaults to: nil)

    Context around the word

Returns:



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.

Parameters:

  • word (String)

    The misspelled word

  • context (Models::Context, nil) (defaults to: nil)

    Context for context-aware suggestions

Returns:



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).

Parameters:

  • word (String)

    The word to check

Returns:

  • (Boolean)

    True if word is valid



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