//go:build !(js && wasm) package database import ( "context" "errors" "sort" "lol.mleku.dev/chk" "next.orly.dev/pkg/database/indexes/types" "git.mleku.dev/mleku/nostr/encoders/event" "git.mleku.dev/mleku/nostr/encoders/filter" "next.orly.dev/pkg/interfaces/store" ) // QueryForIds retrieves a list of IdPkTs based on the provided filter. // It supports filtering by ranges and tags but disallows filtering by Ids. // Results are sorted by timestamp in reverse chronological order by default. // When a search query is present, results are ranked by a 50/50 blend of // match count (how many distinct search terms matched) and recency. // Returns an error if the filter contains Ids or if any operation fails. func (d *D) QueryForIds(c context.Context, f *filter.F) ( idPkTs []*store.IdPkTs, err error, ) { if f.Ids != nil && f.Ids.Len() > 0 { // if there is Ids in the query, this is an error for this query err = errors.New("query for Ids is invalid for a filter with Ids") return } var idxs []Range if idxs, err = GetIndexesFromFilter(f); chk.E(err) { return } var results []*store.IdPkTs var founds []*types.Uint40 // Pre-allocate results slice with estimated capacity to reduce reallocations results = make([]*store.IdPkTs, 0, len(idxs)*100) // Estimate 100 results per index // When searching, we want to count how many index ranges (search terms) // matched each note. We'll track counts by serial. counts := make(map[uint64]int) for _, idx := range idxs { if founds, err = d.GetSerialsByRange(idx); chk.E(err) { return } var tmp []*store.IdPkTs if tmp, err = d.GetFullIdPubkeyBySerials(founds); chk.E(err) { return } // If this query is driven by Search terms, increment count per serial if len(f.Search) > 0 { for _, v := range tmp { counts[v.Ser]++ } } results = append(results, tmp...) } // deduplicate in case this somehow happened (such as two or more // from one tag matched, only need it once) seen := make(map[uint64]struct{}, len(results)) idPkTs = make([]*store.IdPkTs, 0, len(results)) for _, idpk := range results { if _, ok := seen[idpk.Ser]; !ok { seen[idpk.Ser] = struct{}{} idPkTs = append(idPkTs, idpk) } } // If search is combined with Authors/Kinds/Tags, require events to match ALL of those present fields in addition to the word match. 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)) { // Build serial list for fetching full events serials := make([]*types.Uint40, 0, len(idPkTs)) for _, v := range idPkTs { s := new(types.Uint40) s.Set(v.Ser) serials = append(serials, s) } var evs map[uint64]*event.E if evs, err = d.FetchEventsBySerials(serials); chk.E(err) { return } filtered := make([]*store.IdPkTs, 0, len(idPkTs)) for _, v := range idPkTs { ev, ok := evs[v.Ser] if !ok || ev == nil { continue } matchesAll := true if f.Authors != nil && f.Authors.Len() > 0 && !f.Authors.Contains(ev.Pubkey) { matchesAll = false } if matchesAll && f.Kinds != nil && f.Kinds.Len() > 0 && !f.Kinds.Contains(ev.Kind) { matchesAll = false } if matchesAll && f.Tags != nil && f.Tags.Len() > 0 { // Require the event to satisfy all tag filters as in MatchesIgnoringTimestampConstraints tagOK := true for _, t := range *f.Tags { if t.Len() < 2 { continue } key := t.Key() values := t.T[1:] if !ev.Tags.ContainsAny(key, values) { tagOK = false break } } if !tagOK { matchesAll = false } } if matchesAll { filtered = append(filtered, v) } } idPkTs = filtered } if len(f.Search) == 0 { // No search query: sort by timestamp in reverse chronological order sort.Slice( idPkTs, func(i, j int) bool { return idPkTs[i].Ts > idPkTs[j].Ts }, ) } else { // Search query present: blend match count relevance with recency (50/50) // Normalize both match count and timestamp to [0,1] and compute score. var maxCount int var minTs, maxTs int64 if len(idPkTs) > 0 { minTs, maxTs = idPkTs[0].Ts, idPkTs[0].Ts } for _, v := range idPkTs { if c := counts[v.Ser]; c > maxCount { maxCount = c } if v.Ts < minTs { minTs = v.Ts } if v.Ts > maxTs { maxTs = v.Ts } } // Precompute denominator to avoid div-by-zero tsSpan := maxTs - minTs if tsSpan <= 0 { tsSpan = 1 } if maxCount <= 0 { maxCount = 1 } sort.Slice( idPkTs, func(i, j int) bool { ci := float64(counts[idPkTs[i].Ser]) / float64(maxCount) cj := float64(counts[idPkTs[j].Ser]) / float64(maxCount) ai := float64(idPkTs[i].Ts-minTs) / float64(tsSpan) aj := float64(idPkTs[j].Ts-minTs) / float64(tsSpan) si := 0.5*ci + 0.5*ai sj := 0.5*cj + 0.5*aj if si == sj { // tie-break by recency return idPkTs[i].Ts > idPkTs[j].Ts } return si > sj }, ) } if f.Limit != nil && len(idPkTs) > int(*f.Limit) { idPkTs = idPkTs[:*f.Limit] } return }