# Issue and pull request search

GitHub's issue and pull request search over REST and GraphQL, one qualifier grammar over what the scenario holds.

`GET /search/issues` and the GraphQL `search` query both search the issues and pull requests the scenario holds, through one grammar. `gh pr list --search` and `gh issue list --search` send the GraphQL one. Search a repository's pull requests by state, draft flag, label, author or date:

```bash
H='Authorization: Bearer ghp_anything'
curl -s -G "$RYSTIC_URL/search/issues" -H "$H" --data-urlencode 'q=repo:tb-owner/tb-repo is:pr is:open' | jq -c '{total_count, numbers: [.items[].number]}'
# -> {"total_count":4,"numbers":[44,43,42,41]}
curl -s -G "$RYSTIC_URL/search/issues" -H "$H" --data-urlencode 'q=repo:tb-owner/tb-repo is:pr draft:true' | jq -c '{total_count, numbers: [.items[].number]}'
# -> {"total_count":1,"numbers":[43]}
curl -s -G "$RYSTIC_URL/search/issues" -H "$H" --data-urlencode 'q=repo:tb-owner/tb-repo is:pr label:bug' | jq -c '{total_count, numbers: [.items[].number]}'
# -> {"total_count":1,"numbers":[41]}
curl -s -G "$RYSTIC_URL/search/issues" -H "$H" --data-urlencode 'q=repo:tb-owner/tb-repo retry created:2026-01-01..2026-01-31' | jq -c '{total_count, numbers: [.items[].number]}'
# -> {"total_count":1,"numbers":[42]}
curl -s -G "$RYSTIC_URL/search/issues" -H "$H" --data-urlencode 'q=repo:tb-owner/tb-repo is:pr created:garbage' -o /dev/null -w '%{http_code}\n'
# -> 422
curl -s -G "$RYSTIC_URL/search/issues" -H "$H" --data-urlencode 'q=repo:tb-owner/tb-repo is:pr reactions:>1' -D - -o /dev/null | grep -i x-rystic-marker
# -> X-Rystic-Marker: unmodeled_guard; iv=iv_op_get_search_issues; guard=g_qualifiers_modelled
```

### Qualifiers

`repo:`, `user:`, `org:` and `owner:` scope the query, and a query naming none matches nothing. The grammar reads these other qualifiers:

| Kind | Qualifiers |
|---|---|
| Family and state | `is:pr`, `is:pull-request`, `is:issue`, `type:`, `state:`, `reason:completed`, `reason:"not planned"`, `draft:true`, `draft:false`, `archived:`, `language:` and `is:` with `open`, `closed`, `merged`, `unmerged`, `draft`, `locked`, `unlocked`, `public` or `private` |
| People | `author:`, `assignee:`, `review-requested:`, `reviewed-by:`, `commenter:`, `mentions:`, `involves:`, each with `@me` as the caller |
| Content | `label:`, `milestone:`, `no:label`, `no:assignee`, `no:milestone`, `head:` and `base:` (a branch-name prefix), `comments:` (a count or range), `review:none`, `review:approved`, `review:changes_requested` |
| Dates | `created:`, `updated:`, `closed:`, `merged:` |

A date is `YYYY-MM-DD`, covering its whole UTC day, or an ISO 8601 timestamp. `a..b` is inclusive, and `*` leaves an end open. `>=`, `>`, `<=` and `<` bound one side.

### Combining

Terms AND, and `-` negates any of them. `label:a,b` ORs its labels. The advanced syntax is what `gh` sends as `type: ISSUE_ADVANCED`, and what REST takes with `advanced_search=true`. The two syntaxes read a query differently:

| Query | Default syntax | Advanced syntax |
|---|---|---|
| a repeated `author:` or other person qualifier | ORs | ANDs |
| a repeated `label:` | ANDs | ANDs |
| `AND` and `OR` | join only free text | operators |
| parentheses | a bare one matches nothing | operators |

Under the advanced syntax, `-( … )` matches nothing.

### Free text

Words match title, body and comments, case-insensitively, or the fields `in:title`, `in:body` and `in:comments` name. A word in the record also matches the query word one suffix shorter, so `fix` finds "fixes" and "fixed", but `fixes` does not find "fix" and there is no prefix match. A quoted phrase matches its words in order. A key the grammar does not know, like `foo:bar`, is text.

**Differs from GitHub:** free-text matches come within a few pull requests of GitHub's over the imported scenario.

### Refusals

Over REST, an unrecognized date, a login or repository the scenario does not hold or operators with no text return GitHub's `422`. Over GraphQL each matches nothing.

**Differs from GitHub:** a qualifier GitHub accepts that the simulator cannot evaluate is refused rather than ignored. Such qualifiers include `review:required`, `reactions:`, `project:` and `linked:`.

| Over | The refusal |
|---|---|
| REST | `501`, with `X-Rystic-Marker: unmodeled_guard; iv=iv_op_get_search_issues; guard=g_qualifiers_modelled` and GitHub's error envelope (`message`, `documentation_url`, `status`). The body does not name the qualifier. |
| GraphQL | an error naming `g_qualifiers_modelled`, not the qualifier |

### Results

`total_count` and `issueCount` count every match. Over REST, `per_page` (at most 100) and `page` window the items, with GitHub's `Link` header. `sort` and `order`, or an in-query `sort:created-asc`, order them, newest first by default. A pull request's item carries `draft` and its own `node_id`.

**Differs from GitHub:** a write is searchable at once. GitHub's index lags by seconds.

**Differs from GitHub:** `closed:` and `comments:` follow the record. GitHub's index disagrees with its own records: `closed:` misses pull requests closed in bulk, and `comments:` undercounts.

### Other search types

**Differs from GitHub:** code and commit search (`GET /search/code` and `GET /search/commits`) are not served, and will not be. REST returns GitHub's `404 Not Found`, and GraphQL `undefinedField`.

**Differs from GitHub:** GraphQL `search` with a `type` other than `ISSUE` or `ISSUE_ADVANCED` is refused with an error naming `g_search_type_served`. That covers `ISSUE_SEMANTIC`, `ISSUE_HYBRID`, `DISCUSSION`, `REPOSITORY` and `USER`.

Package and wiki search are not served.

---

Source: https://www.rystic.ai/docs/simulators/github-pulls/features/search · Markdown: https://www.rystic.ai/docs/simulators/github-pulls/features/search.md
