# Indexing (/docs/infrastructure/search/indexing)

---
title: Indexing
description: When a write reaches the index, what happens without a queue, the methods for driving indexing yourself, and how to rebuild a collection whose rows predate the search config.
kind: guide
package: questpie
---

You rarely index by hand. A searchable collection writes to the index on its
own, and the interesting part is knowing exactly when, because search is a read
model and a row is not findable until the step below finishes.

## What happens on a write

The core `afterChange` hook fires after the transaction commits, never inside
it. It calls `scheduleIndex(collection, recordId)`, which keys pending work by
record so repeat writes collapse, then publishes the built-in `index-records`
job once 100 ms pass with nothing new arriving. The worker re-reads each record
in every configured locale, builds the projection from your `.searchable()`
config, and upserts the whole batch at once.

With no queue adapter configured, `scheduleIndex` returns `false` and the hook
indexes inline instead, for the record's current locale only. Deletes and purges
skip all of this and call `remove` straight from their own hook.

<Callout
	type="info"
	title="`manual: true` opts a collection out of all of this"
>
	The collection stays searchable and stays queryable, but the write hook and
	the bulk reindex both skip it. You own the lifecycle, which is what you want
	for importers and for synchronizing an external system.
</Callout>

## Indexing yourself

`app.search` exposes the whole service surface, and these delegate to the
adapter.

```ts
await app.search.index({
	collection: "posts",
	recordId,
	locale: "en",
	title,
	content,
});
await app.search.indexBatch(manyOfTheAbove); // one bulk upsert on `postgres`
await app.search.remove({ collection: "posts", recordId }); // omit locale for all
await app.search.clear(); // wipes the index
```

`indexBatch` falls back to sequential `index()` calls when the adapter does not
implement it. `flushPending()` forces any debounced items out immediately, which
is what you want in a test or during a graceful shutdown.

## Rebuilding a collection

Use this after a seed, or when you turn search on over data that predates it.
`POST /search/reindex/:collection` under your handler's base path walks every
record across every configured locale, builds the same projection the job
builds, and upserts in pages of 100. The typed client calls it as
`client.search.reindex("posts")`.

The route requires a session. Past that it consults `search.reindexAccess` on
your HTTP handler config, which may be a boolean or a function receiving the
request, app, session, db, locale and collection name. Leave it unset and access
is derived from the target collection's `update` rule.

<Callout type="warn" title="`app.search.reindex()` is not the same call">
	That one delegates to the adapter, and the Postgres and pgvector adapters
	throw `"reindex() not yet implemented - requires app context"`. Rebuilding
	needs CRUD access to read rows, which an adapter does not have. Use the route
	or the client method.
</Callout>

A reindex that hits an error stops walking there, logs a warning, and returns
the count it managed rather than throwing. It also skips any collection whose
projection is disabled or `manual`.

## Related

- [Search](/docs/infrastructure/search), the adapters behind all of this.
- [Jobs](/docs/code/jobs), the `index-records` job and the queue behind it.
- [Collections](/docs/schema/collections#searchable), the `.searchable()` config.
