@@ -1,43 +1,129 @@ |
| 1 | 1 | # Search |
| 2 | 2 | |
| 3 | | -> **Planned.** Search over the API is not shipped yet. The web UI's |
| 4 | | -> `/search` is the only entry point today. |
| 3 | +Per-type search over shithub's Postgres FTS corpus. Every result |
| 4 | +set is filtered by the caller's visibility — anonymous callers |
| 5 | +see only public rows. |
| 5 | 6 | |
| 6 | | -## Planned routes |
| 7 | +## Response shape |
| 7 | 8 | |
| 8 | | -| Method | Path | Scope | |
| 9 | | -|--------|-----------------------------------|--------------------------------| |
| 10 | | -| GET | `/api/v1/search/code` | None (public corpus only without `repo:read`); `repo:read` to include private. | |
| 11 | | -| GET | `/api/v1/search/repositories` | Same scoping. | |
| 12 | | -| GET | `/api/v1/search/issues` | Same scoping. | |
| 13 | | -| GET | `/api/v1/search/users` | None. | |
| 9 | +Every endpoint returns the canonical gh-compatible envelope: |
| 14 | 10 | |
| 15 | | -Query parameters: |
| 11 | +```json |
| 12 | +{ |
| 13 | + "total_count": 142, |
| 14 | + "incomplete_results": false, |
| 15 | + "items": [ |
| 16 | + { /* type-dependent record */ } |
| 17 | + ] |
| 18 | +} |
| 19 | +``` |
| 20 | + |
| 21 | +`incomplete_results` is always `false` in v1 (the FTS pipeline |
| 22 | +runs to completion before responding). The field is preserved on |
| 23 | +the wire so future ranker timeouts can flip it. |
| 24 | + |
| 25 | +`total_count` is the unbounded result count. |
| 16 | 26 | |
| 17 | | -- `q=` — search query, with the same operators as |
| 18 | | - [Search (user docs)](../user/search.md). |
| 19 | | -- `sort=` — `created`, `updated`, `stars`, `relevance` (default). |
| 20 | | -- `order=` — `asc`, `desc`. |
| 21 | | -- `per_page=`, `cursor=`. |
| 27 | +## Query syntax |
| 22 | 28 | |
| 23 | | -Response shape (proposed): |
| 29 | +The `q=` parameter is parsed by `internal/search/query_parse` — |
| 30 | +free-text terms plus the following operators: |
| 31 | + |
| 32 | +- `repo:owner/name` — restrict to one repo |
| 33 | +- `is:open` / `is:closed` (or `state:open` / `state:closed`) |
| 34 | +- `author:username` |
| 35 | +- `"quoted phrase"` — phrase search (one phrase per query) |
| 36 | + |
| 37 | +Unknown prefixes (e.g. `language:Go`) fall through as free text |
| 38 | +so future operator additions don't break older queries. |
| 39 | + |
| 40 | +## Search repositories |
| 41 | + |
| 42 | +``` |
| 43 | +GET /api/v1/search/repositories?q=...&page=N&per_page=M |
| 44 | +``` |
| 45 | + |
| 46 | +Scope: `repo:read` for authenticated callers; anonymous callers |
| 47 | +fall through to the public-only filter. |
| 24 | 48 | |
| 25 | 49 | ```json |
| 26 | 50 | { |
| 27 | | - "total_count": 142, |
| 51 | + "total_count": 1, |
| 52 | + "incomplete_results": false, |
| 28 | 53 | "items": [ |
| 29 | | - { /* type-dependent record */ } |
| 54 | + { |
| 55 | + "id": 12, |
| 56 | + "name": "demo", |
| 57 | + "full_name": "alice/demo", |
| 58 | + "owner_login": "alice", |
| 59 | + "description": "demo repo", |
| 60 | + "visibility": "public", |
| 61 | + "private": false, |
| 62 | + "star_count": 0, |
| 63 | + "updated_at": "2026-05-12T05:00:00Z", |
| 64 | + "score": 0.42 |
| 65 | + } |
| 30 | 66 | ] |
| 31 | 67 | } |
| 32 | 68 | ``` |
| 33 | 69 | |
| 34 | | -The total is **counted to a cap** (10000) — for larger result |
| 35 | | -sets the API returns "10000+" semantics rather than counting |
| 36 | | -exactly. Consumers that need totals should narrow the query. |
| 70 | +## Search issues |
| 71 | + |
| 72 | +``` |
| 73 | +GET /api/v1/search/issues?q=...&type=issue|pr |
| 74 | +``` |
| 75 | + |
| 76 | +Scope: `repo:read`. `type=issue` or `type=pr` narrows to one |
| 77 | +kind; omit to return both (issues share their table with PRs). |
| 78 | + |
| 79 | +```json |
| 80 | +{ |
| 81 | + "id": 17, |
| 82 | + "number": 1, |
| 83 | + "repo_id": 12, |
| 84 | + "repo": "alice/demo", |
| 85 | + "title": "needle in haystack", |
| 86 | + "state": "open", |
| 87 | + "kind": "issue", |
| 88 | + "author_name": "alice", |
| 89 | + "updated_at": "2026-05-12T05:00:00Z", |
| 90 | + "score": 0.81 |
| 91 | +} |
| 92 | +``` |
| 93 | + |
| 94 | +## Search code |
| 95 | + |
| 96 | +``` |
| 97 | +GET /api/v1/search/code?q=... |
| 98 | +``` |
| 99 | + |
| 100 | +Scope: `repo:read`. Matches against the path-and-content index |
| 101 | +populated by the push pipeline. `preview_line` carries a single |
| 102 | +line of context for content hits; path-only hits omit it. |
| 103 | + |
| 104 | +```json |
| 105 | +{ |
| 106 | + "repo_id": 12, |
| 107 | + "repo": "alice/demo", |
| 108 | + "ref": "trunk", |
| 109 | + "path": "internal/foo/foo.go", |
| 110 | + "preview_line": "func Foo() error { return errors.New(\"foo\") }", |
| 111 | + "score": 0.55 |
| 112 | +} |
| 113 | +``` |
| 114 | + |
| 115 | +## Pagination |
| 116 | + |
| 117 | +`?per_page=` (≤100, default 30) and `?page=` are supported, with |
| 118 | +the standard `Link:` header (see [overview](overview.md)). The |
| 119 | +`total_count` field is independent of the page slice — it counts |
| 120 | +all matching rows regardless of pagination. |
| 37 | 121 | |
| 38 | | -## Visibility |
| 122 | +## Not yet shipped |
| 39 | 123 | |
| 40 | | -Search results are filtered by the requesting PAT's user's |
| 41 | | -visibility, identical to the web UI's behavior. A token cannot |
| 42 | | -see a result it would not see in a browser logged in as the |
| 43 | | -same user. |
| 124 | +- `GET /api/v1/search/commits` — commit message search; requires a |
| 125 | + per-repo commit FTS index that doesn't exist yet. |
| 126 | +- `GET /api/v1/search/users` — backing query exists; REST surface |
| 127 | + pending; will land alongside §7 orgs/users follow-up. |
| 128 | +- `sort=` / `order=` — every endpoint currently sorts by FTS rank; |
| 129 | + exposing alternative sorts (created, updated, stars) is queued. |