-
Notifications
You must be signed in to change notification settings - Fork 3.5k
Description
Describe the Bug
When using the "Reindex All" button to reindex multiple collections simultaneously, not all locales are saved correctly in the search collection. Reindexing collections individually works as expected — all locales are persisted correctly.
To Reproduce
Configure the search plugin with multiple collections and localization enabled (3 locales)
Click the "Reindex" button and select all collections at once
Observe that some search documents are missing locale data for 1 or 2 of the configured locales
Expected: All search documents have data for all configured locales.
Actual: Some search documents are missing locale data. The issue is non-deterministic — different documents may be affected each time.
Root Cause
In src/utilities/generateReindexHandler.ts, all collections are reindexed concurrently via Promise.all:
const promises = collections.map(async (collection) => {
try {
await deleteIndexes(collection)
await reindexCollection(collection)
} catch (err) {
// ...
}
})
await Promise.all(promises)
The reindexCollection function processes each document by:
First locale → operation = 'create' → calls payload.create() to insert the search doc
Subsequent locales → operation = 'update' → calls payload.find() to locate the search doc, then payload.update()
When multiple collections run concurrently via Promise.all, they all write to the same search collection within a single shared transaction (initiated by initTransaction(req) before the Promise.all). This causes race conditions:
- Concurrent create and find/update calls on the search collection's locale table interleave within the same transaction
- A payload.find() call in the update path for one collection may not see a recently created document from another collection's create path, or may conflict with concurrent writes
- The shared req.context.syncedDocsSet is mutated concurrently across all parallel collection promises, potentially causing deduplication logic to skip legitimate syncs
This explains why reindexing collections individually works correctly — there is no concurrency, so all create/find/update operations execute sequentially without conflicts.
Suggested Fix
Replace Promise.all with sequential execution:
for (const collection of collections) {
try {
await deleteIndexes(collection)
await reindexCollection(collection)
} catch (err) {
const message = t('error:unableToReindexCollection', { collection })
payload.logger.error({ err, msg: message })
}
}
This serializes collection reindexing, eliminating the race condition while maintaining the same error handling behavior. The performance difference is negligible since reindexing is an infrequent admin operation and each collection still processes its documents in batches.
Link to the code that reproduces this issue
Reproduction Steps
I made small changes to official test/plugin-search:
- localize Pages and Posts collections excerpt - To allow localized search
- localize search Collection excerpt and allow for admin to update search collection
Then run "pnpm dev plugin-search"
Then create documents for testing:
- Atleast 2 Pages with all different locales that have different excerpt defined per locale. For example "Some page 1 - en"
- Atleast 2 Posts with all different locales that have different excerpt defined per locale
- Navigate to search results -> reindex -> All collections
- Check the created search collection documents -> Some of them could have wrong data for some of the languages.
Which area(s) are affected?
plugin: search
Environment Info
@payloadcms/plugin-search: 3.75.0
payload: 3.75.0
Database: MongoDB
Locales configured: 3