Monorepo for Tangled tangled.org
5

Configure Feed

Select the types of activity you want to include in your feed.

1package state 2 3import ( 4 "cmp" 5 "net/http" 6 "slices" 7 "strings" 8 "time" 9 10 "github.com/posthog/posthog-go" 11 "tangled.org/core/appview/db" 12 "tangled.org/core/appview/models" 13 "tangled.org/core/appview/pages" 14 "tangled.org/core/appview/pagination" 15 "tangled.org/core/appview/searchquery" 16 "tangled.org/core/orm" 17) 18 19func (s *State) Search(w http.ResponseWriter, r *http.Request) { 20 l := s.logger.With("handler", "Search") 21 22 params := r.URL.Query() 23 page := pagination.FromContext(r.Context()) 24 25 query := searchquery.Parse(params.Get("q")) 26 27 sortParam := params.Get("sort") 28 sortField, sortDesc := parseSortParam(sortParam) 29 30 var language string 31 if lang := cmp.Or(query.Get("language"), query.Get("lang")); lang != nil { 32 language = *lang 33 } 34 35 tf := searchquery.ExtractTextFilters(query) 36 37 searchOpts := models.RepoSearchOptions{ 38 Keywords: tf.Keywords, 39 Phrases: tf.Phrases, 40 NegatedKeywords: tf.NegatedKeywords, 41 NegatedPhrases: tf.NegatedPhrases, 42 Language: language, 43 SortField: sortField, 44 SortDesc: sortDesc, 45 Page: page, 46 } 47 48 var repos []models.Repo 49 var err error 50 var resultCount int 51 var searchDuration time.Duration 52 var docCount int64 53 method := "bleve" 54 55 if searchOpts.HasSearchFilters() || sortParam != "" { 56 res, err := s.indexer.Repos.Search(r.Context(), searchOpts) 57 if err != nil { 58 l.Error("failed to search repos", "err", err) 59 s.pages.Error500(w) 60 return 61 } 62 63 searchDuration = res.Duration 64 65 if len(res.Hits) > 0 { 66 repos, err = db.GetRepos(s.db, orm.FilterIn("id", res.Hits)) 67 if err != nil { 68 l.Error("failed to get repos by IDs", "err", err) 69 s.pages.Error500(w) 70 return 71 } 72 73 hitIdx := make(map[int64]int, len(res.Hits)) 74 for i, id := range res.Hits { 75 hitIdx[id] = i 76 } 77 slices.SortFunc(repos, func(a, b models.Repo) int { 78 return cmp.Compare(hitIdx[a.Id], hitIdx[b.Id]) 79 }) 80 } 81 resultCount = int(res.Total) 82 83 dc, err := (s.indexer.Repos.TotalDocCount()) 84 if err != nil { 85 l.Error("failed to get total doc count", "err", err) 86 } 87 docCount = int64(dc) 88 89 } else { 90 method = "db" 91 repos, err = db.GetReposPaginated( 92 s.db, 93 page, 94 ) 95 if err != nil { 96 l.Error("failed to get repos", "err", err) 97 s.pages.Error500(w) 98 return 99 } 100 101 rc, err := db.CountRepos( 102 s.db, 103 ) 104 if err != nil { 105 l.Error("failed to count repos", "err", err) 106 s.pages.Error500(w) 107 return 108 } 109 110 resultCount = int(rc) 111 docCount = int64(rc) 112 } 113 114 l.Info( 115 "RepoSearch", 116 "method", method, 117 "resultCount", resultCount, 118 "docCount", docCount, 119 "time", searchDuration, 120 "filterQuery", query.String(), 121 "sortParam", sortParam, 122 ) 123 124 if !s.config.Core.Dev && query.String() != "" { 125 distinctId := s.oauth.GetDid(r) 126 if distinctId == "" { 127 distinctId = "anonymous" 128 } 129 go func() { 130 if err := s.posthog.Enqueue(posthog.Capture{ 131 DistinctId: distinctId, 132 Event: "search", 133 Properties: posthog.Properties{ 134 "query": query.String(), 135 "result_count": resultCount, 136 "method": method, 137 }, 138 }); err != nil { 139 l.Error("failed to enqueue posthog event", "err", err) 140 } 141 }() 142 } 143 144 err = s.pages.SearchRepos(w, pages.SearchReposParams{ 145 BaseParams: pages.BaseParamsFromContext(r.Context()), 146 Repos: repos, 147 Page: page, 148 FilterQuery: query.String(), 149 SortParam: sortParam, 150 TimeTaken: searchDuration, 151 ResultCount: resultCount, 152 DocCount: docCount, 153 }) 154 if err != nil { 155 l.Error("failed to render page", "err", err) 156 } 157} 158 159func (s *State) SearchQuick(w http.ResponseWriter, r *http.Request) { 160 s.searchQuick(w, r, false) 161} 162 163func (s *State) SearchQuickMobile(w http.ResponseWriter, r *http.Request) { 164 s.searchQuick(w, r, true) 165} 166 167func (s *State) searchQuick(w http.ResponseWriter, r *http.Request, mobile bool) { 168 rawQuery := r.URL.Query().Get("q") 169 if rawQuery == "" { 170 w.WriteHeader(http.StatusOK) 171 return 172 } 173 174 const pageSize = 5 175 176 query := searchquery.Parse(rawQuery) 177 tf := searchquery.ExtractTextFilters(query) 178 179 searchOpts := models.RepoSearchOptions{ 180 Keywords: tf.Keywords, 181 Phrases: tf.Phrases, 182 NegatedKeywords: tf.NegatedKeywords, 183 NegatedPhrases: tf.NegatedPhrases, 184 Page: pagination.Page{Limit: pageSize}, 185 } 186 187 var repos []models.Repo 188 var total int 189 190 if searchOpts.HasSearchFilters() { 191 res, err := s.indexer.Repos.Search(r.Context(), searchOpts) 192 if err != nil { 193 s.logger.Error("failed quick search", "err", err) 194 http.Error(w, "search failed", http.StatusInternalServerError) 195 return 196 } 197 total = int(res.Total) 198 if len(res.Hits) > 0 { 199 repos, err = db.GetRepos(s.db, orm.FilterIn("id", res.Hits)) 200 if err != nil { 201 s.logger.Error("failed to get repos for quick search", "err", err) 202 http.Error(w, "search failed", http.StatusInternalServerError) 203 return 204 } 205 hitIdx := make(map[int64]int, len(res.Hits)) 206 for i, id := range res.Hits { 207 hitIdx[id] = i 208 } 209 slices.SortFunc(repos, func(a, b models.Repo) int { 210 return cmp.Compare(hitIdx[a.Id], hitIdx[b.Id]) 211 }) 212 } 213 } 214 215 params := pages.SearchQuickParams{ 216 Repos: repos, 217 Query: rawQuery, 218 Total: total, 219 } 220 221 render := s.pages.SearchQuick 222 if mobile { 223 render = s.pages.SearchQuickMobile 224 } 225 if err := render(w, params); err != nil { 226 s.logger.Error("failed to render quick search", "err", err) 227 } 228} 229 230// parseSortParam parses sort parameter like "stars-desc" or "created-asc" 231func parseSortParam(sortParam string) (string, bool) { 232 defaultSort := func() (string, bool) { return "relevance", true } 233 234 // no sort param supplied, just go default 235 if sortParam == "" { 236 return defaultSort() 237 } 238 239 parts := strings.Split(sortParam, "-") 240 if len(parts) != 2 { 241 return defaultSort() 242 } 243 244 field := parts[0] 245 desc := parts[1] == "desc" 246 247 // validate field 248 validFields := map[string]bool{ 249 "relevance": true, 250 "created": true, 251 "stars": true, 252 "issues": true, 253 "pulls": true, 254 } 255 256 // invalid fields, just go default 257 if !validFields[field] { 258 return defaultSort() 259 } 260 261 return field, desc 262}