forked from
tangled.org/core
Monorepo for Tangled
1package state
2
3import (
4 "net/http"
5 "strings"
6 "time"
7
8 "github.com/posthog/posthog-go"
9 "tangled.org/core/appview/db"
10 "tangled.org/core/appview/models"
11 "tangled.org/core/appview/pages"
12 "tangled.org/core/appview/pagination"
13 "tangled.org/core/appview/searchquery"
14 "tangled.org/core/orm"
15)
16
17func (s *State) Search(w http.ResponseWriter, r *http.Request) {
18 l := s.logger.With("handler", "Search")
19
20 params := r.URL.Query()
21 page := pagination.FromContext(r.Context())
22
23 query := searchquery.Parse(params.Get("q"))
24
25 sortParam := params.Get("sort")
26 sortField, sortDesc := parseSortParam(sortParam)
27
28 var language string
29 if lang := query.Get("language"); lang != nil {
30 language = *lang
31 }
32
33 tf := searchquery.ExtractTextFilters(query)
34
35 searchOpts := models.RepoSearchOptions{
36 Keywords: tf.Keywords,
37 Phrases: tf.Phrases,
38 NegatedKeywords: tf.NegatedKeywords,
39 NegatedPhrases: tf.NegatedPhrases,
40 Language: language,
41 SortField: sortField,
42 SortDesc: sortDesc,
43 Page: page,
44 }
45
46 var repos []models.Repo
47 var err error
48 var resultCount int
49 var searchDuration time.Duration
50 var docCount int64
51 method := "bleve"
52
53 if searchOpts.HasSearchFilters() || sortParam != "" {
54 res, err := s.indexer.Repos.Search(r.Context(), searchOpts)
55 if err != nil {
56 l.Error("failed to search repos", "err", err)
57 s.pages.Error500(w)
58 return
59 }
60
61 searchDuration = res.Duration
62
63 if len(res.Hits) > 0 {
64 repos, err = db.GetRepos(s.db, orm.FilterIn("id", res.Hits))
65 if err != nil {
66 l.Error("failed to get repos by IDs", "err", err)
67 s.pages.Error500(w)
68 return
69 }
70
71 // sort repos to match search result order (by relevance)
72 repoMap := make(map[int64]models.Repo, len(repos))
73 for _, repo := range repos {
74 repoMap[repo.Id] = repo
75 }
76 repos = make([]models.Repo, 0, len(res.Hits))
77 for _, id := range res.Hits {
78 if repo, ok := repoMap[id]; ok {
79 repos = append(repos, repo)
80 }
81 }
82 }
83 resultCount = int(res.Total)
84
85 dc, err := (s.indexer.Repos.TotalDocCount())
86 if err != nil {
87 l.Error("failed to get total doc count", "err", err)
88 }
89 docCount = int64(dc)
90
91 } else {
92 method = "db"
93 repos, err = db.GetReposPaginated(
94 s.db,
95 page,
96 )
97 if err != nil {
98 l.Error("failed to get repos", "err", err)
99 s.pages.Error500(w)
100 return
101 }
102
103 rc, err := db.CountRepos(
104 s.db,
105 )
106 if err != nil {
107 l.Error("failed to count repos", "err", err)
108 s.pages.Error500(w)
109 return
110 }
111
112 resultCount = int(rc)
113 docCount = int64(rc)
114 }
115
116 l.Info(
117 "RepoSearch",
118 "method", method,
119 "resultCount", resultCount,
120 "docCount", docCount,
121 "time", searchDuration,
122 "filterQuery", query.String(),
123 "sortParam", sortParam,
124 )
125
126 if !s.config.Core.Dev && query.String() != "" {
127 distinctId := s.oauth.GetDid(r)
128 if distinctId == "" {
129 distinctId = "anonymous"
130 }
131 go func() {
132 if err := s.posthog.Enqueue(posthog.Capture{
133 DistinctId: distinctId,
134 Event: "search",
135 Properties: posthog.Properties{
136 "query": query.String(),
137 "result_count": resultCount,
138 "method": method,
139 },
140 }); err != nil {
141 l.Error("failed to enqueue posthog event", "err", err)
142 }
143 }()
144 }
145
146 err = s.pages.SearchRepos(w, pages.SearchReposParams{
147 LoggedInUser: s.oauth.GetMultiAccountUser(r),
148 Repos: repos,
149 Page: page,
150 FilterQuery: query.String(),
151 SortParam: sortParam,
152 TimeTaken: searchDuration,
153 ResultCount: resultCount,
154 DocCount: docCount,
155 })
156 if err != nil {
157 l.Error("failed to render page", "err", err)
158 }
159}
160
161// parseSortParam parses sort parameter like "stars-desc" or "created-asc"
162func parseSortParam(sortParam string) (string, bool) {
163 defaultSort := func() (string, bool) { return "relevance", true }
164
165 // no sort param supplied, just go default
166 if sortParam == "" {
167 return defaultSort()
168 }
169
170 parts := strings.Split(sortParam, "-")
171 if len(parts) != 2 {
172 return defaultSort()
173 }
174
175 field := parts[0]
176 desc := parts[1] == "desc"
177
178 // validate field
179 validFields := map[string]bool{
180 "relevance": true,
181 "created": true,
182 "stars": true,
183 "issues": true,
184 "pulls": true,
185 }
186
187 // invalid fields, just go default
188 if !validFields[field] {
189 return defaultSort()
190 }
191
192 return field, desc
193}