diff --git a/src/Umbraco.Infrastructure/Examine/ExamineUmbracoIndexingHandler.cs b/src/Umbraco.Infrastructure/Examine/ExamineUmbracoIndexingHandler.cs index d19db3b285..5afe010b82 100644 --- a/src/Umbraco.Infrastructure/Examine/ExamineUmbracoIndexingHandler.cs +++ b/src/Umbraco.Infrastructure/Examine/ExamineUmbracoIndexingHandler.cs @@ -118,6 +118,20 @@ namespace Umbraco.Cms.Infrastructure.Examine } } + /// + public void DeleteIndexForEntities(IReadOnlyCollection entityIds, bool keepIfUnpublished) + { + var actions = DeferedActions.Get(_scopeProvider); + if (actions != null) + { + actions.Add(new DeferedDeleteIndex(this, entityIds, keepIfUnpublished)); + } + else + { + DeferedDeleteIndex.Execute(this, entityIds, keepIfUnpublished); + } + } + /// public void ReIndexForContent(IContent sender, bool isPublished) { @@ -371,6 +385,7 @@ namespace Umbraco.Cms.Infrastructure.Examine { private readonly ExamineUmbracoIndexingHandler _examineUmbracoIndexingHandler; private readonly int _id; + private readonly IReadOnlyCollection _ids; private readonly bool _keepIfUnpublished; public DeferedDeleteIndex(ExamineUmbracoIndexingHandler examineUmbracoIndexingHandler, int id, bool keepIfUnpublished) @@ -380,16 +395,42 @@ namespace Umbraco.Cms.Infrastructure.Examine _keepIfUnpublished = keepIfUnpublished; } - public override void Execute() => Execute(_examineUmbracoIndexingHandler, _id, _keepIfUnpublished); + public DeferedDeleteIndex(ExamineUmbracoIndexingHandler examineUmbracoIndexingHandler, IReadOnlyCollection ids, bool keepIfUnpublished) + { + _examineUmbracoIndexingHandler = examineUmbracoIndexingHandler; + _ids = ids; + _keepIfUnpublished = keepIfUnpublished; + } + + public override void Execute() + { + if (_ids is null) + { + Execute(_examineUmbracoIndexingHandler, _id, _keepIfUnpublished); + } + else + { + Execute(_examineUmbracoIndexingHandler, _ids, _keepIfUnpublished); + } + } public static void Execute(ExamineUmbracoIndexingHandler examineUmbracoIndexingHandler, int id, bool keepIfUnpublished) { - var strId = id.ToString(CultureInfo.InvariantCulture); foreach (var index in examineUmbracoIndexingHandler._examineManager.Indexes.OfType() .Where(x => x.PublishedValuesOnly || !keepIfUnpublished) .Where(x => x.EnableDefaultEventHandler)) { - index.DeleteFromIndex(strId); + index.DeleteFromIndex(id.ToString(CultureInfo.InvariantCulture)); + } + } + + public static void Execute(ExamineUmbracoIndexingHandler examineUmbracoIndexingHandler, IReadOnlyCollection ids, bool keepIfUnpublished) + { + foreach (var index in examineUmbracoIndexingHandler._examineManager.Indexes.OfType() + .Where(x => x.PublishedValuesOnly || !keepIfUnpublished) + .Where(x => x.EnableDefaultEventHandler)) + { + index.DeleteFromIndex(ids.Select(x => x.ToString(CultureInfo.InvariantCulture))); } } } diff --git a/src/Umbraco.Infrastructure/Search/IUmbracoIndexingHandler.cs b/src/Umbraco.Infrastructure/Search/IUmbracoIndexingHandler.cs index 24c82c055d..7fe37d7152 100644 --- a/src/Umbraco.Infrastructure/Search/IUmbracoIndexingHandler.cs +++ b/src/Umbraco.Infrastructure/Search/IUmbracoIndexingHandler.cs @@ -25,7 +25,7 @@ namespace Umbraco.Cms.Infrastructure.Search void DeleteDocumentsForContentTypes(IReadOnlyCollection removedContentTypes); /// - /// Remove items from an index + /// Remove an item from an index /// /// /// @@ -33,5 +33,15 @@ namespace Umbraco.Cms.Infrastructure.Search /// If false it will delete this from all indexes regardless. /// void DeleteIndexForEntity(int entityId, bool keepIfUnpublished); + + /// + /// Remove items from an index + /// + /// + /// + /// If true, indicates that we will only delete this item from indexes that don't support unpublished content. + /// If false it will delete this from all indexes regardless. + /// + void DeleteIndexForEntities(IReadOnlyCollection entityIds, bool keepIfUnpublished); } } diff --git a/src/Umbraco.Infrastructure/Search/IndexingNotificationHandler.Content.cs b/src/Umbraco.Infrastructure/Search/IndexingNotificationHandler.Content.cs index ebebdb7f34..c80e61af0e 100644 --- a/src/Umbraco.Infrastructure/Search/IndexingNotificationHandler.Content.cs +++ b/src/Umbraco.Infrastructure/Search/IndexingNotificationHandler.Content.cs @@ -44,13 +44,21 @@ namespace Umbraco.Cms.Infrastructure.Search throw new NotSupportedException(); } + // Used to track permanent deletions so we can bulk delete from the index + // when needed. For example, when emptying the recycle bin, else it will + // individually update the index which will be much slower. + HashSet deleteBatch = null; + foreach (var payload in (ContentCacheRefresher.JsonPayload[])args.MessageObject) { if (payload.ChangeTypes.HasType(TreeChangeTypes.Remove)) { - // delete content entirely (with descendants) - // false: remove entirely from all indexes - _umbracoIndexingHandler.DeleteIndexForEntity(payload.Id, false); + if (deleteBatch == null) + { + deleteBatch = new HashSet(); + } + + deleteBatch.Add(payload.Id); } else if (payload.ChangeTypes.HasType(TreeChangeTypes.RefreshAll)) { @@ -62,6 +70,15 @@ namespace Umbraco.Cms.Infrastructure.Search } else // RefreshNode or RefreshBranch (maybe trashed) { + if (deleteBatch != null && deleteBatch.Contains(payload.Id)) + { + // the same node has already been deleted, to ensure ordering is + // handled, we'll need to execute all queued deleted items now + // and reset the deleted items list. + _umbracoIndexingHandler.DeleteIndexForEntities(deleteBatch, false); + deleteBatch = null; + } + // don't try to be too clever - refresh entirely // there has to be race conditions in there ;-( @@ -132,6 +149,12 @@ namespace Umbraco.Cms.Infrastructure.Search // // BUT ... pretty sure it is! see test "Index_Delete_Index_Item_Ensure_Heirarchy_Removed" } + + if (deleteBatch != null) + { + // process the delete batch + _umbracoIndexingHandler.DeleteIndexForEntities(deleteBatch, false); + } } } } diff --git a/src/Umbraco.Infrastructure/Search/IndexingNotificationHandler.Media.cs b/src/Umbraco.Infrastructure/Search/IndexingNotificationHandler.Media.cs index 8b37d047de..4ac1d1ca09 100644 --- a/src/Umbraco.Infrastructure/Search/IndexingNotificationHandler.Media.cs +++ b/src/Umbraco.Infrastructure/Search/IndexingNotificationHandler.Media.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Notifications; @@ -37,12 +38,21 @@ namespace Umbraco.Cms.Infrastructure.Search throw new NotSupportedException(); } + // Used to track permanent deletions so we can bulk delete from the index + // when needed. For example, when emptying the recycle bin, else it will + // individually update the index which will be much slower. + HashSet deleteBatch = null; + foreach (var payload in (MediaCacheRefresher.JsonPayload[])args.MessageObject) { if (payload.ChangeTypes.HasType(TreeChangeTypes.Remove)) { - // remove from *all* indexes - _umbracoIndexingHandler.DeleteIndexForEntity(payload.Id, false); + if (deleteBatch == null) + { + deleteBatch = new HashSet(); + } + + deleteBatch.Add(payload.Id); } else if (payload.ChangeTypes.HasType(TreeChangeTypes.RefreshAll)) { @@ -52,6 +62,15 @@ namespace Umbraco.Cms.Infrastructure.Search } else // RefreshNode or RefreshBranch (maybe trashed) { + if (deleteBatch != null && deleteBatch.Contains(payload.Id)) + { + // the same node has already been deleted, to ensure ordering is + // handled, we'll need to execute all queued deleted items now + // and reset the deleted items list. + _umbracoIndexingHandler.DeleteIndexForEntities(deleteBatch, false); + deleteBatch = null; + } + var media = _mediaService.GetById(payload.Id); if (media == null) { @@ -83,7 +102,13 @@ namespace Umbraco.Cms.Infrastructure.Search } } } - } + } + } + + if (deleteBatch != null) + { + // process the delete batch + _umbracoIndexingHandler.DeleteIndexForEntities(deleteBatch, false); } } }