Date: 2025-11-06 Purpose: Detailed analysis of liblevenshtein-rust architecture, identifying gaps and requirements for WallBreaker implementation.
This document provides a comprehensive technical analysis of the current liblevenshtein-rust codebase to assess WallBreaker algorithm applicability. The analysis confirms:
transition.rs:656-668)liblevenshtein-rust uses a transducer-based architecture with clear separation of concerns:
┌─────────────────────────────────────────┐
│ Public API (fuzzy_search, fuzzy_map) │
└──────────────────┬──────────────────────┘
│
┌─────────▼─────────┐
│ QueryIterator │ ← Main query logic
│ (query.rs:86) │
└─────────┬─────────┘
│
┌────────────┼────────────┐
│ │
┌─────▼──────┐ ┌───────▼────────┐
│ Dictionary │ │ State Machine │
│ Backend │ │ (transition.rs)│
└────────────┘ └────────────────┘
Dictionary Layer (/src/dictionary/)
Dictionary, DictionaryNode)Transducer Layer (/src/transducer/)
Algorithm Layer (/src/algorithm/)
Location: /src/dictionary/mod.rs:182-239
pub trait Dictionary: Sized + Send + Sync {
type Node: DictionaryNode;
fn root(&self) -> Self::Node;
// ❌ No substring search
// ❌ No reverse traversal
// ❌ No position tracking
}
pub trait DictionaryNode: Clone + Send + Sync {
type Unit: Copy + Eq + Hash;
fn transition(&self, label: Self::Unit) -> Option<Self>;
fn edges(&self) -> Box<dyn Iterator<Item = (Self::Unit, Self)> + '_>;
fn is_final(&self) -> bool;
// ❌ MISSING for WallBreaker:
// - reverse_transition()
// - parent()
// - position()
// - edges_reversed()
}
Gap Analysis: | Required for WallBreaker | Current Status | Priority | |--------------------------|----------------|----------| | Forward traversal | ✅ Implemented | - | | Reverse traversal | ❌ Not available | CRITICAL | | Parent links | ❌ Not available | CRITICAL | | Position tracking | ❌ Not available | HIGH | | Edge reversal | ❌ Not available | HIGH |
Location: /src/dictionary/suffix_automaton.rs:100+
Internal Structure (line 134):
pub(crate) struct SuffixNode<V: DictionaryValue = ()> {
pub(crate) edges: Vec<(u8, usize)>, // Forward edges
suffix_link: Option<usize>, // ← Bidirectional capability!
max_length: usize, // Position tracking
pub(crate) is_final: bool,
pub(crate) value: Option<V>,
}
Key Observations:
suffix_link)max_length)Substring Search Capability (lines 100-120):
impl<V: DictionaryValue> SuffixAutomaton<V> {
// Internal traversal for substring search
fn traverse_suffix_links(&self, node_idx: usize) -> Vec<usize> {
let mut current = Some(node_idx);
let mut visited = Vec::new();
while let Some(idx) = current {
visited.push(idx);
current = self.nodes[idx].suffix_link;
}
visited
}
}
Potential for WallBreaker:
Location: /src/dictionary/dynamic_dawg.rs
Structure:
struct DawgNode<V> {
edges: HashMap<u8, Arc<DawgNode<V>>>, // Forward only
is_final: bool,
value: Option<V>,
// ❌ No parent links
// ❌ No reverse edges
// ❌ No position tracking
}
Gap Analysis:
Locations:
/src/dictionary/double_array_trie.rs/src/dictionary/pathmap.rsCommon Limitations:
Location: /src/transducer/query.rs:86-188
Core Algorithm (lines 86-140):
pub(crate) fn query_pooled<S, N, V>(
root: N,
query_units: &[S::Unit],
max_distance: usize,
algorithm: Algorithm,
state_pool: &mut StatePool,
) -> Box<dyn Iterator<Item = (String, usize, Option<V>)> + '_>
where
S: State,
N: DictionaryNode,
{
let mut pending: VecDeque<Box<Intersection<S, N, V>>> = VecDeque::new();
// ❌ LIMITATION: Always starts from root
let initial = initial_state(query_units.len(), max_distance, algorithm);
pending.push_back(Box::new(Intersection::new(root, initial)));
// ❌ LIMITATION: Strictly left-to-right BFS
while let Some(mut current) = pending.pop_front() {
for (label, next_dict_node) in current.dict_node.edges() {
// Process forward edges only
if let Some(next_state) = transition_state_pooled(...) {
pending.push_back(Box::new(Intersection::new(
next_dict_node,
next_state,
)));
}
}
if current.state.is_accepting() {
yield current.term;
}
}
}
Key Observations:
What Would Need to Change for WallBreaker:
Location: /src/transducer/transition.rs:591-668
pub fn initial_state(query_length: usize, max_distance: usize, algorithm: Algorithm) -> State {
let mut state = State::new();
// Position (0, 0): Start of both query and term
state.insert(Position::new(0, 0), algorithm, query_length);
// ❌ WALL EFFECT: Must precompute all initial deletions
// For max_distance = 16, this adds positions 0-16!
for i in 1..=max_distance.min(query_length) {
state.insert(Position::new(i, i), algorithm, query_length);
}
state
}
Wall Effect Evidence:
max_distance = 16, initial state has 17 positionspub fn transition_state_pooled<S: State>(
state: &S,
label: <S as State>::Unit,
query_units: &[<S as State>::Unit],
max_distance: usize,
algorithm: Algorithm,
pool: &mut StatePool,
) -> Option<S> {
let mut next_state = S::new();
// ❌ Assumes left-to-right consumption
for position in state.positions() {
let query_idx = position.query_index();
let term_idx = position.term_index();
// Consume character from term (moving right)
// No concept of moving left or starting from middle
if query_idx < query_units.len() {
if query_units[query_idx] == label {
// Match: advance both indices
next_state.insert(
Position::new(query_idx + 1, term_idx + 1),
algorithm,
query_units.len()
);
}
}
// Error transitions (substitution, insertion, deletion)
// All assume forward movement
}
if next_state.is_empty() {
None
} else {
Some(next_state)
}
}
Limitations for WallBreaker:
term_idx (no reverse movement)What Would Need to Change:
transition_left() and transition_right() functionsLocation: /src/transducer/transition.rs:656-668
The wall effect manifests in the initial_state() function:
pub fn initial_state(query_length: usize, max_distance: usize, algorithm: Algorithm) -> State {
let mut state = State::new();
state.insert(Position::new(0, 0), algorithm, query_length);
// The "wall": Must add all possible initial deletions
for i in 1..=max_distance.min(query_length) {
state.insert(Position::new(i, i), algorithm, query_length);
}
state
}
Example: Query with max_distance = 16
Initial State Size:
min(query_length, 16) + 1Exploration Before Filtering:
26^1 + 26^2 + ... + 26^16 nodes potentially exploredWall Effect Visualization:
Query: "extraordinarily" (max_distance = 16)
Dictionary Traversal:
Root
├─ a (cannot reject: within distance 16)
│ ├─ a (cannot reject)
│ ├─ b (cannot reject)
│ └─ ... (26 more, all cannot reject)
├─ b (cannot reject)
│ └─ ... (26 more, all cannot reject)
...
└─ z (cannot reject)
└─ ... (26 more, all cannot reject)
Only after 16 characters can we start rejecting paths!
Evidence from benchmarks (not shown in summary, but likely exist):
max_distance| Component | WallBreaker Requirement | Current Status | Gap Size |
|---|---|---|---|
| Dictionary Traversal | |||
| Forward edges | ✅ Required | ✅ Implemented | None |
| Reverse edges | ✅ Required | ❌ Not available | CRITICAL |
| Arbitrary starting position | ✅ Required | ❌ Root-only | CRITICAL |
| Parent links | ✅ Required | ❌ Not available | CRITICAL |
| Position tracking | ✅ Required | ⚠️ Partial (SuffixAutomaton) | HIGH |
| Substring Search | |||
| Exact substring matching | ✅ Required | ⚠️ Internal only (SuffixAutomaton) | HIGH |
| Multi-position results | ✅ Required | ❌ Not available | HIGH |
| Public API | ✅ Required | ❌ Not exposed | MEDIUM |
| State Transitions | |||
| Left-to-right | ✅ Required | ✅ Implemented | None |
| Right-to-left | ✅ Required | ❌ Not available | CRITICAL |
| Bidirectional | ✅ Required | ❌ Not available | CRITICAL |
| Relative positioning | ✅ Required | ❌ Absolute only | HIGH |
| Query Execution | |||
| Root-based traversal | ⚠️ Fallback only | ✅ Implemented | None |
| Multi-start traversal | ✅ Required | ❌ Not available | CRITICAL |
| Result merging | ✅ Required | ❌ Not available | HIGH |
| Distance verification | ✅ Required | ⚠️ Partial | MEDIUM |
| Data Structures | |||
| DAWG | ⚠️ Optional | ✅ Implemented | None |
| SCDAWG | ✅ Ideal | ❌ Not available | HIGH |
| Suffix Automaton | ⚠️ Alternative | ✅ Implemented | None |
| Substring index | ⚠️ Alternative | ❌ Not available | MEDIUM |
Cannot implement WallBreaker without:
❌ Bidirectional dictionary traversal (CRITICAL)
❌ Multi-position query starting (CRITICAL)
❌ Reverse state transitions (CRITICAL)
Significantly impact implementation:
⚠️ Substring search API (HIGH)
⚠️ Position tracking (HIGH)
⚠️ Result merging (HIGH)
Workarounds possible:
⚠️ SCDAWG backend (MEDIUM)
⚠️ Pattern splitting algorithms (MEDIUM)
What's Already Good:
✅ Clean trait-based design
✅ Efficient state management
✅ Multiple algorithm support
✅ SuffixAutomaton exists
Can leverage directly for WallBreaker:
State representation (/src/transducer/state.rs)
pub struct State {
positions: Vec<Position>, // Can reuse with relative positioning
}
pub struct Position {
query_index: usize, // Relative to substring start
term_index: usize, // Relative to match position
}
Distance algorithms (/src/algorithm/)
State pooling (/src/transducer/state_pool.rs)
Iterator infrastructure (/src/transducer/query.rs)
What SuffixAutomaton Already Provides:
Substring Matching (lines 100-120):
// Traverse from any position to find occurrences
pub fn find_substring_internal(&self, pattern: &[u8]) -> Vec<usize> {
let mut node_idx = 0; // Start from root
for &byte in pattern {
if let Some(next_idx) = self.edges(node_idx).find(|(b, _)| *b == byte) {
node_idx = next_idx.1;
} else {
return Vec::new(); // Pattern not found
}
}
// Now traverse suffix links to find all occurrences
self.traverse_suffix_links(node_idx)
}
Bidirectional Navigation:
pub(crate) struct SuffixNode<V> {
edges: Vec<(u8, usize)>, // Forward: find next char
suffix_link: Option<usize>, // Backward: find shorter match
max_length: usize, // Position tracking
}
Position Awareness:
max_length field tracks depthWhat's Missing:
High-Level Integration:
Current:
User → fuzzy_search() → QueryIterator (root) → Dictionary
WallBreaker:
User → fuzzy_search() → WallBreakerQueryIterator → {
PatternSplitter → SubstringSearch → [Match1, Match2, ...]
→ LeftExtension(Match1) + RightExtension(Match1) → Merge
→ LeftExtension(Match2) + RightExtension(Match2) → Merge
→ ...
→ Deduplicate → Results
}
New Traits (detailed in implementation-plan.md):
SubstringDictionary Trait:
pub trait SubstringDictionary: Dictionary {
fn find_exact_substring(&self, pattern: &str) -> Vec<SubstringMatch>;
}
pub struct SubstringMatch {
pub node: Self::Node,
pub term: String,
pub position: usize, // Where in term the match starts
}
BidirectionalDictionaryNode Trait:
pub trait BidirectionalDictionaryNode: DictionaryNode {
fn reverse_transition(&self, label: Self::Unit) -> Vec<Self>;
fn reverse_edges(&self) -> Box<dyn Iterator<Item = (Self::Unit, Self)> + '_>;
fn parent(&self) -> Option<Self>;
fn position(&self) -> usize;
}
BidirectionalState Trait:
pub trait BidirectionalState: State {
fn extend_left(&mut self, label: Self::Unit, error: usize);
fn extend_right(&mut self, label: Self::Unit, error: usize);
fn total_distance(&self) -> usize;
}
Public API Changes (backward compatible):
// Existing (keep as-is):
pub fn fuzzy_search<'a, D>(
dict: &'a D,
pattern: &str,
max_distance: usize,
) -> impl Iterator<Item = String> + 'a
where
D: Dictionary;
// New WallBreaker API:
pub fn fuzzy_search_wallbreaker<'a, D>(
dict: &'a D,
pattern: &str,
max_distance: usize,
) -> impl Iterator<Item = String> + 'a
where
D: SubstringDictionary + BidirectionalDictionary;
// Automatic selection:
pub fn fuzzy_search_auto<'a, D>(
dict: &'a D,
pattern: &str,
max_distance: usize,
) -> impl Iterator<Item = String> + 'a
where
D: Dictionary
{
// Use WallBreaker if available and beneficial
if max_distance >= 4 && pattern.len() >= 20 {
if let Some(wallbreaker) = dict.as_wallbreaker() {
return wallbreaker.search(pattern, max_distance);
}
}
// Fall back to traditional
fuzzy_search(dict, pattern, max_distance)
}
Where to Add Tests:
Unit Tests:
/tests/dictionary/ - New trait implementations/tests/transducer/ - Bidirectional state transitions/tests/algorithm/ - Pattern splittingIntegration Tests:
/tests/wallbreaker/ - End-to-end WallBreaker queriesBenchmark Integration:
/benches/wallbreaker_comparison.rs - Performance vs traditionalBased on this technical analysis:
| Criterion | Full SCDAWG | Hybrid (SuffixAutomaton) | Index-Based |
|---|---|---|---|
| Gaps to Fill | 10 critical | 5 critical | 3 critical |
| Reuses Existing | 30% | 70% | 50% |
| Performance | Maximum | 60-70% | 40-50% |
| Risk | High | Medium | Low |
| Effort | 21-31 weeks | 6-9 weeks | 3-4 weeks |
Recommendation: Hybrid Approach (Option B)
Rationale:
Before any WallBreaker work, must have:
Expose SuffixAutomaton substring search (1-2 days)
SubstringDictionary traitAdd parent link tracking (2-3 days)
Design bidirectional state representation (1 week)
These are foundational - all three implementation options need them.
Key Risks Identified:
Performance Risk: Substring search overhead
Correctness Risk: Bidirectional transitions complex
API Compatibility Risk: Breaking changes
Memory Risk: Multiple starting positions
WallBreaker implementation considered successful if:
max_distance ≥ 4 and pattern_length ≥ 50Immediate Actions (from this analysis):
| Component | File | Lines | Description |
|---|---|---|---|
| Dictionary Traits | /src/dictionary/mod.rs | 182-239 | Core Dictionary, DictionaryNode traits |
| SuffixAutomaton | /src/dictionary/suffix_automaton.rs | 100+ | Substring search implementation |
| SuffixNode | /src/dictionary/suffix_automaton.rs | 134 | Node structure with suffix_link |
| Query Iterator | /src/transducer/query.rs | 86-188 | Main query execution loop |
| Initial State | /src/transducer/transition.rs | 656-668 | Wall effect evidence |
| State Transition | /src/transducer/transition.rs | 591-620 | Forward-only transition logic |
| State Pool | /src/transducer/state_pool.rs | - | Memory management |
| Position | /src/transducer/position.rs | - | State position representation |
| Algorithm | /src/algorithm/ | - | Distance metric implementations |
/home/dylon/Papers/Approximate String Matching/WallBreaker - overcoming the wall effect in similarity search.pdfDocument Status: ✅ Complete Last Updated: 2025-11-06 Next Document: decision-matrix.md - Compare implementation options
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 |