diff --git a/packages/plugin-search/src/types.ts b/packages/plugin-search/src/types.ts index 9d5ec0b4be..48bdf0749e 100644 --- a/packages/plugin-search/src/types.ts +++ b/packages/plugin-search/src/types.ts @@ -42,10 +42,25 @@ export type SearchPluginConfig = { defaultPriorities?: { [collection: string]: ((doc: any) => number | Promise) | number } + /** + * Controls whether drafts are deleted from the search index + * + * @default true + */ deleteDrafts?: boolean localize?: boolean + /** + * We use batching when re-indexing large collections. You can control the amount of items per batch, lower numbers should help with memory. + * + * @default 50 + */ reindexBatchSize?: number searchOverrides?: { fields?: FieldsOverride } & Partial> + /** + * Controls whether drafts are synced to the search index + * + * @default false + */ syncDrafts?: boolean } diff --git a/packages/plugin-search/src/utilities/syncDocAsSearchIndex.ts b/packages/plugin-search/src/utilities/syncDocAsSearchIndex.ts index b567c0f26e..199a6c3b16 100644 --- a/packages/plugin-search/src/utilities/syncDocAsSearchIndex.ts +++ b/packages/plugin-search/src/utilities/syncDocAsSearchIndex.ts @@ -142,15 +142,42 @@ export const syncDocAsSearchIndex = async ({ } } if (deleteDrafts && status === 'draft') { - // do not include draft docs in search results, so delete the record - try { - await payload.delete({ - id: searchDocID, - collection: searchSlug, - req, - }) - } catch (err: unknown) { - payload.logger.error({ err, msg: `Error deleting ${searchSlug} document.` }) + // Check to see if there's a published version of the doc + // We don't want to remove the search doc if there is a published version but a new draft has been created + const { + docs: [docWithPublish], + } = await payload.find({ + collection, + draft: false, + locale: syncLocale, + req, + where: { + and: [ + { + _status: { + equals: 'published', + }, + }, + { + id: { + equals: id, + }, + }, + ], + }, + }) + + if (!docWithPublish) { + // do not include draft docs in search results, so delete the record + try { + await payload.delete({ + id: searchDocID, + collection: searchSlug, + req, + }) + } catch (err: unknown) { + payload.logger.error({ err, msg: `Error deleting ${searchSlug} document.` }) + } } } } else if (doSync) { diff --git a/test/plugin-search/config.ts b/test/plugin-search/config.ts index 8d2f95cc4d..b77a214ec3 100644 --- a/test/plugin-search/config.ts +++ b/test/plugin-search/config.ts @@ -64,7 +64,7 @@ export default buildConfigWithDefaults({ searchOverrides: { access: { // Used for int test - delete: ({ req: { user } }) => user.email === devUser.email, + delete: ({ req: { user } }) => user?.email === devUser.email, }, fields: ({ defaultFields }) => [ ...defaultFields, diff --git a/test/plugin-search/int.spec.ts b/test/plugin-search/int.spec.ts index db75950652..faaa3bdb3d 100644 --- a/test/plugin-search/int.spec.ts +++ b/test/plugin-search/int.spec.ts @@ -133,6 +133,78 @@ describe('@payloadcms/plugin-search', () => { expect(results).toHaveLength(0) }) + it('should not delete a search doc if a published item has a new draft but remains published', async () => { + const publishedPage = await payload.create({ + collection: 'pages', + data: { + _status: 'published', + title: 'Published title!', + }, + }) + + // wait for the search document to be potentially created + // we do not await this within the `syncToSearch` hook + await wait(200) + + const { docs: results } = await payload.find({ + collection: 'search', + depth: 0, + where: { + 'doc.value': { + equals: publishedPage.id, + }, + }, + }) + + expect(results).toHaveLength(1) + + // Create a new draft + await payload.update({ + collection: 'pages', + id: publishedPage.id, + draft: true, + data: { + _status: 'draft', + title: 'Draft title!', + }, + }) + + // This should remain with the published content + const { docs: updatedResults } = await payload.find({ + collection: 'search', + depth: 0, + where: { + 'doc.value': { + equals: publishedPage.id, + }, + }, + }) + + expect(updatedResults).toHaveLength(1) + + await payload.update({ + collection: 'pages', + id: publishedPage.id, + data: { + _status: 'draft', + title: 'Drafted again', + }, + }) + + // Should now be deleted given we've unpublished the page + const { docs: deletedResults } = await payload.find({ + collection: 'search', + depth: 0, + where: { + 'doc.value': { + equals: publishedPage.id, + }, + }, + }) + + expect(deletedResults).toHaveLength(0) + }) + it('should sync changes made to an existing search document', async () => { const pageToReceiveUpdates = await payload.create({ collection: 'pages',