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