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.
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 indexindexBatch 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.
Related
- Search, the adapters behind all of this.
- Jobs, the
index-recordsjob and the queue behind it. - Collections, the
.searchable()config.
Writing an adapter
The obligations behind each optional method on QueueAdapter, what the framework guarantees you in return, and why declaring a capability you cannot actually honor is the one mistake that costs data.
Embeddings
The vectors behind semantic search. What the pgvector adapter embeds on its own, the two provider factories QUESTPIE ships, and how to override the vector for one collection.