query-for-ids.go raw

   1  //go:build !(js && wasm)
   2  
   3  package database
   4  
   5  import (
   6  	"context"
   7  	"errors"
   8  	"sort"
   9  
  10  	"next.orly.dev/pkg/lol/chk"
  11  	"next.orly.dev/pkg/database/indexes/types"
  12  	"next.orly.dev/pkg/nostr/encoders/event"
  13  	"next.orly.dev/pkg/nostr/encoders/filter"
  14  	"next.orly.dev/pkg/interfaces/store"
  15  )
  16  
  17  // QueryForIds retrieves a list of IdPkTs based on the provided filter.
  18  // It supports filtering by ranges and tags but disallows filtering by Ids.
  19  // Results are sorted by timestamp in reverse chronological order by default.
  20  // When a search query is present, results are ranked by a 50/50 blend of
  21  // match count (how many distinct search terms matched) and recency.
  22  // Returns an error if the filter contains Ids or if any operation fails.
  23  func (d *D) QueryForIds(c context.Context, f *filter.F) (
  24  	idPkTs []*store.IdPkTs, err error,
  25  ) {
  26  	if f.Ids != nil && f.Ids.Len() > 0 {
  27  		// if there is Ids in the query, this is an error for this query
  28  		err = errors.New("query for Ids is invalid for a filter with Ids")
  29  		return
  30  	}
  31  	var idxs []Range
  32  	if idxs, err = GetIndexesFromFilter(f); chk.E(err) {
  33  		return
  34  	}
  35  	var results []*store.IdPkTs
  36  	var founds []*types.Uint40
  37  	// Pre-allocate results slice with estimated capacity to reduce reallocations
  38  	results = make([]*store.IdPkTs, 0, len(idxs)*100) // Estimate 100 results per index
  39  	// When searching, we want to count how many index ranges (search terms)
  40  	// matched each note. We'll track counts by serial.
  41  	counts := make(map[uint64]int)
  42  	for _, idx := range idxs {
  43  		if founds, err = d.GetSerialsByRange(idx); chk.E(err) {
  44  			return
  45  		}
  46  		var tmp []*store.IdPkTs
  47  		if tmp, err = d.GetFullIdPubkeyBySerials(founds); chk.E(err) {
  48  			return
  49  		}
  50  		// If this query is driven by Search terms, increment count per serial
  51  		if len(f.Search) > 0 {
  52  			for _, v := range tmp {
  53  				counts[v.Ser]++
  54  			}
  55  		}
  56  		results = append(results, tmp...)
  57  	}
  58  	// deduplicate in case this somehow happened (such as two or more
  59  	// from one tag matched, only need it once)
  60  	seen := make(map[uint64]struct{}, len(results))
  61  	idPkTs = make([]*store.IdPkTs, 0, len(results))
  62  	for _, idpk := range results {
  63  		if _, ok := seen[idpk.Ser]; !ok {
  64  			seen[idpk.Ser] = struct{}{}
  65  			idPkTs = append(idPkTs, idpk)
  66  		}
  67  	}
  68  	
  69  	// If search is combined with Authors/Kinds/Tags, require events to match ALL of those present fields in addition to the word match.
  70  	if len(f.Search) > 0 && ((f.Authors != nil && f.Authors.Len() > 0) || (f.Kinds != nil && f.Kinds.Len() > 0) || (f.Tags != nil && f.Tags.Len() > 0)) {
  71  		// Build serial list for fetching full events
  72  		serials := make([]*types.Uint40, 0, len(idPkTs))
  73  		for _, v := range idPkTs {
  74  			s := new(types.Uint40)
  75  			s.Set(v.Ser)
  76  			serials = append(serials, s)
  77  		}
  78  		var evs map[uint64]*event.E
  79  		if evs, err = d.FetchEventsBySerials(serials); chk.E(err) {
  80  			return
  81  		}
  82  		filtered := make([]*store.IdPkTs, 0, len(idPkTs))
  83  		for _, v := range idPkTs {
  84  			ev, ok := evs[v.Ser]
  85  			if !ok || ev == nil {
  86  				continue
  87  			}
  88  			matchesAll := true
  89  			if f.Authors != nil && f.Authors.Len() > 0 && !f.Authors.Contains(ev.Pubkey) {
  90  				matchesAll = false
  91  			}
  92  			if matchesAll && f.Kinds != nil && f.Kinds.Len() > 0 && !f.Kinds.Contains(ev.Kind) {
  93  				matchesAll = false
  94  			}
  95  			if matchesAll && f.Tags != nil && f.Tags.Len() > 0 {
  96  				// Require the event to satisfy all tag filters as in MatchesIgnoringTimestampConstraints
  97  				tagOK := true
  98  				for _, t := range *f.Tags {
  99  					if t.Len() < 2 {
 100  						continue
 101  					}
 102  					key := t.Key()
 103  					values := t.T[1:]
 104  					if !ev.Tags.ContainsAny(key, values) {
 105  						tagOK = false
 106  						break
 107  					}
 108  				}
 109  				if !tagOK {
 110  					matchesAll = false
 111  				}
 112  			}
 113  			if matchesAll {
 114  				filtered = append(filtered, v)
 115  			}
 116  		}
 117  		idPkTs = filtered
 118  	}
 119  	
 120  	if len(f.Search) == 0 {
 121  		// No search query: sort by timestamp in reverse chronological order
 122  		sort.Slice(
 123  			idPkTs, func(i, j int) bool {
 124  				return idPkTs[i].Ts > idPkTs[j].Ts
 125  			},
 126  		)
 127  	} else {
 128  		// Search query present: blend match count relevance with recency (50/50)
 129  		// Normalize both match count and timestamp to [0,1] and compute score.
 130  		var maxCount int
 131  		var minTs, maxTs int64
 132  		if len(idPkTs) > 0 {
 133  			minTs, maxTs = idPkTs[0].Ts, idPkTs[0].Ts
 134  		}
 135  		for _, v := range idPkTs {
 136  			if c := counts[v.Ser]; c > maxCount {
 137  				maxCount = c
 138  			}
 139  			if v.Ts < minTs {
 140  				minTs = v.Ts
 141  			}
 142  			if v.Ts > maxTs {
 143  				maxTs = v.Ts
 144  			}
 145  		}
 146  		// Precompute denominator to avoid div-by-zero
 147  		tsSpan := maxTs - minTs
 148  		if tsSpan <= 0 {
 149  			tsSpan = 1
 150  		}
 151  		if maxCount <= 0 {
 152  			maxCount = 1
 153  		}
 154  		sort.Slice(
 155  			idPkTs, func(i, j int) bool {
 156  				ci := float64(counts[idPkTs[i].Ser]) / float64(maxCount)
 157  				cj := float64(counts[idPkTs[j].Ser]) / float64(maxCount)
 158  				ai := float64(idPkTs[i].Ts-minTs) / float64(tsSpan)
 159  				aj := float64(idPkTs[j].Ts-minTs) / float64(tsSpan)
 160  				si := 0.5*ci + 0.5*ai
 161  				sj := 0.5*cj + 0.5*aj
 162  				if si == sj {
 163  					// tie-break by recency
 164  					return idPkTs[i].Ts > idPkTs[j].Ts
 165  				}
 166  				return si > sj
 167  			},
 168  		)
 169  	}
 170  
 171  	if f.Limit != nil && len(idPkTs) > int(*f.Limit) {
 172  		idPkTs = idPkTs[:*f.Limit]
 173  	}
 174  	return
 175  }
 176