From 70e57c9e42d4a7c351cf808df17d8692b0544acc Mon Sep 17 00:00:00 2001 From: Zeegaan Date: Tue, 11 Nov 2025 17:26:32 +0900 Subject: [PATCH 01/10] Rebuild indexes when language gets deleted --- .../RebuildIndexesNotificationHandler.cs | 11 +++++++++- .../Umbraco.Cms.Search.Core.csproj | 3 +++ .../IndexService/VariantContentTreeTests.cs | 4 ++-- .../IndexService/VariantDocumentTests.cs | 4 ++-- .../SearchService/VariantDocumentTests.cs | 21 +++++++++++++++---- .../SearchService/VariantFilterTest.cs | 4 ++-- .../Tests/TestBase.cs | 6 +++++- 7 files changed, 41 insertions(+), 12 deletions(-) diff --git a/src/Umbraco.Cms.Search.Core/NotificationHandlers/RebuildIndexesNotificationHandler.cs b/src/Umbraco.Cms.Search.Core/NotificationHandlers/RebuildIndexesNotificationHandler.cs index 7066b6c..23c45ac 100644 --- a/src/Umbraco.Cms.Search.Core/NotificationHandlers/RebuildIndexesNotificationHandler.cs +++ b/src/Umbraco.Cms.Search.Core/NotificationHandlers/RebuildIndexesNotificationHandler.cs @@ -5,7 +5,7 @@ namespace Umbraco.Cms.Search.Core.NotificationHandlers; -internal sealed class RebuildIndexesNotificationHandler : INotificationHandler +internal sealed class RebuildIndexesNotificationHandler : INotificationHandler, INotificationHandler { private readonly IContentIndexingService _contentIndexingService; private readonly ILogger _logger; @@ -26,4 +26,13 @@ public void Handle(UmbracoApplicationStartedNotification notification) _contentIndexingService.Rebuild(Constants.IndexAliases.DraftMedia); _contentIndexingService.Rebuild(Constants.IndexAliases.DraftMembers); } + + public void Handle(LanguageDeletedNotification notification) + { + _logger.LogInformation("Rebuilding core search indexes..."); + _contentIndexingService.Rebuild(Constants.IndexAliases.PublishedContent); + _contentIndexingService.Rebuild(Constants.IndexAliases.DraftContent); + _contentIndexingService.Rebuild(Constants.IndexAliases.DraftMedia); + _contentIndexingService.Rebuild(Constants.IndexAliases.DraftMembers); + } } diff --git a/src/Umbraco.Cms.Search.Core/Umbraco.Cms.Search.Core.csproj b/src/Umbraco.Cms.Search.Core/Umbraco.Cms.Search.Core.csproj index c5de816..98df4c3 100644 --- a/src/Umbraco.Cms.Search.Core/Umbraco.Cms.Search.Core.csproj +++ b/src/Umbraco.Cms.Search.Core/Umbraco.Cms.Search.Core.csproj @@ -23,6 +23,9 @@ <_Parameter1>Umbraco.Test.Search.Unit + + <_Parameter1>Umbraco.Test.Search.Examine.Integration + <_Parameter1>DynamicProxyGenAssembly2 diff --git a/src/Umbraco.Test.Search.Examine.Integration/Tests/ContentTests/IndexService/VariantContentTreeTests.cs b/src/Umbraco.Test.Search.Examine.Integration/Tests/ContentTests/IndexService/VariantContentTreeTests.cs index 0d23395..a9e8c68 100644 --- a/src/Umbraco.Test.Search.Examine.Integration/Tests/ContentTests/IndexService/VariantContentTreeTests.cs +++ b/src/Umbraco.Test.Search.Examine.Integration/Tests/ContentTests/IndexService/VariantContentTreeTests.cs @@ -228,8 +228,8 @@ public async Task CreateVariantDocumentTree() .WithCultureInfo("ja-JP") .Build(); - LocalizationService.Save(langDk); - LocalizationService.Save(langJp); + await LanguageService.CreateAsync(langDk, Constants.Security.SuperUserKey); + await LanguageService.CreateAsync(langJp, Constants.Security.SuperUserKey); IContentType contentType = new ContentTypeBuilder() .WithAlias("variant") diff --git a/src/Umbraco.Test.Search.Examine.Integration/Tests/ContentTests/IndexService/VariantDocumentTests.cs b/src/Umbraco.Test.Search.Examine.Integration/Tests/ContentTests/IndexService/VariantDocumentTests.cs index a4b977a..ff5f43a 100644 --- a/src/Umbraco.Test.Search.Examine.Integration/Tests/ContentTests/IndexService/VariantDocumentTests.cs +++ b/src/Umbraco.Test.Search.Examine.Integration/Tests/ContentTests/IndexService/VariantDocumentTests.cs @@ -177,8 +177,8 @@ private async Task CreateVariantDocument() .WithCultureInfo("ja-JP") .Build(); - LocalizationService.Save(langDk); - LocalizationService.Save(langJp); + await LanguageService.CreateAsync(langDk, Cms.Core.Constants.Security.SuperUserKey); + await LanguageService.CreateAsync(langJp, Cms.Core.Constants.Security.SuperUserKey); IContentType contentType = new ContentTypeBuilder() .WithAlias("variant") diff --git a/src/Umbraco.Test.Search.Examine.Integration/Tests/ContentTests/SearchService/VariantDocumentTests.cs b/src/Umbraco.Test.Search.Examine.Integration/Tests/ContentTests/SearchService/VariantDocumentTests.cs index 91297a1..0402687 100644 --- a/src/Umbraco.Test.Search.Examine.Integration/Tests/ContentTests/SearchService/VariantDocumentTests.cs +++ b/src/Umbraco.Test.Search.Examine.Integration/Tests/ContentTests/SearchService/VariantDocumentTests.cs @@ -9,7 +9,6 @@ namespace Umbraco.Test.Search.Examine.Integration.Tests.ContentTests.SearchServi public class VariantDocumentTests : SearcherTestBase { - [TestCase(true, "en-US", "Name")] [TestCase(false, "en-US", "Name")] [TestCase(true, "da-DK", "Navn")] @@ -83,20 +82,34 @@ public async Task CanSearchVariantTextBySegment(bool publish, string culture, st Assert.That(results.Documents.First().Id, Is.EqualTo(RootKey)); } + [TestCase(true, "da-DK", "Roden")] + [TestCase(true, "ja-JP", "ル-ト")] + public async Task CannotSearchNonExistingNameAfterLanguageDelete(bool publish, string culture, string name) + { + var indexAlias = GetIndexAlias(publish); + + await LanguageService.DeleteAsync(culture, Constants.Security.SuperUserKey); + + // We can't wait for indexing here, as it's an entire rebuild, not just a single action. + await Task.Delay(4000); + + SearchResult results = await Searcher.SearchAsync(indexAlias, name, null, null, null, culture, null, null, 0, 100); + Assert.That(results.Total, Is.EqualTo(0)); + } + [SetUp] public async Task CreateVariantDocument() { ILanguage langDk = new LanguageBuilder() .WithCultureInfo("da-DK") - .WithIsDefault(true) .Build(); ILanguage langJp = new LanguageBuilder() .WithCultureInfo("ja-JP") .Build(); - LocalizationService.Save(langDk); - LocalizationService.Save(langJp); + await LanguageService.CreateAsync(langDk, Constants.Security.SuperUserKey); + await LanguageService.CreateAsync(langJp, Constants.Security.SuperUserKey); IContentType contentType = new ContentTypeBuilder() .WithAlias("variant") diff --git a/src/Umbraco.Test.Search.Examine.Integration/Tests/ContentTests/SearchService/VariantFilterTest.cs b/src/Umbraco.Test.Search.Examine.Integration/Tests/ContentTests/SearchService/VariantFilterTest.cs index 502fdc7..c5f9d48 100644 --- a/src/Umbraco.Test.Search.Examine.Integration/Tests/ContentTests/SearchService/VariantFilterTest.cs +++ b/src/Umbraco.Test.Search.Examine.Integration/Tests/ContentTests/SearchService/VariantFilterTest.cs @@ -100,8 +100,8 @@ public async Task CreateVariantDocument() .WithCultureInfo("ja-JP") .Build(); - LocalizationService.Save(langDk); - LocalizationService.Save(langJp); + await LanguageService.CreateAsync(langDk, Constants.Security.SuperUserKey); + await LanguageService.CreateAsync(langJp, Constants.Security.SuperUserKey); IContentType contentType = new ContentTypeBuilder() .WithAlias("variant") diff --git a/src/Umbraco.Test.Search.Examine.Integration/Tests/TestBase.cs b/src/Umbraco.Test.Search.Examine.Integration/Tests/TestBase.cs index d6f2d8d..57d5890 100644 --- a/src/Umbraco.Test.Search.Examine.Integration/Tests/TestBase.cs +++ b/src/Umbraco.Test.Search.Examine.Integration/Tests/TestBase.cs @@ -5,9 +5,11 @@ using NUnit.Framework; using Umbraco.Cms.Core.HostedServices; using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Notifications; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Sync; using Umbraco.Cms.Search.Core.DependencyInjection; +using Umbraco.Cms.Search.Core.NotificationHandlers; using Umbraco.Cms.Tests.Common.Testing; using Umbraco.Cms.Tests.Integration.Testing; using Umbraco.Test.Search.Examine.Integration.Attributes; @@ -37,7 +39,8 @@ public abstract class TestBase : UmbracoIntegrationTest protected IDataTypeService DataTypeService => GetRequiredService(); - protected ILocalizationService LocalizationService => GetRequiredService(); + protected ILanguageService LanguageService => GetRequiredService(); + protected void SaveAndPublish(IContent content) { @@ -55,6 +58,7 @@ protected override void CustomTestSetup(IUmbracoBuilder builder) builder.Services.AddUnique(); builder.Services.AddUnique(); + builder.AddNotificationHandler(); // the core ConfigureBuilderAttribute won't execute from other assemblies at the moment, so this is a workaround var testType = Type.GetType(TestContext.CurrentContext.Test.ClassName!); From c575205cbd9ece41a92355746424fafe6a292d0e Mon Sep 17 00:00:00 2001 From: Zeegaan Date: Tue, 11 Nov 2025 19:45:40 +0900 Subject: [PATCH 02/10] Add setting to only rebuild content indexes --- .../Configuration/IndexSettings.cs | 6 ++++++ .../RebuildIndexesNotificationHandler.cs | 18 ++++++++++++------ 2 files changed, 18 insertions(+), 6 deletions(-) create mode 100644 src/Umbraco.Cms.Search.Core/Configuration/IndexSettings.cs diff --git a/src/Umbraco.Cms.Search.Core/Configuration/IndexSettings.cs b/src/Umbraco.Cms.Search.Core/Configuration/IndexSettings.cs new file mode 100644 index 0000000..c238ee7 --- /dev/null +++ b/src/Umbraco.Cms.Search.Core/Configuration/IndexSettings.cs @@ -0,0 +1,6 @@ +namespace Umbraco.Cms.Search.Core.Configuration; + +public class IndexSettings +{ + public required string[] IncludeRebuildWhenLanguageDeleted { get; set; } = [Constants.IndexAliases.DraftContent, Constants.IndexAliases.PublishedContent]; +} diff --git a/src/Umbraco.Cms.Search.Core/NotificationHandlers/RebuildIndexesNotificationHandler.cs b/src/Umbraco.Cms.Search.Core/NotificationHandlers/RebuildIndexesNotificationHandler.cs index 23c45ac..eb6ebf1 100644 --- a/src/Umbraco.Cms.Search.Core/NotificationHandlers/RebuildIndexesNotificationHandler.cs +++ b/src/Umbraco.Cms.Search.Core/NotificationHandlers/RebuildIndexesNotificationHandler.cs @@ -1,6 +1,8 @@ using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Notifications; +using Umbraco.Cms.Search.Core.Configuration; using Umbraco.Cms.Search.Core.Services.ContentIndexing; namespace Umbraco.Cms.Search.Core.NotificationHandlers; @@ -9,13 +11,16 @@ internal sealed class RebuildIndexesNotificationHandler : INotificationHandler _logger; + private readonly IndexSettings _options; public RebuildIndexesNotificationHandler( IContentIndexingService contentIndexingService, - ILogger logger) + ILogger logger, + IOptions options) { _contentIndexingService = contentIndexingService; _logger = logger; + _options = options.Value; } public void Handle(UmbracoApplicationStartedNotification notification) @@ -29,10 +34,11 @@ public void Handle(UmbracoApplicationStartedNotification notification) public void Handle(LanguageDeletedNotification notification) { - _logger.LogInformation("Rebuilding core search indexes..."); - _contentIndexingService.Rebuild(Constants.IndexAliases.PublishedContent); - _contentIndexingService.Rebuild(Constants.IndexAliases.DraftContent); - _contentIndexingService.Rebuild(Constants.IndexAliases.DraftMedia); - _contentIndexingService.Rebuild(Constants.IndexAliases.DraftMembers); + _logger.LogInformation("Rebuilding search indexes after language deletion..."); + + foreach (var indexAlias in _options.IncludeRebuildWhenLanguageDeleted) + { + _contentIndexingService.Rebuild(indexAlias); + } } } From 6ac7469a97a26329dacf3b90aa854dd0090741c5 Mon Sep 17 00:00:00 2001 From: Zeegaan Date: Wed, 12 Nov 2025 16:59:04 +0900 Subject: [PATCH 03/10] Move IncludeRebuildWhenLanguageDeleted to IndexOptions --- src/Umbraco.Cms.Search.Core/Configuration/IndexOptions.cs | 2 ++ src/Umbraco.Cms.Search.Core/Configuration/IndexSettings.cs | 6 ------ .../RebuildIndexesNotificationHandler.cs | 4 ++-- 3 files changed, 4 insertions(+), 8 deletions(-) delete mode 100644 src/Umbraco.Cms.Search.Core/Configuration/IndexSettings.cs diff --git a/src/Umbraco.Cms.Search.Core/Configuration/IndexOptions.cs b/src/Umbraco.Cms.Search.Core/Configuration/IndexOptions.cs index ec4f743..ca59ed0 100644 --- a/src/Umbraco.Cms.Search.Core/Configuration/IndexOptions.cs +++ b/src/Umbraco.Cms.Search.Core/Configuration/IndexOptions.cs @@ -9,6 +9,8 @@ public sealed class IndexOptions { private readonly Dictionary _register = []; + public required string[] IncludeRebuildWhenLanguageDeleted { get; set; } = [Constants.IndexAliases.DraftContent, Constants.IndexAliases.PublishedContent]; + public void RegisterIndex(string indexAlias, params UmbracoObjectTypes[] containedObjectTypes) where TIndexer : class, IIndexer where TSearcher : class, ISearcher diff --git a/src/Umbraco.Cms.Search.Core/Configuration/IndexSettings.cs b/src/Umbraco.Cms.Search.Core/Configuration/IndexSettings.cs deleted file mode 100644 index c238ee7..0000000 --- a/src/Umbraco.Cms.Search.Core/Configuration/IndexSettings.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace Umbraco.Cms.Search.Core.Configuration; - -public class IndexSettings -{ - public required string[] IncludeRebuildWhenLanguageDeleted { get; set; } = [Constants.IndexAliases.DraftContent, Constants.IndexAliases.PublishedContent]; -} diff --git a/src/Umbraco.Cms.Search.Core/NotificationHandlers/RebuildIndexesNotificationHandler.cs b/src/Umbraco.Cms.Search.Core/NotificationHandlers/RebuildIndexesNotificationHandler.cs index eb6ebf1..90ea095 100644 --- a/src/Umbraco.Cms.Search.Core/NotificationHandlers/RebuildIndexesNotificationHandler.cs +++ b/src/Umbraco.Cms.Search.Core/NotificationHandlers/RebuildIndexesNotificationHandler.cs @@ -11,12 +11,12 @@ internal sealed class RebuildIndexesNotificationHandler : INotificationHandler _logger; - private readonly IndexSettings _options; + private readonly IndexOptions _options; public RebuildIndexesNotificationHandler( IContentIndexingService contentIndexingService, ILogger logger, - IOptions options) + IOptions options) { _contentIndexingService = contentIndexingService; _logger = logger; From 3d162164ade81542b1b62a3fd430c2fba62a456c Mon Sep 17 00:00:00 2001 From: Zeegaan Date: Thu, 13 Nov 2025 17:45:04 +0900 Subject: [PATCH 04/10] Add handling of notification --- .../RebuildIndexesNotificationHandler.cs | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/src/Umbraco.Cms.Search.Core/NotificationHandlers/RebuildIndexesNotificationHandler.cs b/src/Umbraco.Cms.Search.Core/NotificationHandlers/RebuildIndexesNotificationHandler.cs index eb6ebf1..6a2814f 100644 --- a/src/Umbraco.Cms.Search.Core/NotificationHandlers/RebuildIndexesNotificationHandler.cs +++ b/src/Umbraco.Cms.Search.Core/NotificationHandlers/RebuildIndexesNotificationHandler.cs @@ -2,12 +2,13 @@ using Microsoft.Extensions.Options; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Notifications; +using Umbraco.Cms.Core.Services.Changes; using Umbraco.Cms.Search.Core.Configuration; using Umbraco.Cms.Search.Core.Services.ContentIndexing; namespace Umbraco.Cms.Search.Core.NotificationHandlers; -internal sealed class RebuildIndexesNotificationHandler : INotificationHandler, INotificationHandler +internal sealed class RebuildIndexesNotificationHandler : INotificationHandler, INotificationHandler, INotificationHandler { private readonly IContentIndexingService _contentIndexingService; private readonly ILogger _logger; @@ -41,4 +42,17 @@ public void Handle(LanguageDeletedNotification notification) _contentIndexingService.Rebuild(indexAlias); } } + + public void Handle(ContentTypeChangedNotification notification) + { + if (notification.Changes.Any(x => x.ChangeTypes == ContentTypeChangeTypes.RefreshMain)) + { + _logger.LogInformation("Rebuilding search indexes after content type update..."); + + _contentIndexingService.Rebuild(Constants.IndexAliases.PublishedContent); + _contentIndexingService.Rebuild(Constants.IndexAliases.DraftContent); + _contentIndexingService.Rebuild(Constants.IndexAliases.DraftMedia); + _contentIndexingService.Rebuild(Constants.IndexAliases.DraftMembers); + } + } } From 4c375167a2e5fa73b0dc09b5564e100365ba52bf Mon Sep 17 00:00:00 2001 From: Zeegaan Date: Fri, 14 Nov 2025 13:01:08 +0900 Subject: [PATCH 05/10] Rebuild on content type deletion or removal of property --- .../RebuildIndexesNotificationHandler.cs | 6 +- .../SearchService/DocumentTypeTests.cs | 139 ++++++++++++++++++ 2 files changed, 143 insertions(+), 2 deletions(-) create mode 100644 src/Umbraco.Test.Search.Examine.Integration/Tests/ContentTests/SearchService/DocumentTypeTests.cs diff --git a/src/Umbraco.Cms.Search.Core/NotificationHandlers/RebuildIndexesNotificationHandler.cs b/src/Umbraco.Cms.Search.Core/NotificationHandlers/RebuildIndexesNotificationHandler.cs index 6a2814f..871e4f6 100644 --- a/src/Umbraco.Cms.Search.Core/NotificationHandlers/RebuildIndexesNotificationHandler.cs +++ b/src/Umbraco.Cms.Search.Core/NotificationHandlers/RebuildIndexesNotificationHandler.cs @@ -8,7 +8,9 @@ namespace Umbraco.Cms.Search.Core.NotificationHandlers; -internal sealed class RebuildIndexesNotificationHandler : INotificationHandler, INotificationHandler, INotificationHandler +internal sealed class RebuildIndexesNotificationHandler : INotificationHandler, + INotificationHandler, + INotificationHandler { private readonly IContentIndexingService _contentIndexingService; private readonly ILogger _logger; @@ -45,7 +47,7 @@ public void Handle(LanguageDeletedNotification notification) public void Handle(ContentTypeChangedNotification notification) { - if (notification.Changes.Any(x => x.ChangeTypes == ContentTypeChangeTypes.RefreshMain)) + if (notification.Changes.Any(x => x.ChangeTypes is ContentTypeChangeTypes.RefreshMain or ContentTypeChangeTypes.Remove )) { _logger.LogInformation("Rebuilding search indexes after content type update..."); diff --git a/src/Umbraco.Test.Search.Examine.Integration/Tests/ContentTests/SearchService/DocumentTypeTests.cs b/src/Umbraco.Test.Search.Examine.Integration/Tests/ContentTests/SearchService/DocumentTypeTests.cs new file mode 100644 index 0000000..0c7de37 --- /dev/null +++ b/src/Umbraco.Test.Search.Examine.Integration/Tests/ContentTests/SearchService/DocumentTypeTests.cs @@ -0,0 +1,139 @@ +using NUnit.Framework; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Cache; +using Umbraco.Cms.Core.HostedServices; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models.ContentEditing; +using Umbraco.Cms.Core.Models.ContentTypeEditing; +using Umbraco.Cms.Core.Notifications; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Core.Services.ContentTypeEditing; +using Umbraco.Cms.Core.Services.OperationStatus; +using Umbraco.Cms.Core.Sync; +using Umbraco.Cms.Search.Core.DependencyInjection; +using Umbraco.Cms.Search.Core.Models.Searching; +using Umbraco.Cms.Search.Core.NotificationHandlers; +using Umbraco.Cms.Search.Core.Services; +using Umbraco.Cms.Tests.Common.Builders; +using Umbraco.Cms.Tests.Common.TestHelpers; +using Umbraco.Cms.Tests.Common.Testing; +using Umbraco.Cms.Tests.Integration.Testing; +using Umbraco.Test.Search.Examine.Integration.Extensions; +using Umbraco.Test.Search.Examine.Integration.Tests.ContentTests.IndexService; + +namespace Umbraco.Test.Search.Examine.Integration.Tests.ContentTests.SearchService; + +[TestFixture] +[UmbracoTest(Database = UmbracoTestOptions.Database.NewSchemaPerTest)] +public class DocumentTypeTests : UmbracoIntegrationTest +{ + private IContentTypeService ContentTypeService => GetRequiredService(); + + private ISearcher Searcher => GetRequiredService(); + + private IContentTypeEditingService ContentTypeEditingService => GetRequiredService(); + + private IContentEditingService ContentEditingService => GetRequiredService(); + + private IContentType _parentContentType = null!; + private IContentType _childContentType = null!; + + + protected override void CustomTestSetup(IUmbracoBuilder builder) + { + builder.AddNotificationHandler(); + builder.Services.AddUnique(); + builder.Services.AddUnique(); + builder.AddExamineSearchProviderForTest(); + builder.AddSearchCore(); + builder.AddNotificationHandler(); + builder.AddNotificationHandler(); + } + + [Test] + public async Task CannotSearchForRemovedProperty() + { + await CreateDocuments(); + // Act + _childContentType.RemovePropertyType("title"); + await ContentTypeService.UpdateAsync(_childContentType, Constants.Security.SuperUserKey); + + await Task.Delay(3000); + IContentType? contentType = await ContentTypeService.GetAsync(_childContentType.Key); + // Assert.That(contentType!.PropertyTypes.Any(), Is.False); + + SearchResult finalResults = await Searcher.SearchAsync( + Cms.Search.Core.Constants.IndexAliases.DraftContent, + query: "Home Page"); + + // We should still find the + Assert.That(finalResults.Total, Is.EqualTo(1)); + } + + [Test] + public async Task CannotSearchForRemovedDocument() + { + await CreateDocuments(); + + // Act + await ContentTypeService.DeleteAsync(_childContentType.Key, Constants.Security.SuperUserKey); + + await Task.Delay(3000); + IContentType? contentType = await ContentTypeService.GetAsync(_childContentType.Key); + + SearchResult finalResults = await Searcher.SearchAsync( + Cms.Search.Core.Constants.IndexAliases.DraftContent, + query: "Home Page"); + + Assert.That(finalResults.Total, Is.EqualTo(1)); + Assert.That(contentType, Is.Null); + } + + private async Task CreateDocuments() + { + ContentTypeCreateModel parentContentTypeCreateModel = ContentTypeEditingBuilder.CreateSimpleContentType( + "parentType", + "Parent Type"); + Attempt parentContentTypeAttempt = await ContentTypeEditingService.CreateAsync( + parentContentTypeCreateModel, + Constants.Security.SuperUserKey); + Assert.IsTrue(parentContentTypeAttempt.Success); + _parentContentType = parentContentTypeAttempt.Result!; + + // Create Child ContentType + ContentTypeCreateModel childContentTypeCreateModel = ContentTypeEditingBuilder.CreateSimpleContentType( + "childType", + "Child Type"); + Attempt childContentTypeAttempt = await ContentTypeEditingService.CreateAsync( + childContentTypeCreateModel, + Constants.Security.SuperUserKey); + Assert.IsTrue(childContentTypeAttempt.Success); + _childContentType = childContentTypeAttempt.Result!; + + // Update Parent ContentType to allow Child ContentType + ContentTypeUpdateModel parentContentTypeUpdateModel = ContentTypeUpdateHelper.CreateContentTypeUpdateModel(_parentContentType); + parentContentTypeUpdateModel.AllowedContentTypes = + [ + new ContentTypeSort(_childContentType.Key, 0, childContentTypeCreateModel.Alias) + ]; + Attempt updatedParentResult = await ContentTypeEditingService.UpdateAsync( + _parentContentType, + parentContentTypeUpdateModel, + Constants.Security.SuperUserKey); + Assert.IsTrue(updatedParentResult.Success); + + // Create Root Document (Parent) + ContentCreateModel rootCreateModel = ContentEditingBuilder.CreateSimpleContent(_parentContentType.Key, "Root Document"); + Attempt createRootResult = await ContentEditingService.CreateAsync(rootCreateModel, Constants.Security.SuperUserKey); + Assert.IsTrue(createRootResult.Success); + IContent? rootDocument = createRootResult.Result.Content; + + // Create Child Document under Root + ContentCreateModel childCreateModel = ContentEditingBuilder.CreateSimpleContent( + _childContentType.Key, + "Child Document", + rootDocument!.Key); + Attempt createChildResult = await ContentEditingService.CreateAsync(childCreateModel, Constants.Security.SuperUserKey); + Assert.IsTrue(createChildResult.Success); + } +} From 61184c5277d89276414e7891e8ee15f214c51324 Mon Sep 17 00:00:00 2001 From: Zeegaan Date: Wed, 19 Nov 2025 17:05:56 +0900 Subject: [PATCH 06/10] Remove obsolete messages --- .../Tests/ContentTests/SearchService/DocumentTypeTests.cs | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/Umbraco.Test.Search.Examine.Integration/Tests/ContentTests/SearchService/DocumentTypeTests.cs b/src/Umbraco.Test.Search.Examine.Integration/Tests/ContentTests/SearchService/DocumentTypeTests.cs index 0c7de37..f35b534 100644 --- a/src/Umbraco.Test.Search.Examine.Integration/Tests/ContentTests/SearchService/DocumentTypeTests.cs +++ b/src/Umbraco.Test.Search.Examine.Integration/Tests/ContentTests/SearchService/DocumentTypeTests.cs @@ -54,19 +54,16 @@ protected override void CustomTestSetup(IUmbracoBuilder builder) public async Task CannotSearchForRemovedProperty() { await CreateDocuments(); - // Act + _childContentType.RemovePropertyType("title"); await ContentTypeService.UpdateAsync(_childContentType, Constants.Security.SuperUserKey); await Task.Delay(3000); - IContentType? contentType = await ContentTypeService.GetAsync(_childContentType.Key); - // Assert.That(contentType!.PropertyTypes.Any(), Is.False); SearchResult finalResults = await Searcher.SearchAsync( Cms.Search.Core.Constants.IndexAliases.DraftContent, query: "Home Page"); - // We should still find the Assert.That(finalResults.Total, Is.EqualTo(1)); } @@ -75,7 +72,6 @@ public async Task CannotSearchForRemovedDocument() { await CreateDocuments(); - // Act await ContentTypeService.DeleteAsync(_childContentType.Key, Constants.Security.SuperUserKey); await Task.Delay(3000); From 57801c3a3f5370fea969c997df02f893ba1e354d Mon Sep 17 00:00:00 2001 From: Zeegaan Date: Thu, 20 Nov 2025 12:55:16 +0900 Subject: [PATCH 07/10] Rebuild when index has object type document instead, so we don't rebuild members etc --- src/Umbraco.Cms.Search.Core/Configuration/IndexOptions.cs | 2 -- .../RebuildIndexesNotificationHandler.cs | 8 ++++++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/Umbraco.Cms.Search.Core/Configuration/IndexOptions.cs b/src/Umbraco.Cms.Search.Core/Configuration/IndexOptions.cs index ca59ed0..ec4f743 100644 --- a/src/Umbraco.Cms.Search.Core/Configuration/IndexOptions.cs +++ b/src/Umbraco.Cms.Search.Core/Configuration/IndexOptions.cs @@ -9,8 +9,6 @@ public sealed class IndexOptions { private readonly Dictionary _register = []; - public required string[] IncludeRebuildWhenLanguageDeleted { get; set; } = [Constants.IndexAliases.DraftContent, Constants.IndexAliases.PublishedContent]; - public void RegisterIndex(string indexAlias, params UmbracoObjectTypes[] containedObjectTypes) where TIndexer : class, IIndexer where TSearcher : class, ISearcher diff --git a/src/Umbraco.Cms.Search.Core/NotificationHandlers/RebuildIndexesNotificationHandler.cs b/src/Umbraco.Cms.Search.Core/NotificationHandlers/RebuildIndexesNotificationHandler.cs index 90ea095..03ea9ef 100644 --- a/src/Umbraco.Cms.Search.Core/NotificationHandlers/RebuildIndexesNotificationHandler.cs +++ b/src/Umbraco.Cms.Search.Core/NotificationHandlers/RebuildIndexesNotificationHandler.cs @@ -1,6 +1,7 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Umbraco.Cms.Core.Events; +using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Notifications; using Umbraco.Cms.Search.Core.Configuration; using Umbraco.Cms.Search.Core.Services.ContentIndexing; @@ -36,9 +37,12 @@ public void Handle(LanguageDeletedNotification notification) { _logger.LogInformation("Rebuilding search indexes after language deletion..."); - foreach (var indexAlias in _options.IncludeRebuildWhenLanguageDeleted) + foreach (var indexRegistration in _options.GetIndexRegistrations()) { - _contentIndexingService.Rebuild(indexAlias); + if (indexRegistration.ContainedObjectTypes.Contains(UmbracoObjectTypes.Document)) + { + _contentIndexingService.Rebuild(indexRegistration.IndexAlias); + } } } } From 6af25f2516b4c59a0bc293edc416ddc5fd8dae03 Mon Sep 17 00:00:00 2001 From: Zeegaan Date: Thu, 20 Nov 2025 14:34:28 +0900 Subject: [PATCH 08/10] Fix content type changes, implement one for members and media aswell --- .../RebuildIndexesNotificationHandler.cs | 39 +++++++++++++++---- 1 file changed, 32 insertions(+), 7 deletions(-) diff --git a/src/Umbraco.Cms.Search.Core/NotificationHandlers/RebuildIndexesNotificationHandler.cs b/src/Umbraco.Cms.Search.Core/NotificationHandlers/RebuildIndexesNotificationHandler.cs index 864334c..e63bd76 100644 --- a/src/Umbraco.Cms.Search.Core/NotificationHandlers/RebuildIndexesNotificationHandler.cs +++ b/src/Umbraco.Cms.Search.Core/NotificationHandlers/RebuildIndexesNotificationHandler.cs @@ -11,7 +11,10 @@ namespace Umbraco.Cms.Search.Core.NotificationHandlers; internal sealed class RebuildIndexesNotificationHandler : INotificationHandler, INotificationHandler, - INotificationHandler + INotificationHandler, + INotificationHandler, + INotificationHandler + { private readonly IContentIndexingService _contentIndexingService; private readonly ILogger _logger; @@ -51,14 +54,36 @@ public void Handle(LanguageDeletedNotification notification) public void Handle(ContentTypeChangedNotification notification) { - if (notification.Changes.Any(x => x.ChangeTypes is ContentTypeChangeTypes.RefreshMain or ContentTypeChangeTypes.Remove )) + RebuildByObjectType(notification.Changes, UmbracoObjectTypes.Document); + } + + public void Handle(MemberTypeChangedNotification notification) + { + RebuildByObjectType(notification.Changes, UmbracoObjectTypes.Member); + } + + public void Handle(MediaTypeChangedNotification notification) + { + RebuildByObjectType(notification.Changes, UmbracoObjectTypes.Media); + } + + private void RebuildByObjectType(IEnumerable> changes, UmbracoObjectTypes objectType) + where T : class, IContentTypeComposition + { + foreach (var change in changes) { - _logger.LogInformation("Rebuilding search indexes after content type update..."); + if (change.ChangeTypes is not (ContentTypeChangeTypes.RefreshMain or ContentTypeChangeTypes.Remove)) + { + continue; + } - _contentIndexingService.Rebuild(Constants.IndexAliases.PublishedContent); - _contentIndexingService.Rebuild(Constants.IndexAliases.DraftContent); - _contentIndexingService.Rebuild(Constants.IndexAliases.DraftMedia); - _contentIndexingService.Rebuild(Constants.IndexAliases.DraftMembers); + foreach (var indexRegistration in _options.GetIndexRegistrations()) + { + if (indexRegistration.ContainedObjectTypes.Contains(objectType)) + { + _contentIndexingService.Rebuild(indexRegistration.IndexAlias); + } + } } } } From 56828bfba9238d1b088afd71bb7083ae52ae4abe Mon Sep 17 00:00:00 2001 From: kjac Date: Thu, 20 Nov 2025 08:54:32 +0100 Subject: [PATCH 09/10] Clean up after merge, make the tests inherit SearcherTestBase and verify assertions before making changes --- .../SearchService/DocumentTypeTests.cs | 75 ++++++++++--------- 1 file changed, 39 insertions(+), 36 deletions(-) diff --git a/src/Umbraco.Test.Search.Examine.Integration/Tests/ContentTests/SearchService/DocumentTypeTests.cs b/src/Umbraco.Test.Search.Examine.Integration/Tests/ContentTests/SearchService/DocumentTypeTests.cs index f35b534..c999d6f 100644 --- a/src/Umbraco.Test.Search.Examine.Integration/Tests/ContentTests/SearchService/DocumentTypeTests.cs +++ b/src/Umbraco.Test.Search.Examine.Integration/Tests/ContentTests/SearchService/DocumentTypeTests.cs @@ -1,7 +1,5 @@ using NUnit.Framework; using Umbraco.Cms.Core; -using Umbraco.Cms.Core.Cache; -using Umbraco.Cms.Core.HostedServices; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.ContentEditing; using Umbraco.Cms.Core.Models.ContentTypeEditing; @@ -9,28 +7,18 @@ using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Services.ContentTypeEditing; using Umbraco.Cms.Core.Services.OperationStatus; -using Umbraco.Cms.Core.Sync; -using Umbraco.Cms.Search.Core.DependencyInjection; using Umbraco.Cms.Search.Core.Models.Searching; using Umbraco.Cms.Search.Core.NotificationHandlers; -using Umbraco.Cms.Search.Core.Services; using Umbraco.Cms.Tests.Common.Builders; using Umbraco.Cms.Tests.Common.TestHelpers; using Umbraco.Cms.Tests.Common.Testing; -using Umbraco.Cms.Tests.Integration.Testing; -using Umbraco.Test.Search.Examine.Integration.Extensions; -using Umbraco.Test.Search.Examine.Integration.Tests.ContentTests.IndexService; namespace Umbraco.Test.Search.Examine.Integration.Tests.ContentTests.SearchService; [TestFixture] [UmbracoTest(Database = UmbracoTestOptions.Database.NewSchemaPerTest)] -public class DocumentTypeTests : UmbracoIntegrationTest +public class DocumentTypeTests : SearcherTestBase { - private IContentTypeService ContentTypeService => GetRequiredService(); - - private ISearcher Searcher => GetRequiredService(); - private IContentTypeEditingService ContentTypeEditingService => GetRequiredService(); private IContentEditingService ContentEditingService => GetRequiredService(); @@ -38,53 +26,65 @@ public class DocumentTypeTests : UmbracoIntegrationTest private IContentType _parentContentType = null!; private IContentType _childContentType = null!; - protected override void CustomTestSetup(IUmbracoBuilder builder) { - builder.AddNotificationHandler(); - builder.Services.AddUnique(); - builder.Services.AddUnique(); - builder.AddExamineSearchProviderForTest(); - builder.AddSearchCore(); + base.CustomTestSetup(builder); builder.AddNotificationHandler(); - builder.AddNotificationHandler(); } [Test] - public async Task CannotSearchForRemovedProperty() + public async Task CannotSearchForRemovedPropertyType() { - await CreateDocuments(); + await CreateDocumentsAndWaitForIndexing(); + + SearchResult results = await Searcher.SearchAsync( + Cms.Search.Core.Constants.IndexAliases.DraftContent, + query: "Home Page"); + + Assert.That(results.Total, Is.EqualTo(2)); _childContentType.RemovePropertyType("title"); await ContentTypeService.UpdateAsync(_childContentType, Constants.Security.SuperUserKey); - await Task.Delay(3000); + await WaitForIndexesToRebuild(); - SearchResult finalResults = await Searcher.SearchAsync( + results = await Searcher.SearchAsync( Cms.Search.Core.Constants.IndexAliases.DraftContent, query: "Home Page"); - Assert.That(finalResults.Total, Is.EqualTo(1)); + Assert.That(results.Total, Is.EqualTo(1)); } [Test] - public async Task CannotSearchForRemovedDocument() + public async Task CannotSearchForRemovedDocumentType() { - await CreateDocuments(); + await CreateDocumentsAndWaitForIndexing(); + + SearchResult results = await Searcher.SearchAsync( + Cms.Search.Core.Constants.IndexAliases.DraftContent, + query: "Home Page"); + + Assert.That(results.Total, Is.EqualTo(2)); await ContentTypeService.DeleteAsync(_childContentType.Key, Constants.Security.SuperUserKey); - await Task.Delay(3000); - IContentType? contentType = await ContentTypeService.GetAsync(_childContentType.Key); + await WaitForIndexesToRebuild(); - SearchResult finalResults = await Searcher.SearchAsync( + results = await Searcher.SearchAsync( Cms.Search.Core.Constants.IndexAliases.DraftContent, query: "Home Page"); - Assert.That(finalResults.Total, Is.EqualTo(1)); + Assert.That(results.Total, Is.EqualTo(1)); + + IContentType? contentType = await ContentTypeService.GetAsync(_childContentType.Key); Assert.That(contentType, Is.Null); } + private async Task CreateDocumentsAndWaitForIndexing() + => await WaitForIndexing( + Cms.Search.Core.Constants.IndexAliases.DraftContent, + async () => await CreateDocuments()); + private async Task CreateDocuments() { ContentTypeCreateModel parentContentTypeCreateModel = ContentTypeEditingBuilder.CreateSimpleContentType( @@ -93,7 +93,7 @@ private async Task CreateDocuments() Attempt parentContentTypeAttempt = await ContentTypeEditingService.CreateAsync( parentContentTypeCreateModel, Constants.Security.SuperUserKey); - Assert.IsTrue(parentContentTypeAttempt.Success); + Assert.That(parentContentTypeAttempt.Success, Is.True); _parentContentType = parentContentTypeAttempt.Result!; // Create Child ContentType @@ -103,7 +103,7 @@ private async Task CreateDocuments() Attempt childContentTypeAttempt = await ContentTypeEditingService.CreateAsync( childContentTypeCreateModel, Constants.Security.SuperUserKey); - Assert.IsTrue(childContentTypeAttempt.Success); + Assert.That(childContentTypeAttempt.Success, Is.True); _childContentType = childContentTypeAttempt.Result!; // Update Parent ContentType to allow Child ContentType @@ -116,12 +116,12 @@ private async Task CreateDocuments() _parentContentType, parentContentTypeUpdateModel, Constants.Security.SuperUserKey); - Assert.IsTrue(updatedParentResult.Success); + Assert.That(updatedParentResult.Success, Is.True); // Create Root Document (Parent) ContentCreateModel rootCreateModel = ContentEditingBuilder.CreateSimpleContent(_parentContentType.Key, "Root Document"); Attempt createRootResult = await ContentEditingService.CreateAsync(rootCreateModel, Constants.Security.SuperUserKey); - Assert.IsTrue(createRootResult.Success); + Assert.That(createRootResult.Success, Is.True); IContent? rootDocument = createRootResult.Result.Content; // Create Child Document under Root @@ -130,6 +130,9 @@ private async Task CreateDocuments() "Child Document", rootDocument!.Key); Attempt createChildResult = await ContentEditingService.CreateAsync(childCreateModel, Constants.Security.SuperUserKey); - Assert.IsTrue(createChildResult.Success); + Assert.That(createChildResult.Success, Is.True); } + + private async Task WaitForIndexesToRebuild() + => await Task.Delay(3000); } From 36e9497b39db8962c9832344d92d4bba1c949820 Mon Sep 17 00:00:00 2001 From: kjac Date: Thu, 20 Nov 2025 08:55:38 +0100 Subject: [PATCH 10/10] Clean up after merge and make the notigication handler confirm to code standard --- .../RebuildIndexesNotificationHandler.cs | 20 +++++++------------ 1 file changed, 7 insertions(+), 13 deletions(-) diff --git a/src/Umbraco.Cms.Search.Core/NotificationHandlers/RebuildIndexesNotificationHandler.cs b/src/Umbraco.Cms.Search.Core/NotificationHandlers/RebuildIndexesNotificationHandler.cs index e7d14af..49d4fd3 100644 --- a/src/Umbraco.Cms.Search.Core/NotificationHandlers/RebuildIndexesNotificationHandler.cs +++ b/src/Umbraco.Cms.Search.Core/NotificationHandlers/RebuildIndexesNotificationHandler.cs @@ -5,7 +5,7 @@ using Umbraco.Cms.Core.Notifications; using Umbraco.Cms.Core.Services.Changes; using Umbraco.Cms.Search.Core.Configuration; -using Umbraco.Cms.Search.Core.Configuration; +using Umbraco.Cms.Search.Core.Models.Configuration; using Umbraco.Cms.Search.Core.Services.ContentIndexing; namespace Umbraco.Cms.Search.Core.NotificationHandlers; @@ -44,7 +44,7 @@ public void Handle(LanguageDeletedNotification notification) { _logger.LogInformation("Rebuilding search indexes after language deletion..."); - foreach (var indexRegistration in _options.GetIndexRegistrations()) + foreach (IndexRegistration indexRegistration in _options.GetIndexRegistrations()) { if (indexRegistration.ContainedObjectTypes.Contains(UmbracoObjectTypes.Document)) { @@ -54,31 +54,25 @@ public void Handle(LanguageDeletedNotification notification) } public void Handle(ContentTypeChangedNotification notification) - { - RebuildByObjectType(notification.Changes, UmbracoObjectTypes.Document); - } + => RebuildByObjectType(notification.Changes, UmbracoObjectTypes.Document); public void Handle(MemberTypeChangedNotification notification) - { - RebuildByObjectType(notification.Changes, UmbracoObjectTypes.Member); - } + => RebuildByObjectType(notification.Changes, UmbracoObjectTypes.Member); public void Handle(MediaTypeChangedNotification notification) - { - RebuildByObjectType(notification.Changes, UmbracoObjectTypes.Media); - } + => RebuildByObjectType(notification.Changes, UmbracoObjectTypes.Media); private void RebuildByObjectType(IEnumerable> changes, UmbracoObjectTypes objectType) where T : class, IContentTypeComposition { - foreach (var change in changes) + foreach (ContentTypeChange change in changes) { if (change.ChangeTypes is not (ContentTypeChangeTypes.RefreshMain or ContentTypeChangeTypes.Remove)) { continue; } - foreach (var indexRegistration in _options.GetIndexRegistrations()) + foreach (IndexRegistration indexRegistration in _options.GetIndexRegistrations()) { if (indexRegistration.ContainedObjectTypes.Contains(objectType)) {