Go · 1946 bytes Raw Blame History
1 // SPDX-License-Identifier: AGPL-3.0-or-later
2
3 package search
4
5 import (
6 "context"
7 "fmt"
8 )
9
10 // SearchUsers runs a user search. No visibility predicate — user
11 // profiles are public by definition; suspended/deleted accounts are
12 // excluded so they don't taint search results (matches the spec
13 // pitfall: "search on suspended user content").
14 func SearchUsers(ctx context.Context, deps Deps, q ParsedQuery, limit, offset int) ([]UserResult, int64, error) {
15 if !q.HasContent() {
16 return nil, 0, ErrEmptyQuery
17 }
18 tsText, tsCtor, hasFTS := tsQueryBindAndCtor(q)
19 if !hasFTS {
20 // Operators don't apply to user search; with no FTS payload
21 // there's nothing to match.
22 return nil, 0, nil
23 }
24
25 args := []any{tsText, limit, offset}
26 queryStr := fmt.Sprintf(`
27 SELECT u.id, u.username::text, u.display_name, coalesce(u.bio, ''),
28 ts_rank_cd(s.tsv, %[1]s('shithub_search', $1)) AS rank
29 FROM users_search s
30 JOIN users u ON u.id = s.user_id
31 WHERE s.tsv @@ %[1]s('shithub_search', $1)
32 AND u.suspended_at IS NULL
33 AND u.deleted_at IS NULL
34 ORDER BY rank DESC, u.username
35 LIMIT $2 OFFSET $3
36 `, tsCtor)
37 rows, err := deps.Pool.Query(ctx, queryStr, args...)
38 if err != nil {
39 return nil, 0, fmt.Errorf("search users: %w", err)
40 }
41 defer rows.Close()
42 out := make([]UserResult, 0, limit)
43 for rows.Next() {
44 var r UserResult
45 if err := rows.Scan(&r.ID, &r.Username, &r.DisplayName, &r.Bio, &r.Rank); err != nil {
46 return nil, 0, err
47 }
48 out = append(out, r)
49 }
50 if err := rows.Err(); err != nil {
51 return nil, 0, err
52 }
53
54 countQuery := fmt.Sprintf(`
55 SELECT count(*) FROM users_search s
56 JOIN users u ON u.id = s.user_id
57 WHERE s.tsv @@ %[1]s('shithub_search', $1)
58 AND u.suspended_at IS NULL
59 AND u.deleted_at IS NULL
60 `, tsCtor)
61 var total int64
62 if err := deps.Pool.QueryRow(ctx, countQuery, tsText).Scan(&total); err != nil {
63 return nil, 0, fmt.Errorf("count users: %w", err)
64 }
65 return out, total, nil
66 }
67