How Paper Concepts Map to liblevenshtein-rust
Date: 2025-11-06
Source paper: Schulz, K. U. & Mihov, S. (2002). Fast string correction with Levenshtein automata. IJDAR 5, 67–85. doi:10.1007/s10032-002-0082-8
This document provides a detailed mapping between concepts, algorithms, and structures from the paper "Fast String Correction with Levenshtein-Automata" and their implementations in the liblevenshtein-rust codebase.
Purpose: Enable developers to:
| Paper Concept | Code Location | Type |
|---|---|---|
| Position i#e | /src/transducer/position.rs:11-35 | Struct |
Subsumption $\sqsubseteq$ | /src/transducer/position.rs:231-269 | Method |
Characteristic Vector $\chi$ | /src/transducer/position.rs | Function |
Elementary Transition $\delta$ | /src/transducer/transition.rs:119-438 | Function |
State Transition $\Delta$ | /src/transducer/query.rs | Method |
| Algorithm Variants | /src/transducer/algorithm.rs | Enum |
| LEV_n(W) Construction | /src/transducer/builder.rs | Builder |
| Imitation Method | /src/transducer/query.rs:86-188 | Iterator |
Paper Definition: i#e where $0 \le i \le |W|, 0 \le e \le n$
Code: /src/transducer/position.rs
pub struct Position {
pub term_index: usize, // Corresponds to 'i' in paper
pub num_errors: usize, // Corresponds to 'e' in paper
pub is_special: bool, // Flag for transposition/merge/split
}
Mapping:
term_index = i (index into query word)num_errors = e (error count)is_special = t or s flag (for extended operations)
Usage Example:
let pos = Position::new(3, 1, false); // Represents 3#1 from paper
Paper Definition: Set M of positions with specific properties
Code: State type alias (likely HashSet<Position> or similar)
Properties Enforced:
Note: State validation may be implicit in transition logic rather than explicit checks.
Paper Definition: i#$e \sqsubseteq j$#f if $(e < f) \land (|j-i| \le f-e)$
Code: /src/transducer/position.rs (subsumption logic)
Implementation Notes:
Example from Paper:
3#0 ⊑ 4#1?
Check: 0 < 1 ✓ and |4-3| = 1 ≤ 1-0 = 1 ✓
Result: YES
Corresponding Code Logic (conceptual):
impl Position {
pub fn subsumes(&self, other: &Position) -> bool {
self.num_errors < other.num_errors &&
(self.term_index.abs_diff(other.term_index) <=
other.num_errors - self.num_errors)
}
}
Paper Definition: $\chi (x,V) = \langle b_{1},...,b_v\rangle$ where b_j = 1 if V[j] = x
Code: /src/transducer/position.rs (characteristic vector functions)
Purpose: Determine which transitions are possible from a position
Implementation:
W[\pi ]$Example from Paper:
χ('l', "hello") = ⟨0,0,1,1,0⟩
Corresponding Code (conceptual):
fn characteristic_vector(ch: char, word: &[char]) -> Vec<bool> {
word.iter().map(|&c| c == ch).collect()
}
Paper: Defines transition from single position $\pi$ under character x
Code: /src/transducer/transition.rs
Function Signature (conceptual):
pub(crate) fn standard_transition(
curr_state: &State,
dict_char: char,
query_chars: &[char],
max_distance: usize,
) -> State
Implements:
Case 1: First character matches $(\chi = \langle 1,...\rangle)$
Case 2: Match later $(\chi = \langle 0,...,0,1,...\rangle$ at position j)
Case 3: No match $(\chi = \langle 0,...,0\rangle)$
Code Structure:
// Simplified from actual implementation
for position in curr_state {
let i = position.term_index;
let e = position.num_errors;
// Match
if query_chars[i] == dict_char {
next_state.insert(Position::new(i + 1, e, false));
}
// Substitution (if no match and within error budget)
if query_chars[i] != dict_char && e < max_distance {
next_state.insert(Position::new(i + 1, e + 1, false));
}
// Deletion
if e < max_distance {
next_state.insert(Position::new(i + 1, e + 1, false));
}
// Insertion
if e < max_distance {
next_state.insert(Position::new(i, e + 1, false));
}
}
Function: transposition_transition()
Additional Logic:
is_special flag (t in paper)(W[i+1] \ne x$ but W[i+2] = x)Code Pattern:
// Check for transposition opportunity
if !is_special && i + 1 < query_len {
let next_char = query_chars[i + 1];
if query_chars[i] != dict_char && next_char == dict_char {
// Set special flag for transposition
next_state.insert(Position::new(i, e + 1, true));
}
}
// Complete transposition
if is_special && query_chars[i] == dict_char {
next_state.insert(Position::new(i + 1, e, false));
}
Function: merge_split_transition()
Additional Logic:
is_special flag (s in paper)Paper: Three families of automata
Code: /src/transducer/algorithm.rs
pub enum Algorithm {
Standard, // Chapters 4-6: Insertions, deletions, substitutions
Transposition, // Chapter 7: + adjacent character swaps
MergeAndSplit, // Chapter 8: + two chars ↔ one char
}
Usage:
let dict = TransducerBuilder::new()
.algorithm(Algorithm::Standard) // Choose variant
.build_from_iter(words);
Paper: Construction algorithm using parametric tables
Code: /src/transducer/builder.rs
Builder Methods:
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
}
pub fn build_from_iter<I>(self, terms: I) -> Transducer<D> {
// Construct dictionary and initialize automaton
}
}
Corresponds to: Algorithm in Theorem 5.2.1
Paper: Simulate LEV_n(W) without explicit construction
Code: /src/transducer/query.rs:86-188
Implementation:
pub struct QueryIterator<D> {
dictionary: &D,
query_chars: Vec<char>,
algorithm: Algorithm,
max_distance: usize,
// State management for traversal
}
impl<D> Iterator for QueryIterator<D> {
fn next(&mut self) -> Option<String> {
// Parallel traversal of dictionary and simulated automaton
// Uses transition functions based on algorithm variant
}
}
Corresponds to: Algorithm in Chapter 6, Figure 6.1
Key Aspects:
Paper: $\Delta (M,x)$ uses algorithm-specific elementary transitions
Code: /src/transducer/query.rs (transition dispatch logic)
Conceptual Implementation:
impl QueryIterator {
fn next_state(&self, dict_char: char) -> State {
match self.algorithm {
Algorithm::Standard => {
standard_transition(
&self.current_state,
dict_char,
&self.query_chars,
self.max_distance,
)
}
Algorithm::Transposition => {
transposition_transition(
&self.current_state,
dict_char,
&self.query_chars,
self.max_distance,
)
}
Algorithm::MergeAndSplit => {
merge_split_transition(
&self.current_state,
dict_char,
&self.query_chars,
self.max_distance,
)
}
}
}
}
Paper: Dictionary automaton $A^D$ traversed in parallel with LEV_n(W)
Code: Dictionary implementations live in the libdictenstein crate (the dictionary family was extracted from liblevenshtein-rust into its own crate; see the project's dictionary-family layout). Only src/dictionary/mod.rs and src/dictionary/phonetic_normalized.rs remain in this crate as the integration surface.
Dictionary Types (in libdictenstein):
DynamicDawg, DynamicDawgChar)SuffixAutomaton, SuffixAutomatonChar)DoubleArrayTrie, DoubleArrayTrieChar)Interface (conceptual):
pub trait Dictionary {
fn root(&self) -> NodeRef;
fn transition(&self, node: NodeRef, ch: char) -> Option<NodeRef>;
fn is_final(&self, node: NodeRef) -> bool;
}
Parallel Traversal:
// Conceptual parallel traversal
let mut dict_node = dictionary.root();
let mut automaton_state = initial_state();
for ch in input_chars {
dict_node = dictionary.transition(dict_node, ch)?;
automaton_state = next_state(automaton_state, ch);
if dictionary.is_final(dict_node) && is_accepting(automaton_state) {
yield current_word;
}
}
Paper: Tables T_n defining state types and transitions for fixed degree n
Code: Not explicitly stored as tables in current implementation
Reason: The imitation method (Chapter 6) computes transitions on-demand using characteristic vectors, avoiding need to materialize full parametric tables.
Implicit Encoding: The transition functions (standard_transition, etc.) encode the table logic:
Trade-off:
\mathcal{O}(1)$ lookup, $\mathcal{O}(4^n)$ preprocessing\mathcal{O}(\text{state} \text{size})$ computation, no preprocessingBoth have $\mathcal{O}(\lvert W\rvert)$ total complexity for constructing LEV_n(W).
\mathcal{O}(\lvert W\rvert)$Paper: Theorem 5.2.1
Code:
\mathcal{O}(\lvert W\rvert)$ to store query characters\mathcal{O}(1)$\mathcal{O}(\text{state} \text{size})$ = $\mathcal{O}(1)$ for fixed nTotal: $\mathcal{O}(\lvert W\rvert)$ ✓
\mathcal{O}(\lvert D\rvert)$Paper: Chapter 3, parallel traversal
Code:
\mathcal{O}(\lvert D\rvert)$\mathcal{O}(\lvert D\rvert \times \text{state} \text{size})$ = $\mathcal{O}(\lvert D\rvert)$ for fixed nTotal: $\mathcal{O}(\lvert D\rvert)$ ✓
Paper: Implicitly assumes valid inputs
Code: Rust's type system and error handling
Safety Mechanisms:
usize for indices (no negative indices)Example:
// Safe array access
if i < query_chars.len() {
let ch = query_chars[i]; // Guaranteed safe
}
Paper: Experimental results (Chapter 8.3)
Code: Test suites in /tests/
Test Categories:
Validation Against Paper:
\mathcal{O}(\lvert W\rvert)$ + $\mathcal{O}(\lvert D\rvert)$ complexityPaper Approach (Chapters 7-8):
\mathcal{O}(\lvert W\rvert)$ complexityCode Approach:
Position structAlgorithm enumtransition.rsExample: Adding context-dependent costs
// Extend Position
pub struct Position {
pub term_index: usize,
pub num_errors: usize,
pub is_special: bool,
pub context: Context, // NEW: track context
}
// Add Algorithm variant
pub enum Algorithm {
Standard,
Transposition,
MergeAndSplit,
ContextDependent, // NEW
}
// Implement transition
pub(crate) fn context_transition(...) -> State {
// Use context to determine costs
}
Paper Extension: See /docs/research/universal-levenshtein/
Code Integration: Add substitution set parameter
pub struct TransducerBuilder<D> {
algorithm: Algorithm,
substitution_set: Option<SubstitutionSet>, // NEW
}
Modify transition logic to check substitution validity.
Check:
Tool: Add logging to transition functions, compare with paper examples
Check:
Tool: Trace automaton state during query execution
Check:
\mathcal{O}(n)$ positionsTool: Profile transition function calls, measure state sizes
position.rs, transition.rs, algorithm.rsbuilder.rsquery.rsdictionary/ moduleLast Updated: 2025-11-06 Status: Complete mapping of paper to code Next: Implement new features based on paper insights
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 |