Monorepo for Tangled tangled.org

appview: add issue search endpoint #496

merged opened by boltless.me targeting master from boltless.me/core: feat/search
Labels
enhancement
assignee

None yet.

Participants 2
AT URI
at://did:plc:xasnlahkri4ewmbuzly2rlc5/sh.tangled.repo.pull/3lwecnvoz5422
+123 -118
Interdiff #2 #3
+91 -81
appview/db/issues.go
··· 3 3 import ( 4 4 "database/sql" 5 5 "fmt" 6 - "strconv" 7 6 "strings" 8 7 "time" 9 8 10 9 "github.com/bluesky-social/indigo/atproto/syntax" 11 10 "tangled.sh/tangled.sh/core/api/tangled" 12 - "tangled.sh/tangled.sh/core/appview/models" 13 11 "tangled.sh/tangled.sh/core/appview/pagination" 14 12 ) 15 13 ··· 164 162 return issues, nil 165 163 } 166 164 167 - // GetIssueIDs gets list of all existing issue's IDs 168 - func GetIssueIDs(e Execer, opts models.IssueSearchOptions) ([]int64, error) { 169 - var ids []int64 170 - 171 - var filters []filter 165 + func GetIssuesPaginated(e Execer, repoAt syntax.ATURI, isOpen bool, page pagination.Page) ([]Issue, error) { 166 + var issues []Issue 172 167 openValue := 0 173 - if opts.IsOpen { 168 + if isOpen { 174 169 openValue = 1 175 170 } 176 - filters = append(filters, FilterEq("open", openValue)) 177 - if opts.RepoAt != "" { 178 - filters = append(filters, FilterEq("repo_at", opts.RepoAt)) 179 - } 180 171 181 - var conditions []string 182 - var args []any 183 - 184 - for _, filter := range filters { 185 - conditions = append(conditions, filter.Condition()) 186 - args = append(args, filter.Arg()...) 187 - } 188 - 189 - whereClause := "" 190 - if conditions != nil { 191 - whereClause = " where " + strings.Join(conditions, " and ") 192 - } 193 - query := fmt.Sprintf( 172 + rows, err := e.Query( 194 173 ` 174 + with numbered_issue as ( 175 + select 176 + i.id, 177 + i.owner_did, 178 + i.rkey, 179 + i.issue_id, 180 + i.created, 181 + i.title, 182 + i.body, 183 + i.open, 184 + count(c.id) as comment_count, 185 + row_number() over (order by i.created desc) as row_num 186 + from 187 + issues i 188 + left join 189 + comments c on i.repo_at = c.repo_at and i.issue_id = c.issue_id 190 + where 191 + i.repo_at = ? and i.open = ? 192 + group by 193 + i.id, i.owner_did, i.issue_id, i.created, i.title, i.body, i.open 194 + ) 195 195 select 196 - id 196 + id, 197 + owner_did, 198 + rkey, 199 + issue_id, 200 + created, 201 + title, 202 + body, 203 + open, 204 + comment_count 197 205 from 198 - issues 199 - %s 200 - limit ? offset ?`, 201 - whereClause, 202 - ) 203 - args = append(args, opts.Page.Limit, opts.Page.Offset) 204 - rows, err := e.Query(query, args...) 206 + numbered_issue 207 + where 208 + row_num between ? and ?`, 209 + repoAt, openValue, page.Offset+1, page.Offset+page.Limit) 205 210 if err != nil { 206 211 return nil, err 207 212 } 208 - defer rows.Close() 209 213 210 - for rows.Next() { 211 - var id int64 212 - err := rows.Scan(&id) 213 - if err != nil { 214 - return nil, err 215 - } 216 214 217 - ids = append(ids, id) 218 - } 219 215 220 - return ids, nil 221 - } 222 216 223 - // GetIssuesByIDs gets list of issues from given IDs 224 - func GetIssuesByIDs(e Execer, ids []int64) ([]Issue, error) { 225 - var issues []Issue 226 217 227 - // HACK: would be better to create "?,?,?,..." or use something like sqlx 228 - idStrings := make([]string, len(ids)) 229 - for i, id := range ids { 230 - idStrings[i] = strconv.FormatInt(id, 10) 231 - } 232 - idList := strings.Join(idStrings, ",") 233 - query := fmt.Sprintf( 234 - ` 235 - select 236 - i.id, 237 - i.owner_did, 238 - i.rkey, 239 - i.issue_id, 240 - i.created, 241 - i.title, 242 - i.body, 243 - i.open, 244 - count(c.id) as comment_count 245 - from 246 - issues i 247 - left join 248 - comments c on i.repo_at = c.repo_at and i.issue_id = c.issue_id 249 - where 250 - i.id in (%s) 251 - group by 252 - i.id 253 - order by i.created desc`, 254 - idList, 255 - ) 256 - rows, err := e.Query(query) 257 - if err != nil { 258 - return nil, err 259 - } 260 218 261 219 262 220 ··· 306 264 307 265 308 266 267 + return &issue, nil 268 + } 309 269 270 + // GetIssueIDs gets list of all existing issue's IDs 271 + func GetIssueIDs(e Execer, opts models.IssueSearchOptions) ([]int64, error) { 272 + var ids []int64 310 273 274 + var filters []filter 275 + openValue := 0 276 + if opts.IsOpen { 277 + openValue = 1 278 + } 279 + filters = append(filters, FilterEq("open", openValue)) 280 + if opts.RepoAt != "" { 281 + filters = append(filters, FilterEq("repo_at", opts.RepoAt)) 282 + } 311 283 284 + var conditions []string 285 + var args []any 312 286 287 + for _, filter := range filters { 288 + conditions = append(conditions, filter.Condition()) 289 + args = append(args, filter.Arg()...) 290 + } 313 291 314 - return &issue, nil 292 + whereClause := "" 293 + if conditions != nil { 294 + whereClause = " where " + strings.Join(conditions, " and ") 295 + } 296 + query := fmt.Sprintf( 297 + ` 298 + select 299 + id 300 + from 301 + issues 302 + %s 303 + limit ? offset ?`, 304 + whereClause, 305 + ) 306 + args = append(args, opts.Page.Limit, opts.Page.Offset) 307 + rows, err := e.Query(query, args...) 308 + if err != nil { 309 + return nil, err 310 + } 311 + defer rows.Close() 312 + 313 + for rows.Next() { 314 + var id int64 315 + err := rows.Scan(&id) 316 + if err != nil { 317 + return nil, err 318 + } 319 + 320 + ids = append(ids, id) 321 + } 322 + 323 + return ids, nil 315 324 } 325 + 316 326 317 327 func AddIssueComment(e Execer, c models.IssueComment) (int64, error) { 318 328 result, err := e.Exec(
+28 -33
appview/issues/issues.go
··· 19 19 "tangled.sh/tangled.sh/core/appview/config" 20 20 "tangled.sh/tangled.sh/core/appview/db" 21 21 issues_indexer "tangled.sh/tangled.sh/core/appview/indexer/issues" 22 - "tangled.sh/tangled.sh/core/appview/models" 23 22 "tangled.sh/tangled.sh/core/appview/notify" 24 23 "tangled.sh/tangled.sh/core/appview/oauth" 25 24 "tangled.sh/tangled.sh/core/appview/pages" ··· 609 608 return 610 609 } 611 610 612 - keyword := params.Get("q") 613 - 614 - var ids []int64 615 - searchOpts := models.IssueSearchOptions{ 616 - Keyword: keyword, 617 - RepoAt: f.RepoAt().String(), 618 - IsOpen: isOpen, 619 - Page: page, 620 - } 621 - if keyword != "" { 622 - res, err := rp.indexer.Search(r.Context(), searchOpts) 623 - if err != nil { 624 - log.Println("failed to search for issues", err) 625 - return 626 - } 627 - log.Println("searched issues:", res.Hits) 628 - ids = res.Hits 629 - } else { 630 - ids, err = db.GetIssueIDs(rp.db, searchOpts) 631 - if err != nil { 632 - log.Println("failed to search for issues", err) 633 - return 634 - } 635 - } 636 - 637 - issues, err := db.GetIssuesByIDs(rp.db, ids) 611 + issues, err := db.GetIssuesPaginated(rp.db, f.RepoAt(), isOpen, page) 638 612 if err != nil { 639 613 log.Println("failed to get issues", err) 640 614 rp.pages.Notice(w, "issues", "Failed to load issues. Try again later.") ··· 646 620 RepoInfo: f.RepoInfo(user), 647 621 Issues: issues, 648 622 FilteringByOpen: isOpen, 649 - FilterQuery: keyword, 650 623 Page: page, 651 624 }) 652 625 } ··· 814 787 return 815 788 } 816 789 817 - openVal := 0 818 - if isOpen { 819 - openVal = 1 790 + keyword := params.Get("q") 791 + 792 + var ids []int64 793 + searchOpts := models.IssueSearchOptions{ 794 + Keyword: keyword, 795 + RepoAt: f.RepoAt().String(), 796 + IsOpen: isOpen, 797 + Page: page, 820 798 } 799 + if keyword != "" { 800 + res, err := rp.indexer.Search(r.Context(), searchOpts) 801 + if err != nil { 802 + l.Error("failed to search for issues", "err", err) 803 + return 804 + } 805 + l.Debug("searched issues with indexer", "res.Hits", res.Hits) 806 + ids = res.Hits 807 + } else { 808 + ids, err = db.GetIssueIDs(rp.db, searchOpts) 809 + if err != nil { 810 + l.Error("failed to search for issues", "err", err) 811 + return 812 + } 813 + l.Debug("indexed all issues from the db", "ids", ids) 814 + } 815 + 821 816 issues, err := db.GetIssuesPaginated( 822 817 rp.db, 823 818 page, 824 - db.FilterEq("repo_at", f.RepoAt()), 825 - db.FilterEq("open", openVal), 819 + db.FilterIn("id", ids), 826 820 ) 827 821 if err != nil { 828 822 l.Error("failed to get issues", "err", err) ··· 852 846 Issues: issues, 853 847 LabelDefs: defs, 854 848 FilteringByOpen: isOpen, 849 + FilterQuery: keyword, 855 850 Page: page, 856 851 }) 857 852 }
+4 -4
appview/pages/templates/repo/issues/issues.html
··· 62 62 <a 63 63 class="btn flex items-center gap-2 no-underline hover:no-underline dark:text-white dark:hover:bg-gray-700" 64 64 hx-boost="true" 65 - href = "/{{ $.RepoInfo.FullName }}/issues?state={{ $currentState }}&offset={{ $prev.Offset }}&limit={{ $prev.Limit }}" 65 + href = "/{{ $.RepoInfo.FullName }}/issues?state={{ $currentState }}&q={{ .FilterQuery }}&offset={{ $prev.Offset }}&limit={{ $prev.Limit }}" 66 66 > 67 67 {{ i "chevron-left" "w-4 h-4" }} 68 68 previous ··· 76 76 <a 77 77 class="btn flex items-center gap-2 no-underline hover:no-underline dark:text-white dark:hover:bg-gray-700" 78 78 hx-boost="true" 79 - href = "/{{ $.RepoInfo.FullName }}/issues?state={{ $currentState }}&offset={{ $next.Offset }}&limit={{ $next.Limit }}" 79 + href = "/{{ $.RepoInfo.FullName }}/issues?state={{ $currentState }}&q={{ .FilterQuery }}&offset={{ $next.Offset }}&limit={{ $next.Limit }}" 80 80 > 81 81 next 82 82 {{ i "chevron-right" "w-4 h-4" }} ··· 107 107 <a 108 108 class="btn flex items-center gap-2 no-underline hover:no-underline dark:text-white dark:hover:bg-gray-700" 109 109 hx-boost="true" 110 - href = "/{{ $.RepoInfo.FullName }}/issues?state={{ $currentState }}&q={{ .FilterQuery }}&offset={{ $prev.Offset }}&limit={{ $prev.Limit }}" 110 + href = "/{{ $.RepoInfo.FullName }}/issues?state={{ $currentState }}&offset={{ $prev.Offset }}&limit={{ $prev.Limit }}" 111 111 > 112 112 {{ i "chevron-left" "w-4 h-4" }} 113 113 previous ··· 121 121 <a 122 122 class="btn flex items-center gap-2 no-underline hover:no-underline dark:text-white dark:hover:bg-gray-700" 123 123 hx-boost="true" 124 - href = "/{{ $.RepoInfo.FullName }}/issues?state={{ $currentState }}&q={{ .FilterQuery }}&offset={{ $next.Offset }}&limit={{ $next.Limit }}" 124 + href = "/{{ $.RepoInfo.FullName }}/issues?state={{ $currentState }}&offset={{ $next.Offset }}&limit={{ $next.Limit }}" 125 125 > 126 126 next 127 127 {{ i "chevron-right" "w-4 h-4" }}

History

14 rounds 5 comments
sign up or login to add to the discussion
1 commit
expand
appview: add issue search endpoint
expand 0 comments
pull request successfully merged
1 commit
expand
appview: add issue search endpoint
expand 0 comments
1 commit
expand
appview: add issue search endpoint
expand 0 comments
1 commit
expand
appview: add issue search endpoint
expand 0 comments
1 commit
expand
appview: add issue search endpoint
expand 0 comments
1 commit
expand
appview: add issue search endpoint
expand 0 comments
1 commit
expand
appview: add issue search endpoint
expand 0 comments
1 commit
expand
appview: add issue search endpoint
expand 0 comments
1 commit
expand
appview: add issue search endpoint
expand 3 comments

do we need the log here? i believe that log message should be changed atleast because indexing does not occur there.

I can change the wording a bit. The intention was to log the list of issue ids gathered from one of two methods (indexer/db)

1 commit
expand
appview: add issue search endpoint
expand 0 comments
1 commit
expand
appview: add issue search endpoint
expand 0 comments
1 commit
expand
appview: add issue search endpoint
expand 1 comment
1 commit
expand
appview: add issue search endpoint
expand 0 comments
1 commit
expand
appview: add issue search endpoint
expand 1 comment

Indexer mappings/tokenization rules have some room to be improved, but this should be enough as a MVP for now.