# Code search (/v1/search)

Regex code search across every indexed contributed project and Drupal core,
through Zoekt.

## Request

### GET /v1/search

| Name | In | Type | What it does |
|---|---|---|---|
| q | query | string | Required. The query, a regex by default. Without it, the response is this page. |
| num | query | int | Files to return. |
| context | query | int | Lines of context around each match, as `grep -C`. |
| maxmatches | query | int | Stop after this many matches in total. |
| chunks | query | bool | `false` returns the older line-match shape instead of chunks. |

Example: `/v1/search?q=hook_help&num=10&context=3`

### GET /v1/search/code

The same handler under an explicit path, with the same parameters and the
same answers. Without `q` it also answers this page.

Example: `/v1/search/code?q=hook_help&num=10`

### GET /v1/search/repo

Every indexed repository with its metadata.

| Name | In | Type | What it does |
|---|---|---|---|
| q | query | string | `r:^(webform\|token)$` narrows to some repositories. Every repository when absent. |

Example: `/v1/search/repo?q=r:^(webform|token|pathauto)$`

## Query syntax

### Text

| Pattern | Meaning |
|---|---|
| `foo bar` | Files with both `foo` and `bar`. |
| `"foo bar"` | Files with the exact phrase. |
| `foo OR bar` | Files with either. |
| `foo -bar` | Files with `foo` and without `bar`. |

### Filters

| Filter | Example | Meaning |
|---|---|---|
| `r:` or `repo:` | `r:webform` | Repositories matching the regex. |
| | `r:^(webform\|token)$` | Several repositories in one call. |
| `f:` or `file:` | `f:\.module$` | File paths matching the regex. |
| `lang:` | `lang:php` | One language. `.module`, `.install`, `.theme`, `.engine`, `.profile` and `.inc` count as PHP. |
| `b:` or `branch:` | `b:main` | One branch. |
| `case:yes` | `case:yes Foo` | Case-sensitive. |
| `sym:` | `sym:className` | Symbol definitions. |

### Regex

Queries are regex by default. Escape `.`, `(`, `)`, `{` and the like with
`\`. The index runs RE2: no lookaround, no backreferences.

| Query | Matches |
|---|---|
| `hook_\w+_alter` | `hook_form_alter`, `hook_node_alter` and the like. |
| `function\s+mymodule_` | Function declarations in `mymodule`. |
| `once\(` | The literal `once(`. |
| `\$form_state` | The literal `$form_state`. |

### Several repositories in one call

A repository filter takes a regex, so `r:^(repo1|repo2|repo3)$` covers three
repositories in one query. The same filter works on `/v1/search/repo`.

`/v1/search?q=hook_help+r:^(webform|token|pathauto)$&num=10`

## Response

### /v1/search, chunks (default)

Each chunk has byte offsets, line numbers and columns. A chunk can span
several lines when `context` is set. `Ranges` gives the exact match bounds
inside it.

```json
{
  "Result": {
    "Files": [
      {
        "Repository": "webform",
        "FileName": "webform.module",
        "ChunkMatches": [
          {
            "Content": "<base64-encoded chunk content>",
            "ContentStart": {"ByteOffset": 1223, "LineNumber": 35, "Column": 1},
            "Ranges": [
              {"Start": {"ByteOffset": 1237, "LineNumber": 35, "Column": 15},
               "End": {"ByteOffset": 1246, "LineNumber": 35, "Column": 24}}
            ]
          }
        ]
      }
    ],
    "MatchCount": 150,
    "FileCount": 12
  }
}
```

`Content` is base64-encoded. Each `Range` is relative to the file start.

### /v1/search, chunks=false

One entry per matched line, with flat offsets.

```json
{
  "Result": {
    "Files": [
      {
        "Repository": "webform",
        "FileName": "webform.module",
        "LineMatches": [
          {
            "Line": "<base64-encoded line content>",
            "LineNumber": 35,
            "LineStart": 1223,
            "LineEnd": 1250,
            "LineFragments": [{"LineOffset": 14, "Offset": 1237, "MatchLength": 9}]
          }
        ]
      }
    ],
    "MatchCount": 150,
    "FileCount": 12
  }
}
```

`Line` is base64-encoded.

### /v1/search/repo

```json
{
  "List": {
    "Repos": [
      {
        "Repository": {
          "Name": "drupal/webform",
          "URL": "...",
          "RawConfig": {
            "drupal-core": "3.x:^10.3 || ^11;4.0.x:^11",
            "drupal-usage": "3.x:12345;4.0.x:678",
            "drupal-security": "covered",
            "priority": "80"
          }
        },
        "Stats": {"Documents": 500, "ContentBytes": 2048000}
      }
    ]
  }
}
```

| RawConfig key | What it holds |
|---|---|
| `drupal-core` | The core constraint per indexed branch, `ref:constraint;…`. |
| `drupal-usage` | The install count per indexed branch, `ref:count;…`. |
| `drupal-security` | `covered`, or empty. |
| `priority` | The indexing priority score. |

## Limits

A request asking for more than a ceiling is clamped to it.

| What | Cap |
|---|---|
| `num`, files per search | 1000 |
| `context`, lines around a match | 50 |
| `maxmatches`, matches scanned | 100000 |
| Search response body | 10 MB |
| Repository listing body | 100 MB |
| One upstream call | 30 seconds |

The api-wide per-IP rate limit applies, with a ban for repeated violations.
A search result is cached for 5 minutes. A repository listing and this page
are cached for 24 hours.

## Errors

| Status | When |
|---|---|
| 400 | The index refused the query. A malformed regex is the usual cause. |
| 502 | The index did not answer, or failed on its own side. |
