Date: 2025-11-06 Status: Analysis Complete
This document analyzes the current liblevenshtein-rust codebase to identify:
The library currently supports three algorithm variants defined in /src/transducer/algorithm.rs:
pub enum Algorithm {
Standard, // Basic Levenshtein (insert, delete, substitute)
Transposition, // Standard + adjacent character swaps
MergeAndSplit, // Standard + character merge/split operations
}
Key characteristics:
Current state: All operations (insert, delete, substitute, transpose, merge, split) have uniform cost = 1.
Implication for Universal LA:
\infty$)Defined in /src/transducer/position.rs:11-35:
pub struct Position {
pub term_index: usize, // Index into query term
pub num_errors: usize, // Accumulated edit distance
pub is_special: bool, // Flag for Transposition/MergeAndSplit
}
Analysis:
term_index: Tracks position in query stringnum_errors: Cumulative edit operations (all cost=1)is_special: Used by Transposition and MergeAndSplit algorithmsImpact for Universal LA:
num_errors continues to track count of operations (allowed operations only)Current implementation in /src/transducer/position.rs:231-269:
// Standard algorithm characteristic vector
pub(crate) fn characteristic_vector(
term: &str,
query_chars: &[char],
) -> Vec<u64> {
// Creates binary vector: 1 if character matches at position, 0 otherwise
// Used to determine valid transitions
}
Key concept: The characteristic vector $\chi(a, w)$ represents where character a appears in word w.
Universal LA modification needed:
\chi(a, w[i]) = 1$ if w[i] == a, else 0\chi_s(a, w[i]) = 1$ if $(w[i], a) \in S$ (substitution allowed), else 0Implementation approach:
// Enhanced characteristic vector with substitution set
pub(crate) fn s_characteristic_vector(
term: &str,
query_chars: &[char],
substitution_set: Option<&SubstitutionSet>,
) -> Vec<u64> {
match substitution_set {
None => characteristic_vector(term, query_chars), // Standard behavior
Some(s) => {
// Check (term_char, query_char) ∈ S for each position
// Set bit to 1 if substitution is allowed
}
}
}
All transition logic is in /src/transducer/transition.rs:
Current logic (simplified):
// Match: same character, no error increment
if query_char == dict_char {
next_state.insert(Position::new(i + 1, e, false));
}
// Substitution: different characters, increment error
if query_char != dict_char {
next_state.insert(Position::new(i + 1, e + 1, false));
}
// Insertion: consume dict char, increment error
next_state.insert(Position::new(i, e + 1, false));
// Deletion: consume query char, increment error
next_state.insert(Position::new(i + 1, e + 1, false));
Critical observation: Substitution currently allows any character pair (query_char, dict_char) when they differ.
Change needed in substitution transition:
// OLD: Unconditional substitution
if query_char != dict_char {
next_state.insert(Position::new(i + 1, e + 1, false));
}
// NEW: Check substitution set
if query_char != dict_char {
// Check if substitution is allowed by the set S
let substitution_allowed = match substitution_set {
None => true, // No restrictions = all substitutions allowed
Some(s) => s.is_allowed(query_char, dict_char),
};
if substitution_allowed {
next_state.insert(Position::new(i + 1, e + 1, false));
}
}
Impact:
substitution_set: Option<&SubstitutionSet> through transition functionsCurrent behavior: Allows swapping adjacent characters (ab → ba)
Universal LA extension: Paper covers restricted substitutions + transposition
Modification needed:
Current behavior:
Universal LA extension: Paper discusses combining with merge/split
Modification needed:
Located in /src/transducer/builder.rs:
pub struct TransducerBuilder<D> {
algorithm: Algorithm,
// ... other configuration fields
}
impl<D> TransducerBuilder<D> {
pub fn new() -> Self { /* ... */ }
pub fn algorithm(mut self, algorithm: Algorithm) -> Self {
self.algorithm = algorithm;
self
}
// ... other builder methods
}
Usage example:
let dict = TransducerBuilder::new()
.algorithm(Algorithm::Standard)
.build_from_iter(words);
Option A: New Algorithm Variant (Not Recommended)
pub enum Algorithm {
Standard,
Transposition,
MergeAndSplit,
RestrictedSubstitution, // NEW
}
Problems:
Option B: Configuration Field (Recommended)
pub struct TransducerBuilder<D> {
algorithm: Algorithm,
substitution_set: Option<SubstitutionSet>, // NEW field
}
impl<D> TransducerBuilder<D> {
pub fn with_substitution_set(mut self, set: SubstitutionSet) -> Self {
self.substitution_set = Some(set);
self
}
pub fn with_qwerty_substitutions(self) -> Self {
self.with_substitution_set(SubstitutionSet::qwerty())
}
pub fn with_ocr_substitutions(self) -> Self {
self.with_substitution_set(SubstitutionSet::ocr_confusions())
}
}
Advantages:
Located in /src/transducer/query.rs:86-188:
Integration point for Universal LA:
substitution_set from TransducerBuilder through to transition functionsLocated in /src/transducer/position.rs:
// Position A subsumes Position B if:
// - A.term_index == B.term_index
// - A.num_errors <= B.num_errors
// - A.is_special matches appropriately
Purpose: Prune redundant states to reduce search space.
Example: If position (i=5, e=2) exists, position (i=5, e=3) is subsumed (same progress, more errors).
Paper warning (Section 3): The generalized distance d_L^S may not satisfy triangle inequality when substitutions are restricted.
Implication: Subsumption logic may need adjustment.
Potential issue:
Standard LA: d(A, C) ≤ d(A, B) + d(B, C) (triangle inequality holds)
Universal LA: May violate triangle inequality when substitution paths are blocked
Mitigation strategy:
Expected impact: Likely minimal, as subsumption compares positions with same term_index (not different paths through edit graph).
\chi_s$ computation(a, b) \in S$ in transitionsWhere: New module /src/transducer/substitution.rs
What:
pub struct SubstitutionSet {
allowed: HashSet<(char, char)>,
}
impl SubstitutionSet {
pub fn new() -> Self;
pub fn unrestricted() -> Self;
pub fn add(&mut self, a: char, b: char);
pub fn add_bidirectional(&mut self, a: char, b: char);
pub fn is_allowed(&self, a: char, b: char) -> bool;
// Preset constructors
pub fn qwerty() -> Self;
pub fn azerty() -> Self;
pub fn dvorak() -> Self;
pub fn ocr_confusions() -> Self;
pub fn phonetic_english() -> Self;
}
Where: /src/transducer/builder.rs
What:
substitution_set: Option<SubstitutionSet> fieldwith_substitution_set() methodWhere: /src/transducer/transition.rs
What:
substitution_set: Option<&SubstitutionSet> parameterWhere: /src/transducer/query.rs
What:
substitution_set from builder to transition functionsWhere: /src/transducer/position.rs
What:
s_characteristic_vector() function(\text{term\_char}, \text{query\_char}) \in S$ for substitutions\chi$ when substitution_set is NoneKey principle: Universal LA features are opt-in via configuration.
Ensuring compatibility:
Default behavior unchanged:
// This still works exactly as before
let dict = TransducerBuilder::new()
.algorithm(Algorithm::Standard)
.build_from_iter(words);
None means unrestricted:
substitution_set: Option<SubstitutionSet>
// None → all substitutions allowed (current behavior)
// Some(set) → only substitutions in set allowed (new behavior)
No breaking changes:
Feature flag (optional):
[features]
universal-la = []
Can gate code behind feature flag if needed.
Optimistic: 5-10% slowdown (if substitution checks are well-optimized)
Realistic: 10-20% slowdown (typical case with HashSet lookup)
Worst-case: 30% slowdown (large substitution sets, poor cache locality)
Substitution validity check: $\mathcal{O}(1)$ HashSet lookup per potential substitution
Memory overhead: SubstitutionSet storage
Cache effects: Additional memory accesses for substitution checks
Perfect hashing: For static substitution sets
\mathcal{O}(1)$ lookup with zero collisionsBit vectors: For small alphabets (ASCII, DNA)
SIMD: Batch substitution checks
Caching: Memoize recent substitution checks
SubstitutionSet structure:
Transition functions:
End-to-end query:
Real dictionary queries:
Performance benchmarks:
Correctness validation:
/src/transducer/builder.rs
substitution_set fieldwith_substitution_set() method/src/transducer/transition.rs
/src/transducer/query.rs
/src/transducer/substitution.rs (NEW)
/src/transducer/position.rs
/src/transducer/algorithm.rs
None for basic implementation. Standard library is sufficient:
std::collections::HashSet for SubstitutionSet storagePerfect hashing (optimization):
phf crate for compile-time perfect hash functionsSIMD (optimization):
Performance overhead: 10-20% slowdown acceptable for most use cases
Subsumption logic: May need refinement for restricted substitutions
None identified. Implementation is well-understood with clear path forward.
The liblevenshtein-rust codebase is well-positioned for Universal LA implementation:
Total: 2-4 weeks for complete implementation.
Next Steps:
Last Updated: 2025-11-06 Status: Analysis Complete, Ready for Implementation Planning
Can you improve this documentation?Edit on GitHub
cljdoc builds & hosts documentation for Clojure/Script libraries
| Ctrl+k | Jump to recent docs |
| ← | Move to previous article |
| → | Move to next article |
| Ctrl+/ | Jump to the search field |