From 87208b8517761768635ac9f624fed32896666480 Mon Sep 17 00:00:00 2001 From: bkapustik <82807109+bkapustik@users.noreply.github.com> Date: Mon, 18 Nov 2024 17:47:27 +0100 Subject: [PATCH] feat(indexing-delay): added support for optional customizable delay between each reindexing --- README.md | 6 +- database/DancingGoat_AzureAISearch.bak | Bin 38920192 -> 38916970 bytes examples/DancingGoat/Data/Template.zip | Bin 5103015 -> 5103014 bytes examples/DancingGoat/appsettings.json | 3 +- .../Admin/AzureSearchAdminModule.cs | 110 +++---- .../Admin/AzureSearchIndexIncludedPath.cs | 74 ++--- .../Admin/AzureSearchIndexPermissions.cs | 12 +- .../AzureSearchIndexStatisticsViewModel.cs | 28 +- .../AzureSearchIndexConfigurationComponent.cs | 194 +++++------ ...IAzureSearchConfigurationStorageService.cs | 34 +- .../Admin/ModificationResponse.cs | 26 +- .../Admin/ModificationResult.cs | 14 +- .../Admin/Providers/ChannelOptionsProvider.cs | 46 +-- .../IndexingStrategyOptionsProvider.cs | 28 +- .../Providers/LanguageOptionsProvider.cs | 144 ++++----- .../Admin/UIPages/AzureSearchApplication.cs | 58 ++-- .../Admin/UIPages/IndexAliasEditPage.cs | 138 ++++---- .../Admin/UIPages/IndexEditPage.cs | 140 ++++---- .../Aliasing/AzureSearchIndexAlias.cs | 62 ++-- .../Aliasing/AzureSearchIndexAliasService.cs | 80 ++--- .../Aliasing/AzureSearchIndexAliasStore.cs | 186 +++++------ .../Aliasing/IAzureSearchIndexAliasService.cs | 64 ++-- .../AzureSearchQueueWorker.cs | 126 ++++---- .../AzureSearchStartupExtensions.cs | 286 ++++++++-------- .../Indexing/AzureSearchIndexClientService.cs | 186 +++++------ .../Indexing/AzureSearchIndexComparer.cs | 96 +++--- .../Indexing/AzureSearchIndexStore.cs | 208 ++++++------ .../Indexing/AzureSearchOptions.cs | 5 + .../Indexing/AzureSearchQueueItem.cs | 94 +++--- .../Indexing/AzureSearchTaskType.cs | 58 ++-- .../BaseAzureSearchIndexingStrategy.cs | 116 +++---- .../DefaultAzureSearchTaskProcessor.cs | 37 ++- .../Indexing/IAzureSearchClient.cs | 140 ++++---- .../IAzureSearchIndexClientService.cs | 64 ++-- .../Indexing/IAzureSearchIndexingStrategy.cs | 94 +++--- .../Indexing/IAzureSearchModel.cs | 28 +- .../Indexing/IAzureSearchTaskLogger.cs | 50 +-- .../Indexing/IAzureSearchTaskProcessor.cs | 36 +-- .../Indexing/IIndexEventItemModel.cs | 304 +++++++++--------- .../Indexing/SemanticRankingConfiguration.cs | 76 ++--- .../Indexing/StrategyStorage.cs | 32 +- .../Resources/AzureSearchResources.cs | 28 +- .../Search/AzureSearchQueryClientOptions.cs | 26 +- .../Search/AzureSearchQueryClientService.cs | 38 +-- .../Search/IAzureSearchQueryClientService.cs | 22 +- .../ServiceProviderExtensions.cs | 50 +-- .../Data/MockDataProvider.cs | 116 +++---- .../Tests/IndexStoreTests.cs | 116 +++---- .../Tests/IndexedItemModelExtensionsTests.cs | 296 ++++++++--------- 49 files changed, 2102 insertions(+), 2073 deletions(-) diff --git a/README.md b/README.md index 48f293a..0feabed 100644 --- a/README.md +++ b/README.md @@ -38,7 +38,11 @@ dotnet add package Kentico.Xperience.AzureSearch "CMSAzureSearch": { "SearchServiceEndPoint": "", "SearchServiceAdminApiKey": "", - "SearchServiceQueryApiKey": "" + "SearchServiceQueryApiKey": "", + + // Opotionally add delay between indexing items. + // This is useful if there are a lot of pages and items on your web which when crawled would increase the performance requirements. + "IndexItemDelay" : 0 } ``` diff --git a/database/DancingGoat_AzureAISearch.bak b/database/DancingGoat_AzureAISearch.bak index 20dee53da12e670f065c0befd06bfdba4eff4be2..015ec20c2ddff82933cafa55a46c511206bcaf43 100644 GIT binary patch delta 27950 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 Gn*Rrn2zo65 delta 32318 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 diff --git a/examples/DancingGoat/Data/Template.zip b/examples/DancingGoat/Data/Template.zip index 8ec3f17dfcc3dd8f22bfdda1f2a5280631ca50e3..764976d14f2c89aa9cb4d58f6c44450e9cffd46e 100644 GIT binary patch delta 247 zcmWN=TQWic0D$4CTtcF9tt6MqwGv%Ugou>ZVhtYt89UJi%=BPI-p>O3-^@4tokhR> zNrWgd=9p)JMdB>6%nAusNwUT|Dbi%vV3RGj*TMnaGavVlw zANklPuK3hvuDa%PU%2i|U-{ZMzIDS*hrV;m_inr62S57BUHAO#7r*+=eZTv|pB_xY IX!!N_4 -/// Manages administration features and integration. -/// -internal class AzureSearchAdminModule : AdminModule -{ - private IAzureSearchConfigurationStorageService storageService = null!; - private AzureSearchModuleInstaller installer = null!; - - public AzureSearchAdminModule() : base(nameof(AzureSearchAdminModule)) { } - - protected override void OnInit(ModuleInitParameters parameters) - { - base.OnInit(parameters); - - var options = Service.Resolve>(); - - if (!options.Value?.SearchServiceEnabled ?? false) - { - return; - } - - RegisterClientModule("kentico", "xperience-integrations-azuresearch"); - - var services = parameters.Services; - - installer = services.GetRequiredService(); - storageService = services.GetRequiredService(); - - ApplicationEvents.Initialized.Execute += InitializeModule; - } - - private void InitializeModule(object? sender, EventArgs e) - { - installer.Install(); - - AzureSearchIndexStore.SetIndicies(storageService); - AzureSearchIndexAliasStore.SetAliases(storageService); - } -} +using CMS; +using CMS.Base; +using CMS.Core; + +using Kentico.Xperience.Admin.Base; +using Kentico.Xperience.AzureSearch.Admin; +using Kentico.Xperience.AzureSearch.Aliasing; +using Kentico.Xperience.AzureSearch.Indexing; + +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; + +[assembly: RegisterModule(typeof(AzureSearchAdminModule))] + +namespace Kentico.Xperience.AzureSearch.Admin; + +/// +/// Manages administration features and integration. +/// +internal class AzureSearchAdminModule : AdminModule +{ + private IAzureSearchConfigurationStorageService storageService = null!; + private AzureSearchModuleInstaller installer = null!; + + public AzureSearchAdminModule() : base(nameof(AzureSearchAdminModule)) { } + + protected override void OnInit(ModuleInitParameters parameters) + { + base.OnInit(parameters); + + var options = Service.Resolve>(); + + if (!options.Value?.SearchServiceEnabled ?? false) + { + return; + } + + RegisterClientModule("kentico", "xperience-integrations-azuresearch"); + + var services = parameters.Services; + + installer = services.GetRequiredService(); + storageService = services.GetRequiredService(); + + ApplicationEvents.Initialized.Execute += InitializeModule; + } + + private void InitializeModule(object? sender, EventArgs e) + { + installer.Install(); + + AzureSearchIndexStore.SetIndicies(storageService); + AzureSearchIndexAliasStore.SetAliases(storageService); + } +} diff --git a/src/Kentico.Xperience.AzureSearch/Admin/AzureSearchIndexIncludedPath.cs b/src/Kentico.Xperience.AzureSearch/Admin/AzureSearchIndexIncludedPath.cs index 09f4d76..832e0fa 100644 --- a/src/Kentico.Xperience.AzureSearch/Admin/AzureSearchIndexIncludedPath.cs +++ b/src/Kentico.Xperience.AzureSearch/Admin/AzureSearchIndexIncludedPath.cs @@ -1,37 +1,37 @@ -using System.Text.Json.Serialization; - -namespace Kentico.Xperience.AzureSearch.Admin; - -public class AzureSearchIndexIncludedPath -{ - /// - /// The node alias pattern that will be used to match pages in the content tree for indexing. - /// - /// For example, "/Blogs/Products/" will index all pages under the "Products" page. - public string AliasPath { get; } - - /// - /// A list of content types under the specified that will be indexed. - /// - public List ContentTypes { get; set; } = []; - - /// - /// The internal identifier of the included path. - /// - public string? Identifier { get; set; } - - [JsonConstructor] - public AzureSearchIndexIncludedPath(string aliasPath) => AliasPath = aliasPath; - - /// - /// - /// - /// - /// - public AzureSearchIndexIncludedPath(AzureSearchIncludedPathItemInfo indexPath, IEnumerable contentTypes) - { - AliasPath = indexPath.AzureSearchIncludedPathItemAliasPath; - ContentTypes = contentTypes.ToList(); - Identifier = indexPath.AzureSearchIncludedPathItemId.ToString(); - } -} +using System.Text.Json.Serialization; + +namespace Kentico.Xperience.AzureSearch.Admin; + +public class AzureSearchIndexIncludedPath +{ + /// + /// The node alias pattern that will be used to match pages in the content tree for indexing. + /// + /// For example, "/Blogs/Products/" will index all pages under the "Products" page. + public string AliasPath { get; } + + /// + /// A list of content types under the specified that will be indexed. + /// + public List ContentTypes { get; set; } = []; + + /// + /// The internal identifier of the included path. + /// + public string? Identifier { get; set; } + + [JsonConstructor] + public AzureSearchIndexIncludedPath(string aliasPath) => AliasPath = aliasPath; + + /// + /// + /// + /// + /// + public AzureSearchIndexIncludedPath(AzureSearchIncludedPathItemInfo indexPath, IEnumerable contentTypes) + { + AliasPath = indexPath.AzureSearchIncludedPathItemAliasPath; + ContentTypes = contentTypes.ToList(); + Identifier = indexPath.AzureSearchIncludedPathItemId.ToString(); + } +} diff --git a/src/Kentico.Xperience.AzureSearch/Admin/AzureSearchIndexPermissions.cs b/src/Kentico.Xperience.AzureSearch/Admin/AzureSearchIndexPermissions.cs index b7c1c75..8e0feb5 100644 --- a/src/Kentico.Xperience.AzureSearch/Admin/AzureSearchIndexPermissions.cs +++ b/src/Kentico.Xperience.AzureSearch/Admin/AzureSearchIndexPermissions.cs @@ -1,6 +1,6 @@ -namespace Kentico.Xperience.AzureSearch.Admin; - -internal static class AzureSearchIndexPermissions -{ - public const string REBUILD = "Rebuild"; -} +namespace Kentico.Xperience.AzureSearch.Admin; + +internal static class AzureSearchIndexPermissions +{ + public const string REBUILD = "Rebuild"; +} diff --git a/src/Kentico.Xperience.AzureSearch/Admin/AzureSearchIndexStatisticsViewModel.cs b/src/Kentico.Xperience.AzureSearch/Admin/AzureSearchIndexStatisticsViewModel.cs index 66b4e90..982e107 100644 --- a/src/Kentico.Xperience.AzureSearch/Admin/AzureSearchIndexStatisticsViewModel.cs +++ b/src/Kentico.Xperience.AzureSearch/Admin/AzureSearchIndexStatisticsViewModel.cs @@ -1,14 +1,14 @@ -namespace Kentico.Xperience.AzureSearch.Admin; - -public class AzureSearchIndexStatisticsViewModel -{ - // - // Summary: - // Index name. - public string? Name { get; set; } - - // - // Summary: - // Number of records contained in the index - public long Entries { get; set; } -} +namespace Kentico.Xperience.AzureSearch.Admin; + +public class AzureSearchIndexStatisticsViewModel +{ + // + // Summary: + // Index name. + public string? Name { get; set; } + + // + // Summary: + // Number of records contained in the index + public long Entries { get; set; } +} diff --git a/src/Kentico.Xperience.AzureSearch/Admin/Components/AzureSearchIndexConfigurationComponent.cs b/src/Kentico.Xperience.AzureSearch/Admin/Components/AzureSearchIndexConfigurationComponent.cs index 4396861..7df2a98 100644 --- a/src/Kentico.Xperience.AzureSearch/Admin/Components/AzureSearchIndexConfigurationComponent.cs +++ b/src/Kentico.Xperience.AzureSearch/Admin/Components/AzureSearchIndexConfigurationComponent.cs @@ -1,97 +1,97 @@ -using CMS.DataEngine; - -using Kentico.Xperience.Admin.Base; -using Kentico.Xperience.Admin.Base.FormAnnotations; -using Kentico.Xperience.Admin.Base.Forms; -using Kentico.Xperience.AzureSearch.Admin; - -[assembly: RegisterFormComponent( - identifier: AzureSearchIndexConfigurationComponent.IDENTIFIER, - componentType: typeof(AzureSearchIndexConfigurationComponent), - name: "AzureSearch Search Index Configuration")] - -namespace Kentico.Xperience.AzureSearch.Admin; - -#pragma warning disable S2094 // intentionally empty class -public class AzureSearchIndexConfigurationComponentProperties : FormComponentProperties -{ -} -#pragma warning restore - -public class AzureSearchIndexConfigurationComponentClientProperties : FormComponentClientProperties> -{ - public IEnumerable? PossibleContentTypeItems { get; set; } -} - -public sealed class AzureSearchIndexConfigurationComponentAttribute : FormComponentAttribute -{ -} - -[ComponentAttribute(typeof(AzureSearchIndexConfigurationComponentAttribute))] -public class AzureSearchIndexConfigurationComponent : FormComponent> -{ - public const string IDENTIFIER = "kentico.xperience-integrations-azuresearch.azuresearch-index-configuration"; - - internal List? Value { get; set; } - - public override string ClientComponentName => "@kentico/xperience-integrations-azuresearch/AzureSearchIndexConfiguration"; - - public override IEnumerable GetValue() => Value ?? []; - public override void SetValue(IEnumerable value) => Value = value.ToList(); - - [FormComponentCommand] - public Task> DeletePath(string path) - { - var toRemove = Value?.Find(x => Equals(x.AliasPath == path, StringComparison.OrdinalIgnoreCase)); - - if (toRemove != null) - { - Value?.Remove(toRemove); - return Task.FromResult(ResponseFrom(new RowActionResult(false))); - } - - return Task.FromResult(ResponseFrom(new RowActionResult(false))); - } - - [FormComponentCommand] - public Task> SavePath(AzureSearchIndexIncludedPath path) - { - var value = Value?.SingleOrDefault(x => Equals(x.AliasPath == path.AliasPath, StringComparison.OrdinalIgnoreCase)); - - if (value is not null) - { - Value?.Remove(value); - } - - Value?.Add(path); - - return Task.FromResult(ResponseFrom(new RowActionResult(false))); - } - - [FormComponentCommand] - public Task> AddPath(string path) - { - if (Value?.Exists(x => x.AliasPath == path) ?? false) - { - return Task.FromResult(ResponseFrom(new RowActionResult(false))); - } - - Value?.Add(new AzureSearchIndexIncludedPath(path)); - - return Task.FromResult(ResponseFrom(new RowActionResult(false))); - } - - protected override async Task ConfigureClientProperties(AzureSearchIndexConfigurationComponentClientProperties properties) - { - var allWebsiteContentTypes = DataClassInfoProvider.ProviderObject - .Get() - .WhereEquals(nameof(DataClassInfo.ClassContentTypeType), "Website") - .GetEnumerableTypedResult() - .Select(x => new AzureSearchIndexContentType(x.ClassName, x.ClassDisplayName)); - - properties.Value = Value ?? []; - properties.PossibleContentTypeItems = allWebsiteContentTypes.ToList(); - - await base.ConfigureClientProperties(properties); - } -} +using CMS.DataEngine; + +using Kentico.Xperience.Admin.Base; +using Kentico.Xperience.Admin.Base.FormAnnotations; +using Kentico.Xperience.Admin.Base.Forms; +using Kentico.Xperience.AzureSearch.Admin; + +[assembly: RegisterFormComponent( + identifier: AzureSearchIndexConfigurationComponent.IDENTIFIER, + componentType: typeof(AzureSearchIndexConfigurationComponent), + name: "AzureSearch Search Index Configuration")] + +namespace Kentico.Xperience.AzureSearch.Admin; + +#pragma warning disable S2094 // intentionally empty class +public class AzureSearchIndexConfigurationComponentProperties : FormComponentProperties +{ +} +#pragma warning restore + +public class AzureSearchIndexConfigurationComponentClientProperties : FormComponentClientProperties> +{ + public IEnumerable? PossibleContentTypeItems { get; set; } +} + +public sealed class AzureSearchIndexConfigurationComponentAttribute : FormComponentAttribute +{ +} + +[ComponentAttribute(typeof(AzureSearchIndexConfigurationComponentAttribute))] +public class AzureSearchIndexConfigurationComponent : FormComponent> +{ + public const string IDENTIFIER = "kentico.xperience-integrations-azuresearch.azuresearch-index-configuration"; + + internal List? Value { get; set; } + + public override string ClientComponentName => "@kentico/xperience-integrations-azuresearch/AzureSearchIndexConfiguration"; + + public override IEnumerable GetValue() => Value ?? []; + public override void SetValue(IEnumerable value) => Value = value.ToList(); + + [FormComponentCommand] + public Task> DeletePath(string path) + { + var toRemove = Value?.Find(x => Equals(x.AliasPath == path, StringComparison.OrdinalIgnoreCase)); + + if (toRemove != null) + { + Value?.Remove(toRemove); + return Task.FromResult(ResponseFrom(new RowActionResult(false))); + } + + return Task.FromResult(ResponseFrom(new RowActionResult(false))); + } + + [FormComponentCommand] + public Task> SavePath(AzureSearchIndexIncludedPath path) + { + var value = Value?.SingleOrDefault(x => Equals(x.AliasPath == path.AliasPath, StringComparison.OrdinalIgnoreCase)); + + if (value is not null) + { + Value?.Remove(value); + } + + Value?.Add(path); + + return Task.FromResult(ResponseFrom(new RowActionResult(false))); + } + + [FormComponentCommand] + public Task> AddPath(string path) + { + if (Value?.Exists(x => x.AliasPath == path) ?? false) + { + return Task.FromResult(ResponseFrom(new RowActionResult(false))); + } + + Value?.Add(new AzureSearchIndexIncludedPath(path)); + + return Task.FromResult(ResponseFrom(new RowActionResult(false))); + } + + protected override async Task ConfigureClientProperties(AzureSearchIndexConfigurationComponentClientProperties properties) + { + var allWebsiteContentTypes = DataClassInfoProvider.ProviderObject + .Get() + .WhereEquals(nameof(DataClassInfo.ClassContentTypeType), "Website") + .GetEnumerableTypedResult() + .Select(x => new AzureSearchIndexContentType(x.ClassName, x.ClassDisplayName)); + + properties.Value = Value ?? []; + properties.PossibleContentTypeItems = allWebsiteContentTypes.ToList(); + + await base.ConfigureClientProperties(properties); + } +} diff --git a/src/Kentico.Xperience.AzureSearch/Admin/IAzureSearchConfigurationStorageService.cs b/src/Kentico.Xperience.AzureSearch/Admin/IAzureSearchConfigurationStorageService.cs index 37558e2..2167b00 100644 --- a/src/Kentico.Xperience.AzureSearch/Admin/IAzureSearchConfigurationStorageService.cs +++ b/src/Kentico.Xperience.AzureSearch/Admin/IAzureSearchConfigurationStorageService.cs @@ -1,17 +1,17 @@ -namespace Kentico.Xperience.AzureSearch.Admin; - -public interface IAzureSearchConfigurationStorageService -{ - bool TryCreateIndex(AzureSearchConfigurationModel configuration); - bool TryCreateAlias(AzureSearchAliasConfigurationModel configuration); - bool TryEditIndex(AzureSearchConfigurationModel configuration); - bool TryEditAlias(AzureSearchAliasConfigurationModel configuration); - bool TryDeleteIndex(int id); - bool TryDeleteAlias(int id); - AzureSearchConfigurationModel? GetIndexDataOrNull(int indexId); - AzureSearchAliasConfigurationModel? GetAliasDataOrNull(int aliasId); - List GetIndexIds(); - List GetAliasIds(); - IEnumerable GetAllIndexData(); - IEnumerable GetAllAliasData(); -} +namespace Kentico.Xperience.AzureSearch.Admin; + +public interface IAzureSearchConfigurationStorageService +{ + bool TryCreateIndex(AzureSearchConfigurationModel configuration); + bool TryCreateAlias(AzureSearchAliasConfigurationModel configuration); + bool TryEditIndex(AzureSearchConfigurationModel configuration); + bool TryEditAlias(AzureSearchAliasConfigurationModel configuration); + bool TryDeleteIndex(int id); + bool TryDeleteAlias(int id); + AzureSearchConfigurationModel? GetIndexDataOrNull(int indexId); + AzureSearchAliasConfigurationModel? GetAliasDataOrNull(int aliasId); + List GetIndexIds(); + List GetAliasIds(); + IEnumerable GetAllIndexData(); + IEnumerable GetAllAliasData(); +} diff --git a/src/Kentico.Xperience.AzureSearch/Admin/ModificationResponse.cs b/src/Kentico.Xperience.AzureSearch/Admin/ModificationResponse.cs index c39ab40..bb284b2 100644 --- a/src/Kentico.Xperience.AzureSearch/Admin/ModificationResponse.cs +++ b/src/Kentico.Xperience.AzureSearch/Admin/ModificationResponse.cs @@ -1,13 +1,13 @@ -namespace Kentico.Xperience.AzureSearch.Admin; - -public class ModificationResponse -{ - public ModificationResult ModificationResult { get; set; } - public List? ErrorMessages { get; set; } - - public ModificationResponse(ModificationResult result, List? errorMessage = null) - { - ModificationResult = result; - ErrorMessages = errorMessage; - } -} +namespace Kentico.Xperience.AzureSearch.Admin; + +public class ModificationResponse +{ + public ModificationResult ModificationResult { get; set; } + public List? ErrorMessages { get; set; } + + public ModificationResponse(ModificationResult result, List? errorMessage = null) + { + ModificationResult = result; + ErrorMessages = errorMessage; + } +} diff --git a/src/Kentico.Xperience.AzureSearch/Admin/ModificationResult.cs b/src/Kentico.Xperience.AzureSearch/Admin/ModificationResult.cs index c81143f..df4b5df 100644 --- a/src/Kentico.Xperience.AzureSearch/Admin/ModificationResult.cs +++ b/src/Kentico.Xperience.AzureSearch/Admin/ModificationResult.cs @@ -1,7 +1,7 @@ -namespace Kentico.Xperience.AzureSearch.Admin; - -public enum ModificationResult -{ - Success, - Failure -} +namespace Kentico.Xperience.AzureSearch.Admin; + +public enum ModificationResult +{ + Success, + Failure +} diff --git a/src/Kentico.Xperience.AzureSearch/Admin/Providers/ChannelOptionsProvider.cs b/src/Kentico.Xperience.AzureSearch/Admin/Providers/ChannelOptionsProvider.cs index aa9b119..345bf79 100644 --- a/src/Kentico.Xperience.AzureSearch/Admin/Providers/ChannelOptionsProvider.cs +++ b/src/Kentico.Xperience.AzureSearch/Admin/Providers/ChannelOptionsProvider.cs @@ -1,23 +1,23 @@ -using CMS.ContentEngine; -using CMS.DataEngine; - -using Kentico.Xperience.Admin.Base.FormAnnotations; - -namespace Kentico.Xperience.AzureSearch.Admin; - -internal class ChannelOptionsProvider : IDropDownOptionsProvider -{ - private readonly IInfoProvider channelInfoProvider; - - public ChannelOptionsProvider(IInfoProvider channelInfoProvider) => this.channelInfoProvider = channelInfoProvider; - - public async Task> GetOptionItems() => - (await channelInfoProvider.Get() - .WhereEquals(nameof(ChannelInfo.ChannelType), nameof(ChannelType.Website)) - .GetEnumerableTypedResultAsync()) - .Select(x => new DropDownOptionItem() - { - Value = x.ChannelName, - Text = x.ChannelDisplayName - }); -} +using CMS.ContentEngine; +using CMS.DataEngine; + +using Kentico.Xperience.Admin.Base.FormAnnotations; + +namespace Kentico.Xperience.AzureSearch.Admin; + +internal class ChannelOptionsProvider : IDropDownOptionsProvider +{ + private readonly IInfoProvider channelInfoProvider; + + public ChannelOptionsProvider(IInfoProvider channelInfoProvider) => this.channelInfoProvider = channelInfoProvider; + + public async Task> GetOptionItems() => + (await channelInfoProvider.Get() + .WhereEquals(nameof(ChannelInfo.ChannelType), nameof(ChannelType.Website)) + .GetEnumerableTypedResultAsync()) + .Select(x => new DropDownOptionItem() + { + Value = x.ChannelName, + Text = x.ChannelDisplayName + }); +} diff --git a/src/Kentico.Xperience.AzureSearch/Admin/Providers/IndexingStrategyOptionsProvider.cs b/src/Kentico.Xperience.AzureSearch/Admin/Providers/IndexingStrategyOptionsProvider.cs index 7246b24..2d94170 100644 --- a/src/Kentico.Xperience.AzureSearch/Admin/Providers/IndexingStrategyOptionsProvider.cs +++ b/src/Kentico.Xperience.AzureSearch/Admin/Providers/IndexingStrategyOptionsProvider.cs @@ -1,14 +1,14 @@ -using Kentico.Xperience.Admin.Base.FormAnnotations; -using Kentico.Xperience.AzureSearch.Indexing; - -namespace Kentico.Xperience.AzureSearch.Admin; - -internal class IndexingStrategyOptionsProvider : IDropDownOptionsProvider -{ - public Task> GetOptionItems() => - Task.FromResult(StrategyStorage.Strategies.Keys.Select(x => new DropDownOptionItem() - { - Value = x, - Text = x - })); -} +using Kentico.Xperience.Admin.Base.FormAnnotations; +using Kentico.Xperience.AzureSearch.Indexing; + +namespace Kentico.Xperience.AzureSearch.Admin; + +internal class IndexingStrategyOptionsProvider : IDropDownOptionsProvider +{ + public Task> GetOptionItems() => + Task.FromResult(StrategyStorage.Strategies.Keys.Select(x => new DropDownOptionItem() + { + Value = x, + Text = x + })); +} diff --git a/src/Kentico.Xperience.AzureSearch/Admin/Providers/LanguageOptionsProvider.cs b/src/Kentico.Xperience.AzureSearch/Admin/Providers/LanguageOptionsProvider.cs index 6079495..897082d 100644 --- a/src/Kentico.Xperience.AzureSearch/Admin/Providers/LanguageOptionsProvider.cs +++ b/src/Kentico.Xperience.AzureSearch/Admin/Providers/LanguageOptionsProvider.cs @@ -1,72 +1,72 @@ -using CMS.ContentEngine; -using CMS.DataEngine; - -using Kentico.Xperience.Admin.Base.FormAnnotations; -using Kentico.Xperience.Admin.Base.Forms; - -namespace Kentico.Xperience.AzureSearch.Admin; - -internal class LanguageOptionsProvider : IGeneralSelectorDataProvider -{ - private readonly IInfoProvider contentLanguageInfoProvider; - - public LanguageOptionsProvider(IInfoProvider contentLanguageInfoProvider) => this.contentLanguageInfoProvider = contentLanguageInfoProvider; - - public async Task> GetItemsAsync(string searchTerm, int pageIndex, CancellationToken cancellationToken) - { - // Prepares a query for retrieving user objects - var itemQuery = contentLanguageInfoProvider.Get(); - - // If a search term is entered, only loads users users whose first name starts with the term - if (!string.IsNullOrEmpty(searchTerm)) - { - itemQuery.WhereStartsWith(nameof(ContentLanguageInfo.ContentLanguageDisplayName), searchTerm); - } - - // Ensures paging of items - itemQuery.Page(pageIndex, 20); - - // Retrieves the users and converts them into ObjectSelectorListItem options - var items = (await itemQuery.GetEnumerableTypedResultAsync()).Select(x => new ObjectSelectorListItem() - { - Value = x.ContentLanguageName, - Text = x.ContentLanguageDisplayName, - IsValid = true - }); - - return new PagedSelectListItems() - { - NextPageAvailable = itemQuery.NextPageAvailable, - Items = items - }; - } - - // Returns ObjectSelectorListItem options for all item values that are currently selected - public async Task>> GetSelectedItemsAsync(IEnumerable selectedValues, CancellationToken cancellationToken) - { - var itemQuery = contentLanguageInfoProvider.Get().Page(0, 20); - var items = (await itemQuery.GetEnumerableTypedResultAsync()).Select(x => new ObjectSelectorListItem() - { - Value = x.ContentLanguageName, - Text = x.ContentLanguageDisplayName, - IsValid = true - }); - - var selectedItems = new List>(); - - if (selectedValues is not null) - { - foreach (string? value in selectedValues) - { - var item = items.FirstOrDefault(x => x.Value == value); - - if (item != default) - { - selectedItems.Add(item); - } - } - } - - return selectedItems; - } -} +using CMS.ContentEngine; +using CMS.DataEngine; + +using Kentico.Xperience.Admin.Base.FormAnnotations; +using Kentico.Xperience.Admin.Base.Forms; + +namespace Kentico.Xperience.AzureSearch.Admin; + +internal class LanguageOptionsProvider : IGeneralSelectorDataProvider +{ + private readonly IInfoProvider contentLanguageInfoProvider; + + public LanguageOptionsProvider(IInfoProvider contentLanguageInfoProvider) => this.contentLanguageInfoProvider = contentLanguageInfoProvider; + + public async Task> GetItemsAsync(string searchTerm, int pageIndex, CancellationToken cancellationToken) + { + // Prepares a query for retrieving user objects + var itemQuery = contentLanguageInfoProvider.Get(); + + // If a search term is entered, only loads users users whose first name starts with the term + if (!string.IsNullOrEmpty(searchTerm)) + { + itemQuery.WhereStartsWith(nameof(ContentLanguageInfo.ContentLanguageDisplayName), searchTerm); + } + + // Ensures paging of items + itemQuery.Page(pageIndex, 20); + + // Retrieves the users and converts them into ObjectSelectorListItem options + var items = (await itemQuery.GetEnumerableTypedResultAsync()).Select(x => new ObjectSelectorListItem() + { + Value = x.ContentLanguageName, + Text = x.ContentLanguageDisplayName, + IsValid = true + }); + + return new PagedSelectListItems() + { + NextPageAvailable = itemQuery.NextPageAvailable, + Items = items + }; + } + + // Returns ObjectSelectorListItem options for all item values that are currently selected + public async Task>> GetSelectedItemsAsync(IEnumerable selectedValues, CancellationToken cancellationToken) + { + var itemQuery = contentLanguageInfoProvider.Get().Page(0, 20); + var items = (await itemQuery.GetEnumerableTypedResultAsync()).Select(x => new ObjectSelectorListItem() + { + Value = x.ContentLanguageName, + Text = x.ContentLanguageDisplayName, + IsValid = true + }); + + var selectedItems = new List>(); + + if (selectedValues is not null) + { + foreach (string? value in selectedValues) + { + var item = items.FirstOrDefault(x => x.Value == value); + + if (item != default) + { + selectedItems.Add(item); + } + } + } + + return selectedItems; + } +} diff --git a/src/Kentico.Xperience.AzureSearch/Admin/UIPages/AzureSearchApplication.cs b/src/Kentico.Xperience.AzureSearch/Admin/UIPages/AzureSearchApplication.cs index dd47bdc..085b8fe 100644 --- a/src/Kentico.Xperience.AzureSearch/Admin/UIPages/AzureSearchApplication.cs +++ b/src/Kentico.Xperience.AzureSearch/Admin/UIPages/AzureSearchApplication.cs @@ -1,29 +1,29 @@ -using CMS.Membership; - -using Kentico.Xperience.Admin.Base; -using Kentico.Xperience.Admin.Base.UIPages; -using Kentico.Xperience.AzureSearch.Admin; - -[assembly: UIApplication( - identifier: AzureSearchApplicationPage.IDENTIFIER, - type: typeof(AzureSearchApplicationPage), - slug: "azuresearch", - name: "Azure AI Search", - category: BaseApplicationCategories.DEVELOPMENT, - icon: Icons.Magnifier, - templateName: TemplateNames.SECTION_LAYOUT)] - -namespace Kentico.Xperience.AzureSearch.Admin; - -/// -/// The root application page for the AzureSearch integration. -/// -[UIPermission(SystemPermissions.VIEW)] -[UIPermission(SystemPermissions.CREATE)] -[UIPermission(SystemPermissions.UPDATE)] -[UIPermission(SystemPermissions.DELETE)] -[UIPermission(AzureSearchIndexPermissions.REBUILD, "Rebuild")] -internal class AzureSearchApplicationPage : ApplicationPage -{ - public const string IDENTIFIER = "Kentico.Xperience.Integrations.AzureSearch"; -} +using CMS.Membership; + +using Kentico.Xperience.Admin.Base; +using Kentico.Xperience.Admin.Base.UIPages; +using Kentico.Xperience.AzureSearch.Admin; + +[assembly: UIApplication( + identifier: AzureSearchApplicationPage.IDENTIFIER, + type: typeof(AzureSearchApplicationPage), + slug: "azuresearch", + name: "Azure AI Search", + category: BaseApplicationCategories.DEVELOPMENT, + icon: Icons.Magnifier, + templateName: TemplateNames.SECTION_LAYOUT)] + +namespace Kentico.Xperience.AzureSearch.Admin; + +/// +/// The root application page for the AzureSearch integration. +/// +[UIPermission(SystemPermissions.VIEW)] +[UIPermission(SystemPermissions.CREATE)] +[UIPermission(SystemPermissions.UPDATE)] +[UIPermission(SystemPermissions.DELETE)] +[UIPermission(AzureSearchIndexPermissions.REBUILD, "Rebuild")] +internal class AzureSearchApplicationPage : ApplicationPage +{ + public const string IDENTIFIER = "Kentico.Xperience.Integrations.AzureSearch"; +} diff --git a/src/Kentico.Xperience.AzureSearch/Admin/UIPages/IndexAliasEditPage.cs b/src/Kentico.Xperience.AzureSearch/Admin/UIPages/IndexAliasEditPage.cs index e8549ea..35d869b 100644 --- a/src/Kentico.Xperience.AzureSearch/Admin/UIPages/IndexAliasEditPage.cs +++ b/src/Kentico.Xperience.AzureSearch/Admin/UIPages/IndexAliasEditPage.cs @@ -1,69 +1,69 @@ -using CMS.Membership; - -using Kentico.Xperience.Admin.Base; -using Kentico.Xperience.Admin.Base.Forms; -using Kentico.Xperience.AzureSearch.Admin; -using Kentico.Xperience.AzureSearch.Aliasing; - -[assembly: UIPage( - parentType: typeof(IndexAliasListingPage), - slug: PageParameterConstants.PARAMETERIZED_SLUG, - uiPageType: typeof(IndexAliasEditPage), - name: "Edit index alias", - templateName: TemplateNames.EDIT, - order: UIPageOrder.NoOrder)] - -namespace Kentico.Xperience.AzureSearch.Admin; - -[UIEvaluatePermission(SystemPermissions.UPDATE)] -internal class IndexAliasEditPage : BaseIndexAliasEditPage -{ - private AzureSearchAliasConfigurationModel? model = null; - - [PageParameter(typeof(IntPageModelBinder))] - public int IndexIdentifier { get; set; } - - public IndexAliasEditPage(Xperience.Admin.Base.Forms.Internal.IFormItemCollectionProvider formItemCollectionProvider, - IFormDataBinder formDataBinder, - IAzureSearchConfigurationStorageService storageService, - IAzureSearchIndexAliasService azureSearchIndexAliasService) - : base(formItemCollectionProvider, formDataBinder, azureSearchIndexAliasService, storageService) { } - - protected override AzureSearchAliasConfigurationModel Model - { - get - { - model ??= StorageService.GetAliasDataOrNull(IndexIdentifier) ?? new(); - - return model; - } - } - - protected override async Task ProcessFormData(AzureSearchAliasConfigurationModel model, ICollection formItems) - { - var result = await ValidateAndProcess(model); - - var response = ResponseFrom(new FormSubmissionResult( - result.ModificationResult == ModificationResult.Success - ? FormSubmissionStatus.ValidationSuccess - : FormSubmissionStatus.ValidationFailure)); - - if (result.ModificationResult == ModificationResult.Failure) - { - if (result.ErrorMessages is not null) - { - result.ErrorMessages.ForEach(errorMessage => response.AddErrorMessage(errorMessage)); - } - else - { - response.AddErrorMessage("Could not create index alias."); - } - } - else - { - response.AddSuccessMessage("Index alias edited"); - } - - return response; - } -} +using CMS.Membership; + +using Kentico.Xperience.Admin.Base; +using Kentico.Xperience.Admin.Base.Forms; +using Kentico.Xperience.AzureSearch.Admin; +using Kentico.Xperience.AzureSearch.Aliasing; + +[assembly: UIPage( + parentType: typeof(IndexAliasListingPage), + slug: PageParameterConstants.PARAMETERIZED_SLUG, + uiPageType: typeof(IndexAliasEditPage), + name: "Edit index alias", + templateName: TemplateNames.EDIT, + order: UIPageOrder.NoOrder)] + +namespace Kentico.Xperience.AzureSearch.Admin; + +[UIEvaluatePermission(SystemPermissions.UPDATE)] +internal class IndexAliasEditPage : BaseIndexAliasEditPage +{ + private AzureSearchAliasConfigurationModel? model = null; + + [PageParameter(typeof(IntPageModelBinder))] + public int IndexIdentifier { get; set; } + + public IndexAliasEditPage(Xperience.Admin.Base.Forms.Internal.IFormItemCollectionProvider formItemCollectionProvider, + IFormDataBinder formDataBinder, + IAzureSearchConfigurationStorageService storageService, + IAzureSearchIndexAliasService azureSearchIndexAliasService) + : base(formItemCollectionProvider, formDataBinder, azureSearchIndexAliasService, storageService) { } + + protected override AzureSearchAliasConfigurationModel Model + { + get + { + model ??= StorageService.GetAliasDataOrNull(IndexIdentifier) ?? new(); + + return model; + } + } + + protected override async Task ProcessFormData(AzureSearchAliasConfigurationModel model, ICollection formItems) + { + var result = await ValidateAndProcess(model); + + var response = ResponseFrom(new FormSubmissionResult( + result.ModificationResult == ModificationResult.Success + ? FormSubmissionStatus.ValidationSuccess + : FormSubmissionStatus.ValidationFailure)); + + if (result.ModificationResult == ModificationResult.Failure) + { + if (result.ErrorMessages is not null) + { + result.ErrorMessages.ForEach(errorMessage => response.AddErrorMessage(errorMessage)); + } + else + { + response.AddErrorMessage("Could not create index alias."); + } + } + else + { + response.AddSuccessMessage("Index alias edited"); + } + + return response; + } +} diff --git a/src/Kentico.Xperience.AzureSearch/Admin/UIPages/IndexEditPage.cs b/src/Kentico.Xperience.AzureSearch/Admin/UIPages/IndexEditPage.cs index 770ec20..e9aa911 100644 --- a/src/Kentico.Xperience.AzureSearch/Admin/UIPages/IndexEditPage.cs +++ b/src/Kentico.Xperience.AzureSearch/Admin/UIPages/IndexEditPage.cs @@ -1,70 +1,70 @@ -using CMS.Membership; - -using Kentico.Xperience.Admin.Base; -using Kentico.Xperience.Admin.Base.Forms; -using Kentico.Xperience.AzureSearch.Admin; -using Kentico.Xperience.AzureSearch.Indexing; - -[assembly: UIPage( - parentType: typeof(IndexListingPage), - slug: PageParameterConstants.PARAMETERIZED_SLUG, - uiPageType: typeof(IndexEditPage), - name: "Edit index", - templateName: TemplateNames.EDIT, - order: UIPageOrder.NoOrder)] - -namespace Kentico.Xperience.AzureSearch.Admin; - -[UIEvaluatePermission(SystemPermissions.UPDATE)] -internal class IndexEditPage : BaseIndexEditPage -{ - private AzureSearchConfigurationModel? model = null; - - [PageParameter(typeof(IntPageModelBinder))] - public int IndexIdentifier { get; set; } - - public IndexEditPage( - Xperience.Admin.Base.Forms.Internal.IFormItemCollectionProvider formItemCollectionProvider, - IFormDataBinder formDataBinder, - IAzureSearchConfigurationStorageService storageService, - IAzureSearchIndexClientService indexClientService) - : base(formItemCollectionProvider, formDataBinder, storageService, indexClientService) { } - - protected override AzureSearchConfigurationModel Model - { - get - { - model ??= StorageService.GetIndexDataOrNull(IndexIdentifier) ?? new(); - - return model; - } - } - - protected override async Task ProcessFormData(AzureSearchConfigurationModel model, ICollection formItems) - { - var result = await ValidateAndProcess(model); - - var response = ResponseFrom(new FormSubmissionResult( - result.ModificationResult == ModificationResult.Success - ? FormSubmissionStatus.ValidationSuccess - : FormSubmissionStatus.ValidationFailure)); - - if (result.ModificationResult == ModificationResult.Failure) - { - if (result.ErrorMessages is not null) - { - result.ErrorMessages.ForEach(errorMessage => response.AddErrorMessage(errorMessage)); - } - else - { - response.AddErrorMessage("Could not create index."); - } - } - else - { - response.AddSuccessMessage("Index edited"); - } - - return response; - } -} +using CMS.Membership; + +using Kentico.Xperience.Admin.Base; +using Kentico.Xperience.Admin.Base.Forms; +using Kentico.Xperience.AzureSearch.Admin; +using Kentico.Xperience.AzureSearch.Indexing; + +[assembly: UIPage( + parentType: typeof(IndexListingPage), + slug: PageParameterConstants.PARAMETERIZED_SLUG, + uiPageType: typeof(IndexEditPage), + name: "Edit index", + templateName: TemplateNames.EDIT, + order: UIPageOrder.NoOrder)] + +namespace Kentico.Xperience.AzureSearch.Admin; + +[UIEvaluatePermission(SystemPermissions.UPDATE)] +internal class IndexEditPage : BaseIndexEditPage +{ + private AzureSearchConfigurationModel? model = null; + + [PageParameter(typeof(IntPageModelBinder))] + public int IndexIdentifier { get; set; } + + public IndexEditPage( + Xperience.Admin.Base.Forms.Internal.IFormItemCollectionProvider formItemCollectionProvider, + IFormDataBinder formDataBinder, + IAzureSearchConfigurationStorageService storageService, + IAzureSearchIndexClientService indexClientService) + : base(formItemCollectionProvider, formDataBinder, storageService, indexClientService) { } + + protected override AzureSearchConfigurationModel Model + { + get + { + model ??= StorageService.GetIndexDataOrNull(IndexIdentifier) ?? new(); + + return model; + } + } + + protected override async Task ProcessFormData(AzureSearchConfigurationModel model, ICollection formItems) + { + var result = await ValidateAndProcess(model); + + var response = ResponseFrom(new FormSubmissionResult( + result.ModificationResult == ModificationResult.Success + ? FormSubmissionStatus.ValidationSuccess + : FormSubmissionStatus.ValidationFailure)); + + if (result.ModificationResult == ModificationResult.Failure) + { + if (result.ErrorMessages is not null) + { + result.ErrorMessages.ForEach(errorMessage => response.AddErrorMessage(errorMessage)); + } + else + { + response.AddErrorMessage("Could not create index."); + } + } + else + { + response.AddSuccessMessage("Index edited"); + } + + return response; + } +} diff --git a/src/Kentico.Xperience.AzureSearch/Aliasing/AzureSearchIndexAlias.cs b/src/Kentico.Xperience.AzureSearch/Aliasing/AzureSearchIndexAlias.cs index 4c28158..96349c9 100644 --- a/src/Kentico.Xperience.AzureSearch/Aliasing/AzureSearchIndexAlias.cs +++ b/src/Kentico.Xperience.AzureSearch/Aliasing/AzureSearchIndexAlias.cs @@ -1,31 +1,31 @@ -using Kentico.Xperience.AzureSearch.Admin; - -namespace Kentico.Xperience.AzureSearch.Aliasing; - -/// -/// Represents the configuration of an AzureSearch index alias. -/// -public sealed class AzureSearchIndexAlias -{ - /// - /// An arbitrary ID used to identify the AzureSearch index in the admin UI. - /// - public int Identifier { get; set; } - - /// - /// The Name of the AzureSearch index alias. - /// - public string AliasName { get; } - - /// - /// The code name of the AzureSearch index which is aliased. - /// - public IEnumerable IndexNames { get; } - - internal AzureSearchIndexAlias(AzureSearchAliasConfigurationModel aliasConfiguration) - { - Identifier = aliasConfiguration.Id; - IndexNames = aliasConfiguration.IndexNames; - AliasName = aliasConfiguration.AliasName; - } -} +using Kentico.Xperience.AzureSearch.Admin; + +namespace Kentico.Xperience.AzureSearch.Aliasing; + +/// +/// Represents the configuration of an AzureSearch index alias. +/// +public sealed class AzureSearchIndexAlias +{ + /// + /// An arbitrary ID used to identify the AzureSearch index in the admin UI. + /// + public int Identifier { get; set; } + + /// + /// The Name of the AzureSearch index alias. + /// + public string AliasName { get; } + + /// + /// The code name of the AzureSearch index which is aliased. + /// + public IEnumerable IndexNames { get; } + + internal AzureSearchIndexAlias(AzureSearchAliasConfigurationModel aliasConfiguration) + { + Identifier = aliasConfiguration.Id; + IndexNames = aliasConfiguration.IndexNames; + AliasName = aliasConfiguration.AliasName; + } +} diff --git a/src/Kentico.Xperience.AzureSearch/Aliasing/AzureSearchIndexAliasService.cs b/src/Kentico.Xperience.AzureSearch/Aliasing/AzureSearchIndexAliasService.cs index 97c6465..45bad92 100644 --- a/src/Kentico.Xperience.AzureSearch/Aliasing/AzureSearchIndexAliasService.cs +++ b/src/Kentico.Xperience.AzureSearch/Aliasing/AzureSearchIndexAliasService.cs @@ -1,40 +1,40 @@ -using Azure.Search.Documents.Indexes; -using Azure.Search.Documents.Indexes.Models; - -namespace Kentico.Xperience.AzureSearch.Aliasing; - -/// -/// Default implementation of . -/// -internal class AzureSearchIndexAliasService : IAzureSearchIndexAliasService -{ - private readonly SearchIndexClient indexClient; - - /// - /// Initializes a new instance of the class. - /// - public AzureSearchIndexAliasService(SearchIndexClient indexClient) => this.indexClient = indexClient; - - /// - public async Task EditAlias(string oldAliasName, SearchAlias newAlias, CancellationToken cancellationToken) - { - if (string.IsNullOrEmpty(oldAliasName)) - { - throw new ArgumentNullException(nameof(oldAliasName)); - } - - await DeleteAlias(oldAliasName, cancellationToken); - await indexClient.CreateOrUpdateAliasAsync(newAlias.Name, newAlias, cancellationToken: cancellationToken); - } - - /// - public async Task DeleteAlias(string aliasName, CancellationToken cancellationToken) - { - if (string.IsNullOrEmpty(aliasName)) - { - throw new ArgumentNullException(nameof(aliasName)); - } - - await indexClient.DeleteAliasAsync(aliasName, cancellationToken); - } -} +using Azure.Search.Documents.Indexes; +using Azure.Search.Documents.Indexes.Models; + +namespace Kentico.Xperience.AzureSearch.Aliasing; + +/// +/// Default implementation of . +/// +internal class AzureSearchIndexAliasService : IAzureSearchIndexAliasService +{ + private readonly SearchIndexClient indexClient; + + /// + /// Initializes a new instance of the class. + /// + public AzureSearchIndexAliasService(SearchIndexClient indexClient) => this.indexClient = indexClient; + + /// + public async Task EditAlias(string oldAliasName, SearchAlias newAlias, CancellationToken cancellationToken) + { + if (string.IsNullOrEmpty(oldAliasName)) + { + throw new ArgumentNullException(nameof(oldAliasName)); + } + + await DeleteAlias(oldAliasName, cancellationToken); + await indexClient.CreateOrUpdateAliasAsync(newAlias.Name, newAlias, cancellationToken: cancellationToken); + } + + /// + public async Task DeleteAlias(string aliasName, CancellationToken cancellationToken) + { + if (string.IsNullOrEmpty(aliasName)) + { + throw new ArgumentNullException(nameof(aliasName)); + } + + await indexClient.DeleteAliasAsync(aliasName, cancellationToken); + } +} diff --git a/src/Kentico.Xperience.AzureSearch/Aliasing/AzureSearchIndexAliasStore.cs b/src/Kentico.Xperience.AzureSearch/Aliasing/AzureSearchIndexAliasStore.cs index 20febdc..194fad2 100644 --- a/src/Kentico.Xperience.AzureSearch/Aliasing/AzureSearchIndexAliasStore.cs +++ b/src/Kentico.Xperience.AzureSearch/Aliasing/AzureSearchIndexAliasStore.cs @@ -1,93 +1,93 @@ -using Kentico.Xperience.AzureSearch.Admin; - -namespace Kentico.Xperience.AzureSearch.Aliasing; - -/// -/// Represents a global singleton store of Azure Search index aliases -/// -public sealed class AzureSearchIndexAliasStore -{ - private static readonly Lazy mInstance = new(); - private readonly List registeredAliases = []; - - /// - /// Gets singleton instance of the - /// - public static AzureSearchIndexAliasStore Instance => mInstance.Value; - - /// - /// Gets all registered aliases. - /// - public IEnumerable GetAllAliases() => registeredAliases; - - /// - /// Gets a registered with the specified , - /// or null. - /// - /// The identifier of the alias to retrieve. - /// - /// - public AzureSearchIndexAlias? GetAlias(int identifier) => registeredAliases.Find(i => i.Identifier == identifier); - - /// - /// Gets a registered with the specified . If no alias is found, a is thrown. - /// - /// The name of the alias to retrieve. - /// - /// - public AzureSearchIndexAlias GetRequiredAlias(string aliasName) - { - if (string.IsNullOrEmpty(aliasName)) - { - throw new ArgumentException("Value must not be null or empty"); - } - - return registeredAliases.SingleOrDefault(i => i.AliasName.Equals(aliasName, StringComparison.OrdinalIgnoreCase)) - ?? throw new InvalidOperationException($"The index '{aliasName}' is not registered."); - } - - /// - /// Adds an alias to the store. - /// - /// The alias to add. - /// - /// - internal void AddAlias(AzureSearchIndexAlias alias) - { - if (alias == null) - { - throw new ArgumentNullException(nameof(alias)); - } - - if (registeredAliases.Exists(i => i.AliasName.Equals(alias.AliasName, StringComparison.OrdinalIgnoreCase) || alias.Identifier == i.Identifier)) - { - throw new InvalidOperationException($"Attempted to register AzureSearch index alias with identifier [{alias.Identifier}] and name [{alias.AliasName}] but it is already registered."); - } - - registeredAliases.Add(alias); - } - - /// - /// Resets all aliases - /// - /// - internal void SetAliases(IEnumerable models) - { - registeredAliases.Clear(); - foreach (var alias in models) - { - Instance.AddAlias(new AzureSearchIndexAlias(alias)); - } - } - - /// - /// Sets the current aliases to those provided by - /// - /// - internal static void SetAliases(IAzureSearchConfigurationStorageService configurationService) - { - var aliases = configurationService.GetAllAliasData(); - - Instance.SetAliases(aliases); - } -} +using Kentico.Xperience.AzureSearch.Admin; + +namespace Kentico.Xperience.AzureSearch.Aliasing; + +/// +/// Represents a global singleton store of Azure Search index aliases +/// +public sealed class AzureSearchIndexAliasStore +{ + private static readonly Lazy mInstance = new(); + private readonly List registeredAliases = []; + + /// + /// Gets singleton instance of the + /// + public static AzureSearchIndexAliasStore Instance => mInstance.Value; + + /// + /// Gets all registered aliases. + /// + public IEnumerable GetAllAliases() => registeredAliases; + + /// + /// Gets a registered with the specified , + /// or null. + /// + /// The identifier of the alias to retrieve. + /// + /// + public AzureSearchIndexAlias? GetAlias(int identifier) => registeredAliases.Find(i => i.Identifier == identifier); + + /// + /// Gets a registered with the specified . If no alias is found, a is thrown. + /// + /// The name of the alias to retrieve. + /// + /// + public AzureSearchIndexAlias GetRequiredAlias(string aliasName) + { + if (string.IsNullOrEmpty(aliasName)) + { + throw new ArgumentException("Value must not be null or empty"); + } + + return registeredAliases.SingleOrDefault(i => i.AliasName.Equals(aliasName, StringComparison.OrdinalIgnoreCase)) + ?? throw new InvalidOperationException($"The index '{aliasName}' is not registered."); + } + + /// + /// Adds an alias to the store. + /// + /// The alias to add. + /// + /// + internal void AddAlias(AzureSearchIndexAlias alias) + { + if (alias == null) + { + throw new ArgumentNullException(nameof(alias)); + } + + if (registeredAliases.Exists(i => i.AliasName.Equals(alias.AliasName, StringComparison.OrdinalIgnoreCase) || alias.Identifier == i.Identifier)) + { + throw new InvalidOperationException($"Attempted to register AzureSearch index alias with identifier [{alias.Identifier}] and name [{alias.AliasName}] but it is already registered."); + } + + registeredAliases.Add(alias); + } + + /// + /// Resets all aliases + /// + /// + internal void SetAliases(IEnumerable models) + { + registeredAliases.Clear(); + foreach (var alias in models) + { + Instance.AddAlias(new AzureSearchIndexAlias(alias)); + } + } + + /// + /// Sets the current aliases to those provided by + /// + /// + internal static void SetAliases(IAzureSearchConfigurationStorageService configurationService) + { + var aliases = configurationService.GetAllAliasData(); + + Instance.SetAliases(aliases); + } +} diff --git a/src/Kentico.Xperience.AzureSearch/Aliasing/IAzureSearchIndexAliasService.cs b/src/Kentico.Xperience.AzureSearch/Aliasing/IAzureSearchIndexAliasService.cs index d233c3d..586a05f 100644 --- a/src/Kentico.Xperience.AzureSearch/Aliasing/IAzureSearchIndexAliasService.cs +++ b/src/Kentico.Xperience.AzureSearch/Aliasing/IAzureSearchIndexAliasService.cs @@ -1,32 +1,32 @@ -using Azure.Search.Documents.Indexes.Models; - -namespace Kentico.Xperience.AzureSearch.Aliasing; - -/// -/// Manages instances. -/// -public interface IAzureSearchIndexAliasService -{ - /// - /// Edits the AzureSearch index alias in Azure. - /// - /// The alias to edit. - /// New alias. - /// The cancellation token for the task. - /// - /// - /// Thrown when is null. - /// Thrown when a failure is returned by the Search service. - Task EditAlias(string oldAliasName, SearchAlias newAlias, CancellationToken cancellationToken); - - /// - /// Deletes the AzureSearch index alias by removing existing index alias data from Azure. - /// - /// The index to delete. - /// The cancellation token for the task. - /// - /// - /// Thrown when is null. - /// Thrown when a failure is returned by the Search service. - Task DeleteAlias(string aliasName, CancellationToken cancellationToken); -} +using Azure.Search.Documents.Indexes.Models; + +namespace Kentico.Xperience.AzureSearch.Aliasing; + +/// +/// Manages instances. +/// +public interface IAzureSearchIndexAliasService +{ + /// + /// Edits the AzureSearch index alias in Azure. + /// + /// The alias to edit. + /// New alias. + /// The cancellation token for the task. + /// + /// + /// Thrown when is null. + /// Thrown when a failure is returned by the Search service. + Task EditAlias(string oldAliasName, SearchAlias newAlias, CancellationToken cancellationToken); + + /// + /// Deletes the AzureSearch index alias by removing existing index alias data from Azure. + /// + /// The index to delete. + /// The cancellation token for the task. + /// + /// + /// Thrown when is null. + /// Thrown when a failure is returned by the Search service. + Task DeleteAlias(string aliasName, CancellationToken cancellationToken); +} diff --git a/src/Kentico.Xperience.AzureSearch/AzureSearchQueueWorker.cs b/src/Kentico.Xperience.AzureSearch/AzureSearchQueueWorker.cs index d36626c..7faa7fd 100644 --- a/src/Kentico.Xperience.AzureSearch/AzureSearchQueueWorker.cs +++ b/src/Kentico.Xperience.AzureSearch/AzureSearchQueueWorker.cs @@ -1,63 +1,63 @@ -using CMS.Base; -using CMS.Core; - -using Kentico.Xperience.AzureSearch.Indexing; - -namespace Kentico.Xperience.AzureSearch; - -/// -/// Thread worker which enqueues recently updated or deleted nodes indexed -/// by AzureSearch and processes the tasks in the background thread. -/// -internal class AzureSearchQueueWorker : ThreadQueueWorker -{ - private readonly IAzureSearchTaskProcessor azureSearchTaskProcessor; - - /// - protected override int DefaultInterval => 10000; - - /// - /// Initializes a new instance of the class. - /// Should not be called directly- the worker should be initialized during startup using - /// . - /// - public AzureSearchQueueWorker() => azureSearchTaskProcessor = Service.Resolve() ?? throw new InvalidOperationException($"{nameof(IAzureSearchTaskProcessor)} is not registered."); - - /// - /// Adds an to the worker queue to be processed. - /// - /// The item to be added to the queue. - /// - public static void EnqueueAzureSearchQueueItem(AzureSearchQueueItem queueItem) - { - if ((queueItem.ItemToIndex == null && queueItem.TaskType != AzureSearchTaskType.PUBLISH_INDEX) || string.IsNullOrEmpty(queueItem.IndexName)) - { - return; - } - - if (queueItem.TaskType == AzureSearchTaskType.UNKNOWN) - { - return; - } - - if (AzureSearchIndexStore.Instance.GetIndex(queueItem.IndexName) == null) - { - throw new InvalidOperationException($"Attempted to log task for AzureSearch index '{queueItem.IndexName},' but it is not registered."); - } - - Current.Enqueue(queueItem, false); - } - - /// - protected override void Finish() => RunProcess(); - - /// - protected override void ProcessItem(AzureSearchQueueItem item) - { - } - - /// - protected override int ProcessItems(IEnumerable items) => - azureSearchTaskProcessor.ProcessAzureSearchTasks(items, CancellationToken.None).GetAwaiter().GetResult(); - -} +using CMS.Base; +using CMS.Core; + +using Kentico.Xperience.AzureSearch.Indexing; + +namespace Kentico.Xperience.AzureSearch; + +/// +/// Thread worker which enqueues recently updated or deleted nodes indexed +/// by AzureSearch and processes the tasks in the background thread. +/// +internal class AzureSearchQueueWorker : ThreadQueueWorker +{ + private readonly IAzureSearchTaskProcessor azureSearchTaskProcessor; + + /// + protected override int DefaultInterval => 10000; + + /// + /// Initializes a new instance of the class. + /// Should not be called directly- the worker should be initialized during startup using + /// . + /// + public AzureSearchQueueWorker() => azureSearchTaskProcessor = Service.Resolve() ?? throw new InvalidOperationException($"{nameof(IAzureSearchTaskProcessor)} is not registered."); + + /// + /// Adds an to the worker queue to be processed. + /// + /// The item to be added to the queue. + /// + public static void EnqueueAzureSearchQueueItem(AzureSearchQueueItem queueItem) + { + if ((queueItem.ItemToIndex == null && queueItem.TaskType != AzureSearchTaskType.PUBLISH_INDEX) || string.IsNullOrEmpty(queueItem.IndexName)) + { + return; + } + + if (queueItem.TaskType == AzureSearchTaskType.UNKNOWN) + { + return; + } + + if (AzureSearchIndexStore.Instance.GetIndex(queueItem.IndexName) == null) + { + throw new InvalidOperationException($"Attempted to log task for AzureSearch index '{queueItem.IndexName},' but it is not registered."); + } + + Current.Enqueue(queueItem, false); + } + + /// + protected override void Finish() => RunProcess(); + + /// + protected override void ProcessItem(AzureSearchQueueItem item) + { + } + + /// + protected override int ProcessItems(IEnumerable items) => + azureSearchTaskProcessor.ProcessAzureSearchTasks(items, CancellationToken.None).GetAwaiter().GetResult(); + +} diff --git a/src/Kentico.Xperience.AzureSearch/AzureSearchStartupExtensions.cs b/src/Kentico.Xperience.AzureSearch/AzureSearchStartupExtensions.cs index eb996a8..62f26b4 100644 --- a/src/Kentico.Xperience.AzureSearch/AzureSearchStartupExtensions.cs +++ b/src/Kentico.Xperience.AzureSearch/AzureSearchStartupExtensions.cs @@ -1,143 +1,143 @@ -using System.Reflection; - -using Azure; -using Azure.Search.Documents.Indexes; - -using Kentico.Xperience.AzureSearch.Admin; -using Kentico.Xperience.AzureSearch.Aliasing; -using Kentico.Xperience.AzureSearch.Indexing; -using Kentico.Xperience.AzureSearch.Search; - -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.Options; - -namespace Microsoft.Extensions.DependencyInjection; - -public static class AzureSearchStartupExtensions -{ - /// - /// Adds Azure search services and custom module to application using the for all indexes - /// - /// - /// - /// - public static IServiceCollection AddKenticoAzureSearch(this IServiceCollection serviceCollection, IConfiguration configuration) - { - serviceCollection.AddAzureSearchServicesInternal(configuration); - - return serviceCollection; - } - - /// - /// Adds AzureSearch services and custom module to application with customized options provided by the - /// in the action. - /// - /// - /// - /// The application configuration. - /// - public static IServiceCollection AddKenticoAzureSearch(this IServiceCollection serviceCollection, Action configure, IConfiguration configuration) - { - serviceCollection.AddAzureSearchServicesInternal(configuration); - - var builder = new AzureSearchBuilder(serviceCollection); - - configure(builder); - - if (builder.IncludeDefaultStrategy) - { - serviceCollection.AddTransient>(); - builder.RegisterStrategy, BaseAzureSearchModel>("Default"); - } - - return serviceCollection; - } - - private static IServiceCollection AddAzureSearchServicesInternal(this IServiceCollection services, IConfiguration configuration) => - services - .Configure(configuration.GetSection(AzureSearchOptions.CMS_AZURE_SEARCH_SECTION_NAME)) - .AddSingleton() - .AddSingleton(x => - { - var options = x.GetRequiredService>(); - return new SearchIndexClient(new Uri(options.Value.SearchServiceEndPoint), new AzureKeyCredential(options.Value.SearchServiceAdminApiKey)); - }) - .AddSingleton(x => - { - var options = x.GetRequiredService>(); - return new AzureSearchQueryClientService(new AzureSearchQueryClientOptions(options.Value.SearchServiceEndPoint, options.Value.SearchServiceQueryApiKey)); - }) - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton(); -} - -public interface IAzureSearchBuilder -{ - /// - /// Registers the given as a transient service under - /// - /// The custom type of - /// The custom rype of used to create and use an index. - /// Used internally to enable dynamic assignment of strategies to search indexes. Names must be unique. - /// - /// Thrown if an strategy has already been registered with the given - /// - /// - IAzureSearchBuilder RegisterStrategy(string strategyName) where TStrategy : BaseAzureSearchIndexingStrategy where TSearchModel : IAzureSearchModel, new(); -} - -internal class AzureSearchBuilder : IAzureSearchBuilder -{ - private readonly IServiceCollection serviceCollection; - private const string ErrorMessage = "Exactly one field in your index must serve as the document key (IsKey = true). It must be a string, and it must uniquely identify each document. It's also required to have IsHidden = false."; - - /// - /// If true, the will be available as an explicitly selectable indexing strategy - /// within the Admin UI. Defaults to true - /// - public bool IncludeDefaultStrategy { get; set; } = true; - - public AzureSearchBuilder(IServiceCollection serviceCollection) => this.serviceCollection = serviceCollection; - - /// - /// Registers the strategy in DI and - /// as a selectable strategy in the Admin UI - /// - /// - /// The custom rype of used to create and use an index. - /// - /// - public IAzureSearchBuilder RegisterStrategy(string strategyName) where TStrategy : BaseAzureSearchIndexingStrategy where TSearchModel : IAzureSearchModel, new() - { - ValidateIndexSearchModelProperties(); - - StrategyStorage.AddStrategy(strategyName); - serviceCollection.AddTransient(); - - return this; - } - - private void ValidateIndexSearchModelProperties() where TSearchModel : IAzureSearchModel, new() - { - var type = typeof(TSearchModel); - - var propertiesWithAttributes = type.GetProperties().Select(x => new - { - Attribute = x.GetCustomAttributes().SingleOrDefault() - ?? throw new InvalidOperationException(ErrorMessage), - Type = x.PropertyType - }); - - var keyAttribute = propertiesWithAttributes.SingleOrDefault(x => x.Attribute.IsKey) - ?? throw new InvalidOperationException(ErrorMessage); - - if (keyAttribute.Type != typeof(string) || keyAttribute.Attribute.IsHidden) - { - throw new InvalidOperationException(ErrorMessage); - } - } -} +using System.Reflection; + +using Azure; +using Azure.Search.Documents.Indexes; + +using Kentico.Xperience.AzureSearch.Admin; +using Kentico.Xperience.AzureSearch.Aliasing; +using Kentico.Xperience.AzureSearch.Indexing; +using Kentico.Xperience.AzureSearch.Search; + +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Options; + +namespace Microsoft.Extensions.DependencyInjection; + +public static class AzureSearchStartupExtensions +{ + /// + /// Adds Azure search services and custom module to application using the for all indexes + /// + /// + /// + /// + public static IServiceCollection AddKenticoAzureSearch(this IServiceCollection serviceCollection, IConfiguration configuration) + { + serviceCollection.AddAzureSearchServicesInternal(configuration); + + return serviceCollection; + } + + /// + /// Adds AzureSearch services and custom module to application with customized options provided by the + /// in the action. + /// + /// + /// + /// The application configuration. + /// + public static IServiceCollection AddKenticoAzureSearch(this IServiceCollection serviceCollection, Action configure, IConfiguration configuration) + { + serviceCollection.AddAzureSearchServicesInternal(configuration); + + var builder = new AzureSearchBuilder(serviceCollection); + + configure(builder); + + if (builder.IncludeDefaultStrategy) + { + serviceCollection.AddTransient>(); + builder.RegisterStrategy, BaseAzureSearchModel>("Default"); + } + + return serviceCollection; + } + + private static IServiceCollection AddAzureSearchServicesInternal(this IServiceCollection services, IConfiguration configuration) => + services + .Configure(configuration.GetSection(AzureSearchOptions.CMS_AZURE_SEARCH_SECTION_NAME)) + .AddSingleton() + .AddSingleton(x => + { + var options = x.GetRequiredService>(); + return new SearchIndexClient(new Uri(options.Value.SearchServiceEndPoint), new AzureKeyCredential(options.Value.SearchServiceAdminApiKey)); + }) + .AddSingleton(x => + { + var options = x.GetRequiredService>(); + return new AzureSearchQueryClientService(new AzureSearchQueryClientOptions(options.Value.SearchServiceEndPoint, options.Value.SearchServiceQueryApiKey)); + }) + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton(); +} + +public interface IAzureSearchBuilder +{ + /// + /// Registers the given as a transient service under + /// + /// The custom type of + /// The custom rype of used to create and use an index. + /// Used internally to enable dynamic assignment of strategies to search indexes. Names must be unique. + /// + /// Thrown if an strategy has already been registered with the given + /// + /// + IAzureSearchBuilder RegisterStrategy(string strategyName) where TStrategy : BaseAzureSearchIndexingStrategy where TSearchModel : IAzureSearchModel, new(); +} + +internal class AzureSearchBuilder : IAzureSearchBuilder +{ + private readonly IServiceCollection serviceCollection; + private const string ErrorMessage = "Exactly one field in your index must serve as the document key (IsKey = true). It must be a string, and it must uniquely identify each document. It's also required to have IsHidden = false."; + + /// + /// If true, the will be available as an explicitly selectable indexing strategy + /// within the Admin UI. Defaults to true + /// + public bool IncludeDefaultStrategy { get; set; } = true; + + public AzureSearchBuilder(IServiceCollection serviceCollection) => this.serviceCollection = serviceCollection; + + /// + /// Registers the strategy in DI and + /// as a selectable strategy in the Admin UI + /// + /// + /// The custom rype of used to create and use an index. + /// + /// + public IAzureSearchBuilder RegisterStrategy(string strategyName) where TStrategy : BaseAzureSearchIndexingStrategy where TSearchModel : IAzureSearchModel, new() + { + ValidateIndexSearchModelProperties(); + + StrategyStorage.AddStrategy(strategyName); + serviceCollection.AddTransient(); + + return this; + } + + private void ValidateIndexSearchModelProperties() where TSearchModel : IAzureSearchModel, new() + { + var type = typeof(TSearchModel); + + var propertiesWithAttributes = type.GetProperties().Select(x => new + { + Attribute = x.GetCustomAttributes().SingleOrDefault() + ?? throw new InvalidOperationException(ErrorMessage), + Type = x.PropertyType + }); + + var keyAttribute = propertiesWithAttributes.SingleOrDefault(x => x.Attribute.IsKey) + ?? throw new InvalidOperationException(ErrorMessage); + + if (keyAttribute.Type != typeof(string) || keyAttribute.Attribute.IsHidden) + { + throw new InvalidOperationException(ErrorMessage); + } + } +} diff --git a/src/Kentico.Xperience.AzureSearch/Indexing/AzureSearchIndexClientService.cs b/src/Kentico.Xperience.AzureSearch/Indexing/AzureSearchIndexClientService.cs index 35a9ad3..2d54e30 100644 --- a/src/Kentico.Xperience.AzureSearch/Indexing/AzureSearchIndexClientService.cs +++ b/src/Kentico.Xperience.AzureSearch/Indexing/AzureSearchIndexClientService.cs @@ -1,93 +1,93 @@ -using Azure.Search.Documents; -using Azure.Search.Documents.Indexes; -using Azure.Search.Documents.Indexes.Models; - -using Kentico.Xperience.AzureSearch.Admin; - -using Microsoft.Extensions.DependencyInjection; - -namespace Kentico.Xperience.AzureSearch.Indexing; - -public sealed class AzureSearchIndexClientService : IAzureSearchIndexClientService -{ - private readonly SearchIndexClient indexClient; - private readonly IServiceProvider serviceProvider; - - public AzureSearchIndexClientService(SearchIndexClient indexClient, IServiceProvider serviceProvider) - { - this.indexClient = indexClient; - this.serviceProvider = serviceProvider; - } - - /// - public async Task InitializeIndexClient(string indexName, CancellationToken cancellationToken) - { - var azureSearchIndex = AzureSearchIndexStore.Instance.GetIndex(indexName) ?? - throw new InvalidOperationException($"Registered index with name '{indexName}' doesn't exist."); - - var azureSearchStrategy = serviceProvider.GetRequiredStrategy(azureSearchIndex); - var searchFields = azureSearchStrategy.GetSearchFields(); - - await CreateOrUpdateIndexInternal(searchFields, azureSearchStrategy, indexName, cancellationToken); - - return indexClient.GetSearchClient(indexName); - } - - /// - public async Task EditIndex(string oldIndexName, AzureSearchConfigurationModel newIndexConfiguration, CancellationToken cancellationToken) - { - var oldIndex = AzureSearchIndexStore.Instance.GetIndex(oldIndexName) ?? - throw new InvalidOperationException($"Registered index with name '{oldIndexName}' doesn't exist."); - var oldStrategy = serviceProvider.GetRequiredStrategy(oldIndex); - var oldSearchFields = oldStrategy.GetSearchFields(); - - var newIndex = AzureSearchIndexStore.Instance.GetIndex(newIndexConfiguration.IndexName) ?? - throw new InvalidOperationException($"Registered index with name '{oldIndexName}' doesn't exist."); - var newStrategy = serviceProvider.GetRequiredStrategy(newIndex); - var newSearchFields = newStrategy.GetSearchFields(); - - if (Enumerable.SequenceEqual(oldSearchFields, newSearchFields, new AzureSearchIndexComparer())) - { - await DeleteIndex(oldIndexName, cancellationToken); - } - - await CreateOrUpdateIndexInternal(newSearchFields, newStrategy, newIndex.IndexName, cancellationToken); - } - - private async Task DeleteIndex(string indexName, CancellationToken cancellationToken) - { - if (string.IsNullOrEmpty(indexName)) - { - throw new ArgumentNullException(nameof(indexName)); - } - - await indexClient.DeleteIndexAsync(indexName, cancellationToken); - } - - private async Task CreateOrUpdateIndexInternal(IList? searchFields, IAzureSearchIndexingStrategy strategy, string indexName, CancellationToken cancellationToken) - { - var semanticSearchConfiguration = strategy.CreateSemanticRankingConfigurationOrNull(); - - var definition = new SearchIndex(indexName, searchFields); - - if (semanticSearchConfiguration is not null) - { - foreach (var suggester in semanticSearchConfiguration.Suggesters) - { - definition.Suggesters.Add(suggester); - } - - definition.SemanticSearch = semanticSearchConfiguration.SemanticSearch; - } - - try - { - await indexClient.CreateOrUpdateIndexAsync(definition, cancellationToken: cancellationToken); - } - catch - { - await indexClient.DeleteIndexAsync(indexName, cancellationToken); - await indexClient.CreateOrUpdateIndexAsync(definition, cancellationToken: cancellationToken); - } - } -} +using Azure.Search.Documents; +using Azure.Search.Documents.Indexes; +using Azure.Search.Documents.Indexes.Models; + +using Kentico.Xperience.AzureSearch.Admin; + +using Microsoft.Extensions.DependencyInjection; + +namespace Kentico.Xperience.AzureSearch.Indexing; + +public sealed class AzureSearchIndexClientService : IAzureSearchIndexClientService +{ + private readonly SearchIndexClient indexClient; + private readonly IServiceProvider serviceProvider; + + public AzureSearchIndexClientService(SearchIndexClient indexClient, IServiceProvider serviceProvider) + { + this.indexClient = indexClient; + this.serviceProvider = serviceProvider; + } + + /// + public async Task InitializeIndexClient(string indexName, CancellationToken cancellationToken) + { + var azureSearchIndex = AzureSearchIndexStore.Instance.GetIndex(indexName) ?? + throw new InvalidOperationException($"Registered index with name '{indexName}' doesn't exist."); + + var azureSearchStrategy = serviceProvider.GetRequiredStrategy(azureSearchIndex); + var searchFields = azureSearchStrategy.GetSearchFields(); + + await CreateOrUpdateIndexInternal(searchFields, azureSearchStrategy, indexName, cancellationToken); + + return indexClient.GetSearchClient(indexName); + } + + /// + public async Task EditIndex(string oldIndexName, AzureSearchConfigurationModel newIndexConfiguration, CancellationToken cancellationToken) + { + var oldIndex = AzureSearchIndexStore.Instance.GetIndex(oldIndexName) ?? + throw new InvalidOperationException($"Registered index with name '{oldIndexName}' doesn't exist."); + var oldStrategy = serviceProvider.GetRequiredStrategy(oldIndex); + var oldSearchFields = oldStrategy.GetSearchFields(); + + var newIndex = AzureSearchIndexStore.Instance.GetIndex(newIndexConfiguration.IndexName) ?? + throw new InvalidOperationException($"Registered index with name '{oldIndexName}' doesn't exist."); + var newStrategy = serviceProvider.GetRequiredStrategy(newIndex); + var newSearchFields = newStrategy.GetSearchFields(); + + if (Enumerable.SequenceEqual(oldSearchFields, newSearchFields, new AzureSearchIndexComparer())) + { + await DeleteIndex(oldIndexName, cancellationToken); + } + + await CreateOrUpdateIndexInternal(newSearchFields, newStrategy, newIndex.IndexName, cancellationToken); + } + + private async Task DeleteIndex(string indexName, CancellationToken cancellationToken) + { + if (string.IsNullOrEmpty(indexName)) + { + throw new ArgumentNullException(nameof(indexName)); + } + + await indexClient.DeleteIndexAsync(indexName, cancellationToken); + } + + private async Task CreateOrUpdateIndexInternal(IList? searchFields, IAzureSearchIndexingStrategy strategy, string indexName, CancellationToken cancellationToken) + { + var semanticSearchConfiguration = strategy.CreateSemanticRankingConfigurationOrNull(); + + var definition = new SearchIndex(indexName, searchFields); + + if (semanticSearchConfiguration is not null) + { + foreach (var suggester in semanticSearchConfiguration.Suggesters) + { + definition.Suggesters.Add(suggester); + } + + definition.SemanticSearch = semanticSearchConfiguration.SemanticSearch; + } + + try + { + await indexClient.CreateOrUpdateIndexAsync(definition, cancellationToken: cancellationToken); + } + catch + { + await indexClient.DeleteIndexAsync(indexName, cancellationToken); + await indexClient.CreateOrUpdateIndexAsync(definition, cancellationToken: cancellationToken); + } + } +} diff --git a/src/Kentico.Xperience.AzureSearch/Indexing/AzureSearchIndexComparer.cs b/src/Kentico.Xperience.AzureSearch/Indexing/AzureSearchIndexComparer.cs index f2591f4..49cd6bd 100644 --- a/src/Kentico.Xperience.AzureSearch/Indexing/AzureSearchIndexComparer.cs +++ b/src/Kentico.Xperience.AzureSearch/Indexing/AzureSearchIndexComparer.cs @@ -1,48 +1,48 @@ -using Azure.Search.Documents.Indexes.Models; - -namespace Kentico.Xperience.AzureSearch.Indexing; - -/// -/// Implements used to compare search fields of two indexes. -/// -internal class AzureSearchIndexComparer : IEqualityComparer -{ - public bool Equals(SearchField? x, SearchField? y) - { - if (x is null && y is null) - { - return true; - } - - if ((x is null) || (y is null)) - { - return false; - } - - return x!.IsKey == y!.IsKey && - x!.Name == y!.Name && - x!.IsSearchable == y!.IsSearchable && - x!.IsSortable == y!.IsSortable && - x!.Type == y!.Type && - x!.AnalyzerName == y!.AnalyzerName && - Equals(x!.Fields, y!.Fields) && - x!.IndexAnalyzerName == y!.IndexAnalyzerName && - x!.IsFacetable == y!.IsFacetable && - x!.IsFilterable == y!.IsFilterable && - x!.IsHidden == y!.IsHidden && - x!.SearchAnalyzerName == y!.SearchAnalyzerName && - Equals(x!.SynonymMapNames, y!.SynonymMapNames) && - x!.VectorSearchDimensions == y!.VectorSearchDimensions && - x!.VectorSearchProfileName == y!.VectorSearchProfileName; - } - - public int GetHashCode(SearchField obj) - { - if (obj == null) - { - return 0; - } - - return obj.Name.GetHashCode(); - } -} +using Azure.Search.Documents.Indexes.Models; + +namespace Kentico.Xperience.AzureSearch.Indexing; + +/// +/// Implements used to compare search fields of two indexes. +/// +internal class AzureSearchIndexComparer : IEqualityComparer +{ + public bool Equals(SearchField? x, SearchField? y) + { + if (x is null && y is null) + { + return true; + } + + if ((x is null) || (y is null)) + { + return false; + } + + return x!.IsKey == y!.IsKey && + x!.Name == y!.Name && + x!.IsSearchable == y!.IsSearchable && + x!.IsSortable == y!.IsSortable && + x!.Type == y!.Type && + x!.AnalyzerName == y!.AnalyzerName && + Equals(x!.Fields, y!.Fields) && + x!.IndexAnalyzerName == y!.IndexAnalyzerName && + x!.IsFacetable == y!.IsFacetable && + x!.IsFilterable == y!.IsFilterable && + x!.IsHidden == y!.IsHidden && + x!.SearchAnalyzerName == y!.SearchAnalyzerName && + Equals(x!.SynonymMapNames, y!.SynonymMapNames) && + x!.VectorSearchDimensions == y!.VectorSearchDimensions && + x!.VectorSearchProfileName == y!.VectorSearchProfileName; + } + + public int GetHashCode(SearchField obj) + { + if (obj == null) + { + return 0; + } + + return obj.Name.GetHashCode(); + } +} diff --git a/src/Kentico.Xperience.AzureSearch/Indexing/AzureSearchIndexStore.cs b/src/Kentico.Xperience.AzureSearch/Indexing/AzureSearchIndexStore.cs index 857b231..1094d57 100644 --- a/src/Kentico.Xperience.AzureSearch/Indexing/AzureSearchIndexStore.cs +++ b/src/Kentico.Xperience.AzureSearch/Indexing/AzureSearchIndexStore.cs @@ -1,104 +1,104 @@ -using Kentico.Xperience.AzureSearch.Admin; - -namespace Kentico.Xperience.AzureSearch.Indexing; - -/// -/// Represents a global singleton store of AzureSearch indexes -/// -public sealed class AzureSearchIndexStore -{ - private static readonly Lazy mInstance = new(); - private readonly List registeredIndexes = []; - - /// - /// Gets singleton instance of the - /// - public static AzureSearchIndexStore Instance => mInstance.Value; - - /// - /// Gets all registered indexes. - /// - public IEnumerable GetAllIndices() => registeredIndexes; - - /// - /// Gets a registered with the specified , - /// or null. - /// - /// The name of the index to retrieve. - /// - /// - public AzureSearchIndex? GetIndex(string indexName) => string.IsNullOrEmpty(indexName) ? null - : registeredIndexes.SingleOrDefault(i => i.IndexName.Equals(indexName, StringComparison.OrdinalIgnoreCase)); - - /// - /// Gets a registered with the specified , - /// or null. - /// - /// The identifier of the index to retrieve. - /// - /// - public AzureSearchIndex? GetIndex(int identifier) => registeredIndexes.Find(i => i.Identifier == identifier); - - /// - /// Gets a registered with the specified . If no index is found, a is thrown. - /// - /// The name of the index to retrieve. - /// - /// - public AzureSearchIndex GetRequiredIndex(string indexName) - { - if (string.IsNullOrEmpty(indexName)) - { - throw new ArgumentException("Value must not be null or empty"); - } - - return registeredIndexes.SingleOrDefault(i => i.IndexName.Equals(indexName, StringComparison.OrdinalIgnoreCase)) - ?? throw new InvalidOperationException($"The index '{indexName}' is not registered."); - } - - /// - /// Adds an index to the store. - /// - /// The index to add. - /// - /// - internal void AddIndex(AzureSearchIndex index) - { - if (index == null) - { - throw new ArgumentNullException(nameof(index)); - } - - if (registeredIndexes.Exists(i => i.IndexName.Equals(index.IndexName, StringComparison.OrdinalIgnoreCase) || index.Identifier == i.Identifier)) - { - throw new InvalidOperationException($"Attempted to register AzureSearch index with identifier [{index.Identifier}] and name [{index.IndexName}] but it is already registered."); - } - - registeredIndexes.Add(index); - } - - /// - /// Resets all indicies - /// - /// - internal void SetIndicies(IEnumerable models) - { - registeredIndexes.Clear(); - - foreach (var index in models) - { - Instance.AddIndex(new AzureSearchIndex(index, StrategyStorage.Strategies)); - } - } - - /// - /// Sets the current indicies to those provided by - /// - /// - internal static void SetIndicies(IAzureSearchConfigurationStorageService configurationService) - { - var indices = configurationService.GetAllIndexData(); - - Instance.SetIndicies(indices); - } -} +using Kentico.Xperience.AzureSearch.Admin; + +namespace Kentico.Xperience.AzureSearch.Indexing; + +/// +/// Represents a global singleton store of AzureSearch indexes +/// +public sealed class AzureSearchIndexStore +{ + private static readonly Lazy mInstance = new(); + private readonly List registeredIndexes = []; + + /// + /// Gets singleton instance of the + /// + public static AzureSearchIndexStore Instance => mInstance.Value; + + /// + /// Gets all registered indexes. + /// + public IEnumerable GetAllIndices() => registeredIndexes; + + /// + /// Gets a registered with the specified , + /// or null. + /// + /// The name of the index to retrieve. + /// + /// + public AzureSearchIndex? GetIndex(string indexName) => string.IsNullOrEmpty(indexName) ? null + : registeredIndexes.SingleOrDefault(i => i.IndexName.Equals(indexName, StringComparison.OrdinalIgnoreCase)); + + /// + /// Gets a registered with the specified , + /// or null. + /// + /// The identifier of the index to retrieve. + /// + /// + public AzureSearchIndex? GetIndex(int identifier) => registeredIndexes.Find(i => i.Identifier == identifier); + + /// + /// Gets a registered with the specified . If no index is found, a is thrown. + /// + /// The name of the index to retrieve. + /// + /// + public AzureSearchIndex GetRequiredIndex(string indexName) + { + if (string.IsNullOrEmpty(indexName)) + { + throw new ArgumentException("Value must not be null or empty"); + } + + return registeredIndexes.SingleOrDefault(i => i.IndexName.Equals(indexName, StringComparison.OrdinalIgnoreCase)) + ?? throw new InvalidOperationException($"The index '{indexName}' is not registered."); + } + + /// + /// Adds an index to the store. + /// + /// The index to add. + /// + /// + internal void AddIndex(AzureSearchIndex index) + { + if (index == null) + { + throw new ArgumentNullException(nameof(index)); + } + + if (registeredIndexes.Exists(i => i.IndexName.Equals(index.IndexName, StringComparison.OrdinalIgnoreCase) || index.Identifier == i.Identifier)) + { + throw new InvalidOperationException($"Attempted to register AzureSearch index with identifier [{index.Identifier}] and name [{index.IndexName}] but it is already registered."); + } + + registeredIndexes.Add(index); + } + + /// + /// Resets all indicies + /// + /// + internal void SetIndicies(IEnumerable models) + { + registeredIndexes.Clear(); + + foreach (var index in models) + { + Instance.AddIndex(new AzureSearchIndex(index, StrategyStorage.Strategies)); + } + } + + /// + /// Sets the current indicies to those provided by + /// + /// + internal static void SetIndicies(IAzureSearchConfigurationStorageService configurationService) + { + var indices = configurationService.GetAllIndexData(); + + Instance.SetIndicies(indices); + } +} diff --git a/src/Kentico.Xperience.AzureSearch/Indexing/AzureSearchOptions.cs b/src/Kentico.Xperience.AzureSearch/Indexing/AzureSearchOptions.cs index ec738be..65ff642 100644 --- a/src/Kentico.Xperience.AzureSearch/Indexing/AzureSearchOptions.cs +++ b/src/Kentico.Xperience.AzureSearch/Indexing/AzureSearchOptions.cs @@ -42,4 +42,9 @@ public string SearchServiceQueryApiKey get; set; } = string.Empty; + + /// + /// Optional delay between indexing individual s. + /// + public int IndexItemDelay { get; set; } } diff --git a/src/Kentico.Xperience.AzureSearch/Indexing/AzureSearchQueueItem.cs b/src/Kentico.Xperience.AzureSearch/Indexing/AzureSearchQueueItem.cs index 772f71a..ebd3e7a 100644 --- a/src/Kentico.Xperience.AzureSearch/Indexing/AzureSearchQueueItem.cs +++ b/src/Kentico.Xperience.AzureSearch/Indexing/AzureSearchQueueItem.cs @@ -1,47 +1,47 @@ -namespace Kentico.Xperience.AzureSearch.Indexing; - -/// -/// A queued item to be processed by which -/// represents a recent change made to an indexed which is a representation of a . -/// -public sealed class AzureSearchQueueItem -{ - /// - /// The that was changed. - /// - public IIndexEventItemModel ItemToIndex { get; } - - /// - /// The type of the AzureSearch task. - /// - public AzureSearchTaskType TaskType { get; } - - /// - /// The code name of the AzureSearch index to be updated. - /// - public string IndexName { get; } - - /// - /// Initializes a new instance of the class. - /// - /// The that was changed. - /// The type of the AzureSearch task. - /// The code name of the AzureSearch index to be updated. - /// - public AzureSearchQueueItem(IIndexEventItemModel itemToIndex, AzureSearchTaskType taskType, string indexName) - { - if (string.IsNullOrEmpty(indexName)) - { - throw new ArgumentNullException(nameof(indexName)); - } - - if (taskType != AzureSearchTaskType.PUBLISH_INDEX && itemToIndex == null) - { - throw new ArgumentNullException(nameof(itemToIndex)); - } - - ItemToIndex = itemToIndex; - TaskType = taskType; - IndexName = indexName; - } -} +namespace Kentico.Xperience.AzureSearch.Indexing; + +/// +/// A queued item to be processed by which +/// represents a recent change made to an indexed which is a representation of a . +/// +public sealed class AzureSearchQueueItem +{ + /// + /// The that was changed. + /// + public IIndexEventItemModel ItemToIndex { get; } + + /// + /// The type of the AzureSearch task. + /// + public AzureSearchTaskType TaskType { get; } + + /// + /// The code name of the AzureSearch index to be updated. + /// + public string IndexName { get; } + + /// + /// Initializes a new instance of the class. + /// + /// The that was changed. + /// The type of the AzureSearch task. + /// The code name of the AzureSearch index to be updated. + /// + public AzureSearchQueueItem(IIndexEventItemModel itemToIndex, AzureSearchTaskType taskType, string indexName) + { + if (string.IsNullOrEmpty(indexName)) + { + throw new ArgumentNullException(nameof(indexName)); + } + + if (taskType != AzureSearchTaskType.PUBLISH_INDEX && itemToIndex == null) + { + throw new ArgumentNullException(nameof(itemToIndex)); + } + + ItemToIndex = itemToIndex; + TaskType = taskType; + IndexName = indexName; + } +} diff --git a/src/Kentico.Xperience.AzureSearch/Indexing/AzureSearchTaskType.cs b/src/Kentico.Xperience.AzureSearch/Indexing/AzureSearchTaskType.cs index f39c376..95a7f2a 100644 --- a/src/Kentico.Xperience.AzureSearch/Indexing/AzureSearchTaskType.cs +++ b/src/Kentico.Xperience.AzureSearch/Indexing/AzureSearchTaskType.cs @@ -1,29 +1,29 @@ -using Kentico.Xperience.AzureSearch.Indexing; - -namespace Kentico.Xperience.AzureSearch; - -/// -/// Represents the type of a -/// -public enum AzureSearchTaskType -{ - /// - /// Unsupported task type - /// - UNKNOWN, - - /// - /// A task for a page which should be removed from the index - /// - DELETE, - - /// - /// Task marks the end of indexed items, index is published after this task occurs - /// - PUBLISH_INDEX, - - /// - /// A task for a page which should be updated - /// - UPDATE -} +using Kentico.Xperience.AzureSearch.Indexing; + +namespace Kentico.Xperience.AzureSearch; + +/// +/// Represents the type of a +/// +public enum AzureSearchTaskType +{ + /// + /// Unsupported task type + /// + UNKNOWN, + + /// + /// A task for a page which should be removed from the index + /// + DELETE, + + /// + /// Task marks the end of indexed items, index is published after this task occurs + /// + PUBLISH_INDEX, + + /// + /// A task for a page which should be updated + /// + UPDATE +} diff --git a/src/Kentico.Xperience.AzureSearch/Indexing/BaseAzureSearchIndexingStrategy.cs b/src/Kentico.Xperience.AzureSearch/Indexing/BaseAzureSearchIndexingStrategy.cs index 8346e70..6b1f25a 100644 --- a/src/Kentico.Xperience.AzureSearch/Indexing/BaseAzureSearchIndexingStrategy.cs +++ b/src/Kentico.Xperience.AzureSearch/Indexing/BaseAzureSearchIndexingStrategy.cs @@ -1,58 +1,58 @@ -using Azure.Search.Documents; -using Azure.Search.Documents.Indexes; -using Azure.Search.Documents.Indexes.Models; -using Azure.Search.Documents.Models; - -namespace Kentico.Xperience.AzureSearch.Indexing; - -/// -/// Default indexing strategy that provides simple indexing. -/// Search model used for Index definition and data retrieval. -/// -public class BaseAzureSearchIndexingStrategy : IAzureSearchIndexingStrategy where TSearchModel : IAzureSearchModel, new() -{ - private readonly FieldBuilder fieldBuilder = new(); - - /// - public virtual Task MapToAzureSearchModelOrNull(IIndexEventItemModel item) - { - if (item.IsSecured) - { - return Task.FromResult(null); - } - - var indexDocument = new TSearchModel() - { - Name = item.Name - }; - - return Task.FromResult(indexDocument); - } - - /// - public virtual async Task> FindItemsToReindex(IndexEventWebPageItemModel changedItem) => await Task.FromResult(new List() { changedItem }); - - /// - public virtual async Task> FindItemsToReindex(IndexEventReusableItemModel changedItem) => await Task.FromResult(new List()); - - /// - public virtual SemanticRankingConfiguration? CreateSemanticRankingConfigurationOrNull() => null; - - /// - public IList GetSearchFields() => fieldBuilder.Build(typeof(TSearchModel)); - - /// - public async Task UploadDocuments(IEnumerable models, SearchClient searchClient) - { - var batch = new IndexDocumentsBatch(); - - foreach (var model in models) - { - batch.Actions.Add(IndexDocumentsAction.Upload((TSearchModel)model)); - } - - IndexDocumentsResult result = await searchClient.IndexDocumentsAsync(batch); - - return result.Results.Count(x => x.Succeeded); - } -} +using Azure.Search.Documents; +using Azure.Search.Documents.Indexes; +using Azure.Search.Documents.Indexes.Models; +using Azure.Search.Documents.Models; + +namespace Kentico.Xperience.AzureSearch.Indexing; + +/// +/// Default indexing strategy that provides simple indexing. +/// Search model used for Index definition and data retrieval. +/// +public class BaseAzureSearchIndexingStrategy : IAzureSearchIndexingStrategy where TSearchModel : IAzureSearchModel, new() +{ + private readonly FieldBuilder fieldBuilder = new(); + + /// + public virtual Task MapToAzureSearchModelOrNull(IIndexEventItemModel item) + { + if (item.IsSecured) + { + return Task.FromResult(null); + } + + var indexDocument = new TSearchModel() + { + Name = item.Name + }; + + return Task.FromResult(indexDocument); + } + + /// + public virtual async Task> FindItemsToReindex(IndexEventWebPageItemModel changedItem) => await Task.FromResult(new List() { changedItem }); + + /// + public virtual async Task> FindItemsToReindex(IndexEventReusableItemModel changedItem) => await Task.FromResult(new List()); + + /// + public virtual SemanticRankingConfiguration? CreateSemanticRankingConfigurationOrNull() => null; + + /// + public IList GetSearchFields() => fieldBuilder.Build(typeof(TSearchModel)); + + /// + public async Task UploadDocuments(IEnumerable models, SearchClient searchClient) + { + var batch = new IndexDocumentsBatch(); + + foreach (var model in models) + { + batch.Actions.Add(IndexDocumentsAction.Upload((TSearchModel)model)); + } + + IndexDocumentsResult result = await searchClient.IndexDocumentsAsync(batch); + + return result.Results.Count(x => x.Succeeded); + } +} diff --git a/src/Kentico.Xperience.AzureSearch/Indexing/DefaultAzureSearchTaskProcessor.cs b/src/Kentico.Xperience.AzureSearch/Indexing/DefaultAzureSearchTaskProcessor.cs index ee8aa05..7be9cee 100644 --- a/src/Kentico.Xperience.AzureSearch/Indexing/DefaultAzureSearchTaskProcessor.cs +++ b/src/Kentico.Xperience.AzureSearch/Indexing/DefaultAzureSearchTaskProcessor.cs @@ -3,6 +3,7 @@ using CMS.Websites; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; namespace Kentico.Xperience.AzureSearch.Indexing; @@ -18,17 +19,20 @@ internal class DefaultAzureSearchTaskProcessor : IAzureSearchTaskProcessor private readonly IServiceProvider serviceProvider; private readonly IAzureSearchClient azureSearchClient; private readonly IEventLogService eventLogService; + private readonly AzureSearchOptions azureSearchOptions; public DefaultAzureSearchTaskProcessor( IAzureSearchClient azureSearchClient, IEventLogService eventLogService, IWebPageUrlRetriever urlRetriever, - IServiceProvider serviceProvider) + IServiceProvider serviceProvider, + IOptions azureSearchOptions) { this.azureSearchClient = azureSearchClient; this.eventLogService = eventLogService; this.urlRetriever = urlRetriever; this.serviceProvider = serviceProvider; + this.azureSearchOptions = azureSearchOptions.Value; } /// @@ -46,6 +50,20 @@ public async Task ProcessAzureSearchTasks(IEnumerable return batchResults.SuccessfulOperations; } + private async Task AddQueueItemToUpsertOrDelete(AzureSearchQueueItem item, List upsertData, List deleteTasks) + { + var document = await GetSearchModel(item); + + if (document is not null) + { + upsertData.Add(document); + } + else + { + deleteTasks.Add(item); + } + } + private async Task ProcessAzureSearchBatch(IEnumerable queueItems, AzureSearchBatchResult previousBatchResults, CancellationToken cancellationToken) { var groups = queueItems.GroupBy(item => item.IndexName); @@ -60,16 +78,17 @@ private async Task ProcessAzureSearchBatch(IEnumerable que var updateTasks = group.Where(queueItem => queueItem.TaskType is AzureSearchTaskType.PUBLISH_INDEX or AzureSearchTaskType.UPDATE); var upsertData = new List(); - foreach (var queueItem in updateTasks) + if (updateTasks.Any()) { - var document = await GetSearchModel(queueItem); - if (document is not null) - { - upsertData.Add(document); - } - else + await AddQueueItemToUpsertOrDelete(updateTasks.First(), upsertData, deleteTasks); + } + + if (updateTasks.Count() > 1) + { + foreach (var queueItem in updateTasks.Skip(1)) { - deleteTasks.Add(queueItem); + await Task.Delay(azureSearchOptions.IndexItemDelay, cancellationToken); + await AddQueueItemToUpsertOrDelete(queueItem, upsertData, deleteTasks); } } diff --git a/src/Kentico.Xperience.AzureSearch/Indexing/IAzureSearchClient.cs b/src/Kentico.Xperience.AzureSearch/Indexing/IAzureSearchClient.cs index 19e23fb..eb9941c 100644 --- a/src/Kentico.Xperience.AzureSearch/Indexing/IAzureSearchClient.cs +++ b/src/Kentico.Xperience.AzureSearch/Indexing/IAzureSearchClient.cs @@ -1,70 +1,70 @@ -using Kentico.Xperience.AzureSearch.Admin; - -namespace Kentico.Xperience.AzureSearch.Indexing; - -/// -/// Contains methods to interface with the AzureSearch API. -/// -public interface IAzureSearchClient -{ - /// - /// Removes records from the AzureSearch index. - /// - /// The AzureSearch internal IDs of the records to delete. - /// The index containing the objects to delete. - /// The cancellation token for the task. - /// - /// - /// - /// - /// - /// The number of records deleted. - Task DeleteRecords(IEnumerable itemGuids, string indexName, CancellationToken cancellationToken); - - /// - /// Gets the indices of the AzureSearch application with basic statistics. - /// - /// The cancellation token for the task. - /// - /// - /// - Task> GetStatistics(CancellationToken cancellationToken); - - /// - /// Updates the AzureSearch index with the dynamic data in each object of the passed . - /// - /// Logs an error if there are issues loading the node data. - /// The document to upsert into AzureSearch. - /// The index to upsert the data to. - /// The cancellation token for the task. - /// - /// - /// - /// - /// The number of objects processed. - Task UpsertRecords(IEnumerable models, string indexName, CancellationToken cancellationToken); - - /// - /// Rebuilds the AzureSearch index by removing existing data from AzureSearch and indexing all - /// pages in the content tree included in the index. - /// - /// The index to rebuild. - /// The cancellation token for the task. - /// - /// - /// - /// - Task Rebuild(string indexName, CancellationToken? cancellationToken); - - /// - /// Deletes the AzureSearch index by removing existing index data from Azure. - /// - /// The index to delete. - /// The cancellation token for the task. - /// - /// - /// - /// Thrown when is null. - /// Thrown when a failure is returned by the Search service. - Task DeleteIndex(string indexName, CancellationToken cancellationToken); -} +using Kentico.Xperience.AzureSearch.Admin; + +namespace Kentico.Xperience.AzureSearch.Indexing; + +/// +/// Contains methods to interface with the AzureSearch API. +/// +public interface IAzureSearchClient +{ + /// + /// Removes records from the AzureSearch index. + /// + /// The AzureSearch internal IDs of the records to delete. + /// The index containing the objects to delete. + /// The cancellation token for the task. + /// + /// + /// + /// + /// + /// The number of records deleted. + Task DeleteRecords(IEnumerable itemGuids, string indexName, CancellationToken cancellationToken); + + /// + /// Gets the indices of the AzureSearch application with basic statistics. + /// + /// The cancellation token for the task. + /// + /// + /// + Task> GetStatistics(CancellationToken cancellationToken); + + /// + /// Updates the AzureSearch index with the dynamic data in each object of the passed . + /// + /// Logs an error if there are issues loading the node data. + /// The document to upsert into AzureSearch. + /// The index to upsert the data to. + /// The cancellation token for the task. + /// + /// + /// + /// + /// The number of objects processed. + Task UpsertRecords(IEnumerable models, string indexName, CancellationToken cancellationToken); + + /// + /// Rebuilds the AzureSearch index by removing existing data from AzureSearch and indexing all + /// pages in the content tree included in the index. + /// + /// The index to rebuild. + /// The cancellation token for the task. + /// + /// + /// + /// + Task Rebuild(string indexName, CancellationToken? cancellationToken); + + /// + /// Deletes the AzureSearch index by removing existing index data from Azure. + /// + /// The index to delete. + /// The cancellation token for the task. + /// + /// + /// + /// Thrown when is null. + /// Thrown when a failure is returned by the Search service. + Task DeleteIndex(string indexName, CancellationToken cancellationToken); +} diff --git a/src/Kentico.Xperience.AzureSearch/Indexing/IAzureSearchIndexClientService.cs b/src/Kentico.Xperience.AzureSearch/Indexing/IAzureSearchIndexClientService.cs index 4dd3189..94e8623 100644 --- a/src/Kentico.Xperience.AzureSearch/Indexing/IAzureSearchIndexClientService.cs +++ b/src/Kentico.Xperience.AzureSearch/Indexing/IAzureSearchIndexClientService.cs @@ -1,32 +1,32 @@ -using Azure.Search.Documents; -using Azure.Search.Documents.Indexes.Models; - -using Kentico.Xperience.AzureSearch.Admin; - -namespace Kentico.Xperience.AzureSearch.Indexing; - -/// -/// Initializes instances. -/// -public interface IAzureSearchIndexClientService -{ - /// - /// Initializes a new for the given - /// - /// The code name of the index. - /// The cancellation token for the task. - /// - Task InitializeIndexClient(string indexName, CancellationToken cancellationToken); - - /// - /// Edits the AzureSearch index in Azure. - /// - /// The name of index to edit. - /// New index configuration. - /// The cancellation token for the task. - /// - /// - /// - /// Thrown when a failure is returned by the Search service. - Task EditIndex(string oldIndexName, AzureSearchConfigurationModel newIndexConfiguration, CancellationToken cancellationToken); -} +using Azure.Search.Documents; +using Azure.Search.Documents.Indexes.Models; + +using Kentico.Xperience.AzureSearch.Admin; + +namespace Kentico.Xperience.AzureSearch.Indexing; + +/// +/// Initializes instances. +/// +public interface IAzureSearchIndexClientService +{ + /// + /// Initializes a new for the given + /// + /// The code name of the index. + /// The cancellation token for the task. + /// + Task InitializeIndexClient(string indexName, CancellationToken cancellationToken); + + /// + /// Edits the AzureSearch index in Azure. + /// + /// The name of index to edit. + /// New index configuration. + /// The cancellation token for the task. + /// + /// + /// + /// Thrown when a failure is returned by the Search service. + Task EditIndex(string oldIndexName, AzureSearchConfigurationModel newIndexConfiguration, CancellationToken cancellationToken); +} diff --git a/src/Kentico.Xperience.AzureSearch/Indexing/IAzureSearchIndexingStrategy.cs b/src/Kentico.Xperience.AzureSearch/Indexing/IAzureSearchIndexingStrategy.cs index 1f790cf..278ad62 100644 --- a/src/Kentico.Xperience.AzureSearch/Indexing/IAzureSearchIndexingStrategy.cs +++ b/src/Kentico.Xperience.AzureSearch/Indexing/IAzureSearchIndexingStrategy.cs @@ -1,47 +1,47 @@ -using Azure.Search.Documents; -using Azure.Search.Documents.Indexes.Models; - -namespace Kentico.Xperience.AzureSearch.Indexing; - -public interface IAzureSearchIndexingStrategy -{ - /// - /// Called when indexing a search model. Enables overriding of multiple fields with custom data - /// - /// The currently being indexed. - /// Modified AzureSearch document. - Task MapToAzureSearchModelOrNull(IIndexEventItemModel item); - - /// - /// Triggered by modifications to a web page item, which is provided to determine what other items should be included for indexing - /// - /// The web page item that was modified - /// Items that should be passed to for indexing - Task> FindItemsToReindex(IndexEventWebPageItemModel changedItem); - - /// - /// Triggered by modifications to a reusable content item, which is provided to determine what other items should be included for indexing - /// - /// The reusable content item that was modified - /// Items that should be passed to for indexing - Task> FindItemsToReindex(IndexEventReusableItemModel changedItem); - - /// - /// Called when creating a SearchIndex to optionally add Semantic Ranking - /// - /// - SemanticRankingConfiguration? CreateSemanticRankingConfigurationOrNull(); - - /// - /// Called when creating AzureSearch Index and united with properties from - /// - /// A collection of user defined search fields. - IList GetSearchFields(); - - /// - /// Called when uploading data to user defined . - /// Expects an created in - /// - /// Number of uploaded documents - Task UploadDocuments(IEnumerable models, SearchClient searchClient); -} +using Azure.Search.Documents; +using Azure.Search.Documents.Indexes.Models; + +namespace Kentico.Xperience.AzureSearch.Indexing; + +public interface IAzureSearchIndexingStrategy +{ + /// + /// Called when indexing a search model. Enables overriding of multiple fields with custom data + /// + /// The currently being indexed. + /// Modified AzureSearch document. + Task MapToAzureSearchModelOrNull(IIndexEventItemModel item); + + /// + /// Triggered by modifications to a web page item, which is provided to determine what other items should be included for indexing + /// + /// The web page item that was modified + /// Items that should be passed to for indexing + Task> FindItemsToReindex(IndexEventWebPageItemModel changedItem); + + /// + /// Triggered by modifications to a reusable content item, which is provided to determine what other items should be included for indexing + /// + /// The reusable content item that was modified + /// Items that should be passed to for indexing + Task> FindItemsToReindex(IndexEventReusableItemModel changedItem); + + /// + /// Called when creating a SearchIndex to optionally add Semantic Ranking + /// + /// + SemanticRankingConfiguration? CreateSemanticRankingConfigurationOrNull(); + + /// + /// Called when creating AzureSearch Index and united with properties from + /// + /// A collection of user defined search fields. + IList GetSearchFields(); + + /// + /// Called when uploading data to user defined . + /// Expects an created in + /// + /// Number of uploaded documents + Task UploadDocuments(IEnumerable models, SearchClient searchClient); +} diff --git a/src/Kentico.Xperience.AzureSearch/Indexing/IAzureSearchModel.cs b/src/Kentico.Xperience.AzureSearch/Indexing/IAzureSearchModel.cs index 16fc7fd..34e4d31 100644 --- a/src/Kentico.Xperience.AzureSearch/Indexing/IAzureSearchModel.cs +++ b/src/Kentico.Xperience.AzureSearch/Indexing/IAzureSearchModel.cs @@ -1,14 +1,14 @@ -namespace Kentico.Xperience.AzureSearch.Indexing; - -/// -/// Abstraction of properties used to define, create and retrieve data from azure search portal. -/// -public interface IAzureSearchModel -{ - public string? Url { get; set; } - public string ContentTypeName { get; set; } - public string LanguageName { get; set; } - public string ItemGuid { get; set; } - public string ObjectID { get; set; } - public string Name { get; set; } -} +namespace Kentico.Xperience.AzureSearch.Indexing; + +/// +/// Abstraction of properties used to define, create and retrieve data from azure search portal. +/// +public interface IAzureSearchModel +{ + public string? Url { get; set; } + public string ContentTypeName { get; set; } + public string LanguageName { get; set; } + public string ItemGuid { get; set; } + public string ObjectID { get; set; } + public string Name { get; set; } +} diff --git a/src/Kentico.Xperience.AzureSearch/Indexing/IAzureSearchTaskLogger.cs b/src/Kentico.Xperience.AzureSearch/Indexing/IAzureSearchTaskLogger.cs index 154a546..bf62081 100644 --- a/src/Kentico.Xperience.AzureSearch/Indexing/IAzureSearchTaskLogger.cs +++ b/src/Kentico.Xperience.AzureSearch/Indexing/IAzureSearchTaskLogger.cs @@ -1,25 +1,25 @@ -namespace Kentico.Xperience.AzureSearch.Indexing; - -/// -/// Contains methods for logging s and s -/// for processing by and . -/// -public interface IAzureSearchTaskLogger -{ - /// - /// Logs an for each registered crawler. Then, loops - /// through all registered AzureSearch indexes and logs a task if the passed is indexed. - /// - /// The that triggered the event. - /// The name of the Xperience event that was triggered. - Task HandleEvent(IndexEventWebPageItemModel webpageItem, string eventName); - - /// - /// Logs an for each registered crawler. Then, loops - /// through all registered AzureSearch indexes and logs a task if the passed is indexed. - /// - /// - /// - /// - Task HandleReusableItemEvent(IndexEventReusableItemModel reusableItem, string eventName); -} +namespace Kentico.Xperience.AzureSearch.Indexing; + +/// +/// Contains methods for logging s and s +/// for processing by and . +/// +public interface IAzureSearchTaskLogger +{ + /// + /// Logs an for each registered crawler. Then, loops + /// through all registered AzureSearch indexes and logs a task if the passed is indexed. + /// + /// The that triggered the event. + /// The name of the Xperience event that was triggered. + Task HandleEvent(IndexEventWebPageItemModel webpageItem, string eventName); + + /// + /// Logs an for each registered crawler. Then, loops + /// through all registered AzureSearch indexes and logs a task if the passed is indexed. + /// + /// + /// + /// + Task HandleReusableItemEvent(IndexEventReusableItemModel reusableItem, string eventName); +} diff --git a/src/Kentico.Xperience.AzureSearch/Indexing/IAzureSearchTaskProcessor.cs b/src/Kentico.Xperience.AzureSearch/Indexing/IAzureSearchTaskProcessor.cs index dd78965..df59189 100644 --- a/src/Kentico.Xperience.AzureSearch/Indexing/IAzureSearchTaskProcessor.cs +++ b/src/Kentico.Xperience.AzureSearch/Indexing/IAzureSearchTaskProcessor.cs @@ -1,18 +1,18 @@ -namespace Kentico.Xperience.AzureSearch.Indexing; - -/// -/// Processes tasks from . -/// -public interface IAzureSearchTaskProcessor -{ - /// - /// Processes multiple queue items from all AzureSearch indexes in batches. AzureSearch - /// automatically applies batching in multiples of 1,000 when using their API, - /// so all queue items are forwarded to the API. - /// - /// The items to process. - /// The cancellation token for the task. - /// - /// The number of items processed. - Task ProcessAzureSearchTasks(IEnumerable queueItems, CancellationToken cancellationToken, int maximumBatchSize = 100); -} +namespace Kentico.Xperience.AzureSearch.Indexing; + +/// +/// Processes tasks from . +/// +public interface IAzureSearchTaskProcessor +{ + /// + /// Processes multiple queue items from all AzureSearch indexes in batches. AzureSearch + /// automatically applies batching in multiples of 1,000 when using their API, + /// so all queue items are forwarded to the API. + /// + /// The items to process. + /// The cancellation token for the task. + /// + /// The number of items processed. + Task ProcessAzureSearchTasks(IEnumerable queueItems, CancellationToken cancellationToken, int maximumBatchSize = 100); +} diff --git a/src/Kentico.Xperience.AzureSearch/Indexing/IIndexEventItemModel.cs b/src/Kentico.Xperience.AzureSearch/Indexing/IIndexEventItemModel.cs index 580e370..3d479db 100644 --- a/src/Kentico.Xperience.AzureSearch/Indexing/IIndexEventItemModel.cs +++ b/src/Kentico.Xperience.AzureSearch/Indexing/IIndexEventItemModel.cs @@ -1,152 +1,152 @@ -using CMS.ContentEngine; -using CMS.Websites; - -namespace Kentico.Xperience.AzureSearch.Indexing; - -/// -/// Abstraction of different types of events generated from content modifications -/// -public interface IIndexEventItemModel -{ - /// - /// The identifier of the item - /// - int ItemID { get; set; } - Guid ItemGuid { get; set; } - string LanguageName { get; set; } - string ContentTypeName { get; set; } - string Name { get; set; } - bool IsSecured { get; set; } - int ContentTypeID { get; set; } - int ContentLanguageID { get; set; } -} - -/// -/// Represents a modification to a web page -/// -public class IndexEventWebPageItemModel : IIndexEventItemModel -{ - /// - /// The - /// - public int ItemID { get; set; } - /// - /// The - /// - public Guid ItemGuid { get; set; } - public string LanguageName { get; set; } - public string ContentTypeName { get; set; } - /// - /// The - /// - public string Name { get; set; } - public bool IsSecured { get; set; } - public int ContentTypeID { get; set; } - public int ContentLanguageID { get; set; } - - public string WebsiteChannelName { get; set; } - public string WebPageItemTreePath { get; set; } - public int? ParentID { get; set; } - public int Order { get; set; } - - public IndexEventWebPageItemModel( - int itemID, - Guid itemGuid, - string languageName, - string contentTypeName, - string name, - bool isSecured, - int contentTypeID, - int contentLanguageID, - string websiteChannelName, - string webPageItemTreePath, - int parentID, - int order - ) - { - ItemID = itemID; - ItemGuid = itemGuid; - LanguageName = languageName; - ContentTypeName = contentTypeName; - WebsiteChannelName = websiteChannelName; - WebPageItemTreePath = webPageItemTreePath; - ParentID = parentID; - Order = order; - Name = name; - IsSecured = isSecured; - ContentTypeID = contentTypeID; - ContentLanguageID = contentLanguageID; - } - - public IndexEventWebPageItemModel( - int itemID, - Guid itemGuid, - string languageName, - string contentTypeName, - string name, - bool isSecured, - int contentTypeID, - int contentLanguageID, - string websiteChannelName, - string webPageItemTreePath, - int order - ) - { - ItemID = itemID; - ItemGuid = itemGuid; - LanguageName = languageName; - ContentTypeName = contentTypeName; - WebsiteChannelName = websiteChannelName; - WebPageItemTreePath = webPageItemTreePath; - Order = order; - Name = name; - IsSecured = isSecured; - ContentTypeID = contentTypeID; - ContentLanguageID = contentLanguageID; - } -} - -/// -/// Represents a modification to a reusable content item -/// -public class IndexEventReusableItemModel : IIndexEventItemModel -{ - /// - /// The - /// - public int ItemID { get; set; } - /// - /// The - /// - public Guid ItemGuid { get; set; } - public string LanguageName { get; set; } - public string ContentTypeName { get; set; } - /// - /// The - /// - public string Name { get; set; } - public bool IsSecured { get; set; } - public int ContentTypeID { get; set; } - public int ContentLanguageID { get; set; } - - public IndexEventReusableItemModel( - int itemID, - Guid itemGuid, - string languageName, - string contentTypeName, - string name, - bool isSecured, - int contentTypeID, - int contentLanguageID - ) - { - ItemID = itemID; - ItemGuid = itemGuid; - LanguageName = languageName; - ContentTypeName = contentTypeName; - Name = name; - IsSecured = isSecured; - ContentTypeID = contentTypeID; - ContentLanguageID = contentLanguageID; - } -} +using CMS.ContentEngine; +using CMS.Websites; + +namespace Kentico.Xperience.AzureSearch.Indexing; + +/// +/// Abstraction of different types of events generated from content modifications +/// +public interface IIndexEventItemModel +{ + /// + /// The identifier of the item + /// + int ItemID { get; set; } + Guid ItemGuid { get; set; } + string LanguageName { get; set; } + string ContentTypeName { get; set; } + string Name { get; set; } + bool IsSecured { get; set; } + int ContentTypeID { get; set; } + int ContentLanguageID { get; set; } +} + +/// +/// Represents a modification to a web page +/// +public class IndexEventWebPageItemModel : IIndexEventItemModel +{ + /// + /// The + /// + public int ItemID { get; set; } + /// + /// The + /// + public Guid ItemGuid { get; set; } + public string LanguageName { get; set; } + public string ContentTypeName { get; set; } + /// + /// The + /// + public string Name { get; set; } + public bool IsSecured { get; set; } + public int ContentTypeID { get; set; } + public int ContentLanguageID { get; set; } + + public string WebsiteChannelName { get; set; } + public string WebPageItemTreePath { get; set; } + public int? ParentID { get; set; } + public int Order { get; set; } + + public IndexEventWebPageItemModel( + int itemID, + Guid itemGuid, + string languageName, + string contentTypeName, + string name, + bool isSecured, + int contentTypeID, + int contentLanguageID, + string websiteChannelName, + string webPageItemTreePath, + int parentID, + int order + ) + { + ItemID = itemID; + ItemGuid = itemGuid; + LanguageName = languageName; + ContentTypeName = contentTypeName; + WebsiteChannelName = websiteChannelName; + WebPageItemTreePath = webPageItemTreePath; + ParentID = parentID; + Order = order; + Name = name; + IsSecured = isSecured; + ContentTypeID = contentTypeID; + ContentLanguageID = contentLanguageID; + } + + public IndexEventWebPageItemModel( + int itemID, + Guid itemGuid, + string languageName, + string contentTypeName, + string name, + bool isSecured, + int contentTypeID, + int contentLanguageID, + string websiteChannelName, + string webPageItemTreePath, + int order + ) + { + ItemID = itemID; + ItemGuid = itemGuid; + LanguageName = languageName; + ContentTypeName = contentTypeName; + WebsiteChannelName = websiteChannelName; + WebPageItemTreePath = webPageItemTreePath; + Order = order; + Name = name; + IsSecured = isSecured; + ContentTypeID = contentTypeID; + ContentLanguageID = contentLanguageID; + } +} + +/// +/// Represents a modification to a reusable content item +/// +public class IndexEventReusableItemModel : IIndexEventItemModel +{ + /// + /// The + /// + public int ItemID { get; set; } + /// + /// The + /// + public Guid ItemGuid { get; set; } + public string LanguageName { get; set; } + public string ContentTypeName { get; set; } + /// + /// The + /// + public string Name { get; set; } + public bool IsSecured { get; set; } + public int ContentTypeID { get; set; } + public int ContentLanguageID { get; set; } + + public IndexEventReusableItemModel( + int itemID, + Guid itemGuid, + string languageName, + string contentTypeName, + string name, + bool isSecured, + int contentTypeID, + int contentLanguageID + ) + { + ItemID = itemID; + ItemGuid = itemGuid; + LanguageName = languageName; + ContentTypeName = contentTypeName; + Name = name; + IsSecured = isSecured; + ContentTypeID = contentTypeID; + ContentLanguageID = contentLanguageID; + } +} diff --git a/src/Kentico.Xperience.AzureSearch/Indexing/SemanticRankingConfiguration.cs b/src/Kentico.Xperience.AzureSearch/Indexing/SemanticRankingConfiguration.cs index 86f5372..abe0214 100644 --- a/src/Kentico.Xperience.AzureSearch/Indexing/SemanticRankingConfiguration.cs +++ b/src/Kentico.Xperience.AzureSearch/Indexing/SemanticRankingConfiguration.cs @@ -1,38 +1,38 @@ -using Azure.Search.Documents.Indexes.Models; - -namespace Kentico.Xperience.AzureSearch.Indexing; - -/// -/// Semantic configuration which can be added to AzureSearchIndex -/// -public class SemanticRankingConfiguration -{ - /// - /// Gets the suggesters for the index. - /// - public IList Suggesters { get; } - - /// Defines parameters for a search index that influence semantic capabilities. - public SemanticSearch SemanticSearch { get; set; } - - /// - /// Creates Configuration with empty list of - /// - /// - public SemanticRankingConfiguration(SemanticSearch semanticSearch) - { - SemanticSearch = semanticSearch; - Suggesters = new List(); - } - - /// - /// Creates Configuration with defined list of - /// - /// - /// - public SemanticRankingConfiguration(SemanticSearch semanticSearch, IList suggesters) - { - SemanticSearch = semanticSearch; - Suggesters = suggesters; - } -} +using Azure.Search.Documents.Indexes.Models; + +namespace Kentico.Xperience.AzureSearch.Indexing; + +/// +/// Semantic configuration which can be added to AzureSearchIndex +/// +public class SemanticRankingConfiguration +{ + /// + /// Gets the suggesters for the index. + /// + public IList Suggesters { get; } + + /// Defines parameters for a search index that influence semantic capabilities. + public SemanticSearch SemanticSearch { get; set; } + + /// + /// Creates Configuration with empty list of + /// + /// + public SemanticRankingConfiguration(SemanticSearch semanticSearch) + { + SemanticSearch = semanticSearch; + Suggesters = new List(); + } + + /// + /// Creates Configuration with defined list of + /// + /// + /// + public SemanticRankingConfiguration(SemanticSearch semanticSearch, IList suggesters) + { + SemanticSearch = semanticSearch; + Suggesters = suggesters; + } +} diff --git a/src/Kentico.Xperience.AzureSearch/Indexing/StrategyStorage.cs b/src/Kentico.Xperience.AzureSearch/Indexing/StrategyStorage.cs index e68d3dc..80b8c9a 100644 --- a/src/Kentico.Xperience.AzureSearch/Indexing/StrategyStorage.cs +++ b/src/Kentico.Xperience.AzureSearch/Indexing/StrategyStorage.cs @@ -1,16 +1,16 @@ -namespace Kentico.Xperience.AzureSearch.Indexing; - -internal static class StrategyStorage -{ - public static Dictionary Strategies { get; private set; } - - static StrategyStorage() => Strategies = []; - - public static void AddStrategy(string strategyName) where TStrategy : IAzureSearchIndexingStrategy - => Strategies.Add(strategyName, typeof(TStrategy)); - - public static Type GetOrDefault(string strategyName) => - Strategies.TryGetValue(strategyName, out var type) - ? type - : typeof(BaseAzureSearchIndexingStrategy); -} +namespace Kentico.Xperience.AzureSearch.Indexing; + +internal static class StrategyStorage +{ + public static Dictionary Strategies { get; private set; } + + static StrategyStorage() => Strategies = []; + + public static void AddStrategy(string strategyName) where TStrategy : IAzureSearchIndexingStrategy + => Strategies.Add(strategyName, typeof(TStrategy)); + + public static Type GetOrDefault(string strategyName) => + Strategies.TryGetValue(strategyName, out var type) + ? type + : typeof(BaseAzureSearchIndexingStrategy); +} diff --git a/src/Kentico.Xperience.AzureSearch/Resources/AzureSearchResources.cs b/src/Kentico.Xperience.AzureSearch/Resources/AzureSearchResources.cs index ac399a4..e9ebb52 100644 --- a/src/Kentico.Xperience.AzureSearch/Resources/AzureSearchResources.cs +++ b/src/Kentico.Xperience.AzureSearch/Resources/AzureSearchResources.cs @@ -1,14 +1,14 @@ -using CMS.Base; -using CMS.Localization; - -using Kentico.Xperience.AzureSearch.Resources; - -[assembly: RegisterLocalizationResource(typeof(AzureSearchResources), SystemContext.SYSTEM_CULTURE_NAME)] -namespace Kentico.Xperience.AzureSearch.Resources; - -internal class AzureSearchResources -{ - public AzureSearchResources() - { - } -} +using CMS.Base; +using CMS.Localization; + +using Kentico.Xperience.AzureSearch.Resources; + +[assembly: RegisterLocalizationResource(typeof(AzureSearchResources), SystemContext.SYSTEM_CULTURE_NAME)] +namespace Kentico.Xperience.AzureSearch.Resources; + +internal class AzureSearchResources +{ + public AzureSearchResources() + { + } +} diff --git a/src/Kentico.Xperience.AzureSearch/Search/AzureSearchQueryClientOptions.cs b/src/Kentico.Xperience.AzureSearch/Search/AzureSearchQueryClientOptions.cs index ff7ad24..68f8aaa 100644 --- a/src/Kentico.Xperience.AzureSearch/Search/AzureSearchQueryClientOptions.cs +++ b/src/Kentico.Xperience.AzureSearch/Search/AzureSearchQueryClientOptions.cs @@ -1,13 +1,13 @@ -namespace Kentico.Xperience.AzureSearch.Search; - -public class AzureSearchQueryClientOptions -{ - public string ServiceEndpoint { get; set; } - public string QueryApiKey { get; set; } - - public AzureSearchQueryClientOptions(string serviceEndpoint, string queryApiKey) - { - ServiceEndpoint = serviceEndpoint; - QueryApiKey = queryApiKey; - } -} +namespace Kentico.Xperience.AzureSearch.Search; + +public class AzureSearchQueryClientOptions +{ + public string ServiceEndpoint { get; set; } + public string QueryApiKey { get; set; } + + public AzureSearchQueryClientOptions(string serviceEndpoint, string queryApiKey) + { + ServiceEndpoint = serviceEndpoint; + QueryApiKey = queryApiKey; + } +} diff --git a/src/Kentico.Xperience.AzureSearch/Search/AzureSearchQueryClientService.cs b/src/Kentico.Xperience.AzureSearch/Search/AzureSearchQueryClientService.cs index b8c2c57..0e29ae2 100644 --- a/src/Kentico.Xperience.AzureSearch/Search/AzureSearchQueryClientService.cs +++ b/src/Kentico.Xperience.AzureSearch/Search/AzureSearchQueryClientService.cs @@ -1,19 +1,19 @@ -using Azure; -using Azure.Search.Documents; - -namespace Kentico.Xperience.AzureSearch.Search; - -/// -public sealed class AzureSearchQueryClientService : IAzureSearchQueryClientService -{ - private readonly AzureSearchQueryClientOptions settings; - - public AzureSearchQueryClientService(AzureSearchQueryClientOptions settings) => this.settings = settings; - - /// - /// Gets user settings from appsettings.json and initializes - /// - /// - /// Initialized - public SearchClient CreateSearchClientForQueries(string indexName) => new(new Uri(settings.ServiceEndpoint), indexName, new AzureKeyCredential(settings.QueryApiKey)); -} +using Azure; +using Azure.Search.Documents; + +namespace Kentico.Xperience.AzureSearch.Search; + +/// +public sealed class AzureSearchQueryClientService : IAzureSearchQueryClientService +{ + private readonly AzureSearchQueryClientOptions settings; + + public AzureSearchQueryClientService(AzureSearchQueryClientOptions settings) => this.settings = settings; + + /// + /// Gets user settings from appsettings.json and initializes + /// + /// + /// Initialized + public SearchClient CreateSearchClientForQueries(string indexName) => new(new Uri(settings.ServiceEndpoint), indexName, new AzureKeyCredential(settings.QueryApiKey)); +} diff --git a/src/Kentico.Xperience.AzureSearch/Search/IAzureSearchQueryClientService.cs b/src/Kentico.Xperience.AzureSearch/Search/IAzureSearchQueryClientService.cs index 69fb21e..6886545 100644 --- a/src/Kentico.Xperience.AzureSearch/Search/IAzureSearchQueryClientService.cs +++ b/src/Kentico.Xperience.AzureSearch/Search/IAzureSearchQueryClientService.cs @@ -1,11 +1,11 @@ -using Azure.Search.Documents; - -namespace Kentico.Xperience.AzureSearch.Search; - -/// -/// Primary service used for querying azuresearch indexes -/// -public interface IAzureSearchQueryClientService -{ - SearchClient CreateSearchClientForQueries(string indexName); -} +using Azure.Search.Documents; + +namespace Kentico.Xperience.AzureSearch.Search; + +/// +/// Primary service used for querying azuresearch indexes +/// +public interface IAzureSearchQueryClientService +{ + SearchClient CreateSearchClientForQueries(string indexName); +} diff --git a/src/Kentico.Xperience.AzureSearch/ServiceProviderExtensions.cs b/src/Kentico.Xperience.AzureSearch/ServiceProviderExtensions.cs index fd108bd..147e38f 100644 --- a/src/Kentico.Xperience.AzureSearch/ServiceProviderExtensions.cs +++ b/src/Kentico.Xperience.AzureSearch/ServiceProviderExtensions.cs @@ -1,25 +1,25 @@ -using Kentico.Xperience.AzureSearch.Indexing; - -namespace Microsoft.Extensions.DependencyInjection; - -internal static class ServiceProviderExtensions -{ - /// - /// Returns an instance of the assigned to the given . - /// Used to generate instances of a service type that can change at runtime. - /// - /// - /// - /// - /// Thrown if the assigned cannot be instantiated. - /// This shouldn't normally occur because we fallback to if not custom strategy is specified. - /// However, incorrect dependency management in user-code could cause issues. - /// - /// - public static IAzureSearchIndexingStrategy GetRequiredStrategy(this IServiceProvider serviceProvider, AzureSearchIndex index) - { - var strategy = serviceProvider.GetRequiredService(index.AzureSearchIndexingStrategyType) as IAzureSearchIndexingStrategy; - - return strategy!; - } -} +using Kentico.Xperience.AzureSearch.Indexing; + +namespace Microsoft.Extensions.DependencyInjection; + +internal static class ServiceProviderExtensions +{ + /// + /// Returns an instance of the assigned to the given . + /// Used to generate instances of a service type that can change at runtime. + /// + /// + /// + /// + /// Thrown if the assigned cannot be instantiated. + /// This shouldn't normally occur because we fallback to if not custom strategy is specified. + /// However, incorrect dependency management in user-code could cause issues. + /// + /// + public static IAzureSearchIndexingStrategy GetRequiredStrategy(this IServiceProvider serviceProvider, AzureSearchIndex index) + { + var strategy = serviceProvider.GetRequiredService(index.AzureSearchIndexingStrategyType) as IAzureSearchIndexingStrategy; + + return strategy!; + } +} diff --git a/tests/Kentico.Xperience.AzureSearch.Tests/Data/MockDataProvider.cs b/tests/Kentico.Xperience.AzureSearch.Tests/Data/MockDataProvider.cs index 8ab0401..00bacd8 100644 --- a/tests/Kentico.Xperience.AzureSearch.Tests/Data/MockDataProvider.cs +++ b/tests/Kentico.Xperience.AzureSearch.Tests/Data/MockDataProvider.cs @@ -1,58 +1,58 @@ -using DancingGoat.Models; - -using Kentico.Xperience.AzureSearch.Admin; -using Kentico.Xperience.AzureSearch.Indexing; - -namespace Kentico.Xperience.AzureSearch.Tests.Base; -internal static class MockDataProvider -{ - public static IndexEventWebPageItemModel WebModel(IndexEventWebPageItemModel item) - { - item.LanguageName = CzechLanguageName; - item.ContentTypeName = ArticlePage.CONTENT_TYPE_NAME; - item.Name = "Name"; - item.ContentTypeID = 1; - item.ContentLanguageID = 1; - item.WebsiteChannelName = DefaultChannel; - item.WebPageItemTreePath = "/%"; - - return item; - } - - public static AzureSearchIndexIncludedPath Path => new("/%") - { - ContentTypes = [new AzureSearchIndexContentType(ArticlePage.CONTENT_TYPE_NAME, nameof(ArticlePage))] - }; - - - public static AzureSearchIndex Index => new( - new AzureSearchConfigurationModel() - { - IndexName = DefaultIndex, - ChannelName = DefaultChannel, - LanguageNames = new List() { EnglishLanguageName, CzechLanguageName }, - Paths = new List() { Path }, - StrategyName = "strategy" - }, - [] - ); - - public static readonly string DefaultIndex = "SimpleIndex"; - public static readonly string DefaultChannel = "DefaultChannel"; - public static readonly string EnglishLanguageName = "en"; - public static readonly string CzechLanguageName = "cz"; - public static readonly int IndexId = 1; - public static readonly string EventName = "publish"; - - public static AzureSearchIndex GetIndex(string indexName, int id) => new( - new AzureSearchConfigurationModel() - { - Id = id, - IndexName = indexName, - ChannelName = DefaultChannel, - LanguageNames = new List() { EnglishLanguageName, CzechLanguageName }, - Paths = new List() { Path } - }, - [] - ); -} +using DancingGoat.Models; + +using Kentico.Xperience.AzureSearch.Admin; +using Kentico.Xperience.AzureSearch.Indexing; + +namespace Kentico.Xperience.AzureSearch.Tests.Base; +internal static class MockDataProvider +{ + public static IndexEventWebPageItemModel WebModel(IndexEventWebPageItemModel item) + { + item.LanguageName = CzechLanguageName; + item.ContentTypeName = ArticlePage.CONTENT_TYPE_NAME; + item.Name = "Name"; + item.ContentTypeID = 1; + item.ContentLanguageID = 1; + item.WebsiteChannelName = DefaultChannel; + item.WebPageItemTreePath = "/%"; + + return item; + } + + public static AzureSearchIndexIncludedPath Path => new("/%") + { + ContentTypes = [new AzureSearchIndexContentType(ArticlePage.CONTENT_TYPE_NAME, nameof(ArticlePage))] + }; + + + public static AzureSearchIndex Index => new( + new AzureSearchConfigurationModel() + { + IndexName = DefaultIndex, + ChannelName = DefaultChannel, + LanguageNames = new List() { EnglishLanguageName, CzechLanguageName }, + Paths = new List() { Path }, + StrategyName = "strategy" + }, + [] + ); + + public static readonly string DefaultIndex = "SimpleIndex"; + public static readonly string DefaultChannel = "DefaultChannel"; + public static readonly string EnglishLanguageName = "en"; + public static readonly string CzechLanguageName = "cz"; + public static readonly int IndexId = 1; + public static readonly string EventName = "publish"; + + public static AzureSearchIndex GetIndex(string indexName, int id) => new( + new AzureSearchConfigurationModel() + { + Id = id, + IndexName = indexName, + ChannelName = DefaultChannel, + LanguageNames = new List() { EnglishLanguageName, CzechLanguageName }, + Paths = new List() { Path } + }, + [] + ); +} diff --git a/tests/Kentico.Xperience.AzureSearch.Tests/Tests/IndexStoreTests.cs b/tests/Kentico.Xperience.AzureSearch.Tests/Tests/IndexStoreTests.cs index b047332..7d03c87 100644 --- a/tests/Kentico.Xperience.AzureSearch.Tests/Tests/IndexStoreTests.cs +++ b/tests/Kentico.Xperience.AzureSearch.Tests/Tests/IndexStoreTests.cs @@ -1,58 +1,58 @@ -using Kentico.Xperience.AzureSearch.Admin; -using Kentico.Xperience.AzureSearch.Indexing; -using Kentico.Xperience.AzureSearch.Tests.Base; - -namespace Kentico.Xperience.AzureSearch.Tests.Tests; -internal class IndexStoreTests -{ - - [Test] - public void AddAndGetIndex() - { - AzureSearchIndexStore.Instance.SetIndicies(new List()); - - AzureSearchIndexStore.Instance.AddIndex(MockDataProvider.Index); - AzureSearchIndexStore.Instance.AddIndex(MockDataProvider.GetIndex("TestIndex", 1)); - - Assert.Multiple(() => - { - Assert.That(AzureSearchIndexStore.Instance.GetIndex("TestIndex") is not null); - Assert.That(AzureSearchIndexStore.Instance.GetIndex(MockDataProvider.DefaultIndex) is not null); - }); - } - - [Test] - public void AddIndex_AlreadyExists() - { - AzureSearchIndexStore.Instance.SetIndicies(new List()); - AzureSearchIndexStore.Instance.AddIndex(MockDataProvider.Index); - - bool hasThrown = false; - - try - { - AzureSearchIndexStore.Instance.AddIndex(MockDataProvider.Index); - } - catch - { - hasThrown = true; - } - - Assert.That(hasThrown); - } - - [Test] - public void SetIndicies() - { - var defaultIndex = new AzureSearchConfigurationModel { IndexName = "DefaultIndex", Id = 0 }; - var simpleIndex = new AzureSearchConfigurationModel { IndexName = "SimpleIndex", Id = 1 }; - - AzureSearchIndexStore.Instance.SetIndicies(new List() { defaultIndex, simpleIndex }); - - Assert.Multiple(() => - { - Assert.That(AzureSearchIndexStore.Instance.GetIndex(defaultIndex.IndexName) is not null); - Assert.That(AzureSearchIndexStore.Instance.GetIndex(simpleIndex.IndexName) is not null); - }); - } -} +using Kentico.Xperience.AzureSearch.Admin; +using Kentico.Xperience.AzureSearch.Indexing; +using Kentico.Xperience.AzureSearch.Tests.Base; + +namespace Kentico.Xperience.AzureSearch.Tests.Tests; +internal class IndexStoreTests +{ + + [Test] + public void AddAndGetIndex() + { + AzureSearchIndexStore.Instance.SetIndicies(new List()); + + AzureSearchIndexStore.Instance.AddIndex(MockDataProvider.Index); + AzureSearchIndexStore.Instance.AddIndex(MockDataProvider.GetIndex("TestIndex", 1)); + + Assert.Multiple(() => + { + Assert.That(AzureSearchIndexStore.Instance.GetIndex("TestIndex") is not null); + Assert.That(AzureSearchIndexStore.Instance.GetIndex(MockDataProvider.DefaultIndex) is not null); + }); + } + + [Test] + public void AddIndex_AlreadyExists() + { + AzureSearchIndexStore.Instance.SetIndicies(new List()); + AzureSearchIndexStore.Instance.AddIndex(MockDataProvider.Index); + + bool hasThrown = false; + + try + { + AzureSearchIndexStore.Instance.AddIndex(MockDataProvider.Index); + } + catch + { + hasThrown = true; + } + + Assert.That(hasThrown); + } + + [Test] + public void SetIndicies() + { + var defaultIndex = new AzureSearchConfigurationModel { IndexName = "DefaultIndex", Id = 0 }; + var simpleIndex = new AzureSearchConfigurationModel { IndexName = "SimpleIndex", Id = 1 }; + + AzureSearchIndexStore.Instance.SetIndicies(new List() { defaultIndex, simpleIndex }); + + Assert.Multiple(() => + { + Assert.That(AzureSearchIndexStore.Instance.GetIndex(defaultIndex.IndexName) is not null); + Assert.That(AzureSearchIndexStore.Instance.GetIndex(simpleIndex.IndexName) is not null); + }); + } +} diff --git a/tests/Kentico.Xperience.AzureSearch.Tests/Tests/IndexedItemModelExtensionsTests.cs b/tests/Kentico.Xperience.AzureSearch.Tests/Tests/IndexedItemModelExtensionsTests.cs index 4f1ff74..74c18c6 100644 --- a/tests/Kentico.Xperience.AzureSearch.Tests/Tests/IndexedItemModelExtensionsTests.cs +++ b/tests/Kentico.Xperience.AzureSearch.Tests/Tests/IndexedItemModelExtensionsTests.cs @@ -1,148 +1,148 @@ -using CMS.Core; - -using DancingGoat.Models; - -using Kentico.Xperience.AzureSearch.Admin; -using Kentico.Xperience.AzureSearch.Indexing; -using Kentico.Xperience.AzureSearch.Tests.Base; - -namespace Kentico.Xperience.AzureSearch.Tests.Tests; - -internal class IndexedItemModelExtensionsTests -{ - - [Test] - public void IsIndexedByIndex() - { - Service.InitializeContainer(); - var log = Substitute.For(); - - AzureSearchIndexStore.Instance.SetIndicies(new List()); - AzureSearchIndexStore.Instance.AddIndex(MockDataProvider.Index); - - var fixture = new Fixture(); - var item = fixture.Create(); - - var model = MockDataProvider.WebModel(item); - Assert.That(model.IsIndexedByIndex(log, MockDataProvider.DefaultIndex, MockDataProvider.EventName)); - } - - [Test] - public void WildCard() - { - Service.InitializeContainer(); - var log = Substitute.For(); - var fixture = new Fixture(); - var item = fixture.Create(); - - var model = MockDataProvider.WebModel(item); - model.WebPageItemTreePath = "/Home"; - - var index = MockDataProvider.Index; - var path = new AzureSearchIndexIncludedPath("/%") { ContentTypes = [new(ArticlePage.CONTENT_TYPE_NAME, nameof(ArticlePage))] }; - - index.IncludedPaths = new List() { path }; - - AzureSearchIndexStore.Instance.SetIndicies(new List()); - AzureSearchIndexStore.Instance.AddIndex(index); - - Assert.That(model.IsIndexedByIndex(log, MockDataProvider.DefaultIndex, MockDataProvider.EventName)); - } - - [Test] - public void WrongWildCard() - { - Service.InitializeContainer(); - var log = Substitute.For(); - var fixture = new Fixture(); - var item = fixture.Create(); - - var model = MockDataProvider.WebModel(item); - model.WebPageItemTreePath = "/Home"; - - var index = MockDataProvider.Index; - var path = new AzureSearchIndexIncludedPath("/Index/%") { ContentTypes = [new(ArticlePage.CONTENT_TYPE_NAME, nameof(ArticlePage))] }; - - index.IncludedPaths = new List() { path }; - - AzureSearchIndexStore.Instance.SetIndicies(new List()); - AzureSearchIndexStore.Instance.AddIndex(index); - - Assert.That(!model.IsIndexedByIndex(log, MockDataProvider.DefaultIndex, MockDataProvider.EventName)); - } - - [Test] - public void WrongPath() - { - Service.InitializeContainer(); - var log = Substitute.For(); - var fixture = new Fixture(); - var item = fixture.Create(); - - var model = MockDataProvider.WebModel(item); - model.WebPageItemTreePath = "/Home"; - - var index = MockDataProvider.Index; - var path = new AzureSearchIndexIncludedPath("/Index") { ContentTypes = [new(ArticlePage.CONTENT_TYPE_NAME, nameof(ArticlePage))] }; - - index.IncludedPaths = new List() { path }; - - AzureSearchIndexStore.Instance.SetIndicies(new List()); - AzureSearchIndexStore.Instance.AddIndex(index); - - Assert.That(!model.IsIndexedByIndex(log, MockDataProvider.DefaultIndex, MockDataProvider.EventName)); - } - - [Test] - public void WrongContentType() - { - Service.InitializeContainer(); - var log = Substitute.For(); - - var fixture = new Fixture(); - var item = fixture.Create(); - - var model = MockDataProvider.WebModel(item); - model.ContentTypeName = "DancingGoat.HomePage"; - - AzureSearchIndexStore.Instance.SetIndicies(new List()); - AzureSearchIndexStore.Instance.AddIndex(MockDataProvider.Index); - - Assert.That(!model.IsIndexedByIndex(log, MockDataProvider.DefaultIndex, MockDataProvider.EventName)); - } - - [Test] - public void WrongIndex() - { - Service.InitializeContainer(); - var log = Substitute.For(); - - var fixture = new Fixture(); - var item = fixture.Create(); - - var model = MockDataProvider.WebModel(item); - - AzureSearchIndexStore.Instance.SetIndicies(new List()); - AzureSearchIndexStore.Instance.AddIndex(MockDataProvider.Index); - - Assert.That(!MockDataProvider.WebModel(model).IsIndexedByIndex(log, "NewIndex", MockDataProvider.EventName)); - } - - [Test] - public void WrongLanguage() - { - Service.InitializeContainer(); - var log = Substitute.For(); - - var fixture = new Fixture(); - var item = fixture.Create(); - - var model = MockDataProvider.WebModel(item); - model.LanguageName = "sk"; - - AzureSearchIndexStore.Instance.SetIndicies(new List()); - AzureSearchIndexStore.Instance.AddIndex(MockDataProvider.Index); - - Assert.That(!model.IsIndexedByIndex(log, MockDataProvider.DefaultIndex, MockDataProvider.EventName)); - } -} +using CMS.Core; + +using DancingGoat.Models; + +using Kentico.Xperience.AzureSearch.Admin; +using Kentico.Xperience.AzureSearch.Indexing; +using Kentico.Xperience.AzureSearch.Tests.Base; + +namespace Kentico.Xperience.AzureSearch.Tests.Tests; + +internal class IndexedItemModelExtensionsTests +{ + + [Test] + public void IsIndexedByIndex() + { + Service.InitializeContainer(); + var log = Substitute.For(); + + AzureSearchIndexStore.Instance.SetIndicies(new List()); + AzureSearchIndexStore.Instance.AddIndex(MockDataProvider.Index); + + var fixture = new Fixture(); + var item = fixture.Create(); + + var model = MockDataProvider.WebModel(item); + Assert.That(model.IsIndexedByIndex(log, MockDataProvider.DefaultIndex, MockDataProvider.EventName)); + } + + [Test] + public void WildCard() + { + Service.InitializeContainer(); + var log = Substitute.For(); + var fixture = new Fixture(); + var item = fixture.Create(); + + var model = MockDataProvider.WebModel(item); + model.WebPageItemTreePath = "/Home"; + + var index = MockDataProvider.Index; + var path = new AzureSearchIndexIncludedPath("/%") { ContentTypes = [new(ArticlePage.CONTENT_TYPE_NAME, nameof(ArticlePage))] }; + + index.IncludedPaths = new List() { path }; + + AzureSearchIndexStore.Instance.SetIndicies(new List()); + AzureSearchIndexStore.Instance.AddIndex(index); + + Assert.That(model.IsIndexedByIndex(log, MockDataProvider.DefaultIndex, MockDataProvider.EventName)); + } + + [Test] + public void WrongWildCard() + { + Service.InitializeContainer(); + var log = Substitute.For(); + var fixture = new Fixture(); + var item = fixture.Create(); + + var model = MockDataProvider.WebModel(item); + model.WebPageItemTreePath = "/Home"; + + var index = MockDataProvider.Index; + var path = new AzureSearchIndexIncludedPath("/Index/%") { ContentTypes = [new(ArticlePage.CONTENT_TYPE_NAME, nameof(ArticlePage))] }; + + index.IncludedPaths = new List() { path }; + + AzureSearchIndexStore.Instance.SetIndicies(new List()); + AzureSearchIndexStore.Instance.AddIndex(index); + + Assert.That(!model.IsIndexedByIndex(log, MockDataProvider.DefaultIndex, MockDataProvider.EventName)); + } + + [Test] + public void WrongPath() + { + Service.InitializeContainer(); + var log = Substitute.For(); + var fixture = new Fixture(); + var item = fixture.Create(); + + var model = MockDataProvider.WebModel(item); + model.WebPageItemTreePath = "/Home"; + + var index = MockDataProvider.Index; + var path = new AzureSearchIndexIncludedPath("/Index") { ContentTypes = [new(ArticlePage.CONTENT_TYPE_NAME, nameof(ArticlePage))] }; + + index.IncludedPaths = new List() { path }; + + AzureSearchIndexStore.Instance.SetIndicies(new List()); + AzureSearchIndexStore.Instance.AddIndex(index); + + Assert.That(!model.IsIndexedByIndex(log, MockDataProvider.DefaultIndex, MockDataProvider.EventName)); + } + + [Test] + public void WrongContentType() + { + Service.InitializeContainer(); + var log = Substitute.For(); + + var fixture = new Fixture(); + var item = fixture.Create(); + + var model = MockDataProvider.WebModel(item); + model.ContentTypeName = "DancingGoat.HomePage"; + + AzureSearchIndexStore.Instance.SetIndicies(new List()); + AzureSearchIndexStore.Instance.AddIndex(MockDataProvider.Index); + + Assert.That(!model.IsIndexedByIndex(log, MockDataProvider.DefaultIndex, MockDataProvider.EventName)); + } + + [Test] + public void WrongIndex() + { + Service.InitializeContainer(); + var log = Substitute.For(); + + var fixture = new Fixture(); + var item = fixture.Create(); + + var model = MockDataProvider.WebModel(item); + + AzureSearchIndexStore.Instance.SetIndicies(new List()); + AzureSearchIndexStore.Instance.AddIndex(MockDataProvider.Index); + + Assert.That(!MockDataProvider.WebModel(model).IsIndexedByIndex(log, "NewIndex", MockDataProvider.EventName)); + } + + [Test] + public void WrongLanguage() + { + Service.InitializeContainer(); + var log = Substitute.For(); + + var fixture = new Fixture(); + var item = fixture.Create(); + + var model = MockDataProvider.WebModel(item); + model.LanguageName = "sk"; + + AzureSearchIndexStore.Instance.SetIndicies(new List()); + AzureSearchIndexStore.Instance.AddIndex(MockDataProvider.Index); + + Assert.That(!model.IsIndexedByIndex(log, MockDataProvider.DefaultIndex, MockDataProvider.EventName)); + } +}