1 // Package find provides full-text search over stored events using the
2 // word index (wrd). Words are extracted from event content, lowercased,
3 // and hashed into the sorted wrd index at storage time. This package
4 // queries that index to find matching events.
5 package find
6 7 import (
8 "git.smesh.lol/moxie/pkg/mxutil"
9 "sort"
10 11 "git.smesh.lol/nostr/pkg/event"
12 "git.smesh.lol/morly/pkg/store"
13 )
14 15 // Finder performs full-text search.
16 type Finder struct {
17 store *store.Engine
18 }
19 20 // New creates a Finder.
21 func New(s *store.Engine) (f *Finder) { return &Finder{store: s} }
22 23 // Search finds events matching all words in the query.
24 // Results are sorted newest-first. Limit 0 = no limit.
25 func (f *Finder) Search(query []byte, limit int32) (ss []*event.E) {
26 words := SplitWords(query)
27 if len(words) == 0 {
28 return nil
29 }
30 31 // Get serials for each word, intersect.
32 var sets [][]uint64
33 for _, w := range words {
34 serials := f.store.SearchWord(w)
35 if len(serials) == 0 {
36 return nil // all words must match
37 }
38 sets = mxutil.Ensure(sets, 1)
39 sets = push(sets, serials)
40 }
41 42 // Intersect all serial sets.
43 result := sets[0]
44 for i := 1; i < len(sets); i++ {
45 result = intersect(result, sets[i])
46 if len(result) == 0 {
47 return nil
48 }
49 }
50 51 // Fetch events.
52 var events []*event.E
53 for _, ser := range result {
54 ev, err := f.store.GetBySerial(ser)
55 if err != nil {
56 continue
57 }
58 events = mxutil.Ensure(events, 1)
59 events = push(events, ev)
60 }
61 sort.Sort(&event.S{E: events}) // newest first
62 63 if limit > 0 && len(events) > limit {
64 events = events[:limit]
65 }
66 return events
67 }
68 69 // SplitWords splits content into lowercase words (>= 3 chars).
70 // Exported so the store can reuse it for indexing.
71 func SplitWords(content []byte) (ss [][]byte) {
72 var words [][]byte
73 var word []byte
74 for _, b := range content {
75 if b >= 'A' && b <= 'Z' {
76 word = push(word, b+32) // lowercase
77 } else if (b >= 'a' && b <= 'z') || (b >= '0' && b <= '9') {
78 word = mxutil.Ensure(word, 1)
79 word = push(word, b)
80 } else {
81 if len(word) >= 3 {
82 w := []byte{:len(word)}
83 copy(w, word)
84 words = mxutil.Ensure(words, 1)
85 words = push(words, w)
86 }
87 word = word[:0]
88 }
89 }
90 if len(word) >= 3 {
91 w := []byte{:len(word)}
92 copy(w, word)
93 words = mxutil.Ensure(words, 1)
94 words = push(words, w)
95 }
96 return words
97 }
98 99 func intersect(a, b []uint64) (ss []uint64) {
100 set := map[uint64]bool{}
101 for _, v := range b {
102 set[v] = true
103 }
104 var out []uint64
105 for _, v := range a {
106 if set[v] {
107 out = mxutil.Ensure(out, 1)
108 out = push(out, v)
109 }
110 }
111 return out
112 }
113