Monorepo for Tangled tangled.org
766
fork

Configure Feed

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

appview/state: add /search handler

Signed-off-by: oppiliappan <me@oppi.li>

authored by

oppiliappan and committed by tangled.org c21b3e2c 7277bfae

+181
+4
appview/models/search.go
··· 67 67 NegatedPhrases []string 68 68 NegatedTopics []string 69 69 70 + // Sort configuration 71 + SortField string 72 + SortDesc bool 73 + 70 74 Page pagination.Page 71 75 } 72 76
+15
appview/pages/pages.go
··· 1590 1590 return p.execute("strings/string", w, params) 1591 1591 } 1592 1592 1593 + type SearchReposParams struct { 1594 + LoggedInUser *oauth.MultiAccountUser 1595 + Repos []models.Repo 1596 + Page pagination.Page 1597 + ResultCount int 1598 + FilterQuery string 1599 + SortParam string 1600 + TimeTaken time.Duration 1601 + DocCount int64 1602 + } 1603 + 1604 + func (p *Pages) SearchRepos(w io.Writer, params SearchReposParams) error { 1605 + return p.execute("search/search", w, params) 1606 + } 1607 + 1593 1608 func (p *Pages) Home(w io.Writer, params TimelineParams) error { 1594 1609 return p.execute("timeline/home", w, params) 1595 1610 }
+2
appview/state/router.go
··· 156 156 r.Post("/login", s.Login) 157 157 r.Post("/logout", s.Logout) 158 158 159 + r.With(middleware.Paginate).Get("/search", s.Search) 160 + 159 161 r.Post("/account/switch", s.SwitchAccount) 160 162 r.With(middleware.AuthMiddleware(s.oauth)).Delete("/account/{did}", s.RemoveAccount) 161 163
+160
appview/state/search.go
··· 1 + package state 2 + 3 + import ( 4 + "net/http" 5 + "strings" 6 + "time" 7 + 8 + "tangled.org/core/appview/db" 9 + "tangled.org/core/appview/models" 10 + "tangled.org/core/appview/pages" 11 + "tangled.org/core/appview/pagination" 12 + "tangled.org/core/appview/searchquery" 13 + "tangled.org/core/orm" 14 + ) 15 + 16 + func (s *State) Search(w http.ResponseWriter, r *http.Request) { 17 + l := s.logger.With("handler", "Search") 18 + 19 + params := r.URL.Query() 20 + page := pagination.FromContext(r.Context()) 21 + 22 + query := searchquery.Parse(params.Get("q")) 23 + 24 + sortParam := params.Get("sort") 25 + sortField, sortDesc := parseSortParam(sortParam) 26 + 27 + var language string 28 + if lang := query.Get("language"); lang != nil { 29 + language = *lang 30 + } 31 + 32 + tf := searchquery.ExtractTextFilters(query) 33 + 34 + searchOpts := models.RepoSearchOptions{ 35 + Keywords: tf.Keywords, 36 + Phrases: tf.Phrases, 37 + NegatedKeywords: tf.NegatedKeywords, 38 + NegatedPhrases: tf.NegatedPhrases, 39 + Language: language, 40 + SortField: sortField, 41 + SortDesc: sortDesc, 42 + Page: page, 43 + } 44 + 45 + var repos []models.Repo 46 + var err error 47 + var resultCount int 48 + var searchDuration time.Duration 49 + var docCount int64 50 + 51 + if searchOpts.HasSearchFilters() || sortField != "" { 52 + res, err := s.indexer.Repos.Search(r.Context(), searchOpts) 53 + if err != nil { 54 + l.Error("failed to search repos", "err", err) 55 + s.pages.Error500(w) 56 + return 57 + } 58 + 59 + searchDuration = res.Duration 60 + 61 + if len(res.Hits) > 0 { 62 + repos, err = db.GetRepos(s.db, orm.FilterIn("id", res.Hits)) 63 + if err != nil { 64 + l.Error("failed to get repos by IDs", "err", err) 65 + s.pages.Error500(w) 66 + return 67 + } 68 + 69 + // sort repos to match search result order (by relevance) 70 + repoMap := make(map[int64]models.Repo, len(repos)) 71 + for _, repo := range repos { 72 + repoMap[repo.Id] = repo 73 + } 74 + repos = make([]models.Repo, 0, len(res.Hits)) 75 + for _, id := range res.Hits { 76 + if repo, ok := repoMap[id]; ok { 77 + repos = append(repos, repo) 78 + } 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 + repos, err = db.GetReposPaginated( 91 + s.db, 92 + page, 93 + ) 94 + if err != nil { 95 + l.Error("failed to get repos", "err", err) 96 + s.pages.Error500(w) 97 + return 98 + } 99 + 100 + rc, err := db.CountRepos( 101 + s.db, 102 + ) 103 + if err != nil { 104 + l.Error("failed to count repos", "err", err) 105 + s.pages.Error500(w) 106 + return 107 + } 108 + 109 + resultCount = int(rc) 110 + docCount = int64(rc) 111 + } 112 + 113 + err = s.pages.SearchRepos(w, pages.SearchReposParams{ 114 + LoggedInUser: s.oauth.GetMultiAccountUser(r), 115 + Repos: repos, 116 + Page: page, 117 + FilterQuery: query.String(), 118 + SortParam: sortParam, 119 + TimeTaken: searchDuration, 120 + ResultCount: resultCount, 121 + DocCount: docCount, 122 + }) 123 + if err != nil { 124 + l.Error("failed to render page", "err", err) 125 + } 126 + } 127 + 128 + // parseSortParam parses sort parameter like "stars-desc" or "created-asc" 129 + func parseSortParam(sortParam string) (string, bool) { 130 + defaultSort := func() (string, bool) { return "relevance", true } 131 + 132 + // no sort param supplied, just go default 133 + if sortParam == "" { 134 + return defaultSort() 135 + } 136 + 137 + parts := strings.Split(sortParam, "-") 138 + if len(parts) != 2 { 139 + return defaultSort() 140 + } 141 + 142 + field := parts[0] 143 + desc := parts[1] == "desc" 144 + 145 + // validate field 146 + validFields := map[string]bool{ 147 + "relevance": true, 148 + "created": true, 149 + "stars": true, 150 + "issues": true, 151 + "pulls": true, 152 + } 153 + 154 + // invalid fields, just go default 155 + if !validFields[field] { 156 + return defaultSort() 157 + } 158 + 159 + return field, desc 160 + }