Skip to main content

max / makenotwork

tagtree: document the tier-3 fuzzy path, drop dead test helper The TagIndex rustdoc and the architecture.md Performance Characteristics section both still described the two-tier suggestion strategy the crate outgrew, leaving suggest_fuzzy undocumented on the surface a consumer reads from docs.rs. Both now cover tier 3 and what it costs. The bench table was stale alongside it: ten groups, not six, and the fuzzy benches under tag_index and large_tag_index went unnamed. Also removes tagtree_test_dist, a #[cfg(test)] wrapper over edit_distance with zero callers across the tree. Closes GoingsOn problems f805babb, f22e2e4a.
Co-Authored-By
Claude Opus 5 (1M context) <noreply@anthropic.com>
Author: Max Johnson <me@maxj.phd> · 2026-08-01 20:23 UTC
Signed with PGP, not checked
Commit: 0eb77acb2ab1a4e9a09c213c75af01b780a0ba92
Parent: 1a534df
2 files changed, +16 insertions, -7 deletions
@@ -118,7 +118,7 @@
118 118
119 119 ## Performance Characteristics
120 120
121 - Criterion benchmarks cover six groups at multiple scales (1K, 10K, 50K tags):
121 + Criterion benchmarks cover ten groups at multiple scales (1K, 10K, 50K tags):
122 122
123 123 | Benchmark Group | What it measures |
124 124 |----------------|------------------|
@@ -126,11 +126,17 @@
126 126 | `hierarchy` | parent, leaf, depth, ancestors, is_ancestor_of, common_ancestor |
127 127 | `sql` | escape_like, like_descendant_pattern |
128 128 | `tree_ops` | children_at_prefix, subtree, rename_prefix |
129 - | `tag_index` | Build time, suggest (prefix/path/exact/no-match) at 1K/10K/50K |
129 + | `tag_index` | Build time, suggest (prefix/path/exact/no-match), suggest_fuzzy (typo/no-match) |
130 + | `bulk_ops` | rename_prefix_bulk, remove_subtree, merge_tags, batched renames (10K) |
130 131 | `deep_tree` | Validation and traversal at depth 5/10/20, deep-wide trees (~19K nodes) |
132 + | `large_validate` | Validating every tag in a 10K set |
133 + | `large_tree_ops` | Tree operations at 1K/10K/50K |
134 + | `large_tag_index` | Build, suggest and suggest_fuzzy at 1K/10K/50K |
131 135
132 136 At typical corpus sizes (hundreds to low thousands), `suggest` completes in single-digit microseconds. Path-prefix lookups are O(log n); segment-prefix scans are gated by a segment index to avoid unnecessary work.
133 137
138 + `suggest_fuzzy` is the slowest of the three tiers and the one to watch at scale. It runs only when tiers 1 and 2 underfill the limit, and when it does it walks every tag segment with a bounded Levenshtein distance (max 2), so its cost is O(tags * segments) plus the edit-distance work per segment rather than the O(log n) of a path-prefix hit. A query that misses everything (`suggest_fuzzy("zzz", ...)`) is the worst case, since no tier short-circuits it.
139 +
134 140 ## Key Paths
135 141
136 142 | What | Path |
@@ -569,11 +569,6 @@
569 569 if d <= max { Some(d) } else { None }
570 570 }
571 571
572 - #[cfg(test)]
573 - pub fn tagtree_test_dist(a: &str, b: &str) -> Option<usize> {
574 - edit_distance(a, b, 2)
575 - }
576 -
577 572 // Bulk tag operations
578 573
579 574 /// Rename a prefix across all tags in an index. Returns the number of tags modified.
@@ -707,6 +702,14 @@
707 702 /// keystroke that falls through to the segment fallback pays a full O(tags * segments)
708 703 /// scan, into the tens of microseconds at low thousands.
709 704 ///
705 + /// Those two tiers are what [`TagIndex::suggest`] returns. [`TagIndex::suggest_fuzzy`]
706 + /// adds a third: when tiers 1 and 2 come back short of the limit, every remaining tag
707 + /// is scored segment-by-segment with a bounded Levenshtein distance (max 2) and the
708 + /// hits fill the remainder, ordered by distance and then lexicographically. Tier 3
709 + /// pays the same O(tags * segments) walk as the segment fallback plus the edit-distance
710 + /// work per segment, so it costs more than either tier above it and only runs when
711 + /// they underfill.
712 + ///
710 713 /// ```
711 714 /// use tagtree::TagIndex;
712 715 ///