github.com/keybase/client/go@v0.0.0-20240309051027-028f7c731f8b/chat/search/regexp_searcher.go (about) 1 package search 2 3 import ( 4 "context" 5 "regexp" 6 7 "github.com/keybase/client/go/chat/globals" 8 "github.com/keybase/client/go/chat/types" 9 "github.com/keybase/client/go/chat/utils" 10 "github.com/keybase/client/go/protocol/chat1" 11 "github.com/keybase/client/go/protocol/gregor1" 12 ) 13 14 type RegexpSearcher struct { 15 globals.Contextified 16 utils.DebugLabeler 17 18 pageSize int 19 } 20 21 var _ types.RegexpSearcher = (*RegexpSearcher)(nil) 22 23 func NewRegexpSearcher(g *globals.Context) *RegexpSearcher { 24 labeler := utils.NewDebugLabeler(g.ExternalG(), "RegexpSearcher", false) 25 return &RegexpSearcher{ 26 Contextified: globals.NewContextified(g), 27 DebugLabeler: labeler, 28 pageSize: defaultPageSize, 29 } 30 } 31 32 func (s *RegexpSearcher) SetPageSize(pageSize int) { 33 s.pageSize = pageSize 34 } 35 36 func (s *RegexpSearcher) Search(ctx context.Context, uid gregor1.UID, convID chat1.ConversationID, 37 queryRe *regexp.Regexp, uiCh chan chat1.ChatSearchHit, opts chat1.SearchOpts) (hits []chat1.ChatSearchHit, msgHits []chat1.MessageUnboxed, err error) { 38 defer s.Trace(ctx, &err, "Search")() 39 defer func() { 40 if uiCh != nil { 41 close(uiCh) 42 } 43 }() 44 pagination := opts.InitialPagination 45 if pagination == nil { 46 pagination = &chat1.Pagination{Num: s.pageSize} 47 } else { 48 pagination.Num = s.pageSize 49 } 50 maxHits := opts.MaxHits 51 maxMessages := opts.MaxMessages 52 beforeContext := opts.BeforeContext 53 afterContext := opts.AfterContext 54 55 if beforeContext >= MaxContext || beforeContext < 0 { 56 beforeContext = MaxContext - 1 57 } 58 if afterContext >= MaxContext || afterContext < 0 { 59 afterContext = MaxContext - 1 60 } 61 62 if maxHits > MaxAllowedSearchHits || maxHits <= 0 { 63 maxHits = MaxAllowedSearchHits 64 } 65 66 if maxMessages > MaxAllowedSearchMessages || maxMessages <= 0 { 67 maxMessages = MaxAllowedSearchMessages 68 } 69 70 // If we have to gather search result context around a pagination boundary, 71 // we may have to fetch the next page of the thread 72 var prevPage, curPage, nextPage *chat1.ThreadView 73 74 getNextPage := func() (*chat1.ThreadView, error) { 75 thread, err := s.G().ConvSource.Pull(ctx, convID, uid, 76 chat1.GetThreadReason_SEARCHER, nil, 77 &chat1.GetThreadQuery{ 78 MarkAsRead: false, 79 }, pagination) 80 if err != nil { 81 return nil, err 82 } 83 filteredMsgs := []chat1.MessageUnboxed{} 84 // Filter out invalid/exploded messages so our search context is 85 // correct. 86 for _, msg := range thread.Messages { 87 if msg.IsValidFull() && msg.IsVisible() { 88 filteredMsgs = append(filteredMsgs, msg) 89 } 90 } 91 thread.Messages = filteredMsgs 92 return &thread, nil 93 } 94 95 // Returns search context before the search hit, at position `i` in 96 // `cur.Messages` possibly fetching and returning a new page of results if 97 // we are at a pagination boundary. 98 getBeforeMsgs := func(i int, cur, next *chat1.ThreadView) (*chat1.ThreadView, []chat1.MessageUnboxed, error) { 99 // context is contained entirely in this page of the thread. 100 if i+beforeContext < len(cur.Messages) { 101 return next, cur.Messages[i+1 : i+beforeContext+1], nil 102 } 103 // Get all of the context after our hit index of the current page and 104 // fetch a new page if available. 105 hitContext := cur.Messages[i+1:] 106 if next == nil { 107 next, err = getNextPage() 108 if err != nil { 109 if utils.IsPermanentErr(err) { 110 return nil, nil, err 111 } 112 s.Debug(ctx, "transient search failure: %v", err) 113 return nil, nil, nil 114 } 115 } 116 // Get the remaining context from the new current page of the thread. 117 remainingContext := beforeContext - len(hitContext) 118 if remainingContext > len(next.Messages) { 119 remainingContext = len(next.Messages) 120 } 121 hitContext = append(hitContext, next.Messages[:remainingContext]...) 122 return next, hitContext, nil 123 } 124 125 // Returns the search context surrounding a search result at index `i` in 126 // `cur.Messages`, possibly using prev if we are at a 127 // pagination boundary (since msgs are ordered last to first). 128 getAfterMsgs := func(i int, prev, cur *chat1.ThreadView) []chat1.MessageUnboxed { 129 // Return context from the current thread only 130 if afterContext < i { 131 return cur.Messages[i-afterContext : i] 132 } 133 hitContext := cur.Messages[:i] 134 if prev != nil { 135 // Get the remaining context from the previous page of the thread. 136 remainingContext := len(prev.Messages) - (afterContext - len(hitContext)) 137 if remainingContext < 0 { 138 remainingContext = 0 139 } 140 hitContext = append(prev.Messages[remainingContext:], hitContext...) 141 } 142 return hitContext 143 } 144 145 numHits := 0 146 numMessages := 0 147 for !pagination.Last && numHits < maxHits && numMessages < maxMessages { 148 prevPage = curPage 149 if nextPage == nil { 150 curPage, err = getNextPage() 151 if err != nil { 152 return nil, nil, err 153 } else if curPage == nil { 154 break 155 } 156 } else { // we pre-fetched the next page when retrieving context 157 curPage = nextPage 158 nextPage = nil 159 } 160 // update our global pagination so we can correctly fetch the next page. 161 pagination = curPage.Pagination 162 pagination.Num = s.pageSize 163 pagination.Previous = nil 164 165 for i, msg := range curPage.Messages { 166 numMessages++ 167 if !opts.Matches(msg) { 168 continue 169 } 170 matches := searchMatches(msg, queryRe) 171 if len(matches) > 0 { 172 numHits++ 173 174 afterMsgs := getAfterMsgs(i, prevPage, curPage) 175 newThread, beforeMsgs, err := getBeforeMsgs(i, curPage, nextPage) 176 if err != nil { 177 return nil, nil, err 178 } 179 nextPage = newThread 180 searchHit := chat1.ChatSearchHit{ 181 BeforeMessages: getUIMsgs(ctx, s.G(), convID, uid, beforeMsgs), 182 HitMessage: utils.PresentMessageUnboxed(ctx, s.G(), msg, uid, convID), 183 AfterMessages: getUIMsgs(ctx, s.G(), convID, uid, afterMsgs), 184 Matches: matches, 185 } 186 if uiCh != nil { 187 // Stream search hits back to the UI channel 188 select { 189 case <-ctx.Done(): 190 return nil, nil, ctx.Err() 191 case uiCh <- searchHit: 192 } 193 } 194 hits = append(hits, searchHit) 195 msgHits = append(msgHits, msg) 196 } 197 if numHits >= maxHits || numMessages >= maxMessages { 198 break 199 } 200 } 201 } 202 return hits, msgHits, nil 203 }