// Package find provides full-text search over stored events using the // word index (wrd). Words are extracted from event content, lowercased, // and hashed into the sorted wrd index at storage time. This package // queries that index to find matching events. package find import ( "git.smesh.lol/moxie/pkg/mxutil" "sort" "git.smesh.lol/nostr/pkg/event" "git.smesh.lol/morly/pkg/store" ) // Finder performs full-text search. type Finder struct { store *store.Engine } // New creates a Finder. func New(s *store.Engine) (f *Finder) { return &Finder{store: s} } // Search finds events matching all words in the query. // Results are sorted newest-first. Limit 0 = no limit. func (f *Finder) Search(query []byte, limit int32) (ss []*event.E) { words := SplitWords(query) if len(words) == 0 { return nil } // Get serials for each word, intersect. var sets [][]uint64 for _, w := range words { serials := f.store.SearchWord(w) if len(serials) == 0 { return nil // all words must match } sets = mxutil.Ensure(sets, 1) sets = push(sets, serials) } // Intersect all serial sets. result := sets[0] for i := 1; i < len(sets); i++ { result = intersect(result, sets[i]) if len(result) == 0 { return nil } } // Fetch events. var events []*event.E for _, ser := range result { ev, err := f.store.GetBySerial(ser) if err != nil { continue } events = mxutil.Ensure(events, 1) events = push(events, ev) } sort.Sort(&event.S{E: events}) // newest first if limit > 0 && len(events) > limit { events = events[:limit] } return events } // SplitWords splits content into lowercase words (>= 3 chars). // Exported so the store can reuse it for indexing. func SplitWords(content []byte) (ss [][]byte) { var words [][]byte var word []byte for _, b := range content { if b >= 'A' && b <= 'Z' { word = push(word, b+32) // lowercase } else if (b >= 'a' && b <= 'z') || (b >= '0' && b <= '9') { word = mxutil.Ensure(word, 1) word = push(word, b) } else { if len(word) >= 3 { w := []byte{:len(word)} copy(w, word) words = mxutil.Ensure(words, 1) words = push(words, w) } word = word[:0] } } if len(word) >= 3 { w := []byte{:len(word)} copy(w, word) words = mxutil.Ensure(words, 1) words = push(words, w) } return words } func intersect(a, b []uint64) (ss []uint64) { set := map[uint64]bool{} for _, v := range b { set[v] = true } var out []uint64 for _, v := range a { if set[v] { out = mxutil.Ensure(out, 1) out = push(out, v) } } return out }