Skip to content

Commit fa48698

Browse files
committed
Add PrefixSearchIter
1 parent 3c96047 commit fa48698

File tree

2 files changed

+234
-6
lines changed

2 files changed

+234
-6
lines changed

trie.go

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -213,19 +213,28 @@ func (t *Trie[T]) FuzzySearchIter(pre string) iter.Seq[string] {
213213

214214
// PrefixSearch performs a prefix search against the keys in the trie.
215215
func (t *Trie[T]) PrefixSearch(pre string) []string {
216+
// Use PrefixSearchIter internally to avoid code duplication
217+
var keys []string
218+
for key := range t.PrefixSearchIter(pre) {
219+
keys = append(keys, key)
220+
}
221+
return keys
222+
}
223+
224+
// PrefixSearchIter performs a prefix search and returns an iterator over matching key-value pairs.
225+
// Unlike PrefixSearch, this returns an iterator that yields both keys and their associated values.
226+
// This provides lazy evaluation and is more memory efficient for large result sets.
227+
func (t *Trie[T]) PrefixSearchIter(pre string) iter.Seq2[string, T] {
216228
t.mu.RLock()
217229
defer t.mu.RUnlock()
218230

219231
nd := findNode(t.root, []rune(pre))
220232
if nd == nil {
221-
return nil
233+
// Return an empty iterator if no node is found
234+
return func(yield func(string, T) bool) {}
222235
}
223236

224-
keys := make([]string, 0, nd.termCount)
225-
for key := range collectIter(nd) {
226-
keys = append(keys, key)
227-
}
228-
return keys
237+
return collectIter(nd)
229238
}
230239

231240
// newChild creates and returns a pointer to a new child for the node.

trie_test.go

Lines changed: 219 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -443,6 +443,197 @@ func TestPrefixSearchEmpty(t *testing.T) {
443443
}
444444
}
445445

446+
func TestPrefixSearchIter(t *testing.T) {
447+
trie := New[string]()
448+
449+
// Add test data with values
450+
testData := map[string]string{
451+
"foo": "value1",
452+
"foosball": "value2",
453+
"football": "value3",
454+
"foreboding": "value4",
455+
"forementioned": "value5",
456+
"foretold": "value6",
457+
"foreverandeverandeverandever": "value7",
458+
"forbidden": "value8",
459+
"bar": "value9",
460+
"baz": "value10",
461+
}
462+
463+
for key, value := range testData {
464+
trie.Add(key, value)
465+
}
466+
467+
tests := []struct {
468+
prefix string
469+
expected map[string]string
470+
}{
471+
{
472+
prefix: "fo",
473+
expected: map[string]string{
474+
"foo": "value1",
475+
"foosball": "value2",
476+
"football": "value3",
477+
"foreboding": "value4",
478+
"forementioned": "value5",
479+
"foretold": "value6",
480+
"foreverandeverandeverandever": "value7",
481+
"forbidden": "value8",
482+
},
483+
},
484+
{
485+
prefix: "foosbal",
486+
expected: map[string]string{
487+
"foosball": "value2",
488+
},
489+
},
490+
{
491+
prefix: "bar",
492+
expected: map[string]string{
493+
"bar": "value9",
494+
},
495+
},
496+
{
497+
prefix: "xyz",
498+
expected: map[string]string{},
499+
},
500+
{
501+
prefix: "",
502+
expected: testData, // Empty prefix should return all entries
503+
},
504+
}
505+
506+
for _, test := range tests {
507+
t.Run(test.prefix, func(t *testing.T) {
508+
// Collect results from iterator
509+
iterResults := make(map[string]string)
510+
for key, value := range trie.PrefixSearchIter(test.prefix) {
511+
iterResults[key] = value
512+
}
513+
514+
// Compare lengths
515+
if len(iterResults) != len(test.expected) {
516+
t.Errorf("Length mismatch for prefix '%s': got %d, expected %d",
517+
test.prefix, len(iterResults), len(test.expected))
518+
}
519+
520+
// Compare key-value pairs
521+
for expectedKey, expectedValue := range test.expected {
522+
if actualValue, ok := iterResults[expectedKey]; !ok {
523+
t.Errorf("Missing key '%s' for prefix '%s'", expectedKey, test.prefix)
524+
} else if actualValue != expectedValue {
525+
t.Errorf("Value mismatch for key '%s' with prefix '%s': got '%s', expected '%s'",
526+
expectedKey, test.prefix, actualValue, expectedValue)
527+
}
528+
}
529+
530+
// Check for unexpected keys
531+
for actualKey := range iterResults {
532+
if _, ok := test.expected[actualKey]; !ok {
533+
t.Errorf("Unexpected key '%s' for prefix '%s'", actualKey, test.prefix)
534+
}
535+
}
536+
})
537+
}
538+
}
539+
540+
func TestPrefixSearchIterEmpty(t *testing.T) {
541+
trie := New[string]()
542+
543+
count := 0
544+
for range trie.PrefixSearchIter("") {
545+
count++
546+
}
547+
548+
if count != 0 {
549+
t.Errorf("Expected 0 entries from empty trie, got: %d", count)
550+
}
551+
}
552+
553+
func TestPrefixSearchIterEarlyStop(t *testing.T) {
554+
trie := New[int]()
555+
keys := []string{"foo", "foobar", "foobaz", "football", "foosball"}
556+
for i, key := range keys {
557+
trie.Add(key, i)
558+
}
559+
560+
// Test that we can stop iteration early
561+
count := 0
562+
maxCount := 2
563+
for range trie.PrefixSearchIter("foo") {
564+
count++
565+
if count >= maxCount {
566+
break
567+
}
568+
}
569+
570+
if count != maxCount {
571+
t.Errorf("Expected to stop at %d iterations, got %d", maxCount, count)
572+
}
573+
}
574+
575+
func TestPrefixSearchAndIterConsistency(t *testing.T) {
576+
trie := New[int]()
577+
578+
// Add test data
579+
testData := map[string]int{
580+
"apple": 1,
581+
"application": 2,
582+
"apply": 3,
583+
"banana": 4,
584+
"band": 5,
585+
"bandana": 6,
586+
"can": 7,
587+
"candy": 8,
588+
"candid": 9,
589+
}
590+
591+
for key, value := range testData {
592+
trie.Add(key, value)
593+
}
594+
595+
prefixes := []string{"", "app", "ban", "can", "z"}
596+
597+
for _, prefix := range prefixes {
598+
t.Run(prefix, func(t *testing.T) {
599+
// Get results from PrefixSearch
600+
searchResults := trie.PrefixSearch(prefix)
601+
searchSet := make(map[string]bool)
602+
for _, key := range searchResults {
603+
searchSet[key] = true
604+
}
605+
606+
// Collect results from PrefixSearchIter
607+
iterResults := make(map[string]int)
608+
for key, value := range trie.PrefixSearchIter(prefix) {
609+
iterResults[key] = value
610+
}
611+
612+
// Check that all keys match
613+
if len(searchResults) != len(iterResults) {
614+
t.Errorf("Length mismatch for prefix '%s': PrefixSearch=%d, PrefixSearchIter=%d",
615+
prefix, len(searchResults), len(iterResults))
616+
}
617+
618+
// Verify all keys from PrefixSearch are in PrefixSearchIter
619+
for _, key := range searchResults {
620+
if _, ok := iterResults[key]; !ok {
621+
t.Errorf("Key '%s' found in PrefixSearch but not in PrefixSearchIter for prefix '%s'",
622+
key, prefix)
623+
}
624+
}
625+
626+
// Verify all keys from PrefixSearchIter are in PrefixSearch
627+
for key := range iterResults {
628+
if !searchSet[key] {
629+
t.Errorf("Key '%s' found in PrefixSearchIter but not in PrefixSearch for prefix '%s'",
630+
key, prefix)
631+
}
632+
}
633+
})
634+
}
635+
}
636+
446637
func TestFuzzySearch(t *testing.T) {
447638
setup := []string{
448639
"foosball",
@@ -657,6 +848,34 @@ func BenchmarkPrefixSearch(b *testing.B) {
657848
}
658849
}
659850

851+
func BenchmarkPrefixSearchIter(b *testing.B) {
852+
trie := createTrieAndAddFromFile[interface{}]("/usr/share/dict/words", nil)
853+
854+
b.ResetTimer()
855+
for i := 0; i < b.N; i++ {
856+
count := 0
857+
for range trie.PrefixSearchIter("fo") {
858+
count++
859+
}
860+
}
861+
}
862+
863+
func BenchmarkPrefixSearchIterEarlyStop(b *testing.B) {
864+
trie := createTrieAndAddFromFile[interface{}]("/usr/share/dict/words", nil)
865+
866+
b.ResetTimer()
867+
for i := 0; i < b.N; i++ {
868+
count := 0
869+
maxCount := 10
870+
for range trie.PrefixSearchIter("fo") {
871+
count++
872+
if count >= maxCount {
873+
break
874+
}
875+
}
876+
}
877+
}
878+
660879
func BenchmarkFuzzySearch(b *testing.B) {
661880
trie := createTrieAndAddFromFile[interface{}]("fixtures/test.txt", nil)
662881

0 commit comments

Comments
 (0)