Repository navigation
Releases: scientist-labs/spellkit
Release list
v1.0.0
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
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
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@v0for releases (#3, @xrl) - ci: add "type a version in a box" dispatch (rust-gem-release@0.11.0) (#4)
- Bump
actions/checkoutfrom 5 to 6 (#1) - Bump
actions/cachefrom 4 to 5 (#2)
Full Changelog: 0.1.2...0.2.0
SpellKit 0.1.1 - Initial Release 🚀
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 installQuick 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 dictionary2. 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 positives6. 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 downtime7. 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 loadedPerformance
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:
- Sums frequencies: hello (10000) + HELLO (100) = 10100 total
- Keeps highest-frequency canonical: "hello" wins as canonical form
- 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 safelyNote: 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
- SymSpell algorithm by Wolf Garbe
- Magnus for Ruby-Rust bindings
- English frequency dictionary from SymSpell project
**Get Starte...