| 1 | // SPDX-License-Identifier: AGPL-3.0-or-later |
| 2 | |
| 3 | // Package search owns S28's search surface. Postgres FTS |
| 4 | // (`tsvector` + `pg_trgm`) backs everything; no external search |
| 5 | // engine. Visibility scoping flows through `policy.VisibilityPredicate` |
| 6 | // so every query is gated by the same rule the rest of the runtime |
| 7 | // uses. |
| 8 | // |
| 9 | // Entry points are: |
| 10 | // |
| 11 | // SearchRepos / SearchIssues / SearchUsers / SearchCode — per-type |
| 12 | // queries returning slices of result rows + the total count. |
| 13 | // ParseQuery — splits a user query string into the FTS query plus |
| 14 | // operator filters (repo:, is:, author:, state:). |
| 15 | package search |
| 16 | |
| 17 | import ( |
| 18 | "errors" |
| 19 | "log/slog" |
| 20 | "time" |
| 21 | |
| 22 | "github.com/jackc/pgx/v5/pgxpool" |
| 23 | ) |
| 24 | |
| 25 | // Deps wires the package against the rest of the runtime. |
| 26 | type Deps struct { |
| 27 | Pool *pgxpool.Pool |
| 28 | Logger *slog.Logger |
| 29 | } |
| 30 | |
| 31 | // Errors surfaced to handlers. |
| 32 | var ( |
| 33 | ErrEmptyQuery = errors.New("search: query is empty") |
| 34 | ) |
| 35 | |
| 36 | // PageSize is the per-type result count for the full results page. |
| 37 | // Quick-dropdown uses a smaller cap (see QuickResultsLimit). |
| 38 | const PageSize = 20 |
| 39 | |
| 40 | // QuickResultsLimit is the per-type cap for the top-bar quick |
| 41 | // dropdown. Same shape as GitHub's: tight for keystroke speed. |
| 42 | const QuickResultsLimit = 5 |
| 43 | |
| 44 | // MaxQueryBytes caps incoming query strings. Tighter than the |
| 45 | // markdown 1 MiB ceiling because no legitimate search ever needs |
| 46 | // even 1 KiB. |
| 47 | const MaxQueryBytes = 256 |
| 48 | |
| 49 | // RepoResult is one row from SearchRepos. |
| 50 | type RepoResult struct { |
| 51 | ID int64 |
| 52 | OwnerUsername string |
| 53 | Name string |
| 54 | Description string |
| 55 | Visibility string |
| 56 | StarCount int64 |
| 57 | UpdatedAt time.Time |
| 58 | Rank float64 |
| 59 | } |
| 60 | |
| 61 | // IssueResult is one row from SearchIssues. |
| 62 | type IssueResult struct { |
| 63 | ID int64 |
| 64 | RepoID int64 |
| 65 | OwnerUsername string |
| 66 | RepoName string |
| 67 | Number int64 |
| 68 | Title string |
| 69 | State string |
| 70 | Kind string // "issue" | "pr" |
| 71 | AuthorName string |
| 72 | UpdatedAt time.Time |
| 73 | Rank float64 |
| 74 | } |
| 75 | |
| 76 | // UserResult is one row from SearchUsers. |
| 77 | type UserResult struct { |
| 78 | ID int64 |
| 79 | Username string |
| 80 | DisplayName string |
| 81 | Bio string |
| 82 | Rank float64 |
| 83 | } |
| 84 | |
| 85 | // CodeResult is one row from SearchCode. Either Path or Content |
| 86 | // (or both) is populated depending on which subquery hit. |
| 87 | type CodeResult struct { |
| 88 | RepoID int64 |
| 89 | OwnerUsername string |
| 90 | RepoName string |
| 91 | RefName string |
| 92 | Path string |
| 93 | // PreviewLine is a single line of content extracted near the |
| 94 | // match, when content matched. Empty for path-only hits. |
| 95 | PreviewLine string |
| 96 | Rank float64 |
| 97 | } |
| 98 |