# Search

Two searches cover the generated pages, and they answer different questions. Type a phrase
from a docstring into the site's search dialog, and you get the page that docstring appears
on. Type `Report.generate` into the plugin's own search box, and you get that object's
heading. The first search is [Pagefind](https://pagefind.app/), the static full-text search
that ships with Starlight. It indexes rendered prose. The second search is
`<SymbolSearch>`, a component this plugin provides. It searches the API surface by name.

## Prose search

Under Starlight, generated pages render the standard page shell. Pagefind indexes them with
no configuration. A search for a sentence from a docstring finds the module page it belongs
to, and the result links into the page like any other. This site's end-to-end test suite
checks this: a search for `generate_report` finds `/api/demopkg/`.

Two things follow from Pagefind indexing whole pages:

- A module page's search result covers every member on that page. A query that matches a
  method's prose takes the reader to the module page, not to the method's anchor.
- To exclude API pages from search, use the same method as for any Starlight page: add
  Pagefind's own `data-pagefind-ignore` attribute. Apply it from a
  [component override](/starlight-pydocs/guides/component-overrides/) if you want to drop
  part of a page instead of the whole page.

Pagefind builds its index from the built HTML, so there is nothing to search during
development. Starlight's search dialog reports that search only works in production builds.
To try a query, run `astro build` and then `astro preview`. Symbol search does work in
`astro dev`, because its index is a route that the dev server serves live.

A plain Astro site has no Pagefind unless you add it. The symbol search below works either
way.

## Symbol search

Symbol search has two pieces: an endpoint that publishes the index, and a custom element
that queries it in the browser. It is on by default.

The endpoint is served next to each package's pages, at `<base>/symbols.json`. It is the
only pydocs JSON that a browser ever fetches. The griffe dumps stay on disk:

```json title="api/demopkg/symbols.json" wrap '"anchor": ""'
{
  "package": "demopkg",
  "base": "api/demopkg",
  "generated": "2026-08-13T09:00:00.000Z",
  "symbols": [
    { "path": "demopkg.Report", "kind": "class", "page": "api/demopkg", "anchor": "demopkg.Report", "brief": "A named collection of scored sections." },
    { "path": "demopkg.report", "kind": "module", "page": "api/demopkg/report", "anchor": "", "brief": "Report classes and the functions that build them." }
  ]
}
```

Each entry describes one documented object: its dotted path, its kind, the page slug it
lives on, its anchor, and the first line of its docstring. The anchor is empty for a module,
because a module is a page rather than a heading. The element turns `page` and `anchor` into
a URL, so the payload stays base-agnostic.

### Where it appears

The search box appears automatically on each package's root page: the page at the package's
`base`, which documents the package's top-level module. This is where a reader arrives when
looking for a name. One box per package keeps the search box off every module page below.

Put it anywhere else by hand:

```mdx title="src/content/docs/reference.mdx" 'package="numpkg"'
import { SymbolSearch } from 'starlight-pydocs/components';

<SymbolSearch />
<SymbolSearch package="numpkg" class="my-search" labels={{ searchLabel: 'Find a symbol' }} />
```

| Prop      | Type              | Default           | Meaning                            |
| --------- | ----------------- | ----------------- | ---------------------------------- |
| `package` | `string`          | the first package | Which configured package to search |
| `class`   | `string`          | none              | Extra class on the custom element  |
| `labels`  | `StringOverrides` | site translations | Label overrides for this instance  |

`labels` takes the same keys as every other component. See
[Internationalisation](/starlight-pydocs/guides/i18n/#the-keys).

The box below is a hand-placed example, and it is live rather than a screenshot: it searches
this site's `demopkg` pages. Type `report` into it and follow a result:

<SymbolSearch package="api/demopkg" />

With [`symbolSearch: false`](/starlight-pydocs/guides/configuration/), the endpoint is not
served. The component renders nothing in that case, so a hand-placed box never appears
without its index.

### How matching works

The index loads on first focus, not on page load, so a page nobody searches costs nothing.
Matching runs in the browser and ignores case. It ranks hits in a fixed, explainable order,
not a fuzzy one: an exact short name first, then a short-name prefix, then a prefix of the
full dotted path, then a substring of the name, then a substring of the path. Ties go to the
shorter path first, so `demopkg.Report` sorts above `demopkg.report.Report`. Remaining ties
go to alphabetical order, which keeps results stable between keystrokes. Because the whole
dotted path is matched, `report.gen` finds `demopkg.report.generate_report`. The list shows
at most 25 results, grouped by kind, using the same translated kind labels as the badges.

You can operate the element with the keyboard: <kbd>ArrowDown</kbd> and <kbd>ArrowUp</kbd>
move through the results, <kbd>Enter</kbd> follows the active result, and <kbd>Escape</kbd>
closes the list. It is a combobox with `aria-expanded`, `aria-activedescendant`, and one
`role="option"` per result. It defines itself once and queries only its own subtree, so
several boxes can appear on one page, and Astro's view transitions still work.

## Searching several packages

Each package has its own index. Each box searches one package. For a site that documents
several packages, either give the reader one box per package or fetch the endpoints
yourself:

```astro title="src/pages/search.astro" wrap
---
const bases = ['api/mypkg', 'api/otherpkg'];
const indexes = await Promise.all(bases.map((base) => fetch(new URL(`${base}/symbols.json`, Astro.site)).then((r) => r.json())));
---
```

For anything more complex, use the
[Content Layer loader](/starlight-pydocs/guides/vanilla-astro/#the-content-layer-loader). It
gives you the same surface as collection entries. You can query it at build time and render
the results however you like.