Search indexing algorithm¶
Search results are only as fresh as the index. This page explains how changes reach the index so you can reason about delays and diagnose a stale index. Field and table details are in the Search index schema; how the pieces fit is in Search architecture.
Indexing is an outbox plus a periodic reconcile: writes record what changed in the same transaction, a frequent job applies those changes, and an infrequent job rebuilds everything.
Recording changes¶
The search plugin connects post_save and post_delete receivers once. For a
model a registered source watches:
- The source maps the changed instance to the ids of the documents it affects
(
document_ids_for_instance). A deleted object's id is returned too. - The ids are de-duplicated and inserted into the pending table in a single
statement inside the writer's transaction. A document already pending has its
generationincremented instead of getting a second row.
Because the insert shares the transaction, a rolled-back write leaves no pending row, and a committed write cannot lose one. A source that raises while mapping is logged and skipped so it never fails the user's write; the rebuild covers it.
Drain (every 10 seconds)¶
Run by the scheduler job atlas.search.drain.
- Take the PostgreSQL advisory lock shared with the rebuild. If it is held, skip this run.
- Read up to 500 pending rows in id order.
- Group their ids by owning source. Ids that no source owns are removed from the queue with a warning, since nothing can ever index them.
- For each source: call
documents(ids). Returned documents are upserted into the engine; requested ids that were not returned (deleted, removed, ineligible) are deleted from the engine. - Only after the engine accepted the change, delete the processed rows, and only
those whose
generationstill matches what was read. A change that landed during the run keeps its row for the next one. - Repeat from the next batch until no rows remain.
- Record the outcome: success clears a previous drain error and sets
lastDrainAt; any failure stores the message and time.
If a source or the engine fails, that source's rows stay pending and are retried on the next run. Other sources in the same batch still complete.
Rebuild (every 6 hours)¶
Run by atlas.search.rebuild, by the reindex command, and once at scheduler start
if the index is empty.
- Take the advisory lock. A rebuild that finds it held reports that another indexing run is in progress.
- Remember the current pending rows.
- Stream
all_documents()from every source to the engine'sreplace_all, which swaps the index atomically. If it fails partway the previous index stays queryable. - Remove the remembered pending rows, again only when their
generationis unchanged. - Record success, or the failure.
A document id produced by two sources aborts the rebuild before the swap.
First run¶
When the scheduler starts it runs atlas.search.initial_rebuild once. It rebuilds
only if the engine is healthy and reports zero documents while at least one source
has documents. This covers first enablement, switching engines, and lost index
data without waiting for the rebuild interval.
What signals miss¶
Signals fire for save() and delete() on model instances. They do not fire for
queryset update(), bulk_update(), bulk deletes or raw SQL, and nothing is
recorded while the search plugin is inactive. The rebuild repairs all of these,
so freshness for such changes equals the rebuild interval.
Diagnosing a stale index¶
Read GET /api/plugins/atlas.search/status/ and decide which part is at fault:
| Observation | Likely cause | Next step |
|---|---|---|
pendingCount grows and oldestPendingAgeSeconds keeps rising, lastDrainAt is old |
The scheduler is not running, or the drain skips because a long rebuild holds the lock | Check the scheduler process; see Run without a scheduler |
engineHealthy is false |
The engine is unreachable | Fix the engine; pending rows wait and are retried |
hasError is true, lastErrorJob is drain |
A source or the engine keeps failing | Read lastError (administrators) and the backend log for the source id |
| Backlog is zero, one entity is still missing | A bulk operation bypassed signals | Wait for the rebuild, or run reindex |
documentCount is much lower than the catalog |
Index not built or engine switched | Run reindex |
Results can also be missing rather than stale when resolve filters them: a
removed entity or one the caller may not read never appears, regardless of the
index.