QUESTPIE

Indexing

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.

View markdown

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.

`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.

Indexing yourself

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

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.

`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.

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.

  • Search, the adapters behind all of this.
  • Jobs, the index-records job and the queue behind it.
  • Collections, the .searchable() config.

On this page