Skip to content

Add reindex rake tasks for populating SearchDocuments - #9391

Draft
gbp wants to merge 13 commits into
pg-search-adminfrom
pg-search-backfill
Draft

Add reindex rake tasks for populating SearchDocuments#9391
gbp wants to merge 13 commits into
pg-search-adminfrom
pg-search-backfill

Conversation

@gbp

@gbp gbp commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

What does this do?

Adds the tooling to populate and rebuild the PostgreSQL search index at production scale:

  • rake reindex:missing[Model] — indexes records that have no search document yet; resumable and safe to re-run
  • rake reindex:all[Model] — full rebuild of every searchable record

Both accept an optional model argument (all searchable models when omitted) and are tunable via CHUNK_SIZE, BATCH_SIZE, STATE_DIR and RESUME environment variables.


Have you updated the changelog? If this is not necessary, put square brackets around this: skip changelog

gbp added 13 commits July 16, 2026 15:32
The admin user search needs to pass admin_mode (and other backend
options) when it routes through search_scope, but the concern dropped
every argument after the query.

Accept and forward keyword options to `Search.search_scope` so callers
can scope the backend search.

The Xapian adapter already swallows extra keywords, so its path is
unchanged.
Prepare hybrid_search to back a `Search::Backend` adapter behind the
common search interface. A search_scope needs to run within a caller
supplied relation and stay chainable.

Accept an optional base relation (defaulting to model.all) and infer
the model from it.

De-duplicate the joined relation with DISTINCT, fixing duplicate rows
(and wrong counts/pagination) when a record matched through several
translations or sections in admin or exact search.
The admin user search is about to move onto the PostgreSQL search
index, which only finds records that have search documents.

Reindex the records the examples create so they pass under both the
current ILIKE search and the indexed search, and match the about me URL
by its host, which is how PostgreSQL tokenises a link.
Wire up the native PostgreSQL full-text search behind the common
`Search::Backend` interface so callers can reach it alongside the
existing Xapian adapter.

The PG adapter implements `search_scope` and returns the `hybrid_search`
joined relation directly.

The paginated query interface (search, typeahead, similar) is follow-on
work and keeps the base class's NotImplementedError behaviour.
Add the PostgreSQL adapter to the backend registry so a site can
select it as the live query backend with SEARCH_BACKEND postgresql.]

The initializer already resolves the configured name through
`backend_for`, so registration is all that is needed to make the new
adapter switchable.

Xapian stays the default.
During the migration both the Xapian and PostgreSQL indexes must stay
fresh on every write while only one backend answers queries. The
interface already fans `reindex_later` and `queued_jobs_count` across
`index_backends`, but nothing let a site choose which backends those
are.

Add a SEARCH_INDEX_BACKENDS config list (default [xapian], so behaviour
is unchanged).

Backends are resolved by the new `Search.use_configured_backends!` via
the initializer, which sets both the query backend and the indexed
backends from configuration.
Some callers are tied to a single backend's features regardless of which
backend SEARCH_BACKEND configures.

Admin search is the driving case: it relies on the PostgreSQL-only admin
index, routing it via the configured backend would break using Xapian.

Each query method (search, search_scope, typeahead, similar) now takes
an optional backend naming a registered backend for that one call.
Exact mode substring matching uses LIKE, so it is case-sensitive. The
admin user search which is about to move onto this backend replaces an
ILIKE query, and admins search for fragments like partial email
addresses where case rarely matters.

Now `hybrid_search` has a `case_sensitive` option (default true, keeping
current behaviour for existing exact mode callers); when false the exact
mode clauses match with ILIKE instead.

The PostgreSQL adaptor exposes the option on `search_scope`; other
backends ignore it via their option splats.
The admin user listing searched with a raw ILIKE query over name,
email, about_me and exact tag names.

Move it onto the unified `search_scope` so admin search runs through the
search backend interface and its PostgreSQL-only admin index, forced via
the new backend option so SEARCH_BACKEND cannot route it to Xapian.

Behaviour changes: exact tag-name matching is dropped (the admin tag
pages cover that), results are capped at the search_scope limit of
1000 matches, and results now depend on search_documents freshness.
Its only caller, the admin user listing, now searches through the
unified search_scope backend interface, so this is now dead code.
The interface gained a per-call backend override and exact mode gained a
case sensitivity option. Describe search_scope chaining with its
PostgreSQL option set, the backend override with the admin search
example, and note the ILIKE variant where the doc describes the LIKE
exact search.
The chunked reindex rake tasks need to enumerate searchable models,
find records that still lack a search document, and index a record
without aborting on a single failure. Add three class helpers to the
Searchable concern to support this.

`searchable_models` lists the registered models. `not_indexed` returns
records with no search document via a NOT EXISTS subquery, so a reindex
can resume and skip already-indexed records without relying on id order.
`reindex_record` indexes one record, logging and swallowing failures.
A full reindex of a pre-existing database processes millions of records.
Doing so in one long-running process lets memory grow unbounded, and
basing resumption on an id cursor alone re-does already-indexed work.

Add reindex:missing and reindex:all. Both drive a coordinator that does
not load the app and instead spawns a fresh reindex:chunk process per
chunk, so memory is released between chunks rather than accumulating.
Each worker indexes one chunk, advances a per-model id cursor file so a
failing record cannot stall the run, then exits. reindex:missing skips
records that already have a search document; reindex:all rebuilds every
record. The worker also quietens query logging, whose per-upsert writes
otherwise dominate disk IO.
@gbp
gbp force-pushed the pg-search-admin branch 2 times, most recently from 227c4be to 1713d9a Compare July 22, 2026 10:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant