query-for-ptag-graph.go raw

   1  //go:build !(js && wasm)
   2  
   3  package database
   4  
   5  import (
   6  	"bytes"
   7  
   8  	"next.orly.dev/pkg/lol/chk"
   9  	"next.orly.dev/pkg/lol/log"
  10  	"next.orly.dev/pkg/database/indexes"
  11  	"next.orly.dev/pkg/database/indexes/types"
  12  	"next.orly.dev/pkg/nostr/encoders/filter"
  13  	"next.orly.dev/pkg/nostr/encoders/hex"
  14  )
  15  
  16  // CanUsePTagGraph determines if a filter can benefit from p-tag graph optimization.
  17  //
  18  // Requirements:
  19  // - Filter must have #p tags
  20  // - Filter should NOT have authors (different index is better for that case)
  21  // - Optimization works best with kinds filter but is optional
  22  func CanUsePTagGraph(f *filter.F) bool {
  23  	// Must have tags
  24  	if f.Tags == nil || f.Tags.Len() == 0 {
  25  		return false
  26  	}
  27  
  28  	// Check if there are any p-tags
  29  	hasPTags := false
  30  	for _, t := range *f.Tags {
  31  		keyBytes := t.Key()
  32  		if (len(keyBytes) == 1 && keyBytes[0] == 'p') ||
  33  			(len(keyBytes) == 2 && keyBytes[0] == '#' && keyBytes[1] == 'p') {
  34  			hasPTags = true
  35  			break
  36  		}
  37  	}
  38  	if !hasPTags {
  39  		return false
  40  	}
  41  
  42  	// Don't use graph if there's an authors filter
  43  	// (TagPubkey index handles that case better)
  44  	if f.Authors != nil && f.Authors.Len() > 0 {
  45  		return false
  46  	}
  47  
  48  	return true
  49  }
  50  
  51  // QueryPTagGraph uses the pubkey graph index for efficient p-tag queries.
  52  //
  53  // This query path is optimized for filters like:
  54  //   {"#p": ["<pubkey>"], "kinds": [1, 6, 7]}
  55  //
  56  // Performance benefits:
  57  // - 41% smaller index keys (16 bytes vs 27 bytes)
  58  // - No hash collisions (exact serial match)
  59  // - Kind-indexed in key structure
  60  // - Direction-aware filtering
  61  func (d *D) QueryPTagGraph(f *filter.F) (sers types.Uint40s, err error) {
  62  	// Extract p-tags from filter
  63  	var pTags [][]byte
  64  	for _, t := range *f.Tags {
  65  		keyBytes := t.Key()
  66  		if (len(keyBytes) == 1 && keyBytes[0] == 'p') ||
  67  			(len(keyBytes) == 2 && keyBytes[0] == '#' && keyBytes[1] == 'p') {
  68  			// Get all values for this p-tag
  69  			for _, valueBytes := range t.T[1:] {
  70  				pTags = append(pTags, valueBytes)
  71  			}
  72  		}
  73  	}
  74  
  75  	if len(pTags) == 0 {
  76  		return nil, nil
  77  	}
  78  
  79  	// Resolve pubkey hex → serials
  80  	var pubkeySerials []*types.Uint40
  81  	for _, pTagBytes := range pTags {
  82  		var pubkeyBytes []byte
  83  
  84  		// Handle both binary-encoded (33 bytes) and hex-encoded (64 chars) values
  85  		// Filter tags may come as either format depending on how they were parsed
  86  		if IsBinaryEncoded(pTagBytes) {
  87  			// Already binary-encoded, extract the 32-byte hash
  88  			pubkeyBytes = pTagBytes[:HashLen]
  89  		} else {
  90  			// Try to decode as hex using NormalizeTagToHex for consistent handling
  91  			hexBytes := NormalizeTagToHex(pTagBytes)
  92  			var decErr error
  93  			if pubkeyBytes, decErr = hex.Dec(string(hexBytes)); chk.E(decErr) {
  94  				log.D.F("QueryPTagGraph: failed to decode pubkey hex: %v", decErr)
  95  				continue
  96  			}
  97  		}
  98  		if len(pubkeyBytes) != 32 {
  99  			log.D.F("QueryPTagGraph: invalid pubkey length: %d", len(pubkeyBytes))
 100  			continue
 101  		}
 102  
 103  		// Get serial for this pubkey
 104  		var serial *types.Uint40
 105  		if serial, err = d.GetPubkeySerial(pubkeyBytes); chk.E(err) {
 106  			log.D.F("QueryPTagGraph: pubkey not found in database: %s", hex.Enc(pubkeyBytes))
 107  			err = nil // Reset error - this just means no events reference this pubkey
 108  			continue
 109  		}
 110  
 111  		pubkeySerials = append(pubkeySerials, serial)
 112  	}
 113  
 114  	if len(pubkeySerials) == 0 {
 115  		// None of the pubkeys have serials = no events reference them
 116  		return nil, nil
 117  	}
 118  
 119  	// Build index ranges for each pubkey serial
 120  	var ranges []Range
 121  
 122  	// Get kinds from filter (if present)
 123  	var kinds []uint16
 124  	if f.Kinds != nil && f.Kinds.Len() > 0 {
 125  		kinds = f.Kinds.ToUint16()
 126  	}
 127  
 128  	// For each pubkey serial, create a range
 129  	for _, pkSerial := range pubkeySerials {
 130  		if len(kinds) > 0 {
 131  			// With kinds: peg|pubkey_serial|kind|direction|event_serial
 132  			for _, k := range kinds {
 133  				kind := new(types.Uint16)
 134  				kind.Set(k)
 135  				direction := new(types.Letter)
 136  				direction.Set(types.EdgeDirectionPTagIn) // Direction 2: inbound p-tags
 137  
 138  				start := new(bytes.Buffer)
 139  				idx := indexes.PubkeyEventGraphEnc(pkSerial, kind, direction, nil)
 140  				if err = idx.MarshalWrite(start); chk.E(err) {
 141  					return
 142  				}
 143  
 144  				// End range: same prefix with all 0xFF for event serial
 145  				end := start.Bytes()
 146  				endWithSerial := make([]byte, len(end)+5)
 147  				copy(endWithSerial, end)
 148  				for i := 0; i < 5; i++ {
 149  					endWithSerial[len(end)+i] = 0xFF
 150  				}
 151  
 152  				ranges = append(ranges, Range{
 153  					Start: start.Bytes(),
 154  					End:   endWithSerial,
 155  				})
 156  			}
 157  		} else {
 158  			// Without kinds: we need to scan all kinds for this pubkey
 159  			// Key structure: peg|pubkey_serial(5)|kind(2)|direction(1)|event_serial(5)
 160  			// Since direction comes after kind, we can't easily prefix-scan for a specific direction
 161  			// across all kinds. Instead, we'll iterate through common kinds.
 162  			//
 163  			// Common Nostr kinds that use p-tags:
 164  			// 1 (text note), 6 (repost), 7 (reaction), 9735 (zap), 10002 (relay list)
 165  			commonKinds := []uint16{1, 6, 7, 9735, 10002, 3, 4, 5, 30023}
 166  
 167  			for _, k := range commonKinds {
 168  				kind := new(types.Uint16)
 169  				kind.Set(k)
 170  				direction := new(types.Letter)
 171  				direction.Set(types.EdgeDirectionPTagIn) // Direction 2: inbound p-tags
 172  
 173  				start := new(bytes.Buffer)
 174  				idx := indexes.PubkeyEventGraphEnc(pkSerial, kind, direction, nil)
 175  				if err = idx.MarshalWrite(start); chk.E(err) {
 176  					return
 177  				}
 178  
 179  				// End range: same prefix with all 0xFF for event serial
 180  				end := start.Bytes()
 181  				endWithSerial := make([]byte, len(end)+5)
 182  				copy(endWithSerial, end)
 183  				for i := 0; i < 5; i++ {
 184  					endWithSerial[len(end)+i] = 0xFF
 185  				}
 186  
 187  				ranges = append(ranges, Range{
 188  					Start: start.Bytes(),
 189  					End:   endWithSerial,
 190  				})
 191  			}
 192  		}
 193  	}
 194  
 195  	// Execute scans for each range
 196  	sers = make(types.Uint40s, 0, len(ranges)*100)
 197  	for _, rng := range ranges {
 198  		var rangeSers types.Uint40s
 199  		if rangeSers, err = d.GetSerialsByRange(rng); chk.E(err) {
 200  			continue
 201  		}
 202  		sers = append(sers, rangeSers...)
 203  	}
 204  
 205  	log.D.F("QueryPTagGraph: found %d events for %d pubkeys", len(sers), len(pubkeySerials))
 206  	return
 207  }
 208