Skip to content

Releases: scientist-labs/spellkit

v1.0.0

Choose a tag to compare

@github-actions github-actions released this 08 Sep 16:21
c56ce94

What's Changed

  • Dictionary pack mechanism + lazy loading (1.0.0) by @cpetersen in #7

Full Changelog: 0.3.0...1.0.0

v0.3.0

Choose a tag to compare

@github-actions github-actions released this 07 Sep 22:10
4bd0dd1

What's Changed

  • Bump scientist-labs/rust-gem-release/.github/workflows/build.yml from 0.10.0 to 0.11.0 by @dependabot[bot] in #5
  • Support Ruby 4.0 (magnus 0.8), release 0.3.0 by @cpetersen in #6

Full Changelog: 0.2.0...0.3.0

v0.2.0

Choose a tag to compare

@github-actions github-actions released this 20 Jun 02:52

Headline: SpellKit now ships precompiled native gems — on the supported platforms you no longer need a Rust toolchain to install it.

SpellKit is a Ruby gem for fast, safe typo correction in search-term extraction, backed by a native Rust implementation of the SymSpell algorithm. Up to now, installing it meant compiling that Rust extension on your machine. As of 0.2.0, RubyGems can pull a prebuilt binary for the common platforms instead, so gem install spellkit (or a bundle install) just works — no compiler, no cargo, no waiting on a native build.

This is also the first SpellKit release cut on the shared scientist-labs/rust-gem-release workflow, which standardizes how the Rust-backed gems in this ecosystem build and publish their cross-platform artifacts.

There are no functional or API changes in this release. It is a packaging, CI, and tooling release; correction behavior, the dictionary format, and the public API are identical to 0.1.2.

What ships

RubyGems automatically selects the matching binary for your platform and Ruby ABI at install time, and falls back to compiling from source when no precompiled match exists.

Platform Install requirement
arm64-darwin none (precompiled)
x86_64-linux none (precompiled)
aarch64-linux none (precompiled)
ruby (source) Rust toolchain

Precompiled binaries cover Ruby 3.1, 3.2, 3.3, and 3.4 ABIs. On any platform or Ruby version without a matching binary, RubyGems installs the source ruby gem and builds the extension locally, which is where the Rust toolchain is needed.

Changelog

  • Adopt scientist-labs/rust-gem-release@v0 for releases (#3, @xrl)
  • ci: add "type a version in a box" dispatch (rust-gem-release@0.11.0) (#4)
  • Bump actions/checkout from 5 to 6 (#1)
  • Bump actions/cache from 4 to 5 (#2)

Full Changelog: 0.1.2...0.2.0

SpellKit 0.1.1 - Initial Release 🚀

Choose a tag to compare

@cpetersen cpetersen released this 27 Sep 23:18
33d48d1

Fast, safe typo correction for search-term extraction

We're excited to announce the first release of SpellKit - a Ruby gem that brings blazing-fast spell checking and typo correction to Ruby applications, powered by Rust and the SymSpell algorithm.

Why SpellKit?

SpellKit is designed for production applications that need to correct user input typos while preserving domain-specific terminology. Whether you're building search interfaces, data extraction pipelines, or user-facing forms, SpellKit helps you handle real-world messy input.

Key Highlights

✨ Fast: 16,000+ corrections/second with 80k word dictionary (p50: 61µs, p95: 66µs)
🔒 Zero runtime dependencies: Pure Ruby + Rust, no external services or libraries
🛡️ Domain-aware: Protect technical terms, product codes, gene names, and custom patterns
📦 Thread-safe: Concurrent reads with efficient locking
🔄 Hot-reload: Update dictionaries without restarting your application
🎯 Smart: Preserves canonical forms (NASA stays NASA, not nasa)

Installation

Add to your Gemfile:

gem 'spellkit'

Then run:

bundle install

Quick Start

require 'spellkit'

# Load the default English dictionary (80k words)
SpellKit.load!(dictionary: SpellKit::DEFAULT_DICTIONARY_URL)

# Correct single words
SpellKit.correct("helllo")   # => "hello"
SpellKit.correct("wrld")     # => "world"

# Check if a word is correct
SpellKit.correct?("hello")   # => true
SpellKit.correct?("helo")    # => false

# Get suggestions with distances and frequencies
SpellKit.suggestions("helo", 3)
# => [
#   {"term"=>"help", "distance"=>1, "freq"=>99625494},
#   {"term"=>"held", "distance"=>1, "freq"=>89538904},
#   {"term"=>"hell", "distance"=>1, "freq"=>13854897}
# ]
# Batch correction (optimized - single lock acquisition)
tokens = ["helllo", "wrld", "tst"]
SpellKit.correct_tokens(tokens)
# => ["hello", "world", "test"]

Core Features

1. Canonical Form Preservation

SpellKit preserves the exact capitalization and formatting from your dictionary:

# Load a custom dictionary with specific canonical forms
# custom_dict.tsv contains: "NASA\t1000", "iPhone\t5000", "McDonald's\t3000"
SpellKit.load!(dictionary: "custom_dict.tsv")

SpellKit.correct("nasa")      # => "NASA"
SpellKit.correct("iphone")    # => "iPhone"
SpellKit.correct("mcdonalds") # => "McDonald's"

# Note: The default en-80k dictionary uses lowercase forms
# For brand names and acronyms, create a custom dictionary

2. Domain-Specific Protection

Protect technical terms, product codes, gene names, and more from being "corrected":

# Protect specific terms
protected_file = "config/protected_terms.txt"
# File contains: CDK1, BRCA1, IL-6

SpellKit.load!(
  dictionary: SpellKit::DEFAULT_DICTIONARY_URL,
  protected_path: protected_file
)

SpellKit.correct("CDK1")   # => "CDK1" (protected)
SpellKit.correct("wrld")   # => "world" (corrected)

# Protect patterns with regex
SpellKit.load!(
  dictionary: SpellKit::DEFAULT_DICTIONARY_URL,
  protected_patterns: [
    /^SKU-\d+$/,           # Product codes: SKU-12345
    /^\d{3}-\d{2}-\d{4}$/, # SSN format
    /^[A-Z]{2,4}\d+$/i     # Gene names: BRCA1, CDK10
  ]
)

SpellKit.correct("SKU-9999")  # => "SKU-9999" (protected)

3. Skip Patterns (Aspell-Inspired Filters)

Automatically skip common patterns that shouldn't be spell-checked:

SpellKit.load!(
  dictionary: SpellKit::DEFAULT_DICTIONARY_URL,
  skip_urls: true,           # https://example.com, www.site.com
  skip_emails: true,         # user@domain.com
  skip_hostnames: true,      # api.example.com
  skip_code_patterns: true,  # camelCase, snake_case, PascalCase
  skip_numbers: true         # v1.2.3, #123, 100kg
)

SpellKit.correct("https://exampel.com")
# => "https://exampel.com" (URL preserved, not corrected to "example")

4. Configurable Edit Distance

Control how aggressive corrections should be:

# Edit distance 1: only fix single-character typos
SpellKit.load!(
  dictionary: SpellKit::DEFAULT_DICTIONARY_URL,
  edit_distance: 1
)

SpellKit.correct("encyclopedi")   # => "encyclopedia" (1 character off)
SpellKit.correct("encycloped")   # => "encyclop" (unchanged - 2 chars off)

# Edit distance 2: fix up to 2-character typos
SpellKit.load!(
  dictionary: SpellKit::DEFAULT_DICTIONARY_URL,
  edit_distance: 2
)

SpellKit.correct("encycloped")   # => "encyclopedia" (now corrected)

5. Frequency Threshold

Prevent low-confidence corrections:

SpellKit.load!(
  dictionary: SpellKit::DEFAULT_DICTIONARY_URL,
  frequency_threshold: 1000.0  # Only suggest words with freq >= 1000
)

# Rejects rare word suggestions, reducing false positives

6. Hot Reload

Update your dictionary without restarting:

# Initial load
SpellKit.load!(dictionary: "dictionaries/v1.tsv")

# Later: reload with updated dictionary
SpellKit.load!(dictionary: "dictionaries/v2.tsv")

# Application keeps running, no downtime

7. Multiple Independent Instances

Create isolated spell checkers for different domains:

# Medical dictionary
medical = SpellKit::Checker.new
medical.load!(
  dictionary: "medical_terms.tsv",
  protected_patterns: [/^ICD-\d+/]
)

# Legal dictionary
legal = SpellKit::Checker.new
legal.load!(
  dictionary: "legal_terms.tsv",
  protected_patterns: [/^USC §\d+/]
)

medical.correct("diabeetus")  # => "diabetes"
legal.correct("defendnt")     # => "defendant"

8. Statistics and Monitoring

Track dictionary health and usage:

stats = SpellKit.stats

puts stats
# => {
#   "loaded" => true,
#   "dictionary_size" => 80368,
#   "edit_distance" => 2,
#   "loaded_at" => 1706285432,
#   "skipped_malformed" => 0,
#   "skipped_multiword" => 0,
#   "skipped_invalid_freq" => 0,
#   "skipped_duplicates" => 142
# }

# Healthcheck for monitoring
SpellKit.healthcheck  # Raises error if not properly loaded

Performance

Benchmarked on Apple M4 Max, 80k word dictionary:

Metric Value
Throughput 16,192 operations/second
Latency (p50) 61µs
Latency (p95) 66µs
Latency (p99) 105µs
Protection checks 3.2x faster than dictionary lookups
Memory Efficient hashmap storage

Batch optimization: correct_tokens acquires lock once for entire batch, not per-token.

Dictionary Format

SpellKit supports TSV or space-separated dictionaries:

# TSV format (tab-separated)
hello	10000
world	8000
test	5000

# Space-separated (SymSpell format)
hello 10000
world 8000
test 5000

Default dictionary: 80k English words from SymSpell

SpellKit::DEFAULT_DICTIONARY_URL
# => "https://raw.githubusercontent.com/wolfgarbe/SymSpell/master/SymSpell.FrequencyDictionary/en-80k.txt"

Technical Details

Architecture

  • Algorithm: SymSpell (Symmetric Delete spelling correction)
  • Core: Rust extension via Magnus
  • Threading: Thread-safe with Arc<RwLock> for concurrent reads
  • Normalization: Unicode NFKD normalization for consistent matching
  • Zero dependencies: No runtime gems required beyond Ruby standard library

What Gets Normalized

SpellKit normalizes words for lookup but returns canonical forms:

# Normalization: lowercase + strip whitespace + NFKD
"Café" → "cafe" (for lookup)
"résumé" → "resume" (for lookup)

# But returns canonical from dictionary:
SpellKit.correct("cafe")   # => "Café" (if that's in dictionary)

Duplicate Handling

When multiple case variants exist, SpellKit:

  1. Sums frequencies: hello (10000) + HELLO (100) = 10100 total
  2. Keeps highest-frequency canonical: "hello" wins as canonical form
  3. Tracks duplicates: stats["skipped_duplicates"] shows count

What's Fixed in This Release

This release includes several critical fixes discovered during development:

✅ Canonical form preservation - Corrections now return the exact dictionary entry, not normalized forms
✅ Single-character corrections - Words like "a", "I", "o" now work correctly
✅ Frequency-based duplicate handling - High-frequency variants always win as canonical
✅ NaN/Infinity validation - Invalid thresholds now raise clear errors instead of silently breaking
✅ Dictionary format support - Both TSV and space-separated formats work correctly
✅ Accurate statistics - Duplicate counting fixed

See CHANGELOG.md for complete details.

Requirements

  • Ruby: >= 3.1.0
  • Platform: macOS (ARM/Intel), Linux (x86_64/ARM)
  • Build tools: Rust toolchain for native extension compilation

Thread Safety

SpellKit is thread-safe for concurrent reads:

# Multiple threads can correct simultaneously
threads = 10.times.map do |i|
  Thread.new do
    1000.times { SpellKit.correct("helo") }
  end
end

threads.each(&:join)  # All threads complete safely

Note: Loading/reloading (load!) acquires exclusive write lock.

Contributing

We welcome contributions! Please see CONTRIBUTING.md for guidelines.

License

MIT License. See LICENSE for details.

Acknowledgments


**Get Starte...

Read more