Monorepo for Tangled tangled.org
12

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