find.mx raw

   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