diff --git a/images/xperience-administration-search-index-edit-form-paths-edit.jpg b/images/xperience-administration-search-index-edit-form-paths-edit.jpg index 79cd3253..3fd09ba1 100644 Binary files a/images/xperience-administration-search-index-edit-form-paths-edit.jpg and b/images/xperience-administration-search-index-edit-form-paths-edit.jpg differ diff --git a/images/xperience-administration-search-index-edit-form.jpg b/images/xperience-administration-search-index-edit-form.jpg index 10f9204f..df5da578 100644 Binary files a/images/xperience-administration-search-index-edit-form.jpg and b/images/xperience-administration-search-index-edit-form.jpg differ diff --git a/src/Kentico.Xperience.Lucene.Admin/LuceneConfigurationModel.cs b/src/Kentico.Xperience.Lucene.Admin/LuceneConfigurationModel.cs index 3fa43e5c..2e984ab5 100644 --- a/src/Kentico.Xperience.Lucene.Admin/LuceneConfigurationModel.cs +++ b/src/Kentico.Xperience.Lucene.Admin/LuceneConfigurationModel.cs @@ -1,68 +1,68 @@ -using System.ComponentModel.DataAnnotations; - -using Kentico.Xperience.Admin.Base.FormAnnotations; -using Kentico.Xperience.Admin.Base.Forms; -using Kentico.Xperience.Lucene.Admin.Providers; -using Kentico.Xperience.Lucene.Core.Indexing; - -namespace Kentico.Xperience.Lucene.Admin; - -public class LuceneConfigurationModel -{ - public int Id { get; set; } - - [TextInputComponent( - Label = "Index Name", - ExplanationText = "Changing this value on an existing index without changing application code will cause the search experience to stop working.", - Order = 1)] - [Required] - [MinLength(1)] - public string IndexName { get; set; } = ""; - - [GeneralSelectorComponent(dataProviderType: typeof(LanguageOptionsProvider), Label = "Indexed Languages", Order = 2)] - public IEnumerable LanguageNames { get; set; } = Enumerable.Empty(); - - [DropDownComponent(Label = "Channel Name", DataProviderType = typeof(ChannelOptionsProvider), Order = 3)] - public string ChannelName { get; set; } = ""; - - [DropDownComponent(Label = "Indexing Strategy", DataProviderType = typeof(IndexingStrategyOptionsProvider), Order = 4)] - public string StrategyName { get; set; } = ""; - - [DropDownComponent(Label = "Lucene Analyzer", DataProviderType = typeof(AnalyzerOptionsProvider), Order = 5)] - public string AnalyzerName { get; set; } = ""; - - [TextInputComponent(Label = "Rebuild Hook")] - public string RebuildHook { get; set; } = ""; - - [LuceneIndexConfigurationComponent(Label = "Included Paths")] - public IEnumerable Paths { get; set; } = Enumerable.Empty(); - - public LuceneConfigurationModel() { } - - public LuceneConfigurationModel( - LuceneIndexModel luceneModel - ) - { - Id = luceneModel.Id; - IndexName = luceneModel.IndexName; - LanguageNames = luceneModel.LanguageNames; - ChannelName = luceneModel.ChannelName; - StrategyName = luceneModel.StrategyName; - AnalyzerName = luceneModel.AnalyzerName; - RebuildHook = luceneModel.RebuildHook; - Paths = luceneModel.Paths; - } - - public LuceneIndexModel ToLuceneModel() => - new() - { - Id = Id, - IndexName = IndexName, - LanguageNames = LanguageNames, - ChannelName = ChannelName, - AnalyzerName = AnalyzerName, - StrategyName = StrategyName, - RebuildHook = RebuildHook, - Paths = Paths - }; -} +using System.ComponentModel.DataAnnotations; + +using Kentico.Xperience.Admin.Base.FormAnnotations; +using Kentico.Xperience.Admin.Base.Forms; +using Kentico.Xperience.Lucene.Admin.Providers; +using Kentico.Xperience.Lucene.Core.Indexing; + +namespace Kentico.Xperience.Lucene.Admin; + +public class LuceneConfigurationModel +{ + public int Id { get; set; } + + [TextInputComponent( + Label = "Index Name", + ExplanationText = "Changing this value on an existing index without changing application code will cause the search experience to stop working.", + Order = 1)] + [Required] + [MinLength(1)] + public string IndexName { get; set; } = ""; + + [GeneralSelectorComponent(dataProviderType: typeof(LanguageOptionsProvider), Label = "Indexed Languages", Order = 2)] + public IEnumerable LanguageNames { get; set; } = Enumerable.Empty(); + + [DropDownComponent(Label = "Channel Name", DataProviderType = typeof(ChannelOptionsProvider), Order = 3)] + public string ChannelName { get; set; } = ""; + + [DropDownComponent(Label = "Indexing Strategy", DataProviderType = typeof(IndexingStrategyOptionsProvider), Order = 4)] + public string StrategyName { get; set; } = ""; + + [DropDownComponent(Label = "Lucene Analyzer", DataProviderType = typeof(AnalyzerOptionsProvider), Order = 5)] + public string AnalyzerName { get; set; } = ""; + + [TextInputComponent(Label = "Rebuild Hook")] + public string RebuildHook { get; set; } = ""; + + [LuceneIndexConfigurationComponent(Label = "Included Paths")] + public IEnumerable Paths { get; set; } = Enumerable.Empty(); + + public LuceneConfigurationModel() { } + + public LuceneConfigurationModel( + LuceneIndexModel luceneModel + ) + { + Id = luceneModel.Id; + IndexName = luceneModel.IndexName; + LanguageNames = luceneModel.LanguageNames; + ChannelName = luceneModel.ChannelName; + StrategyName = luceneModel.StrategyName; + AnalyzerName = luceneModel.AnalyzerName; + RebuildHook = luceneModel.RebuildHook; + Paths = luceneModel.Paths; + } + + public LuceneIndexModel ToLuceneModel() => + new() + { + Id = Id, + IndexName = IndexName, + LanguageNames = LanguageNames, + ChannelName = ChannelName, + AnalyzerName = AnalyzerName, + StrategyName = StrategyName, + RebuildHook = RebuildHook, + Paths = Paths + }; +} diff --git a/src/Kentico.Xperience.Lucene.Admin/UIPages/BaseIndexEditPage.cs b/src/Kentico.Xperience.Lucene.Admin/UIPages/BaseIndexEditPage.cs index 2fb85c12..837b1d56 100644 --- a/src/Kentico.Xperience.Lucene.Admin/UIPages/BaseIndexEditPage.cs +++ b/src/Kentico.Xperience.Lucene.Admin/UIPages/BaseIndexEditPage.cs @@ -1,75 +1,75 @@ -using System.Text; - -using Kentico.Xperience.Admin.Base; -using Kentico.Xperience.Admin.Base.Forms; -using Kentico.Xperience.Lucene.Core.Indexing; - -using IFormItemCollectionProvider = Kentico.Xperience.Admin.Base.Forms.Internal.IFormItemCollectionProvider; - -namespace Kentico.Xperience.Lucene.Admin; - -internal abstract class BaseIndexEditPage : ModelEditPage -{ - protected readonly ILuceneConfigurationStorageService StorageService; - - private readonly ILuceneIndexManager indexManager; - - protected BaseIndexEditPage( - IFormItemCollectionProvider formItemCollectionProvider, - IFormDataBinder formDataBinder, - ILuceneConfigurationStorageService storageService, - ILuceneIndexManager indexManager) - : base(formItemCollectionProvider, formDataBinder) - { - this.indexManager = indexManager; - StorageService = storageService; - } - - protected async Task ValidateAndProcess(LuceneConfigurationModel configuration) - { - configuration.IndexName = RemoveWhitespacesUsingStringBuilder(configuration.IndexName ?? ""); - - if (StorageService.GetIndexIds().Exists(x => x == configuration.Id)) - { - bool edited = await StorageService.TryEditIndexAsync(configuration.ToLuceneModel()); - - if (edited) - { - return IndexModificationResult.Success; - } - - return IndexModificationResult.Failure; - } - else - { - if (!string.IsNullOrWhiteSpace(configuration.IndexName)) - { - indexManager.AddIndex(configuration.ToLuceneModel()); - - return IndexModificationResult.Success; - } - - return IndexModificationResult.Failure; - } - } - - protected static string RemoveWhitespacesUsingStringBuilder(string source) - { - var builder = new StringBuilder(source.Length); - for (int i = 0; i < source.Length; i++) - { - char c = source[i]; - if (!char.IsWhiteSpace(c)) - { - builder.Append(c); - } - } - return source.Length == builder.Length ? source : builder.ToString(); - } -} - -internal enum IndexModificationResult -{ - Success, - Failure -} +using System.Text; + +using Kentico.Xperience.Admin.Base; +using Kentico.Xperience.Admin.Base.Forms; +using Kentico.Xperience.Lucene.Core.Indexing; + +using IFormItemCollectionProvider = Kentico.Xperience.Admin.Base.Forms.Internal.IFormItemCollectionProvider; + +namespace Kentico.Xperience.Lucene.Admin; + +internal abstract class BaseIndexEditPage : ModelEditPage +{ + protected readonly ILuceneConfigurationStorageService StorageService; + + private readonly ILuceneIndexManager indexManager; + + protected BaseIndexEditPage( + IFormItemCollectionProvider formItemCollectionProvider, + IFormDataBinder formDataBinder, + ILuceneConfigurationStorageService storageService, + ILuceneIndexManager indexManager) + : base(formItemCollectionProvider, formDataBinder) + { + this.indexManager = indexManager; + StorageService = storageService; + } + + protected async Task ValidateAndProcess(LuceneConfigurationModel configuration) + { + configuration.IndexName = RemoveWhitespacesUsingStringBuilder(configuration.IndexName ?? ""); + + if (StorageService.GetIndexIds().Exists(x => x == configuration.Id)) + { + bool edited = await StorageService.TryEditIndexAsync(configuration.ToLuceneModel()); + + if (edited) + { + return IndexModificationResult.Success; + } + + return IndexModificationResult.Failure; + } + else + { + if (!string.IsNullOrWhiteSpace(configuration.IndexName)) + { + indexManager.AddIndex(configuration.ToLuceneModel()); + + return IndexModificationResult.Success; + } + + return IndexModificationResult.Failure; + } + } + + protected static string RemoveWhitespacesUsingStringBuilder(string source) + { + var builder = new StringBuilder(source.Length); + for (int i = 0; i < source.Length; i++) + { + char c = source[i]; + if (!char.IsWhiteSpace(c)) + { + builder.Append(c); + } + } + return source.Length == builder.Length ? source : builder.ToString(); + } +} + +internal enum IndexModificationResult +{ + Success, + Failure +} diff --git a/src/Kentico.Xperience.Lucene.Core/Indexing/DefaultLuceneClient.cs b/src/Kentico.Xperience.Lucene.Core/Indexing/DefaultLuceneClient.cs index 11e5b800..1f436709 100644 --- a/src/Kentico.Xperience.Lucene.Core/Indexing/DefaultLuceneClient.cs +++ b/src/Kentico.Xperience.Lucene.Core/Indexing/DefaultLuceneClient.cs @@ -1,349 +1,349 @@ -using CMS.ContentEngine; -using CMS.Core; -using CMS.DataEngine; -using CMS.Helpers; -using CMS.Helpers.Caching.Abstractions; -using CMS.Websites; - -using Kentico.Xperience.Lucene.Core.Search; - -using Lucene.Net.Documents; -using Lucene.Net.Index; -using Lucene.Net.Search; - -using Microsoft.Extensions.DependencyInjection; - -namespace Kentico.Xperience.Lucene.Core.Indexing; - -/// -/// Default implementation of . -/// -internal class DefaultLuceneClient : ILuceneClient -{ - private readonly ILuceneIndexService luceneIndexService; - private readonly ILuceneSearchService luceneSearchService; - private readonly IContentQueryExecutor executor; - private readonly IServiceProvider serviceProvider; - private readonly IInfoProvider languageProvider; - private readonly IInfoProvider channelProvider; - private readonly IConversionService conversionService; - private readonly IProgressiveCache cache; - private readonly IEventLogService log; - private readonly ICacheAccessor cacheAccessor; - private readonly ILuceneIndexManager indexManager; - - internal const string CACHEKEY_STATISTICS = "Lucene|ListIndices"; - - public DefaultLuceneClient( - ICacheAccessor cacheAccessor, - ILuceneIndexService luceneIndexService, - ILuceneSearchService luceneSearchService, - IContentQueryExecutor executor, - IServiceProvider serviceProvider, - IInfoProvider languageProvider, - IInfoProvider channelProvider, - IConversionService conversionService, - IProgressiveCache cache, - IEventLogService log, - ILuceneIndexManager indexManager - ) - { - this.cacheAccessor = cacheAccessor; - this.luceneIndexService = luceneIndexService; - this.luceneSearchService = luceneSearchService; - this.executor = executor; - this.serviceProvider = serviceProvider; - this.languageProvider = languageProvider; - this.channelProvider = channelProvider; - this.conversionService = conversionService; - this.cache = cache; - this.log = log; - this.indexManager = indexManager; - this.indexManager = indexManager; - } - - /// - public Task DeleteRecords(IEnumerable itemGuids, string indexName) - { - if (string.IsNullOrEmpty(indexName)) - { - throw new ArgumentNullException(nameof(indexName)); - } - - if (itemGuids == null || !itemGuids.Any()) - { - return Task.FromResult(0); - } - - return DeleteRecordsInternal(itemGuids, indexName); - } - - - /// - public Task> GetStatistics(CancellationToken cancellationToken) - { - var stats = indexManager.GetAllIndices().Select(i => - { - var statistics = luceneSearchService.UseSearcher(i, s => new LuceneIndexStatisticsModel() - { - Name = i.IndexName, - Entries = s.IndexReader.NumDocs - }); - - var dir = new DirectoryInfo(i.StorageContext.GetPublishedIndex().Path); - statistics.UpdatedAt = dir.LastWriteTime; - return statistics; - }).ToList(); - - return Task.FromResult>(stats); - } - - /// - public Task Rebuild(string indexName, CancellationToken? cancellationToken) - { - if (string.IsNullOrEmpty(indexName)) - { - throw new ArgumentNullException(nameof(indexName)); - } - - var luceneIndex = indexManager.GetRequiredIndex(indexName); - return RebuildInternal(luceneIndex, cancellationToken); - } - - - /// - public Task UpsertRecords(IEnumerable documents, string indexName, CancellationToken cancellationToken) - { - if (string.IsNullOrEmpty(indexName)) - { - throw new ArgumentNullException(nameof(indexName)); - } - - if (documents == null || !documents.Any()) - { - return Task.FromResult(0); - } - - return UpsertRecordsInternal(documents, indexName); - } - - private Task DeleteRecordsInternal(IEnumerable itemGuids, string indexName) - { - var index = indexManager.GetIndex(indexName); - if (index != null) - { - luceneIndexService.UseWriter(index, (writer) => - { - var booleanQuery = new BooleanQuery(); - foreach (string guid in itemGuids) - { - var termQuery = new TermQuery(new Term(nameof(IIndexEventItemModel.ItemGuid), guid)); - booleanQuery.Add(termQuery, Occur.SHOULD); // Match any of the object IDs - } - writer.DeleteDocuments(booleanQuery); - return "OK"; - }, index.StorageContext.GetLastGeneration(true)); - } - return Task.FromResult(0); - } - - private async Task RebuildInternal(LuceneIndex luceneIndex, CancellationToken? cancellationToken) - { - // Clear statistics cache so listing displays updated data after rebuild - cacheAccessor.Remove(CACHEKEY_STATISTICS); - - luceneIndexService.ResetIndex(luceneIndex); - - var indexedItems = new List(); - foreach (var includedPathAttribute in luceneIndex.IncludedPaths) - { - var pathMatch = - includedPathAttribute.AliasPath.EndsWith("/%", StringComparison.OrdinalIgnoreCase) - ? PathMatch.Children(includedPathAttribute.AliasPath[..^2]) - : PathMatch.Single(includedPathAttribute.AliasPath); - - foreach (string language in luceneIndex.LanguageNames) - { - var queryBuilder = new ContentItemQueryBuilder(); - - if (includedPathAttribute.ContentTypes != null && includedPathAttribute.ContentTypes.Count > 0) - { - foreach (var contentType in includedPathAttribute.ContentTypes) - { - queryBuilder.ForContentType(contentType.ContentTypeName, config => config.ForWebsite(luceneIndex.WebSiteChannelName, includeUrlPath: true, pathMatch: pathMatch)); - } - } - queryBuilder.InLanguage(language); - - var webpages = await executor.GetWebPageResult(queryBuilder, container => container, cancellationToken: cancellationToken ?? default); - - foreach (var page in webpages) - { - var item = await MapToEventItem(page); - indexedItems.Add(item); - } - } - } - - log.LogInformation( - "Kentico.Xperience.Lucene", - "INDEX_REBUILD", - $"Rebuilding index [{luceneIndex.IndexName}]. {indexedItems.Count} web page items queued for re-indexing" - ); - - indexedItems.ForEach(item => LuceneQueueWorker.EnqueueLuceneQueueItem(new LuceneQueueItem(item, LuceneTaskType.PUBLISH_INDEX, luceneIndex.IndexName))); - } - - private async Task MapToEventItem(IWebPageContentQueryDataContainer content) - { - var languages = await GetAllLanguages(); - - string languageName = languages.FirstOrDefault(l => l.ContentLanguageID == content.ContentItemCommonDataContentLanguageID)?.ContentLanguageName ?? ""; - - var websiteChannels = await GetAllWebsiteChannels(); - - string channelName = websiteChannels.FirstOrDefault(c => c.WebsiteChannelID == content.WebPageItemWebsiteChannelID).ChannelName ?? ""; - - var item = new IndexEventWebPageItemModel( - content.WebPageItemID, - content.WebPageItemGUID, - languageName, - content.ContentTypeName, - content.WebPageItemName, - content.ContentItemIsSecured, - content.ContentItemContentTypeID, - content.ContentItemCommonDataContentLanguageID, - channelName, - content.WebPageItemTreePath, - content.WebPageItemOrder); - - return item; - } - - private Task UpsertRecordsInternal(IEnumerable documents, string indexName) - { - var index = indexManager.GetIndex(indexName); - if (index != null) - { - var strategy = serviceProvider.GetRequiredStrategy(index); - // indexing facet requires separate index for toxonomy - if (strategy.FacetsConfigFactory() is { } facetsConfig) - { - int result = luceneIndexService.UseIndexAndTaxonomyWriter(index, (writer, taxonomyWriter) => - { - int count = 0; - foreach (var document in documents) - { - // for now all changes are creates, update to be done later - // delete old document, there is no upsert nor update in Lucene - - string? id = document.Get(nameof(IIndexEventItemModel.ItemGuid)); - string? language = document.Get(nameof(IIndexEventItemModel.LanguageName)); - if (id is not null && language is not null) - { - // for now all changes are creates, update to be done later - // delete old document, there is no upsert nor update in Lucene - var multiTermQuery = new BooleanQuery - { - { new TermQuery(new Term(nameof(IIndexEventItemModel.ItemGuid), id)), Occur.MUST }, - { new TermQuery(new Term(nameof(IIndexEventItemModel.LanguageName), language)), Occur.MUST } - }; - - writer.DeleteDocuments(multiTermQuery); - } - - // add new one -#pragma warning disable S2589 // Boolean expressions should not be gratuitous - if (document is not null) - { - writer.AddDocument(facetsConfig.Build(taxonomyWriter, document)); - count++; - } -#pragma warning restore S2589 // Boolean expressions should not be gratuitous -#pragma warning disable S2583 // Conditionally executed code should be reachable - if (count % 1000 == 0) - { - taxonomyWriter.Commit(); - } -#pragma warning restore S2583 // Conditionally executed code should be reachable - } - taxonomyWriter.Commit(); - - return count; - }, index.StorageContext.GetLastGeneration(true)); - - return Task.FromResult(result); - } - else // no facets, only index writer opened - { - int result = luceneIndexService.UseWriter(index, (writer) => - { - int count = 0; - foreach (var document in documents) - { - string? id = document.Get(nameof(IIndexEventItemModel.ItemGuid)); - string? language = document.Get(nameof(IIndexEventItemModel.LanguageName)); - if (id is not null && language is not null) - { - // for now all changes are creates, update to be done later - // delete old document, there is no upsert nor update in Lucene - var multiTermQuery = new BooleanQuery - { - { new TermQuery(new Term(nameof(IIndexEventItemModel.ItemGuid), id)), Occur.MUST }, - { new TermQuery(new Term(nameof(IIndexEventItemModel.LanguageName), language)), Occur.MUST } - }; - - writer.DeleteDocuments(multiTermQuery); - } - // add new one -#pragma warning disable S2589 // Boolean expressions should not be gratuitous - if (document is not null) - { - writer.AddDocument(document); - count++; - } -#pragma warning restore S2589 // Boolean expressions should not be gratuitous - } - return count; - }, index.StorageContext.GetLastGeneration(true)); - - return Task.FromResult(result); - } - } - return Task.FromResult(0); - } - - private Task> GetAllLanguages() => - cache.LoadAsync(async cs => - { - var results = await languageProvider.Get().GetEnumerableTypedResultAsync(); - - cs.GetCacheDependency = () => CacheHelper.GetCacheDependency($"{ContentLanguageInfo.OBJECT_TYPE}|all"); - - return results; - }, new CacheSettings(5, nameof(DefaultLuceneClient), nameof(GetAllLanguages))); - - private Task> GetAllWebsiteChannels() => - cache.LoadAsync(async cs => - { - - var results = await channelProvider.Get() - .Source(s => s.Join(nameof(ChannelInfo.ChannelID), nameof(WebsiteChannelInfo.WebsiteChannelChannelID))) - .Columns(nameof(WebsiteChannelInfo.WebsiteChannelID), nameof(ChannelInfo.ChannelName)) - .GetDataContainerResultAsync(); - - cs.GetCacheDependency = () => CacheHelper.GetCacheDependency(new[] { $"{ChannelInfo.OBJECT_TYPE}|all", $"{WebsiteChannelInfo.OBJECT_TYPE}|all" }); - - var items = new List<(int WebsiteChannelID, string ChannelName)>(); - - foreach (var item in results) - { - if (item.TryGetValue(nameof(WebsiteChannelInfo.WebsiteChannelID), out object channelID) && item.TryGetValue(nameof(ChannelInfo.ChannelName), out object channelName)) - { - items.Add(new(conversionService.GetInteger(channelID, 0), conversionService.GetString(channelName, ""))); - } - } - - return items.AsEnumerable(); - }, new CacheSettings(5, nameof(DefaultLuceneClient), nameof(GetAllWebsiteChannels))); -} +using CMS.ContentEngine; +using CMS.Core; +using CMS.DataEngine; +using CMS.Helpers; +using CMS.Helpers.Caching.Abstractions; +using CMS.Websites; + +using Kentico.Xperience.Lucene.Core.Search; + +using Lucene.Net.Documents; +using Lucene.Net.Index; +using Lucene.Net.Search; + +using Microsoft.Extensions.DependencyInjection; + +namespace Kentico.Xperience.Lucene.Core.Indexing; + +/// +/// Default implementation of . +/// +internal class DefaultLuceneClient : ILuceneClient +{ + private readonly ILuceneIndexService luceneIndexService; + private readonly ILuceneSearchService luceneSearchService; + private readonly IContentQueryExecutor executor; + private readonly IServiceProvider serviceProvider; + private readonly IInfoProvider languageProvider; + private readonly IInfoProvider channelProvider; + private readonly IConversionService conversionService; + private readonly IProgressiveCache cache; + private readonly IEventLogService log; + private readonly ICacheAccessor cacheAccessor; + private readonly ILuceneIndexManager indexManager; + + internal const string CACHEKEY_STATISTICS = "Lucene|ListIndices"; + + public DefaultLuceneClient( + ICacheAccessor cacheAccessor, + ILuceneIndexService luceneIndexService, + ILuceneSearchService luceneSearchService, + IContentQueryExecutor executor, + IServiceProvider serviceProvider, + IInfoProvider languageProvider, + IInfoProvider channelProvider, + IConversionService conversionService, + IProgressiveCache cache, + IEventLogService log, + ILuceneIndexManager indexManager + ) + { + this.cacheAccessor = cacheAccessor; + this.luceneIndexService = luceneIndexService; + this.luceneSearchService = luceneSearchService; + this.executor = executor; + this.serviceProvider = serviceProvider; + this.languageProvider = languageProvider; + this.channelProvider = channelProvider; + this.conversionService = conversionService; + this.cache = cache; + this.log = log; + this.indexManager = indexManager; + this.indexManager = indexManager; + } + + /// + public Task DeleteRecords(IEnumerable itemGuids, string indexName) + { + if (string.IsNullOrEmpty(indexName)) + { + throw new ArgumentNullException(nameof(indexName)); + } + + if (itemGuids == null || !itemGuids.Any()) + { + return Task.FromResult(0); + } + + return DeleteRecordsInternal(itemGuids, indexName); + } + + + /// + public Task> GetStatistics(CancellationToken cancellationToken) + { + var stats = indexManager.GetAllIndices().Select(i => + { + var statistics = luceneSearchService.UseSearcher(i, s => new LuceneIndexStatisticsModel() + { + Name = i.IndexName, + Entries = s.IndexReader.NumDocs + }); + + var dir = new DirectoryInfo(i.StorageContext.GetPublishedIndex().Path); + statistics.UpdatedAt = dir.LastWriteTime; + return statistics; + }).ToList(); + + return Task.FromResult>(stats); + } + + /// + public Task Rebuild(string indexName, CancellationToken? cancellationToken) + { + if (string.IsNullOrEmpty(indexName)) + { + throw new ArgumentNullException(nameof(indexName)); + } + + var luceneIndex = indexManager.GetRequiredIndex(indexName); + return RebuildInternal(luceneIndex, cancellationToken); + } + + + /// + public Task UpsertRecords(IEnumerable documents, string indexName, CancellationToken cancellationToken) + { + if (string.IsNullOrEmpty(indexName)) + { + throw new ArgumentNullException(nameof(indexName)); + } + + if (documents == null || !documents.Any()) + { + return Task.FromResult(0); + } + + return UpsertRecordsInternal(documents, indexName); + } + + private Task DeleteRecordsInternal(IEnumerable itemGuids, string indexName) + { + var index = indexManager.GetIndex(indexName); + if (index != null) + { + luceneIndexService.UseWriter(index, (writer) => + { + var booleanQuery = new BooleanQuery(); + foreach (string guid in itemGuids) + { + var termQuery = new TermQuery(new Term(nameof(IIndexEventItemModel.ItemGuid), guid)); + booleanQuery.Add(termQuery, Occur.SHOULD); // Match any of the object IDs + } + writer.DeleteDocuments(booleanQuery); + return "OK"; + }, index.StorageContext.GetLastGeneration(true)); + } + return Task.FromResult(0); + } + + private async Task RebuildInternal(LuceneIndex luceneIndex, CancellationToken? cancellationToken) + { + // Clear statistics cache so listing displays updated data after rebuild + cacheAccessor.Remove(CACHEKEY_STATISTICS); + + luceneIndexService.ResetIndex(luceneIndex); + + var indexedItems = new List(); + foreach (var includedPathAttribute in luceneIndex.IncludedPaths) + { + var pathMatch = + includedPathAttribute.AliasPath.EndsWith("/%", StringComparison.OrdinalIgnoreCase) + ? PathMatch.Children(includedPathAttribute.AliasPath[..^2]) + : PathMatch.Single(includedPathAttribute.AliasPath); + + foreach (string language in luceneIndex.LanguageNames) + { + var queryBuilder = new ContentItemQueryBuilder(); + + if (includedPathAttribute.ContentTypes != null && includedPathAttribute.ContentTypes.Count > 0) + { + foreach (var contentType in includedPathAttribute.ContentTypes) + { + queryBuilder.ForContentType(contentType.ContentTypeName, config => config.ForWebsite(luceneIndex.WebSiteChannelName, includeUrlPath: true, pathMatch: pathMatch)); + } + } + queryBuilder.InLanguage(language); + + var webpages = await executor.GetWebPageResult(queryBuilder, container => container, cancellationToken: cancellationToken ?? default); + + foreach (var page in webpages) + { + var item = await MapToEventItem(page); + indexedItems.Add(item); + } + } + } + + log.LogInformation( + "Kentico.Xperience.Lucene", + "INDEX_REBUILD", + $"Rebuilding index [{luceneIndex.IndexName}]. {indexedItems.Count} web page items queued for re-indexing" + ); + + indexedItems.ForEach(item => LuceneQueueWorker.EnqueueLuceneQueueItem(new LuceneQueueItem(item, LuceneTaskType.PUBLISH_INDEX, luceneIndex.IndexName))); + } + + private async Task MapToEventItem(IWebPageContentQueryDataContainer content) + { + var languages = await GetAllLanguages(); + + string languageName = languages.FirstOrDefault(l => l.ContentLanguageID == content.ContentItemCommonDataContentLanguageID)?.ContentLanguageName ?? ""; + + var websiteChannels = await GetAllWebsiteChannels(); + + string channelName = websiteChannels.FirstOrDefault(c => c.WebsiteChannelID == content.WebPageItemWebsiteChannelID).ChannelName ?? ""; + + var item = new IndexEventWebPageItemModel( + content.WebPageItemID, + content.WebPageItemGUID, + languageName, + content.ContentTypeName, + content.WebPageItemName, + content.ContentItemIsSecured, + content.ContentItemContentTypeID, + content.ContentItemCommonDataContentLanguageID, + channelName, + content.WebPageItemTreePath, + content.WebPageItemOrder); + + return item; + } + + private Task UpsertRecordsInternal(IEnumerable documents, string indexName) + { + var index = indexManager.GetIndex(indexName); + if (index != null) + { + var strategy = serviceProvider.GetRequiredStrategy(index); + // indexing facet requires separate index for toxonomy + if (strategy.FacetsConfigFactory() is { } facetsConfig) + { + int result = luceneIndexService.UseIndexAndTaxonomyWriter(index, (writer, taxonomyWriter) => + { + int count = 0; + foreach (var document in documents) + { + // for now all changes are creates, update to be done later + // delete old document, there is no upsert nor update in Lucene + + string? id = document.Get(nameof(IIndexEventItemModel.ItemGuid)); + string? language = document.Get(nameof(IIndexEventItemModel.LanguageName)); + if (id is not null && language is not null) + { + // for now all changes are creates, update to be done later + // delete old document, there is no upsert nor update in Lucene + var multiTermQuery = new BooleanQuery + { + { new TermQuery(new Term(nameof(IIndexEventItemModel.ItemGuid), id)), Occur.MUST }, + { new TermQuery(new Term(nameof(IIndexEventItemModel.LanguageName), language)), Occur.MUST } + }; + + writer.DeleteDocuments(multiTermQuery); + } + + // add new one +#pragma warning disable S2589 // Boolean expressions should not be gratuitous + if (document is not null) + { + writer.AddDocument(facetsConfig.Build(taxonomyWriter, document)); + count++; + } +#pragma warning restore S2589 // Boolean expressions should not be gratuitous +#pragma warning disable S2583 // Conditionally executed code should be reachable + if (count % 1000 == 0) + { + taxonomyWriter.Commit(); + } +#pragma warning restore S2583 // Conditionally executed code should be reachable + } + taxonomyWriter.Commit(); + + return count; + }, index.StorageContext.GetLastGeneration(true)); + + return Task.FromResult(result); + } + else // no facets, only index writer opened + { + int result = luceneIndexService.UseWriter(index, (writer) => + { + int count = 0; + foreach (var document in documents) + { + string? id = document.Get(nameof(IIndexEventItemModel.ItemGuid)); + string? language = document.Get(nameof(IIndexEventItemModel.LanguageName)); + if (id is not null && language is not null) + { + // for now all changes are creates, update to be done later + // delete old document, there is no upsert nor update in Lucene + var multiTermQuery = new BooleanQuery + { + { new TermQuery(new Term(nameof(IIndexEventItemModel.ItemGuid), id)), Occur.MUST }, + { new TermQuery(new Term(nameof(IIndexEventItemModel.LanguageName), language)), Occur.MUST } + }; + + writer.DeleteDocuments(multiTermQuery); + } + // add new one +#pragma warning disable S2589 // Boolean expressions should not be gratuitous + if (document is not null) + { + writer.AddDocument(document); + count++; + } +#pragma warning restore S2589 // Boolean expressions should not be gratuitous + } + return count; + }, index.StorageContext.GetLastGeneration(true)); + + return Task.FromResult(result); + } + } + return Task.FromResult(0); + } + + private Task> GetAllLanguages() => + cache.LoadAsync(async cs => + { + var results = await languageProvider.Get().GetEnumerableTypedResultAsync(); + + cs.GetCacheDependency = () => CacheHelper.GetCacheDependency($"{ContentLanguageInfo.OBJECT_TYPE}|all"); + + return results; + }, new CacheSettings(5, nameof(DefaultLuceneClient), nameof(GetAllLanguages))); + + private Task> GetAllWebsiteChannels() => + cache.LoadAsync(async cs => + { + + var results = await channelProvider.Get() + .Source(s => s.Join(nameof(ChannelInfo.ChannelID), nameof(WebsiteChannelInfo.WebsiteChannelChannelID))) + .Columns(nameof(WebsiteChannelInfo.WebsiteChannelID), nameof(ChannelInfo.ChannelName)) + .GetDataContainerResultAsync(); + + cs.GetCacheDependency = () => CacheHelper.GetCacheDependency(new[] { $"{ChannelInfo.OBJECT_TYPE}|all", $"{WebsiteChannelInfo.OBJECT_TYPE}|all" }); + + var items = new List<(int WebsiteChannelID, string ChannelName)>(); + + foreach (var item in results) + { + if (item.TryGetValue(nameof(WebsiteChannelInfo.WebsiteChannelID), out object channelID) && item.TryGetValue(nameof(ChannelInfo.ChannelName), out object channelName)) + { + items.Add(new(conversionService.GetInteger(channelID, 0), conversionService.GetString(channelName, ""))); + } + } + + return items.AsEnumerable(); + }, new CacheSettings(5, nameof(DefaultLuceneClient), nameof(GetAllWebsiteChannels))); +} diff --git a/src/Kentico.Xperience.Lucene.Core/Indexing/DefaultLuceneConfigurationStorageService.cs b/src/Kentico.Xperience.Lucene.Core/Indexing/DefaultLuceneConfigurationStorageService.cs index 5fd6b5f7..38920e4f 100644 --- a/src/Kentico.Xperience.Lucene.Core/Indexing/DefaultLuceneConfigurationStorageService.cs +++ b/src/Kentico.Xperience.Lucene.Core/Indexing/DefaultLuceneConfigurationStorageService.cs @@ -1,407 +1,407 @@ -using System.Text; - -using CMS.Base; -using CMS.DataEngine; - -namespace Kentico.Xperience.Lucene.Core.Indexing; - -internal class DefaultLuceneConfigurationStorageService : ILuceneConfigurationStorageService -{ - private readonly ILuceneIndexItemInfoProvider indexProvider; - private readonly ILuceneIncludedPathItemInfoProvider pathProvider; - private readonly ILuceneContentTypeItemInfoProvider contentTypeProvider; - private readonly ILuceneIndexLanguageItemInfoProvider languageProvider; - - public DefaultLuceneConfigurationStorageService( - ILuceneIndexItemInfoProvider indexProvider, - ILuceneIncludedPathItemInfoProvider pathProvider, - ILuceneContentTypeItemInfoProvider contentTypeProvider, - ILuceneIndexLanguageItemInfoProvider languageProvider - ) - { - this.indexProvider = indexProvider; - this.pathProvider = pathProvider; - this.contentTypeProvider = contentTypeProvider; - this.languageProvider = languageProvider; - } - - public bool TryCreateIndex(LuceneIndexModel configuration) - { - var existingIndex = indexProvider.Get() - .WhereEquals(nameof(LuceneIndexItemInfo.LuceneIndexItemIndexName), configuration.IndexName) - .TopN(1) - .FirstOrDefault(); - - if (existingIndex is not null) - { - return false; - } - - var newInfo = new LuceneIndexItemInfo() - { - LuceneIndexItemIndexName = configuration.IndexName ?? "", - LuceneIndexItemChannelName = configuration.ChannelName ?? "", - LuceneIndexItemStrategyName = configuration.StrategyName ?? "", - LuceneIndexItemAnalyzerName = configuration.AnalyzerName ?? "", - LuceneIndexItemRebuildHook = configuration.RebuildHook ?? "" - }; - - indexProvider.Set(newInfo); - - configuration.Id = newInfo.LuceneIndexItemId; - - if (configuration.LanguageNames is not null) - { - foreach (string? language in configuration.LanguageNames) - { - var languageInfo = new LuceneIndexLanguageItemInfo() - { - LuceneIndexLanguageItemName = language, - LuceneIndexLanguageItemIndexItemId = newInfo.LuceneIndexItemId - }; - - languageInfo.Insert(); - } - } - - if (configuration.Paths is not null) - { - foreach (var path in configuration.Paths) - { - var pathInfo = new LuceneIncludedPathItemInfo() - { - LuceneIncludedPathItemAliasPath = path.AliasPath, - LuceneIncludedPathItemIndexItemId = newInfo.LuceneIndexItemId - }; - pathProvider.Set(pathInfo); - - if (path.ContentTypes is not null) - { - foreach (var contentType in path.ContentTypes) - { - var contentInfo = new LuceneContentTypeItemInfo() - { - LuceneContentTypeItemContentTypeName = contentType.ContentTypeName, - LuceneContentTypeItemIncludedPathItemId = pathInfo.LuceneIncludedPathItemId, - LuceneContentTypeItemIndexItemId = newInfo.LuceneIndexItemId - }; - contentInfo.Insert(); - } - } - } - } - - return true; - } - - - public async Task GetIndexDataOrNullAsync(int indexId) - { - var indexInfo = indexProvider.Get().WithID(indexId).FirstOrDefault(); - if (indexInfo == default) - { - return default; - } - - var paths = pathProvider.Get().WhereEquals(nameof(LuceneIncludedPathItemInfo.LuceneIncludedPathItemIndexItemId), indexInfo.LuceneIndexItemId).GetEnumerableTypedResult(); - - var contentTypes = await GetLuceneContentTypesAsync(); - - var languages = languageProvider.Get().WhereEquals(nameof(LuceneIndexLanguageItemInfo.LuceneIndexLanguageItemIndexItemId), indexInfo.LuceneIndexItemId).GetEnumerableTypedResult(); - - return new LuceneIndexModel(indexInfo, languages, paths, contentTypes); - } - - - public async Task GetIndexDataOrNullAsync(string indexName) - { - var indexInfo = indexProvider.Get().WhereEquals(nameof(LuceneIndexItemInfo.LuceneIndexItemIndexName), indexName).FirstOrDefault(); - if (indexInfo == default) - { - return default; - } - - var paths = pathProvider.Get().WhereEquals(nameof(LuceneIncludedPathItemInfo.LuceneIncludedPathItemIndexItemId), indexInfo.LuceneIndexItemId).GetEnumerableTypedResult(); - - var contentTypes = await GetLuceneContentTypesAsync(); - - var languages = languageProvider.Get().WhereEquals(nameof(LuceneIndexLanguageItemInfo.LuceneIndexLanguageItemIndexItemId), indexInfo.LuceneIndexItemId).GetEnumerableTypedResult(); - - return new LuceneIndexModel(indexInfo, languages, paths, contentTypes); - } - - - public List GetExistingIndexNames() => indexProvider.Get().Select(x => x.LuceneIndexItemIndexName).ToList(); - - - public List GetIndexIds() => indexProvider.Get().Select(x => x.LuceneIndexItemId).ToList(); - - - public async Task> GetAllIndexDataAsync() - { - var indexInfos = indexProvider.Get().GetEnumerableTypedResult().ToList(); - if (indexInfos.Count == 0) - { - return []; - } - - var paths = pathProvider.Get().ToList(); - - var contentTypes = await GetLuceneContentTypesAsync(); - - var languages = languageProvider.Get().ToList(); - - return indexInfos.Select(index => new LuceneIndexModel(index, languages, paths, contentTypes)); - } - - - public async Task TryEditIndexAsync(LuceneIndexModel configuration) - { - configuration.IndexName = RemoveWhitespacesUsingStringBuilder(configuration.IndexName ?? ""); - - var indexInfo = indexProvider.Get() - .WhereEquals(nameof(LuceneIndexItemInfo.LuceneIndexItemId), configuration.Id) - .TopN(1) - .FirstOrDefault(); - - if (indexInfo is null) - { - return false; - } - - indexInfo.LuceneIndexItemRebuildHook = configuration.RebuildHook ?? ""; - indexInfo.LuceneIndexItemStrategyName = configuration.StrategyName ?? ""; - indexInfo.LuceneIndexItemAnalyzerName = configuration.AnalyzerName ?? ""; - indexInfo.LuceneIndexItemChannelName = configuration.ChannelName ?? ""; - indexInfo.LuceneIndexItemIndexName = configuration.IndexName ?? ""; - - indexProvider.Set(indexInfo); - - RemoveUnusedIndexLanguages(configuration); - await SetNewIndexLanguagesAsync(configuration, indexInfo); - - await RemoveUnusedIndexPathsAsync(configuration); - var existingPaths = await GetExistingIndexPathsAsync(configuration); - SetNewIndexPaths(configuration, existingPaths, indexInfo); - UpdateEditedIndexPaths(configuration, existingPaths); - - var existingContentTypes = await GetExistingIndexContentTypesAsync(configuration); - RemoveUnusedIndexContentTypesFromEditedPaths(existingContentTypes, configuration); - SetNewIndexContentTypes(configuration, indexInfo, existingPaths, existingContentTypes); - - return true; - } - - - public bool TryDeleteIndex(int id) - { - indexProvider.BulkDelete(new WhereCondition($"{nameof(LuceneIndexItemInfo.LuceneIndexItemId)} = {id}")); - pathProvider.BulkDelete(new WhereCondition($"{nameof(LuceneIncludedPathItemInfo.LuceneIncludedPathItemIndexItemId)} = {id}")); - languageProvider.BulkDelete(new WhereCondition($"{nameof(LuceneIndexLanguageItemInfo.LuceneIndexLanguageItemIndexItemId)} = {id}")); - contentTypeProvider.BulkDelete(new WhereCondition($"{nameof(LuceneContentTypeItemInfo.LuceneContentTypeItemIndexItemId)} = {id}")); - - return true; - } - - - public bool TryDeleteIndex(LuceneIndexModel configuration) - { - indexProvider.BulkDelete(new WhereCondition($"{nameof(LuceneIndexItemInfo.LuceneIndexItemId)} = {configuration.Id}")); - pathProvider.BulkDelete(new WhereCondition($"{nameof(LuceneIncludedPathItemInfo.LuceneIncludedPathItemIndexItemId)} = {configuration.Id}")); - languageProvider.BulkDelete(new WhereCondition($"{nameof(LuceneIndexLanguageItemInfo.LuceneIndexLanguageItemIndexItemId)} = {configuration.Id}")); - contentTypeProvider.BulkDelete(new WhereCondition($"{nameof(LuceneContentTypeItemInfo.LuceneContentTypeItemIndexItemId)} = {configuration.Id}")); - - return true; - } - - - private async Task> GetLuceneContentTypesAsync() - => await contentTypeProvider - .Get().Source(x => - x.InnerJoin( - nameof(LuceneContentTypeItemInfo.LuceneContentTypeItemContentTypeName), - nameof(DataClassInfo.ClassName)) - ) - .Columns(nameof(LuceneContentTypeItemInfo.LuceneContentTypeItemContentTypeName), - nameof(DataClassInfo.ClassDisplayName), - nameof(LuceneContentTypeItemInfo.LuceneContentTypeItemIncludedPathItemId)) - .GetEnumerableTypedResultAsync(x => - { - var dataContainer = new DataRecordContainer(x); - return new LuceneIndexContentType( - (string)dataContainer[nameof(LuceneContentTypeItemInfo.LuceneContentTypeItemContentTypeName)], - (string)dataContainer[nameof(DataClassInfo.ClassDisplayName)], - (int)dataContainer[nameof(LuceneContentTypeItemInfo.LuceneContentTypeItemIncludedPathItemId)]); - }); - - - private void RemoveUnusedIndexLanguages(LuceneIndexModel configuration) - { - var removeLanguagesQuery = languageProvider - .Get() - .WhereEquals(nameof(LuceneIndexLanguageItemInfo.LuceneIndexLanguageItemIndexItemId), configuration.Id) - .WhereNotIn(nameof(LuceneIndexLanguageItemInfo.LuceneIndexLanguageItemName), configuration.LanguageNames.ToArray()); - - languageProvider.BulkDelete(new WhereCondition(removeLanguagesQuery)); - } - - - private async Task> GetNewLanguagesOnIndexAsync(LuceneIndexModel configuration) - { - var existingLanguages = await languageProvider - .Get() - .WhereEquals(nameof(LuceneIndexLanguageItemInfo.LuceneIndexLanguageItemIndexItemId), configuration.Id) - .GetEnumerableTypedResultAsync(); - - return configuration.LanguageNames.Where(x => !existingLanguages.Any(y => y.LuceneIndexLanguageItemName == x)); - } - - - private async Task SetNewIndexLanguagesAsync(LuceneIndexModel configuration, LuceneIndexItemInfo indexInfo) - { - var newLanguages = await GetNewLanguagesOnIndexAsync(configuration); - - foreach (string? language in newLanguages) - { - var languageInfo = new LuceneIndexLanguageItemInfo() - { - LuceneIndexLanguageItemName = language, - LuceneIndexLanguageItemIndexItemId = indexInfo.LuceneIndexItemId, - }; - - languageProvider.Set(languageInfo); - } - } - - - private async Task RemoveUnusedIndexPathsAsync(LuceneIndexModel configuration) - { - var removePathsQuery = pathProvider - .Get() - .WhereEquals(nameof(LuceneIncludedPathItemInfo.LuceneIncludedPathItemIndexItemId), configuration.Id) - .WhereNotIn(nameof(LuceneIncludedPathItemInfo.LuceneIncludedPathItemId), configuration.Paths.Select(x => x.Identifier ?? 0).ToArray()); - - var removedPaths = await removePathsQuery.GetEnumerableTypedResultAsync(); - - pathProvider.BulkDelete(removePathsQuery); - - RemoveUnusedIndexContentTypes(removedPaths); - } - - - private async Task> GetExistingIndexPathsAsync(LuceneIndexModel configuration) - => await pathProvider - .Get() - .WhereEquals(nameof(LuceneIncludedPathItemInfo.LuceneIncludedPathItemIndexItemId), configuration.Id) - .GetEnumerableTypedResultAsync(); - - - private void SetNewIndexPaths(LuceneIndexModel configuration, IEnumerable existingPaths, LuceneIndexItemInfo indexInfo) - { - var newPaths = configuration.Paths.Where(x => !existingPaths.Any(y => y.LuceneIncludedPathItemId == x.Identifier)); - - foreach (var path in newPaths) - { - var pathInfo = new LuceneIncludedPathItemInfo() - { - LuceneIncludedPathItemAliasPath = path.AliasPath, - LuceneIncludedPathItemIndexItemId = indexInfo.LuceneIndexItemId, - }; - pathProvider.Set(pathInfo); - - if (path.ContentTypes != null) - { - foreach (var contentType in path.ContentTypes) - { - var contentInfo = new LuceneContentTypeItemInfo() - { - LuceneContentTypeItemContentTypeName = contentType.ContentTypeName ?? "", - LuceneContentTypeItemIncludedPathItemId = pathInfo.LuceneIncludedPathItemId, - LuceneContentTypeItemIndexItemId = indexInfo.LuceneIndexItemId, - }; - contentInfo.Insert(); - } - } - } - } - - - private void RemoveUnusedIndexContentTypes(IEnumerable removedPaths) - { - var removeContentTypesQuery = contentTypeProvider - .Get() - .WhereIn(nameof(LuceneContentTypeItemInfo.LuceneContentTypeItemIncludedPathItemId), removedPaths.Select(x => x.LuceneIncludedPathItemId).ToArray()); - - contentTypeProvider.BulkDelete(removeContentTypesQuery); - } - - - private async Task> GetExistingIndexContentTypesAsync(LuceneIndexModel configuration) - => await contentTypeProvider - .Get() - .WhereIn(nameof(LuceneContentTypeItemInfo.LuceneContentTypeItemIncludedPathItemId), configuration.Paths.Select(x => x.Identifier ?? 0).ToArray()) - .GetEnumerableTypedResultAsync(); - - - private void RemoveUnusedIndexContentTypesFromEditedPaths(IEnumerable allExistingContentTypes, LuceneIndexModel configuration) - { - int[] removedContentTypeIdsFromEditedPaths = allExistingContentTypes - .Where(x => !configuration.Paths - .Any(y => y.ContentTypes - .Exists(z => x.LuceneContentTypeItemIncludedPathItemId == y.Identifier && x.LuceneContentTypeItemContentTypeName == z.ContentTypeName)) - ) - .Select(x => x.LuceneContentTypeItemId) - .ToArray(); - - contentTypeProvider.BulkDelete(contentTypeProvider.Get().WhereIn(nameof(LuceneContentTypeItemInfo.LuceneContentTypeItemId), removedContentTypeIdsFromEditedPaths)); - } - - - private void UpdateEditedIndexPaths(LuceneIndexModel configuration, IEnumerable existingPaths) - { - foreach (var path in existingPaths) - { - path.LuceneIncludedPathItemAliasPath = configuration.Paths.Single(x => x.Identifier == path.LuceneIncludedPathItemId).AliasPath; - path.Update(); - } - } - - - private void SetNewIndexContentTypes(LuceneIndexModel configuration, LuceneIndexItemInfo indexInfo, IEnumerable existingPaths, IEnumerable existingContentTypes) - { - foreach (var path in existingPaths) - { - foreach (var contentType in configuration.Paths - .Single(x => x.Identifier == path.LuceneIncludedPathItemId) - .ContentTypes - .Where(x => !existingContentTypes - .Any(y => y.LuceneContentTypeItemContentTypeName == x.ContentTypeName && y.LuceneContentTypeItemIncludedPathItemId == path.LuceneIncludedPathItemId) - ) - ) - { - var contentInfo = new LuceneContentTypeItemInfo() - { - LuceneContentTypeItemContentTypeName = contentType.ContentTypeName ?? "", - LuceneContentTypeItemIncludedPathItemId = path.LuceneIncludedPathItemId, - LuceneContentTypeItemIndexItemId = indexInfo.LuceneIndexItemId, - }; - contentInfo.Insert(); - } - } - } - - - private static string RemoveWhitespacesUsingStringBuilder(string source) - { - var builder = new StringBuilder(source.Length); - for (int i = 0; i < source.Length; i++) - { - char c = source[i]; - if (!char.IsWhiteSpace(c)) - { - builder.Append(c); - } - } - return source.Length == builder.Length ? source : builder.ToString(); - } -} +using System.Text; + +using CMS.Base; +using CMS.DataEngine; + +namespace Kentico.Xperience.Lucene.Core.Indexing; + +internal class DefaultLuceneConfigurationStorageService : ILuceneConfigurationStorageService +{ + private readonly ILuceneIndexItemInfoProvider indexProvider; + private readonly ILuceneIncludedPathItemInfoProvider pathProvider; + private readonly ILuceneContentTypeItemInfoProvider contentTypeProvider; + private readonly ILuceneIndexLanguageItemInfoProvider languageProvider; + + public DefaultLuceneConfigurationStorageService( + ILuceneIndexItemInfoProvider indexProvider, + ILuceneIncludedPathItemInfoProvider pathProvider, + ILuceneContentTypeItemInfoProvider contentTypeProvider, + ILuceneIndexLanguageItemInfoProvider languageProvider + ) + { + this.indexProvider = indexProvider; + this.pathProvider = pathProvider; + this.contentTypeProvider = contentTypeProvider; + this.languageProvider = languageProvider; + } + + public bool TryCreateIndex(LuceneIndexModel configuration) + { + var existingIndex = indexProvider.Get() + .WhereEquals(nameof(LuceneIndexItemInfo.LuceneIndexItemIndexName), configuration.IndexName) + .TopN(1) + .FirstOrDefault(); + + if (existingIndex is not null) + { + return false; + } + + var newInfo = new LuceneIndexItemInfo() + { + LuceneIndexItemIndexName = configuration.IndexName ?? "", + LuceneIndexItemChannelName = configuration.ChannelName ?? "", + LuceneIndexItemStrategyName = configuration.StrategyName ?? "", + LuceneIndexItemAnalyzerName = configuration.AnalyzerName ?? "", + LuceneIndexItemRebuildHook = configuration.RebuildHook ?? "" + }; + + indexProvider.Set(newInfo); + + configuration.Id = newInfo.LuceneIndexItemId; + + if (configuration.LanguageNames is not null) + { + foreach (string? language in configuration.LanguageNames) + { + var languageInfo = new LuceneIndexLanguageItemInfo() + { + LuceneIndexLanguageItemName = language, + LuceneIndexLanguageItemIndexItemId = newInfo.LuceneIndexItemId + }; + + languageInfo.Insert(); + } + } + + if (configuration.Paths is not null) + { + foreach (var path in configuration.Paths) + { + var pathInfo = new LuceneIncludedPathItemInfo() + { + LuceneIncludedPathItemAliasPath = path.AliasPath, + LuceneIncludedPathItemIndexItemId = newInfo.LuceneIndexItemId + }; + pathProvider.Set(pathInfo); + + if (path.ContentTypes is not null) + { + foreach (var contentType in path.ContentTypes) + { + var contentInfo = new LuceneContentTypeItemInfo() + { + LuceneContentTypeItemContentTypeName = contentType.ContentTypeName, + LuceneContentTypeItemIncludedPathItemId = pathInfo.LuceneIncludedPathItemId, + LuceneContentTypeItemIndexItemId = newInfo.LuceneIndexItemId + }; + contentInfo.Insert(); + } + } + } + } + + return true; + } + + + public async Task GetIndexDataOrNullAsync(int indexId) + { + var indexInfo = indexProvider.Get().WithID(indexId).FirstOrDefault(); + if (indexInfo == default) + { + return default; + } + + var paths = pathProvider.Get().WhereEquals(nameof(LuceneIncludedPathItemInfo.LuceneIncludedPathItemIndexItemId), indexInfo.LuceneIndexItemId).GetEnumerableTypedResult(); + + var contentTypes = await GetLuceneContentTypesAsync(); + + var languages = languageProvider.Get().WhereEquals(nameof(LuceneIndexLanguageItemInfo.LuceneIndexLanguageItemIndexItemId), indexInfo.LuceneIndexItemId).GetEnumerableTypedResult(); + + return new LuceneIndexModel(indexInfo, languages, paths, contentTypes); + } + + + public async Task GetIndexDataOrNullAsync(string indexName) + { + var indexInfo = indexProvider.Get().WhereEquals(nameof(LuceneIndexItemInfo.LuceneIndexItemIndexName), indexName).FirstOrDefault(); + if (indexInfo == default) + { + return default; + } + + var paths = pathProvider.Get().WhereEquals(nameof(LuceneIncludedPathItemInfo.LuceneIncludedPathItemIndexItemId), indexInfo.LuceneIndexItemId).GetEnumerableTypedResult(); + + var contentTypes = await GetLuceneContentTypesAsync(); + + var languages = languageProvider.Get().WhereEquals(nameof(LuceneIndexLanguageItemInfo.LuceneIndexLanguageItemIndexItemId), indexInfo.LuceneIndexItemId).GetEnumerableTypedResult(); + + return new LuceneIndexModel(indexInfo, languages, paths, contentTypes); + } + + + public List GetExistingIndexNames() => indexProvider.Get().Select(x => x.LuceneIndexItemIndexName).ToList(); + + + public List GetIndexIds() => indexProvider.Get().Select(x => x.LuceneIndexItemId).ToList(); + + + public async Task> GetAllIndexDataAsync() + { + var indexInfos = indexProvider.Get().GetEnumerableTypedResult().ToList(); + if (indexInfos.Count == 0) + { + return []; + } + + var paths = pathProvider.Get().ToList(); + + var contentTypes = await GetLuceneContentTypesAsync(); + + var languages = languageProvider.Get().ToList(); + + return indexInfos.Select(index => new LuceneIndexModel(index, languages, paths, contentTypes)); + } + + + public async Task TryEditIndexAsync(LuceneIndexModel configuration) + { + configuration.IndexName = RemoveWhitespacesUsingStringBuilder(configuration.IndexName ?? ""); + + var indexInfo = indexProvider.Get() + .WhereEquals(nameof(LuceneIndexItemInfo.LuceneIndexItemId), configuration.Id) + .TopN(1) + .FirstOrDefault(); + + if (indexInfo is null) + { + return false; + } + + indexInfo.LuceneIndexItemRebuildHook = configuration.RebuildHook ?? ""; + indexInfo.LuceneIndexItemStrategyName = configuration.StrategyName ?? ""; + indexInfo.LuceneIndexItemAnalyzerName = configuration.AnalyzerName ?? ""; + indexInfo.LuceneIndexItemChannelName = configuration.ChannelName ?? ""; + indexInfo.LuceneIndexItemIndexName = configuration.IndexName ?? ""; + + indexProvider.Set(indexInfo); + + RemoveUnusedIndexLanguages(configuration); + await SetNewIndexLanguagesAsync(configuration, indexInfo); + + await RemoveUnusedIndexPathsAsync(configuration); + var existingPaths = await GetExistingIndexPathsAsync(configuration); + SetNewIndexPaths(configuration, existingPaths, indexInfo); + UpdateEditedIndexPaths(configuration, existingPaths); + + var existingContentTypes = await GetExistingIndexContentTypesAsync(configuration); + RemoveUnusedIndexContentTypesFromEditedPaths(existingContentTypes, configuration); + SetNewIndexContentTypes(configuration, indexInfo, existingPaths, existingContentTypes); + + return true; + } + + + public bool TryDeleteIndex(int id) + { + indexProvider.BulkDelete(new WhereCondition($"{nameof(LuceneIndexItemInfo.LuceneIndexItemId)} = {id}")); + pathProvider.BulkDelete(new WhereCondition($"{nameof(LuceneIncludedPathItemInfo.LuceneIncludedPathItemIndexItemId)} = {id}")); + languageProvider.BulkDelete(new WhereCondition($"{nameof(LuceneIndexLanguageItemInfo.LuceneIndexLanguageItemIndexItemId)} = {id}")); + contentTypeProvider.BulkDelete(new WhereCondition($"{nameof(LuceneContentTypeItemInfo.LuceneContentTypeItemIndexItemId)} = {id}")); + + return true; + } + + + public bool TryDeleteIndex(LuceneIndexModel configuration) + { + indexProvider.BulkDelete(new WhereCondition($"{nameof(LuceneIndexItemInfo.LuceneIndexItemId)} = {configuration.Id}")); + pathProvider.BulkDelete(new WhereCondition($"{nameof(LuceneIncludedPathItemInfo.LuceneIncludedPathItemIndexItemId)} = {configuration.Id}")); + languageProvider.BulkDelete(new WhereCondition($"{nameof(LuceneIndexLanguageItemInfo.LuceneIndexLanguageItemIndexItemId)} = {configuration.Id}")); + contentTypeProvider.BulkDelete(new WhereCondition($"{nameof(LuceneContentTypeItemInfo.LuceneContentTypeItemIndexItemId)} = {configuration.Id}")); + + return true; + } + + + private async Task> GetLuceneContentTypesAsync() + => await contentTypeProvider + .Get().Source(x => + x.InnerJoin( + nameof(LuceneContentTypeItemInfo.LuceneContentTypeItemContentTypeName), + nameof(DataClassInfo.ClassName)) + ) + .Columns(nameof(LuceneContentTypeItemInfo.LuceneContentTypeItemContentTypeName), + nameof(DataClassInfo.ClassDisplayName), + nameof(LuceneContentTypeItemInfo.LuceneContentTypeItemIncludedPathItemId)) + .GetEnumerableTypedResultAsync(x => + { + var dataContainer = new DataRecordContainer(x); + return new LuceneIndexContentType( + (string)dataContainer[nameof(LuceneContentTypeItemInfo.LuceneContentTypeItemContentTypeName)], + (string)dataContainer[nameof(DataClassInfo.ClassDisplayName)], + (int)dataContainer[nameof(LuceneContentTypeItemInfo.LuceneContentTypeItemIncludedPathItemId)]); + }); + + + private void RemoveUnusedIndexLanguages(LuceneIndexModel configuration) + { + var removeLanguagesQuery = languageProvider + .Get() + .WhereEquals(nameof(LuceneIndexLanguageItemInfo.LuceneIndexLanguageItemIndexItemId), configuration.Id) + .WhereNotIn(nameof(LuceneIndexLanguageItemInfo.LuceneIndexLanguageItemName), configuration.LanguageNames.ToArray()); + + languageProvider.BulkDelete(new WhereCondition(removeLanguagesQuery)); + } + + + private async Task> GetNewLanguagesOnIndexAsync(LuceneIndexModel configuration) + { + var existingLanguages = await languageProvider + .Get() + .WhereEquals(nameof(LuceneIndexLanguageItemInfo.LuceneIndexLanguageItemIndexItemId), configuration.Id) + .GetEnumerableTypedResultAsync(); + + return configuration.LanguageNames.Where(x => !existingLanguages.Any(y => y.LuceneIndexLanguageItemName == x)); + } + + + private async Task SetNewIndexLanguagesAsync(LuceneIndexModel configuration, LuceneIndexItemInfo indexInfo) + { + var newLanguages = await GetNewLanguagesOnIndexAsync(configuration); + + foreach (string? language in newLanguages) + { + var languageInfo = new LuceneIndexLanguageItemInfo() + { + LuceneIndexLanguageItemName = language, + LuceneIndexLanguageItemIndexItemId = indexInfo.LuceneIndexItemId, + }; + + languageProvider.Set(languageInfo); + } + } + + + private async Task RemoveUnusedIndexPathsAsync(LuceneIndexModel configuration) + { + var removePathsQuery = pathProvider + .Get() + .WhereEquals(nameof(LuceneIncludedPathItemInfo.LuceneIncludedPathItemIndexItemId), configuration.Id) + .WhereNotIn(nameof(LuceneIncludedPathItemInfo.LuceneIncludedPathItemId), configuration.Paths.Select(x => x.Identifier ?? 0).ToArray()); + + var removedPaths = await removePathsQuery.GetEnumerableTypedResultAsync(); + + pathProvider.BulkDelete(removePathsQuery); + + RemoveUnusedIndexContentTypes(removedPaths); + } + + + private async Task> GetExistingIndexPathsAsync(LuceneIndexModel configuration) + => await pathProvider + .Get() + .WhereEquals(nameof(LuceneIncludedPathItemInfo.LuceneIncludedPathItemIndexItemId), configuration.Id) + .GetEnumerableTypedResultAsync(); + + + private void SetNewIndexPaths(LuceneIndexModel configuration, IEnumerable existingPaths, LuceneIndexItemInfo indexInfo) + { + var newPaths = configuration.Paths.Where(x => !existingPaths.Any(y => y.LuceneIncludedPathItemId == x.Identifier)); + + foreach (var path in newPaths) + { + var pathInfo = new LuceneIncludedPathItemInfo() + { + LuceneIncludedPathItemAliasPath = path.AliasPath, + LuceneIncludedPathItemIndexItemId = indexInfo.LuceneIndexItemId, + }; + pathProvider.Set(pathInfo); + + if (path.ContentTypes != null) + { + foreach (var contentType in path.ContentTypes) + { + var contentInfo = new LuceneContentTypeItemInfo() + { + LuceneContentTypeItemContentTypeName = contentType.ContentTypeName ?? "", + LuceneContentTypeItemIncludedPathItemId = pathInfo.LuceneIncludedPathItemId, + LuceneContentTypeItemIndexItemId = indexInfo.LuceneIndexItemId, + }; + contentInfo.Insert(); + } + } + } + } + + + private void RemoveUnusedIndexContentTypes(IEnumerable removedPaths) + { + var removeContentTypesQuery = contentTypeProvider + .Get() + .WhereIn(nameof(LuceneContentTypeItemInfo.LuceneContentTypeItemIncludedPathItemId), removedPaths.Select(x => x.LuceneIncludedPathItemId).ToArray()); + + contentTypeProvider.BulkDelete(removeContentTypesQuery); + } + + + private async Task> GetExistingIndexContentTypesAsync(LuceneIndexModel configuration) + => await contentTypeProvider + .Get() + .WhereIn(nameof(LuceneContentTypeItemInfo.LuceneContentTypeItemIncludedPathItemId), configuration.Paths.Select(x => x.Identifier ?? 0).ToArray()) + .GetEnumerableTypedResultAsync(); + + + private void RemoveUnusedIndexContentTypesFromEditedPaths(IEnumerable allExistingContentTypes, LuceneIndexModel configuration) + { + int[] removedContentTypeIdsFromEditedPaths = allExistingContentTypes + .Where(x => !configuration.Paths + .Any(y => y.ContentTypes + .Exists(z => x.LuceneContentTypeItemIncludedPathItemId == y.Identifier && x.LuceneContentTypeItemContentTypeName == z.ContentTypeName)) + ) + .Select(x => x.LuceneContentTypeItemId) + .ToArray(); + + contentTypeProvider.BulkDelete(contentTypeProvider.Get().WhereIn(nameof(LuceneContentTypeItemInfo.LuceneContentTypeItemId), removedContentTypeIdsFromEditedPaths)); + } + + + private void UpdateEditedIndexPaths(LuceneIndexModel configuration, IEnumerable existingPaths) + { + foreach (var path in existingPaths) + { + path.LuceneIncludedPathItemAliasPath = configuration.Paths.Single(x => x.Identifier == path.LuceneIncludedPathItemId).AliasPath; + path.Update(); + } + } + + + private void SetNewIndexContentTypes(LuceneIndexModel configuration, LuceneIndexItemInfo indexInfo, IEnumerable existingPaths, IEnumerable existingContentTypes) + { + foreach (var path in existingPaths) + { + foreach (var contentType in configuration.Paths + .Single(x => x.Identifier == path.LuceneIncludedPathItemId) + .ContentTypes + .Where(x => !existingContentTypes + .Any(y => y.LuceneContentTypeItemContentTypeName == x.ContentTypeName && y.LuceneContentTypeItemIncludedPathItemId == path.LuceneIncludedPathItemId) + ) + ) + { + var contentInfo = new LuceneContentTypeItemInfo() + { + LuceneContentTypeItemContentTypeName = contentType.ContentTypeName ?? "", + LuceneContentTypeItemIncludedPathItemId = path.LuceneIncludedPathItemId, + LuceneContentTypeItemIndexItemId = indexInfo.LuceneIndexItemId, + }; + contentInfo.Insert(); + } + } + } + + + private static string RemoveWhitespacesUsingStringBuilder(string source) + { + var builder = new StringBuilder(source.Length); + for (int i = 0; i < source.Length; i++) + { + char c = source[i]; + if (!char.IsWhiteSpace(c)) + { + builder.Append(c); + } + } + return source.Length == builder.Length ? source : builder.ToString(); + } +} diff --git a/src/Kentico.Xperience.Lucene.Core/Indexing/DefaultLuceneTaskProcessor.cs b/src/Kentico.Xperience.Lucene.Core/Indexing/DefaultLuceneTaskProcessor.cs index 477c5eb3..be4b11e1 100644 --- a/src/Kentico.Xperience.Lucene.Core/Indexing/DefaultLuceneTaskProcessor.cs +++ b/src/Kentico.Xperience.Lucene.Core/Indexing/DefaultLuceneTaskProcessor.cs @@ -1,154 +1,154 @@ -using CMS.Base; -using CMS.Core; -using CMS.Websites; - -using Lucene.Net.Documents; -using Lucene.Net.Documents.Extensions; - -using Microsoft.Extensions.DependencyInjection; - -namespace Kentico.Xperience.Lucene.Core.Indexing; - -internal class LuceneBatchResult -{ - internal int SuccessfulOperations { get; set; } = 0; - internal HashSet PublishedIndices { get; set; } = []; -} - -internal class DefaultLuceneTaskProcessor : ILuceneTaskProcessor -{ - private readonly IWebPageUrlRetriever urlRetriever; - private readonly IServiceProvider serviceProvider; - private readonly ILuceneClient luceneClient; - private readonly IEventLogService eventLogService; - private readonly ILuceneIndexManager indexManager; - - public DefaultLuceneTaskProcessor( - ILuceneClient luceneClient, - IEventLogService eventLogService, - IWebPageUrlRetriever urlRetriever, - IServiceProvider serviceProvider, - ILuceneIndexManager indexManager) - { - this.luceneClient = luceneClient; - this.eventLogService = eventLogService; - this.urlRetriever = urlRetriever; - this.serviceProvider = serviceProvider; - this.indexManager = indexManager; - } - - /// - public async Task ProcessLuceneTasks(IEnumerable queueItems, CancellationToken cancellationToken, int maximumBatchSize = 100) - { - LuceneBatchResult batchResults = new(); - - var batches = queueItems.Batch(maximumBatchSize); - - foreach (var batch in batches) - { - await ProcessLuceneBatch(batch, batchResults, cancellationToken); - } - - foreach (var index in batchResults.PublishedIndices) - { - var storage = index.StorageContext.GetNextOrOpenNextGeneration(); - index.StorageContext.PublishIndex(storage); - } - - return batchResults.SuccessfulOperations; - } - - private async Task ProcessLuceneBatch(IEnumerable queueItems, LuceneBatchResult previousBatchResults, CancellationToken cancellationToken) - { - - var groups = queueItems.GroupBy(item => item.IndexName); - - foreach (var group in groups) - { - try - { - var deleteIds = new List(); - var deleteTasks = group.Where(queueItem => queueItem.TaskType == LuceneTaskType.DELETE).ToList(); - - var updateTasks = group.Where(queueItem => queueItem.TaskType is LuceneTaskType.PUBLISH_INDEX or LuceneTaskType.UPDATE); - var upsertData = new List(); - foreach (var queueItem in updateTasks) - { - var document = await GetDocument(queueItem); - if (document is not null) - { - upsertData.Add(document); - } - else - { - deleteTasks.Add(queueItem); - } - } - deleteIds.AddRange(GetIdsToDelete(deleteTasks ?? []).Where(x => x is not null).Select(x => x ?? "")); - if (indexManager.GetIndex(group.Key) is { } index) - { - previousBatchResults.SuccessfulOperations += await luceneClient.DeleteRecords(deleteIds, group.Key); - previousBatchResults.SuccessfulOperations += await luceneClient.UpsertRecords(upsertData, group.Key, cancellationToken); - - if (group.Any(t => t.TaskType == LuceneTaskType.PUBLISH_INDEX) && !previousBatchResults.PublishedIndices.Any(x => x.IndexName == index.IndexName)) - { - previousBatchResults.PublishedIndices.Add(index); - } - } - else - { - eventLogService.LogError(nameof(DefaultLuceneTaskProcessor), nameof(ProcessLuceneTasks), "Index instance not exists"); - } - } - catch (Exception ex) - { - eventLogService.LogError(nameof(DefaultLuceneTaskProcessor), nameof(ProcessLuceneTasks), ex.Message); - } - } - } - - private static IEnumerable GetIdsToDelete(IEnumerable deleteTasks) => deleteTasks.Select(queueItem => queueItem.ItemToIndex.ItemGuid.ToString()); - - /// - public async Task GetDocument(LuceneQueueItem queueItem) - { - var luceneIndex = indexManager.GetRequiredIndex(queueItem.IndexName); - - var strategy = serviceProvider.GetRequiredStrategy(luceneIndex); - - var data = await strategy.MapToLuceneDocumentOrNull(queueItem.ItemToIndex); - - if (data is null) - { - return null; - } - - await AddBaseProperties(queueItem.ItemToIndex, data!); - - return data; - } - - private async Task AddBaseProperties(IIndexEventItemModel item, Document document) - { - document.AddStringField(BaseDocumentProperties.CONTENT_TYPE_NAME, item.ContentTypeName, Field.Store.YES); - document.AddStringField(BaseDocumentProperties.LANGUAGE_NAME, item.LanguageName, Field.Store.YES); - document.AddStringField(BaseDocumentProperties.ITEM_GUID, item.ItemGuid.ToString(), Field.Store.YES); - - if (item is IndexEventWebPageItemModel webpageItem && !document.Any(x => string.Equals(x.Name, BaseDocumentProperties.URL, StringComparison.OrdinalIgnoreCase))) - { - string url = string.Empty; - try - { - url = (await urlRetriever.Retrieve(webpageItem.WebPageItemTreePath, webpageItem.WebsiteChannelName, webpageItem.LanguageName)).RelativePath; - } - catch (Exception) - { - // Retrieve can throw an exception when processing a page update LuceneQueueItem - // and the page was deleted before the update task has processed. In this case, upsert an - // empty URL - } - - document.AddStringField(BaseDocumentProperties.URL, url, Field.Store.YES); - } - } -} +using CMS.Base; +using CMS.Core; +using CMS.Websites; + +using Lucene.Net.Documents; +using Lucene.Net.Documents.Extensions; + +using Microsoft.Extensions.DependencyInjection; + +namespace Kentico.Xperience.Lucene.Core.Indexing; + +internal class LuceneBatchResult +{ + internal int SuccessfulOperations { get; set; } = 0; + internal HashSet PublishedIndices { get; set; } = []; +} + +internal class DefaultLuceneTaskProcessor : ILuceneTaskProcessor +{ + private readonly IWebPageUrlRetriever urlRetriever; + private readonly IServiceProvider serviceProvider; + private readonly ILuceneClient luceneClient; + private readonly IEventLogService eventLogService; + private readonly ILuceneIndexManager indexManager; + + public DefaultLuceneTaskProcessor( + ILuceneClient luceneClient, + IEventLogService eventLogService, + IWebPageUrlRetriever urlRetriever, + IServiceProvider serviceProvider, + ILuceneIndexManager indexManager) + { + this.luceneClient = luceneClient; + this.eventLogService = eventLogService; + this.urlRetriever = urlRetriever; + this.serviceProvider = serviceProvider; + this.indexManager = indexManager; + } + + /// + public async Task ProcessLuceneTasks(IEnumerable queueItems, CancellationToken cancellationToken, int maximumBatchSize = 100) + { + LuceneBatchResult batchResults = new(); + + var batches = queueItems.Batch(maximumBatchSize); + + foreach (var batch in batches) + { + await ProcessLuceneBatch(batch, batchResults, cancellationToken); + } + + foreach (var index in batchResults.PublishedIndices) + { + var storage = index.StorageContext.GetNextOrOpenNextGeneration(); + index.StorageContext.PublishIndex(storage); + } + + return batchResults.SuccessfulOperations; + } + + private async Task ProcessLuceneBatch(IEnumerable queueItems, LuceneBatchResult previousBatchResults, CancellationToken cancellationToken) + { + + var groups = queueItems.GroupBy(item => item.IndexName); + + foreach (var group in groups) + { + try + { + var deleteIds = new List(); + var deleteTasks = group.Where(queueItem => queueItem.TaskType == LuceneTaskType.DELETE).ToList(); + + var updateTasks = group.Where(queueItem => queueItem.TaskType is LuceneTaskType.PUBLISH_INDEX or LuceneTaskType.UPDATE); + var upsertData = new List(); + foreach (var queueItem in updateTasks) + { + var document = await GetDocument(queueItem); + if (document is not null) + { + upsertData.Add(document); + } + else + { + deleteTasks.Add(queueItem); + } + } + deleteIds.AddRange(GetIdsToDelete(deleteTasks ?? []).Where(x => x is not null).Select(x => x ?? "")); + if (indexManager.GetIndex(group.Key) is { } index) + { + previousBatchResults.SuccessfulOperations += await luceneClient.DeleteRecords(deleteIds, group.Key); + previousBatchResults.SuccessfulOperations += await luceneClient.UpsertRecords(upsertData, group.Key, cancellationToken); + + if (group.Any(t => t.TaskType == LuceneTaskType.PUBLISH_INDEX) && !previousBatchResults.PublishedIndices.Any(x => x.IndexName == index.IndexName)) + { + previousBatchResults.PublishedIndices.Add(index); + } + } + else + { + eventLogService.LogError(nameof(DefaultLuceneTaskProcessor), nameof(ProcessLuceneTasks), "Index instance not exists"); + } + } + catch (Exception ex) + { + eventLogService.LogError(nameof(DefaultLuceneTaskProcessor), nameof(ProcessLuceneTasks), ex.Message); + } + } + } + + private static IEnumerable GetIdsToDelete(IEnumerable deleteTasks) => deleteTasks.Select(queueItem => queueItem.ItemToIndex.ItemGuid.ToString()); + + /// + public async Task GetDocument(LuceneQueueItem queueItem) + { + var luceneIndex = indexManager.GetRequiredIndex(queueItem.IndexName); + + var strategy = serviceProvider.GetRequiredStrategy(luceneIndex); + + var data = await strategy.MapToLuceneDocumentOrNull(queueItem.ItemToIndex); + + if (data is null) + { + return null; + } + + await AddBaseProperties(queueItem.ItemToIndex, data!); + + return data; + } + + private async Task AddBaseProperties(IIndexEventItemModel item, Document document) + { + document.AddStringField(BaseDocumentProperties.CONTENT_TYPE_NAME, item.ContentTypeName, Field.Store.YES); + document.AddStringField(BaseDocumentProperties.LANGUAGE_NAME, item.LanguageName, Field.Store.YES); + document.AddStringField(BaseDocumentProperties.ITEM_GUID, item.ItemGuid.ToString(), Field.Store.YES); + + if (item is IndexEventWebPageItemModel webpageItem && !document.Any(x => string.Equals(x.Name, BaseDocumentProperties.URL, StringComparison.OrdinalIgnoreCase))) + { + string url = string.Empty; + try + { + url = (await urlRetriever.Retrieve(webpageItem.WebPageItemTreePath, webpageItem.WebsiteChannelName, webpageItem.LanguageName)).RelativePath; + } + catch (Exception) + { + // Retrieve can throw an exception when processing a page update LuceneQueueItem + // and the page was deleted before the update task has processed. In this case, upsert an + // empty URL + } + + document.AddStringField(BaseDocumentProperties.URL, url, Field.Store.YES); + } + } +} diff --git a/src/Kentico.Xperience.Lucene.Core/Indexing/IIndexEventItemModel.cs b/src/Kentico.Xperience.Lucene.Core/Indexing/IIndexEventItemModel.cs index 5837dae9..7ddf82d7 100644 --- a/src/Kentico.Xperience.Lucene.Core/Indexing/IIndexEventItemModel.cs +++ b/src/Kentico.Xperience.Lucene.Core/Indexing/IIndexEventItemModel.cs @@ -1,152 +1,152 @@ -using CMS.ContentEngine; -using CMS.Websites; - -namespace Kentico.Xperience.Lucene.Core.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; } = string.Empty; - public string ContentTypeName { get; set; } = string.Empty; - /// - /// The - /// - public string Name { get; set; } = string.Empty; - public bool IsSecured { get; set; } - public int ContentTypeID { get; set; } - public int ContentLanguageID { get; set; } - - public string WebsiteChannelName { get; set; } = string.Empty; - public string WebPageItemTreePath { get; set; } = string.Empty; - public int? ParentID { get; set; } - public int Order { get; set; } - public IndexEventWebPageItemModel() { } - 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; } = string.Empty; - public string ContentTypeName { get; set; } = string.Empty; - /// - /// The - /// - public string Name { get; set; } = string.Empty; - public bool IsSecured { get; set; } - public int ContentTypeID { get; set; } - public int ContentLanguageID { get; set; } - public IndexEventReusableItemModel() { } - 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.Lucene.Core.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; } = string.Empty; + public string ContentTypeName { get; set; } = string.Empty; + /// + /// The + /// + public string Name { get; set; } = string.Empty; + public bool IsSecured { get; set; } + public int ContentTypeID { get; set; } + public int ContentLanguageID { get; set; } + + public string WebsiteChannelName { get; set; } = string.Empty; + public string WebPageItemTreePath { get; set; } = string.Empty; + public int? ParentID { get; set; } + public int Order { get; set; } + public IndexEventWebPageItemModel() { } + 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; } = string.Empty; + public string ContentTypeName { get; set; } = string.Empty; + /// + /// The + /// + public string Name { get; set; } = string.Empty; + public bool IsSecured { get; set; } + public int ContentTypeID { get; set; } + public int ContentLanguageID { get; set; } + public IndexEventReusableItemModel() { } + 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.Lucene.Core/Indexing/LuceneIndex.cs b/src/Kentico.Xperience.Lucene.Core/Indexing/LuceneIndex.cs index 1c6734f5..da5b3096 100644 --- a/src/Kentico.Xperience.Lucene.Core/Indexing/LuceneIndex.cs +++ b/src/Kentico.Xperience.Lucene.Core/Indexing/LuceneIndex.cs @@ -1,84 +1,84 @@ -using Lucene.Net.Analysis; -using Lucene.Net.Analysis.Standard; -using Lucene.Net.Util; - -namespace Kentico.Xperience.Lucene.Core.Indexing; - -/// -/// Represents the configuration of an Lucene index. -/// -public sealed class LuceneIndex -{ - /// - /// An arbitrary ID used to identify the Lucene index in the admin UI. - /// - public int Identifier { get; set; } - - /// - /// The code name of the Lucene index. - /// - public string IndexName { get; } - - /// - /// The Name of the WebSiteChannel. - /// - public string WebSiteChannelName { get; } - - /// - /// The Language used on the WebSite on the Channel which is indexed. - /// - public List LanguageNames { get; } - - /// - /// Lucene Analyzer used for indexing. - /// - public Analyzer LuceneAnalyzer { get; } - - /// - /// The type of the class which extends . - /// - public Type LuceneIndexingStrategyType { get; } - - /// - /// Index storage context, employs picked storage strategy - /// - public IndexStorageContext StorageContext { get; } - - internal IEnumerable IncludedPaths { get; set; } - - internal LuceneIndex(LuceneIndexModel indexConfiguration, Dictionary strategies, Dictionary analyzers, LuceneVersion matchVersion) - { - Identifier = indexConfiguration.Id; - IndexName = indexConfiguration.IndexName; - WebSiteChannelName = indexConfiguration.ChannelName; - LanguageNames = indexConfiguration.LanguageNames.ToList(); - IncludedPaths = indexConfiguration.Paths; - - var strategy = typeof(DefaultLuceneIndexingStrategy); - - if (strategies.ContainsKey(indexConfiguration.StrategyName)) - { - strategy = strategies[indexConfiguration.StrategyName]; - } - - var analyzerType = typeof(StandardAnalyzer); - - if (analyzers.ContainsKey(indexConfiguration.AnalyzerName)) - { - analyzerType = analyzers[indexConfiguration.AnalyzerName]; - } - - var constructorParameters = analyzerType.GetConstructors().Select(x => new - { - Constructor = x, - Parameters = x.GetParameters() - }); - var constructor = constructorParameters.First(x => x.Parameters.Length == 1 && x.Parameters.Single().ParameterType == typeof(LuceneVersion)).Constructor; - LuceneAnalyzer = (Analyzer)constructor.Invoke([matchVersion]); - - LuceneIndexingStrategyType = strategy; - - string indexStoragePath = Path.Combine(Environment.CurrentDirectory, "App_Data", "LuceneSearch", indexConfiguration.IndexName); - StorageContext = new IndexStorageContext(new GenerationStorageStrategy(), indexStoragePath, new IndexRetentionPolicy(4)); - } -} +using Lucene.Net.Analysis; +using Lucene.Net.Analysis.Standard; +using Lucene.Net.Util; + +namespace Kentico.Xperience.Lucene.Core.Indexing; + +/// +/// Represents the configuration of an Lucene index. +/// +public sealed class LuceneIndex +{ + /// + /// An arbitrary ID used to identify the Lucene index in the admin UI. + /// + public int Identifier { get; set; } + + /// + /// The code name of the Lucene index. + /// + public string IndexName { get; } + + /// + /// The Name of the WebSiteChannel. + /// + public string WebSiteChannelName { get; } + + /// + /// The Language used on the WebSite on the Channel which is indexed. + /// + public List LanguageNames { get; } + + /// + /// Lucene Analyzer used for indexing. + /// + public Analyzer LuceneAnalyzer { get; } + + /// + /// The type of the class which extends . + /// + public Type LuceneIndexingStrategyType { get; } + + /// + /// Index storage context, employs picked storage strategy + /// + public IndexStorageContext StorageContext { get; } + + internal IEnumerable IncludedPaths { get; set; } + + internal LuceneIndex(LuceneIndexModel indexConfiguration, Dictionary strategies, Dictionary analyzers, LuceneVersion matchVersion) + { + Identifier = indexConfiguration.Id; + IndexName = indexConfiguration.IndexName; + WebSiteChannelName = indexConfiguration.ChannelName; + LanguageNames = indexConfiguration.LanguageNames.ToList(); + IncludedPaths = indexConfiguration.Paths; + + var strategy = typeof(DefaultLuceneIndexingStrategy); + + if (strategies.ContainsKey(indexConfiguration.StrategyName)) + { + strategy = strategies[indexConfiguration.StrategyName]; + } + + var analyzerType = typeof(StandardAnalyzer); + + if (analyzers.ContainsKey(indexConfiguration.AnalyzerName)) + { + analyzerType = analyzers[indexConfiguration.AnalyzerName]; + } + + var constructorParameters = analyzerType.GetConstructors().Select(x => new + { + Constructor = x, + Parameters = x.GetParameters() + }); + var constructor = constructorParameters.First(x => x.Parameters.Length == 1 && x.Parameters.Single().ParameterType == typeof(LuceneVersion)).Constructor; + LuceneAnalyzer = (Analyzer)constructor.Invoke([matchVersion]); + + LuceneIndexingStrategyType = strategy; + + string indexStoragePath = Path.Combine(Environment.CurrentDirectory, "App_Data", "LuceneSearch", indexConfiguration.IndexName); + StorageContext = new IndexStorageContext(new GenerationStorageStrategy(), indexStoragePath, new IndexRetentionPolicy(4)); + } +} diff --git a/src/Kentico.Xperience.Lucene.Core/Indexing/LuceneIndexIncludedPath.cs b/src/Kentico.Xperience.Lucene.Core/Indexing/LuceneIndexIncludedPath.cs index 245ecf53..4003a585 100644 --- a/src/Kentico.Xperience.Lucene.Core/Indexing/LuceneIndexIncludedPath.cs +++ b/src/Kentico.Xperience.Lucene.Core/Indexing/LuceneIndexIncludedPath.cs @@ -1,37 +1,37 @@ -using System.Text.Json.Serialization; - -namespace Kentico.Xperience.Lucene.Core.Indexing; - -public class LuceneIndexIncludedPath -{ - /// - /// 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 int? Identifier { get; set; } - - [JsonConstructor] - public LuceneIndexIncludedPath(string aliasPath) => AliasPath = aliasPath; - - /// - /// - /// - /// - /// - public LuceneIndexIncludedPath(LuceneIncludedPathItemInfo indexPath, IEnumerable contentTypes) - { - AliasPath = indexPath.LuceneIncludedPathItemAliasPath; - ContentTypes = contentTypes.ToList(); - Identifier = indexPath.LuceneIncludedPathItemId; - } -} +using System.Text.Json.Serialization; + +namespace Kentico.Xperience.Lucene.Core.Indexing; + +public class LuceneIndexIncludedPath +{ + /// + /// 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 int? Identifier { get; set; } + + [JsonConstructor] + public LuceneIndexIncludedPath(string aliasPath) => AliasPath = aliasPath; + + /// + /// + /// + /// + /// + public LuceneIndexIncludedPath(LuceneIncludedPathItemInfo indexPath, IEnumerable contentTypes) + { + AliasPath = indexPath.LuceneIncludedPathItemAliasPath; + ContentTypes = contentTypes.ToList(); + Identifier = indexPath.LuceneIncludedPathItemId; + } +} diff --git a/src/Kentico.Xperience.Lucene.Core/Indexing/LuceneIndexModel.cs b/src/Kentico.Xperience.Lucene.Core/Indexing/LuceneIndexModel.cs index 3915b353..bd9c4bc3 100644 --- a/src/Kentico.Xperience.Lucene.Core/Indexing/LuceneIndexModel.cs +++ b/src/Kentico.Xperience.Lucene.Core/Indexing/LuceneIndexModel.cs @@ -1,47 +1,47 @@ -namespace Kentico.Xperience.Lucene.Core.Indexing; - -public class LuceneIndexModel -{ - public int Id { get; set; } - - public string IndexName { get; set; } = ""; - - public IEnumerable LanguageNames { get; set; } = Enumerable.Empty(); - - public string ChannelName { get; set; } = ""; - - public string StrategyName { get; set; } = ""; - - public string AnalyzerName { get; set; } = ""; - - public string RebuildHook { get; set; } = ""; - - public IEnumerable Paths { get; set; } = Enumerable.Empty(); - - public LuceneIndexModel() { } - - public LuceneIndexModel( - LuceneIndexItemInfo index, - IEnumerable indexLanguages, - IEnumerable indexPaths, - IEnumerable contentTypes - ) - { - Id = index.LuceneIndexItemId; - IndexName = index.LuceneIndexItemIndexName; - ChannelName = index.LuceneIndexItemChannelName; - RebuildHook = index.LuceneIndexItemRebuildHook; - StrategyName = index.LuceneIndexItemStrategyName; - AnalyzerName = index.LuceneIndexItemAnalyzerName; - LanguageNames = indexLanguages - .Where(l => l.LuceneIndexLanguageItemIndexItemId == index.LuceneIndexItemId) - .Select(l => l.LuceneIndexLanguageItemName) - .ToList(); - Paths = indexPaths - .Where(p => p.LuceneIncludedPathItemIndexItemId == index.LuceneIndexItemId) - .Select(p => new LuceneIndexIncludedPath(p, - contentTypes.Where(x => x.LucenePathItemId == p.LuceneIncludedPathItemId)) - ) - .ToList(); - } -} +namespace Kentico.Xperience.Lucene.Core.Indexing; + +public class LuceneIndexModel +{ + public int Id { get; set; } + + public string IndexName { get; set; } = ""; + + public IEnumerable LanguageNames { get; set; } = Enumerable.Empty(); + + public string ChannelName { get; set; } = ""; + + public string StrategyName { get; set; } = ""; + + public string AnalyzerName { get; set; } = ""; + + public string RebuildHook { get; set; } = ""; + + public IEnumerable Paths { get; set; } = Enumerable.Empty(); + + public LuceneIndexModel() { } + + public LuceneIndexModel( + LuceneIndexItemInfo index, + IEnumerable indexLanguages, + IEnumerable indexPaths, + IEnumerable contentTypes + ) + { + Id = index.LuceneIndexItemId; + IndexName = index.LuceneIndexItemIndexName; + ChannelName = index.LuceneIndexItemChannelName; + RebuildHook = index.LuceneIndexItemRebuildHook; + StrategyName = index.LuceneIndexItemStrategyName; + AnalyzerName = index.LuceneIndexItemAnalyzerName; + LanguageNames = indexLanguages + .Where(l => l.LuceneIndexLanguageItemIndexItemId == index.LuceneIndexItemId) + .Select(l => l.LuceneIndexLanguageItemName) + .ToList(); + Paths = indexPaths + .Where(p => p.LuceneIncludedPathItemIndexItemId == index.LuceneIndexItemId) + .Select(p => new LuceneIndexIncludedPath(p, + contentTypes.Where(x => x.LucenePathItemId == p.LuceneIncludedPathItemId)) + ) + .ToList(); + } +} diff --git a/src/Kentico.Xperience.Lucene.Core/Indexing/LuceneIndexStatisticsModel.cs b/src/Kentico.Xperience.Lucene.Core/Indexing/LuceneIndexStatisticsModel.cs index 15c420d7..a7646747 100644 --- a/src/Kentico.Xperience.Lucene.Core/Indexing/LuceneIndexStatisticsModel.cs +++ b/src/Kentico.Xperience.Lucene.Core/Indexing/LuceneIndexStatisticsModel.cs @@ -1,20 +1,20 @@ -namespace Kentico.Xperience.Lucene.Core.Indexing; - -public class LuceneIndexStatisticsModel -{ - // - // Summary: - // Index name. - public string? Name { get; set; } - - // - // Summary: - // Date of last update. - public DateTime UpdatedAt { get; set; } - - // - // Summary: - // Number of records contained in the index - public int Entries { get; set; } - -} +namespace Kentico.Xperience.Lucene.Core.Indexing; + +public class LuceneIndexStatisticsModel +{ + // + // Summary: + // Index name. + public string? Name { get; set; } + + // + // Summary: + // Date of last update. + public DateTime UpdatedAt { get; set; } + + // + // Summary: + // Number of records contained in the index + public int Entries { get; set; } + +} diff --git a/src/Kentico.Xperience.Lucene.Core/Indexing/LuceneQueueItem.cs b/src/Kentico.Xperience.Lucene.Core/Indexing/LuceneQueueItem.cs index 5f8f71fc..444bab3d 100644 --- a/src/Kentico.Xperience.Lucene.Core/Indexing/LuceneQueueItem.cs +++ b/src/Kentico.Xperience.Lucene.Core/Indexing/LuceneQueueItem.cs @@ -1,46 +1,46 @@ -namespace Kentico.Xperience.Lucene.Core.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 LuceneQueueItem -{ - /// - /// The that was changed. - /// - public IIndexEventItemModel ItemToIndex { get; } - - /// - /// The type of the Lucene task. - /// - public LuceneTaskType TaskType { get; } - - /// - /// The code name of the Lucene index to be updated. - /// - public string IndexName { get; } - - /// - /// Initializes a new instance of the class. - /// - /// The that was changed. - /// The type of the Lucene task. - /// The code name of the Lucene index to be updated. - /// - public LuceneQueueItem(IIndexEventItemModel itemToIndex, LuceneTaskType taskType, string indexName) - { - if (string.IsNullOrEmpty(indexName)) - { - throw new ArgumentNullException(nameof(indexName)); - } - if (taskType != LuceneTaskType.PUBLISH_INDEX && itemToIndex == null) - { - throw new ArgumentNullException(nameof(itemToIndex)); - } - - ItemToIndex = itemToIndex; - TaskType = taskType; - IndexName = indexName; - } -} +namespace Kentico.Xperience.Lucene.Core.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 LuceneQueueItem +{ + /// + /// The that was changed. + /// + public IIndexEventItemModel ItemToIndex { get; } + + /// + /// The type of the Lucene task. + /// + public LuceneTaskType TaskType { get; } + + /// + /// The code name of the Lucene index to be updated. + /// + public string IndexName { get; } + + /// + /// Initializes a new instance of the class. + /// + /// The that was changed. + /// The type of the Lucene task. + /// The code name of the Lucene index to be updated. + /// + public LuceneQueueItem(IIndexEventItemModel itemToIndex, LuceneTaskType taskType, string indexName) + { + if (string.IsNullOrEmpty(indexName)) + { + throw new ArgumentNullException(nameof(indexName)); + } + if (taskType != LuceneTaskType.PUBLISH_INDEX && itemToIndex == null) + { + throw new ArgumentNullException(nameof(itemToIndex)); + } + + ItemToIndex = itemToIndex; + TaskType = taskType; + IndexName = indexName; + } +} diff --git a/src/Kentico.Xperience.Lucene.Core/Indexing/LuceneTaskType.cs b/src/Kentico.Xperience.Lucene.Core/Indexing/LuceneTaskType.cs index 276ad36d..7469cda9 100644 --- a/src/Kentico.Xperience.Lucene.Core/Indexing/LuceneTaskType.cs +++ b/src/Kentico.Xperience.Lucene.Core/Indexing/LuceneTaskType.cs @@ -1,26 +1,26 @@ -using Kentico.Xperience.Lucene.Core.Indexing; - -namespace Kentico.Xperience.Lucene; - -/// -/// Represents the type of a . -/// -public enum LuceneTaskType -{ - /// - /// 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, - - UPDATE -} +using Kentico.Xperience.Lucene.Core.Indexing; + +namespace Kentico.Xperience.Lucene; + +/// +/// Represents the type of a . +/// +public enum LuceneTaskType +{ + /// + /// 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, + + UPDATE +} diff --git a/src/Kentico.Xperience.Lucene.Core/Indexing/StrategyStorage.cs b/src/Kentico.Xperience.Lucene.Core/Indexing/StrategyStorage.cs index ec8b567b..0b3f63d4 100644 --- a/src/Kentico.Xperience.Lucene.Core/Indexing/StrategyStorage.cs +++ b/src/Kentico.Xperience.Lucene.Core/Indexing/StrategyStorage.cs @@ -1,13 +1,13 @@ -namespace Kentico.Xperience.Lucene.Core.Indexing; - -internal static class StrategyStorage -{ - public static Dictionary Strategies { get; private set; } - static StrategyStorage() => Strategies = []; - - public static void AddStrategy(string strategyName) where TStrategy : ILuceneIndexingStrategy => Strategies.Add(strategyName, typeof(TStrategy)); - public static Type GetOrDefault(string strategyName) => - Strategies.TryGetValue(strategyName, out var type) - ? type - : typeof(DefaultLuceneIndexingStrategy); -} +namespace Kentico.Xperience.Lucene.Core.Indexing; + +internal static class StrategyStorage +{ + public static Dictionary Strategies { get; private set; } + static StrategyStorage() => Strategies = []; + + public static void AddStrategy(string strategyName) where TStrategy : ILuceneIndexingStrategy => Strategies.Add(strategyName, typeof(TStrategy)); + public static Type GetOrDefault(string strategyName) => + Strategies.TryGetValue(strategyName, out var type) + ? type + : typeof(DefaultLuceneIndexingStrategy); +} diff --git a/src/Kentico.Xperience.Lucene.Core/InfoModels/LuceneContentTypeItem/ILuceneContentTypeItemInfoProvider.cs b/src/Kentico.Xperience.Lucene.Core/InfoModels/LuceneContentTypeItem/ILuceneContentTypeItemInfoProvider.cs index 4ed616ae..af32c972 100644 --- a/src/Kentico.Xperience.Lucene.Core/InfoModels/LuceneContentTypeItem/ILuceneContentTypeItemInfoProvider.cs +++ b/src/Kentico.Xperience.Lucene.Core/InfoModels/LuceneContentTypeItem/ILuceneContentTypeItemInfoProvider.cs @@ -1,11 +1,11 @@ -using CMS.DataEngine; - -namespace Kentico.Xperience.Lucene.Core; - -/// -/// Declares members for management. -/// -public partial interface ILuceneContentTypeItemInfoProvider -{ - void BulkDelete(IWhereCondition where, BulkDeleteSettings? settings = null); -} +using CMS.DataEngine; + +namespace Kentico.Xperience.Lucene.Core; + +/// +/// Declares members for management. +/// +public partial interface ILuceneContentTypeItemInfoProvider +{ + void BulkDelete(IWhereCondition where, BulkDeleteSettings? settings = null); +} diff --git a/src/Kentico.Xperience.Lucene.Core/InfoModels/LuceneIncludedPathItem/ILuceneIncludedPathItemInfoProvider.cs b/src/Kentico.Xperience.Lucene.Core/InfoModels/LuceneIncludedPathItem/ILuceneIncludedPathItemInfoProvider.cs index fa055dfa..124d3a54 100644 --- a/src/Kentico.Xperience.Lucene.Core/InfoModels/LuceneIncludedPathItem/ILuceneIncludedPathItemInfoProvider.cs +++ b/src/Kentico.Xperience.Lucene.Core/InfoModels/LuceneIncludedPathItem/ILuceneIncludedPathItemInfoProvider.cs @@ -1,8 +1,8 @@ -using CMS.DataEngine; - -namespace Kentico.Xperience.Lucene.Core; - -public partial interface ILuceneIncludedPathItemInfoProvider -{ - void BulkDelete(IWhereCondition where, BulkDeleteSettings? settings = null); -} +using CMS.DataEngine; + +namespace Kentico.Xperience.Lucene.Core; + +public partial interface ILuceneIncludedPathItemInfoProvider +{ + void BulkDelete(IWhereCondition where, BulkDeleteSettings? settings = null); +} diff --git a/src/Kentico.Xperience.Lucene.Core/InfoModels/LuceneIndexItem/ILuceneIndexItemInfoProvider.cs b/src/Kentico.Xperience.Lucene.Core/InfoModels/LuceneIndexItem/ILuceneIndexItemInfoProvider.cs index 94194c5a..4c7ecb2f 100644 --- a/src/Kentico.Xperience.Lucene.Core/InfoModels/LuceneIndexItem/ILuceneIndexItemInfoProvider.cs +++ b/src/Kentico.Xperience.Lucene.Core/InfoModels/LuceneIndexItem/ILuceneIndexItemInfoProvider.cs @@ -1,8 +1,8 @@ -using CMS.DataEngine; - -namespace Kentico.Xperience.Lucene.Core; - -public partial interface ILuceneIndexItemInfoProvider -{ - void BulkDelete(IWhereCondition where, BulkDeleteSettings? settings = null); -} +using CMS.DataEngine; + +namespace Kentico.Xperience.Lucene.Core; + +public partial interface ILuceneIndexItemInfoProvider +{ + void BulkDelete(IWhereCondition where, BulkDeleteSettings? settings = null); +} diff --git a/src/Kentico.Xperience.Lucene.Core/InfoModels/LuceneIndexLanguageItem/ILuceneIndexLanguageItemInfoProvider.cs b/src/Kentico.Xperience.Lucene.Core/InfoModels/LuceneIndexLanguageItem/ILuceneIndexLanguageItemInfoProvider.cs index 4714da71..0578261e 100644 --- a/src/Kentico.Xperience.Lucene.Core/InfoModels/LuceneIndexLanguageItem/ILuceneIndexLanguageItemInfoProvider.cs +++ b/src/Kentico.Xperience.Lucene.Core/InfoModels/LuceneIndexLanguageItem/ILuceneIndexLanguageItemInfoProvider.cs @@ -1,11 +1,11 @@ -using CMS.DataEngine; - -namespace Kentico.Xperience.Lucene.Core; - -/// -/// Declares members for management. -/// -public partial interface ILuceneIndexLanguageItemInfoProvider -{ - void BulkDelete(IWhereCondition where, BulkDeleteSettings? settings = null); -} +using CMS.DataEngine; + +namespace Kentico.Xperience.Lucene.Core; + +/// +/// Declares members for management. +/// +public partial interface ILuceneIndexLanguageItemInfoProvider +{ + void BulkDelete(IWhereCondition where, BulkDeleteSettings? settings = null); +} diff --git a/src/Kentico.Xperience.Lucene.Core/LuceneModuleInstaller.cs b/src/Kentico.Xperience.Lucene.Core/LuceneModuleInstaller.cs index 16af4d77..5beff1b7 100644 --- a/src/Kentico.Xperience.Lucene.Core/LuceneModuleInstaller.cs +++ b/src/Kentico.Xperience.Lucene.Core/LuceneModuleInstaller.cs @@ -1,335 +1,335 @@ -using CMS.DataEngine; -using CMS.FormEngine; -using CMS.Modules; - -namespace Kentico.Xperience.Lucene.Core; - -public class LuceneModuleInstaller(IInfoProvider resourceProvider) -{ - private readonly IInfoProvider resourceProvider = resourceProvider; - - public void Install() - { - var resource = resourceProvider.Get("CMS.Integration.Lucene") - // Handle v4.0.0 resource name manually until migrations are enabled - ?? resourceProvider.Get("Kentico.Xperience.Lucene") - ?? new ResourceInfo(); - - InitializeResource(resource); - InstallLuceneItemInfo(resource); - InstallLuceneLanguageInfo(resource); - InstallLuceneIndexPathItemInfo(resource); - InstallLuceneContentTypeItemInfo(resource); - } - - public ResourceInfo InitializeResource(ResourceInfo resource) - { - resource.ResourceDisplayName = "Kentico Integration - Lucene"; - - // Prefix ResourceName with "CMS" to prevent C# class generation - // Classes are already available through the library itself - resource.ResourceName = "CMS.Integration.Lucene"; - resource.ResourceDescription = "Kentico Lucene custom data"; - resource.ResourceIsInDevelopment = false; - if (resource.HasChanged) - { - resourceProvider.Set(resource); - } - - return resource; - } - - public void InstallLuceneItemInfo(ResourceInfo resource) - { - var info = DataClassInfoProvider.GetDataClassInfo(LuceneIndexItemInfo.OBJECT_TYPE) ?? DataClassInfo.New(LuceneIndexItemInfo.OBJECT_TYPE); - - info.ClassName = LuceneIndexItemInfo.TYPEINFO.ObjectClassName; - info.ClassTableName = LuceneIndexItemInfo.TYPEINFO.ObjectClassName.Replace(".", "_"); - info.ClassDisplayName = "Lucene Index Item"; - info.ClassType = ClassType.OTHER; - info.ClassResourceID = resource.ResourceID; - - var formInfo = FormHelper.GetBasicFormDefinition(nameof(LuceneIndexItemInfo.LuceneIndexItemId)); - - var formItem = new FormFieldInfo - { - Name = nameof(LuceneIndexItemInfo.LuceneIndexItemGuid), - AllowEmpty = false, - Visible = true, - Precision = 0, - DataType = "guid", - Enabled = true, - }; - formInfo.AddFormItem(formItem); - - formItem = new FormFieldInfo - { - Name = nameof(LuceneIndexItemInfo.LuceneIndexItemIndexName), - AllowEmpty = false, - Visible = true, - Precision = 0, - Size = 100, - DataType = "text", - Enabled = true - }; - formInfo.AddFormItem(formItem); - - formItem = new FormFieldInfo - { - Name = nameof(LuceneIndexItemInfo.LuceneIndexItemChannelName), - AllowEmpty = false, - Visible = true, - Precision = 0, - Size = 100, - DataType = "text", - Enabled = true - }; - formInfo.AddFormItem(formItem); - - formItem = new FormFieldInfo - { - Name = nameof(LuceneIndexItemInfo.LuceneIndexItemStrategyName), - AllowEmpty = false, - Visible = true, - Precision = 0, - Size = 100, - DataType = "text", - Enabled = true - }; - formInfo.AddFormItem(formItem); - - formItem = new FormFieldInfo - { - Name = nameof(LuceneIndexItemInfo.LuceneIndexItemAnalyzerName), - AllowEmpty = false, - Visible = true, - Precision = 0, - Size = 100, - DataType = "text", - Enabled = true - }; - formInfo.AddFormItem(formItem); - - formItem = new FormFieldInfo - { - Name = nameof(LuceneIndexItemInfo.LuceneIndexItemRebuildHook), - AllowEmpty = true, - Visible = true, - Precision = 0, - Size = 100, - DataType = "text", - Enabled = true - }; - formInfo.AddFormItem(formItem); - - SetFormDefinition(info, formInfo); - - if (info.HasChanged) - { - DataClassInfoProvider.SetDataClassInfo(info); - } - } - - public void InstallLuceneIndexPathItemInfo(ResourceInfo resource) - { - var info = DataClassInfoProvider.GetDataClassInfo(LuceneIncludedPathItemInfo.OBJECT_TYPE) ?? DataClassInfo.New(LuceneIncludedPathItemInfo.OBJECT_TYPE); - - info.ClassName = LuceneIncludedPathItemInfo.TYPEINFO.ObjectClassName; - info.ClassTableName = LuceneIncludedPathItemInfo.TYPEINFO.ObjectClassName.Replace(".", "_"); - info.ClassDisplayName = "Lucene Path Item"; - info.ClassType = ClassType.OTHER; - info.ClassResourceID = resource.ResourceID; - - var formInfo = FormHelper.GetBasicFormDefinition(nameof(LuceneIncludedPathItemInfo.LuceneIncludedPathItemId)); - - var formItem = new FormFieldInfo - { - Name = nameof(LuceneIncludedPathItemInfo.LuceneIncludedPathItemGuid), - AllowEmpty = false, - Visible = true, - Precision = 0, - DataType = "guid", - Enabled = true, - }; - formInfo.AddFormItem(formItem); - - formItem = new FormFieldInfo - { - Name = nameof(LuceneIncludedPathItemInfo.LuceneIncludedPathItemAliasPath), - AllowEmpty = false, - Visible = true, - Precision = 0, - Size = 100, - DataType = "text", - Enabled = true, - }; - formInfo.AddFormItem(formItem); - - formItem = new FormFieldInfo - { - Name = nameof(LuceneIncludedPathItemInfo.LuceneIncludedPathItemIndexItemId), - AllowEmpty = false, - Visible = true, - Precision = 0, - DataType = "integer", - ReferenceToObjectType = LuceneIndexItemInfo.OBJECT_TYPE, - ReferenceType = ObjectDependencyEnum.Required - }; - - formInfo.AddFormItem(formItem); - - SetFormDefinition(info, formInfo); - - if (info.HasChanged) - { - DataClassInfoProvider.SetDataClassInfo(info); - } - } - - public void InstallLuceneLanguageInfo(ResourceInfo resource) - { - var info = DataClassInfoProvider.GetDataClassInfo(LuceneIndexLanguageItemInfo.OBJECT_TYPE) ?? DataClassInfo.New(LuceneIndexLanguageItemInfo.OBJECT_TYPE); - - info.ClassName = LuceneIndexLanguageItemInfo.TYPEINFO.ObjectClassName; - info.ClassTableName = LuceneIndexLanguageItemInfo.TYPEINFO.ObjectClassName.Replace(".", "_"); - info.ClassDisplayName = "Lucene Indexed Language Item"; - info.ClassType = ClassType.OTHER; - info.ClassResourceID = resource.ResourceID; - - var formInfo = FormHelper.GetBasicFormDefinition(nameof(LuceneIndexLanguageItemInfo.LuceneIndexLanguageItemID)); - - var formItem = new FormFieldInfo - { - Name = nameof(LuceneIndexLanguageItemInfo.LuceneIndexLanguageItemName), - AllowEmpty = false, - Visible = true, - Precision = 0, - Size = 100, - DataType = "text", - Enabled = true, - }; - formInfo.AddFormItem(formItem); - - formItem = new FormFieldInfo - { - Name = nameof(LuceneIndexLanguageItemInfo.LuceneIndexLanguageItemGuid), - AllowEmpty = false, - Visible = true, - Precision = 0, - DataType = "guid", - Enabled = true - }; - - formInfo.AddFormItem(formItem); - - formItem = new FormFieldInfo - { - Name = nameof(LuceneIndexLanguageItemInfo.LuceneIndexLanguageItemIndexItemId), - AllowEmpty = false, - Visible = true, - Precision = 0, - DataType = "integer", - ReferenceToObjectType = LuceneIndexItemInfo.OBJECT_TYPE, - ReferenceType = ObjectDependencyEnum.Required, - }; - - formInfo.AddFormItem(formItem); - - SetFormDefinition(info, formInfo); - - if (info.HasChanged) - { - DataClassInfoProvider.SetDataClassInfo(info); - } - } - - public void InstallLuceneContentTypeItemInfo(ResourceInfo resource) - { - var info = DataClassInfoProvider.GetDataClassInfo(LuceneContentTypeItemInfo.OBJECT_TYPE) ?? DataClassInfo.New(LuceneContentTypeItemInfo.OBJECT_TYPE); - - info.ClassName = LuceneContentTypeItemInfo.TYPEINFO.ObjectClassName; - info.ClassTableName = LuceneContentTypeItemInfo.TYPEINFO.ObjectClassName.Replace(".", "_"); - info.ClassDisplayName = "Lucene Type Item"; - info.ClassType = ClassType.OTHER; - info.ClassResourceID = resource.ResourceID; - - var formInfo = FormHelper.GetBasicFormDefinition(nameof(LuceneContentTypeItemInfo.LuceneContentTypeItemId)); - - var formItem = new FormFieldInfo - { - Name = nameof(LuceneContentTypeItemInfo.LuceneContentTypeItemContentTypeName), - AllowEmpty = false, - Visible = true, - Precision = 0, - Size = 100, - DataType = "text", - Enabled = true, - IsUnique = false - }; - formInfo.AddFormItem(formItem); - - formItem = new FormFieldInfo - { - Name = nameof(LuceneContentTypeItemInfo.LuceneContentTypeItemIncludedPathItemId), - AllowEmpty = false, - Visible = true, - Precision = 0, - DataType = "integer", - ReferenceToObjectType = LuceneIncludedPathItemInfo.OBJECT_TYPE, - ReferenceType = ObjectDependencyEnum.Required, - }; - - formInfo.AddFormItem(formItem); - - formItem = new FormFieldInfo - { - Name = nameof(LuceneContentTypeItemInfo.LuceneContentTypeItemGuid), - Enabled = true, - AllowEmpty = false, - Visible = true, - Precision = 0, - DataType = "guid", - }; - - formInfo.AddFormItem(formItem); - - formItem = new FormFieldInfo - { - Name = nameof(LuceneContentTypeItemInfo.LuceneContentTypeItemIndexItemId), - AllowEmpty = false, - Visible = true, - Precision = 0, - DataType = "integer", - ReferenceToObjectType = LuceneIndexItemInfo.OBJECT_TYPE, - ReferenceType = ObjectDependencyEnum.Required - }; - - formInfo.AddFormItem(formItem); - - SetFormDefinition(info, formInfo); - - if (info.HasChanged) - { - DataClassInfoProvider.SetDataClassInfo(info); - } - } - - /// - /// Ensure that the form is upserted with any existing form - /// - /// - /// - private static void SetFormDefinition(DataClassInfo info, FormInfo form) - { - if (info.ClassID > 0) - { - var existingForm = new FormInfo(info.ClassFormDefinition); - existingForm.CombineWithForm(form, new()); - info.ClassFormDefinition = existingForm.GetXmlDefinition(); - } - else - { - info.ClassFormDefinition = form.GetXmlDefinition(); - } - } -} +using CMS.DataEngine; +using CMS.FormEngine; +using CMS.Modules; + +namespace Kentico.Xperience.Lucene.Core; + +public class LuceneModuleInstaller(IInfoProvider resourceProvider) +{ + private readonly IInfoProvider resourceProvider = resourceProvider; + + public void Install() + { + var resource = resourceProvider.Get("CMS.Integration.Lucene") + // Handle v4.0.0 resource name manually until migrations are enabled + ?? resourceProvider.Get("Kentico.Xperience.Lucene") + ?? new ResourceInfo(); + + InitializeResource(resource); + InstallLuceneItemInfo(resource); + InstallLuceneLanguageInfo(resource); + InstallLuceneIndexPathItemInfo(resource); + InstallLuceneContentTypeItemInfo(resource); + } + + public ResourceInfo InitializeResource(ResourceInfo resource) + { + resource.ResourceDisplayName = "Kentico Integration - Lucene"; + + // Prefix ResourceName with "CMS" to prevent C# class generation + // Classes are already available through the library itself + resource.ResourceName = "CMS.Integration.Lucene"; + resource.ResourceDescription = "Kentico Lucene custom data"; + resource.ResourceIsInDevelopment = false; + if (resource.HasChanged) + { + resourceProvider.Set(resource); + } + + return resource; + } + + public void InstallLuceneItemInfo(ResourceInfo resource) + { + var info = DataClassInfoProvider.GetDataClassInfo(LuceneIndexItemInfo.OBJECT_TYPE) ?? DataClassInfo.New(LuceneIndexItemInfo.OBJECT_TYPE); + + info.ClassName = LuceneIndexItemInfo.TYPEINFO.ObjectClassName; + info.ClassTableName = LuceneIndexItemInfo.TYPEINFO.ObjectClassName.Replace(".", "_"); + info.ClassDisplayName = "Lucene Index Item"; + info.ClassType = ClassType.OTHER; + info.ClassResourceID = resource.ResourceID; + + var formInfo = FormHelper.GetBasicFormDefinition(nameof(LuceneIndexItemInfo.LuceneIndexItemId)); + + var formItem = new FormFieldInfo + { + Name = nameof(LuceneIndexItemInfo.LuceneIndexItemGuid), + AllowEmpty = false, + Visible = true, + Precision = 0, + DataType = "guid", + Enabled = true, + }; + formInfo.AddFormItem(formItem); + + formItem = new FormFieldInfo + { + Name = nameof(LuceneIndexItemInfo.LuceneIndexItemIndexName), + AllowEmpty = false, + Visible = true, + Precision = 0, + Size = 100, + DataType = "text", + Enabled = true + }; + formInfo.AddFormItem(formItem); + + formItem = new FormFieldInfo + { + Name = nameof(LuceneIndexItemInfo.LuceneIndexItemChannelName), + AllowEmpty = false, + Visible = true, + Precision = 0, + Size = 100, + DataType = "text", + Enabled = true + }; + formInfo.AddFormItem(formItem); + + formItem = new FormFieldInfo + { + Name = nameof(LuceneIndexItemInfo.LuceneIndexItemStrategyName), + AllowEmpty = false, + Visible = true, + Precision = 0, + Size = 100, + DataType = "text", + Enabled = true + }; + formInfo.AddFormItem(formItem); + + formItem = new FormFieldInfo + { + Name = nameof(LuceneIndexItemInfo.LuceneIndexItemAnalyzerName), + AllowEmpty = false, + Visible = true, + Precision = 0, + Size = 100, + DataType = "text", + Enabled = true + }; + formInfo.AddFormItem(formItem); + + formItem = new FormFieldInfo + { + Name = nameof(LuceneIndexItemInfo.LuceneIndexItemRebuildHook), + AllowEmpty = true, + Visible = true, + Precision = 0, + Size = 100, + DataType = "text", + Enabled = true + }; + formInfo.AddFormItem(formItem); + + SetFormDefinition(info, formInfo); + + if (info.HasChanged) + { + DataClassInfoProvider.SetDataClassInfo(info); + } + } + + public void InstallLuceneIndexPathItemInfo(ResourceInfo resource) + { + var info = DataClassInfoProvider.GetDataClassInfo(LuceneIncludedPathItemInfo.OBJECT_TYPE) ?? DataClassInfo.New(LuceneIncludedPathItemInfo.OBJECT_TYPE); + + info.ClassName = LuceneIncludedPathItemInfo.TYPEINFO.ObjectClassName; + info.ClassTableName = LuceneIncludedPathItemInfo.TYPEINFO.ObjectClassName.Replace(".", "_"); + info.ClassDisplayName = "Lucene Path Item"; + info.ClassType = ClassType.OTHER; + info.ClassResourceID = resource.ResourceID; + + var formInfo = FormHelper.GetBasicFormDefinition(nameof(LuceneIncludedPathItemInfo.LuceneIncludedPathItemId)); + + var formItem = new FormFieldInfo + { + Name = nameof(LuceneIncludedPathItemInfo.LuceneIncludedPathItemGuid), + AllowEmpty = false, + Visible = true, + Precision = 0, + DataType = "guid", + Enabled = true, + }; + formInfo.AddFormItem(formItem); + + formItem = new FormFieldInfo + { + Name = nameof(LuceneIncludedPathItemInfo.LuceneIncludedPathItemAliasPath), + AllowEmpty = false, + Visible = true, + Precision = 0, + Size = 100, + DataType = "text", + Enabled = true, + }; + formInfo.AddFormItem(formItem); + + formItem = new FormFieldInfo + { + Name = nameof(LuceneIncludedPathItemInfo.LuceneIncludedPathItemIndexItemId), + AllowEmpty = false, + Visible = true, + Precision = 0, + DataType = "integer", + ReferenceToObjectType = LuceneIndexItemInfo.OBJECT_TYPE, + ReferenceType = ObjectDependencyEnum.Required + }; + + formInfo.AddFormItem(formItem); + + SetFormDefinition(info, formInfo); + + if (info.HasChanged) + { + DataClassInfoProvider.SetDataClassInfo(info); + } + } + + public void InstallLuceneLanguageInfo(ResourceInfo resource) + { + var info = DataClassInfoProvider.GetDataClassInfo(LuceneIndexLanguageItemInfo.OBJECT_TYPE) ?? DataClassInfo.New(LuceneIndexLanguageItemInfo.OBJECT_TYPE); + + info.ClassName = LuceneIndexLanguageItemInfo.TYPEINFO.ObjectClassName; + info.ClassTableName = LuceneIndexLanguageItemInfo.TYPEINFO.ObjectClassName.Replace(".", "_"); + info.ClassDisplayName = "Lucene Indexed Language Item"; + info.ClassType = ClassType.OTHER; + info.ClassResourceID = resource.ResourceID; + + var formInfo = FormHelper.GetBasicFormDefinition(nameof(LuceneIndexLanguageItemInfo.LuceneIndexLanguageItemID)); + + var formItem = new FormFieldInfo + { + Name = nameof(LuceneIndexLanguageItemInfo.LuceneIndexLanguageItemName), + AllowEmpty = false, + Visible = true, + Precision = 0, + Size = 100, + DataType = "text", + Enabled = true, + }; + formInfo.AddFormItem(formItem); + + formItem = new FormFieldInfo + { + Name = nameof(LuceneIndexLanguageItemInfo.LuceneIndexLanguageItemGuid), + AllowEmpty = false, + Visible = true, + Precision = 0, + DataType = "guid", + Enabled = true + }; + + formInfo.AddFormItem(formItem); + + formItem = new FormFieldInfo + { + Name = nameof(LuceneIndexLanguageItemInfo.LuceneIndexLanguageItemIndexItemId), + AllowEmpty = false, + Visible = true, + Precision = 0, + DataType = "integer", + ReferenceToObjectType = LuceneIndexItemInfo.OBJECT_TYPE, + ReferenceType = ObjectDependencyEnum.Required, + }; + + formInfo.AddFormItem(formItem); + + SetFormDefinition(info, formInfo); + + if (info.HasChanged) + { + DataClassInfoProvider.SetDataClassInfo(info); + } + } + + public void InstallLuceneContentTypeItemInfo(ResourceInfo resource) + { + var info = DataClassInfoProvider.GetDataClassInfo(LuceneContentTypeItemInfo.OBJECT_TYPE) ?? DataClassInfo.New(LuceneContentTypeItemInfo.OBJECT_TYPE); + + info.ClassName = LuceneContentTypeItemInfo.TYPEINFO.ObjectClassName; + info.ClassTableName = LuceneContentTypeItemInfo.TYPEINFO.ObjectClassName.Replace(".", "_"); + info.ClassDisplayName = "Lucene Type Item"; + info.ClassType = ClassType.OTHER; + info.ClassResourceID = resource.ResourceID; + + var formInfo = FormHelper.GetBasicFormDefinition(nameof(LuceneContentTypeItemInfo.LuceneContentTypeItemId)); + + var formItem = new FormFieldInfo + { + Name = nameof(LuceneContentTypeItemInfo.LuceneContentTypeItemContentTypeName), + AllowEmpty = false, + Visible = true, + Precision = 0, + Size = 100, + DataType = "text", + Enabled = true, + IsUnique = false + }; + formInfo.AddFormItem(formItem); + + formItem = new FormFieldInfo + { + Name = nameof(LuceneContentTypeItemInfo.LuceneContentTypeItemIncludedPathItemId), + AllowEmpty = false, + Visible = true, + Precision = 0, + DataType = "integer", + ReferenceToObjectType = LuceneIncludedPathItemInfo.OBJECT_TYPE, + ReferenceType = ObjectDependencyEnum.Required, + }; + + formInfo.AddFormItem(formItem); + + formItem = new FormFieldInfo + { + Name = nameof(LuceneContentTypeItemInfo.LuceneContentTypeItemGuid), + Enabled = true, + AllowEmpty = false, + Visible = true, + Precision = 0, + DataType = "guid", + }; + + formInfo.AddFormItem(formItem); + + formItem = new FormFieldInfo + { + Name = nameof(LuceneContentTypeItemInfo.LuceneContentTypeItemIndexItemId), + AllowEmpty = false, + Visible = true, + Precision = 0, + DataType = "integer", + ReferenceToObjectType = LuceneIndexItemInfo.OBJECT_TYPE, + ReferenceType = ObjectDependencyEnum.Required + }; + + formInfo.AddFormItem(formItem); + + SetFormDefinition(info, formInfo); + + if (info.HasChanged) + { + DataClassInfoProvider.SetDataClassInfo(info); + } + } + + /// + /// Ensure that the form is upserted with any existing form + /// + /// + /// + private static void SetFormDefinition(DataClassInfo info, FormInfo form) + { + if (info.ClassID > 0) + { + var existingForm = new FormInfo(info.ClassFormDefinition); + existingForm.CombineWithForm(form, new()); + info.ClassFormDefinition = existingForm.GetXmlDefinition(); + } + else + { + info.ClassFormDefinition = form.GetXmlDefinition(); + } + } +} diff --git a/src/Kentico.Xperience.Lucene.Core/LuceneQueueWorker.cs b/src/Kentico.Xperience.Lucene.Core/LuceneQueueWorker.cs index 3f4b5d43..f0aa34ea 100644 --- a/src/Kentico.Xperience.Lucene.Core/LuceneQueueWorker.cs +++ b/src/Kentico.Xperience.Lucene.Core/LuceneQueueWorker.cs @@ -1,71 +1,71 @@ -using CMS.Base; -using CMS.Core; - -using Kentico.Xperience.Lucene.Core.Indexing; - -namespace Kentico.Xperience.Lucene.Core; - -/// -/// Thread worker which enqueues recently updated or deleted nodes indexed -/// by Lucene and processes the tasks in the background thread. -/// -internal class LuceneQueueWorker : ThreadQueueWorker -{ - private readonly ILuceneTaskProcessor luceneTaskProcessor; - - - /// - 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 LuceneQueueWorker() => luceneTaskProcessor = Service.Resolve() ?? throw new InvalidOperationException($"{nameof(ILuceneTaskProcessor)} is not registered."); - - - /// - /// Adds an to the worker queue to be processed. - /// - /// The item to be added to the queue. - /// - public static void EnqueueLuceneQueueItem(LuceneQueueItem queueItem) - { - if (queueItem == null || (queueItem.ItemToIndex == null && queueItem.TaskType != LuceneTaskType.PUBLISH_INDEX) || string.IsNullOrEmpty(queueItem.IndexName)) - { - return; - } - - if (queueItem.TaskType == LuceneTaskType.UNKNOWN) - { - return; - } - - var indexManager = Service.Resolve() ?? throw new InvalidOperationException($"{nameof(ILuceneIndexManager)} is not registered."); - - if (indexManager.GetIndex(queueItem.IndexName) == null) - { - throw new InvalidOperationException($"Attempted to log task for Lucene index '{queueItem.IndexName},' but it is not registered."); - } - - Current.Enqueue(queueItem, false); - } - - - /// - protected override void Finish() => RunProcess(); - - - /// - protected override void ProcessItem(LuceneQueueItem item) - { - } - - - /// - protected override int ProcessItems(IEnumerable items) => - luceneTaskProcessor.ProcessLuceneTasks(items, CancellationToken.None).GetAwaiter().GetResult(); - -} +using CMS.Base; +using CMS.Core; + +using Kentico.Xperience.Lucene.Core.Indexing; + +namespace Kentico.Xperience.Lucene.Core; + +/// +/// Thread worker which enqueues recently updated or deleted nodes indexed +/// by Lucene and processes the tasks in the background thread. +/// +internal class LuceneQueueWorker : ThreadQueueWorker +{ + private readonly ILuceneTaskProcessor luceneTaskProcessor; + + + /// + 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 LuceneQueueWorker() => luceneTaskProcessor = Service.Resolve() ?? throw new InvalidOperationException($"{nameof(ILuceneTaskProcessor)} is not registered."); + + + /// + /// Adds an to the worker queue to be processed. + /// + /// The item to be added to the queue. + /// + public static void EnqueueLuceneQueueItem(LuceneQueueItem queueItem) + { + if (queueItem == null || (queueItem.ItemToIndex == null && queueItem.TaskType != LuceneTaskType.PUBLISH_INDEX) || string.IsNullOrEmpty(queueItem.IndexName)) + { + return; + } + + if (queueItem.TaskType == LuceneTaskType.UNKNOWN) + { + return; + } + + var indexManager = Service.Resolve() ?? throw new InvalidOperationException($"{nameof(ILuceneIndexManager)} is not registered."); + + if (indexManager.GetIndex(queueItem.IndexName) == null) + { + throw new InvalidOperationException($"Attempted to log task for Lucene index '{queueItem.IndexName},' but it is not registered."); + } + + Current.Enqueue(queueItem, false); + } + + + /// + protected override void Finish() => RunProcess(); + + + /// + protected override void ProcessItem(LuceneQueueItem item) + { + } + + + /// + protected override int ProcessItems(IEnumerable items) => + luceneTaskProcessor.ProcessLuceneTasks(items, CancellationToken.None).GetAwaiter().GetResult(); + +} diff --git a/src/Kentico.Xperience.Lucene.Core/LuceneStartupExtensions.cs b/src/Kentico.Xperience.Lucene.Core/LuceneStartupExtensions.cs index 029cc70e..021f9248 100644 --- a/src/Kentico.Xperience.Lucene.Core/LuceneStartupExtensions.cs +++ b/src/Kentico.Xperience.Lucene.Core/LuceneStartupExtensions.cs @@ -1,171 +1,171 @@ -using Kentico.Xperience.Lucene.Core; -using Kentico.Xperience.Lucene.Core.Indexing; -using Kentico.Xperience.Lucene.Core.Search; - -using Lucene.Net.Analysis; -using Lucene.Net.Analysis.Standard; -using Lucene.Net.Util; - -namespace Microsoft.Extensions.DependencyInjection; - -public static class LuceneStartupExtensions -{ - /// - /// Adds Lucene services and custom module to application using the and for all indexes - /// - /// the which will be modified - /// Returns this instance of , allowing for further configuration in a fluent manner. - public static IServiceCollection AddKenticoLucene(this IServiceCollection serviceCollection) - { - serviceCollection.AddLuceneServicesInternal(); - - StrategyStorage.AddStrategy("Default"); - AnalyzerStorage.AddAnalyzer("Standard"); - - return serviceCollection; - } - - - /// - /// Adds Lucene services and custom module to application with customized options provided by the - /// in the action. - /// - /// the which will be modified - /// which will configure the - /// Returns this instance of , allowing for further configuration in a fluent manner. - public static IServiceCollection AddKenticoLucene(this IServiceCollection serviceCollection, Action configure) - { - serviceCollection.AddLuceneServicesInternal(); - - var builder = new LuceneBuilder(serviceCollection); - - configure(builder); - - if (builder.IncludeDefaultStrategy) - { - builder.RegisterStrategy("Default"); - } - - if (builder.IncludeDefaultAnalyzer) - { - builder.RegisterAnalyzer("Standard"); - } - - return serviceCollection; - } - - - private static IServiceCollection AddLuceneServicesInternal(this IServiceCollection services) => - services - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddTransient(); -} - - -public interface ILuceneBuilder -{ - /// - /// Registers the given as a transient service under - /// - /// The custom type of - /// Used internally to enable dynamic assignment of strategies to search indexes. Names must be unique. - /// - /// Thrown if a strategy has already been registered with the given - /// - /// Returns this instance of , allowing for further configuration in a fluent manner. - ILuceneBuilder RegisterStrategy(string strategyName) where TStrategy : class, ILuceneIndexingStrategy; - - - /// - /// Registers the given and - /// as a selectable analyzer in the Admin UI - /// - /// The type of - /// Used internally to enable dynamic assignment of analyzers to search indexes. Names must be unique. - /// - /// Thrown if an analyzer has already been registered with the given - /// - /// Returns this instance of , allowing for further configuration in a fluent manner. - ILuceneBuilder RegisterAnalyzer(string analyzerName) where TAnalyzer : Analyzer; - - - /// - /// Sets the lucene version which will be used by for search indexes. - /// Defaults to - /// - /// to be used by the - /// Returns this instance of , allowing for further configuration in a fluent manner. - ILuceneBuilder SetAnalyzerLuceneVersion(LuceneVersion matchVersion); -} - - -internal class LuceneBuilder : ILuceneBuilder -{ - private readonly IServiceCollection serviceCollection; - - /// - /// 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; - - /// - /// If true, the will be available as an explicitly selectable analyzer - /// within the Admin UI. Defaults to true - /// - public bool IncludeDefaultAnalyzer { get; set; } = true; - - public LuceneBuilder(IServiceCollection serviceCollection) => this.serviceCollection = serviceCollection; - - - /// - /// Registers the strategy in DI and - /// as a selectable strategy in the Admin UI - /// - /// The custom type of - /// Used internally to enable dynamic assignment of strategies to search indexes. Names must be unique. - /// Returns this instance of , allowing for further configuration in a fluent manner. - public ILuceneBuilder RegisterStrategy(string strategyName) where TStrategy : class, ILuceneIndexingStrategy - { - StrategyStorage.AddStrategy(strategyName); - serviceCollection.AddTransient(); - - return this; - } - - - /// - /// Registers the analyzer - /// as a selectable analyzer in the Admin UI. When selected this analyzer will be used to process indexed items. - /// - /// The type of - /// Used internally to enable dynamic assignment of analyzers to search indexes. Names must be unique. - /// Returns this instance of , allowing for further configuration in a fluent manner. - public ILuceneBuilder RegisterAnalyzer(string analyzerName) where TAnalyzer : Analyzer - { - AnalyzerStorage.AddAnalyzer(analyzerName); - - return this; - } - - - /// - /// Sets the lucene version which will be used by for indexing. - /// Defaults to - /// - /// to be used by the - /// Returns this instance of , allowing for further configuration in a fluent manner. - public ILuceneBuilder SetAnalyzerLuceneVersion(LuceneVersion matchVersion) - { - AnalyzerStorage.SetAnalyzerLuceneVersion(matchVersion); - - return this; - } -} +using Kentico.Xperience.Lucene.Core; +using Kentico.Xperience.Lucene.Core.Indexing; +using Kentico.Xperience.Lucene.Core.Search; + +using Lucene.Net.Analysis; +using Lucene.Net.Analysis.Standard; +using Lucene.Net.Util; + +namespace Microsoft.Extensions.DependencyInjection; + +public static class LuceneStartupExtensions +{ + /// + /// Adds Lucene services and custom module to application using the and for all indexes + /// + /// the which will be modified + /// Returns this instance of , allowing for further configuration in a fluent manner. + public static IServiceCollection AddKenticoLucene(this IServiceCollection serviceCollection) + { + serviceCollection.AddLuceneServicesInternal(); + + StrategyStorage.AddStrategy("Default"); + AnalyzerStorage.AddAnalyzer("Standard"); + + return serviceCollection; + } + + + /// + /// Adds Lucene services and custom module to application with customized options provided by the + /// in the action. + /// + /// the which will be modified + /// which will configure the + /// Returns this instance of , allowing for further configuration in a fluent manner. + public static IServiceCollection AddKenticoLucene(this IServiceCollection serviceCollection, Action configure) + { + serviceCollection.AddLuceneServicesInternal(); + + var builder = new LuceneBuilder(serviceCollection); + + configure(builder); + + if (builder.IncludeDefaultStrategy) + { + builder.RegisterStrategy("Default"); + } + + if (builder.IncludeDefaultAnalyzer) + { + builder.RegisterAnalyzer("Standard"); + } + + return serviceCollection; + } + + + private static IServiceCollection AddLuceneServicesInternal(this IServiceCollection services) => + services + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddTransient(); +} + + +public interface ILuceneBuilder +{ + /// + /// Registers the given as a transient service under + /// + /// The custom type of + /// Used internally to enable dynamic assignment of strategies to search indexes. Names must be unique. + /// + /// Thrown if a strategy has already been registered with the given + /// + /// Returns this instance of , allowing for further configuration in a fluent manner. + ILuceneBuilder RegisterStrategy(string strategyName) where TStrategy : class, ILuceneIndexingStrategy; + + + /// + /// Registers the given and + /// as a selectable analyzer in the Admin UI + /// + /// The type of + /// Used internally to enable dynamic assignment of analyzers to search indexes. Names must be unique. + /// + /// Thrown if an analyzer has already been registered with the given + /// + /// Returns this instance of , allowing for further configuration in a fluent manner. + ILuceneBuilder RegisterAnalyzer(string analyzerName) where TAnalyzer : Analyzer; + + + /// + /// Sets the lucene version which will be used by for search indexes. + /// Defaults to + /// + /// to be used by the + /// Returns this instance of , allowing for further configuration in a fluent manner. + ILuceneBuilder SetAnalyzerLuceneVersion(LuceneVersion matchVersion); +} + + +internal class LuceneBuilder : ILuceneBuilder +{ + private readonly IServiceCollection serviceCollection; + + /// + /// 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; + + /// + /// If true, the will be available as an explicitly selectable analyzer + /// within the Admin UI. Defaults to true + /// + public bool IncludeDefaultAnalyzer { get; set; } = true; + + public LuceneBuilder(IServiceCollection serviceCollection) => this.serviceCollection = serviceCollection; + + + /// + /// Registers the strategy in DI and + /// as a selectable strategy in the Admin UI + /// + /// The custom type of + /// Used internally to enable dynamic assignment of strategies to search indexes. Names must be unique. + /// Returns this instance of , allowing for further configuration in a fluent manner. + public ILuceneBuilder RegisterStrategy(string strategyName) where TStrategy : class, ILuceneIndexingStrategy + { + StrategyStorage.AddStrategy(strategyName); + serviceCollection.AddTransient(); + + return this; + } + + + /// + /// Registers the analyzer + /// as a selectable analyzer in the Admin UI. When selected this analyzer will be used to process indexed items. + /// + /// The type of + /// Used internally to enable dynamic assignment of analyzers to search indexes. Names must be unique. + /// Returns this instance of , allowing for further configuration in a fluent manner. + public ILuceneBuilder RegisterAnalyzer(string analyzerName) where TAnalyzer : Analyzer + { + AnalyzerStorage.AddAnalyzer(analyzerName); + + return this; + } + + + /// + /// Sets the lucene version which will be used by for indexing. + /// Defaults to + /// + /// to be used by the + /// Returns this instance of , allowing for further configuration in a fluent manner. + public ILuceneBuilder SetAnalyzerLuceneVersion(LuceneVersion matchVersion) + { + AnalyzerStorage.SetAnalyzerLuceneVersion(matchVersion); + + return this; + } +} diff --git a/src/Kentico.Xperience.Lucene.Core/Search/DefaultLuceneSearchService.cs b/src/Kentico.Xperience.Lucene.Core/Search/DefaultLuceneSearchService.cs index 1a61ed1b..fdba1e3d 100644 --- a/src/Kentico.Xperience.Lucene.Core/Search/DefaultLuceneSearchService.cs +++ b/src/Kentico.Xperience.Lucene.Core/Search/DefaultLuceneSearchService.cs @@ -1,81 +1,81 @@ -using Kentico.Xperience.Lucene.Core.Indexing; - -using Lucene.Net.Facet; -using Lucene.Net.Facet.Taxonomy; -using Lucene.Net.Facet.Taxonomy.Directory; -using Lucene.Net.Index; -using Lucene.Net.Search; -using Lucene.Net.Store; - -using Microsoft.Extensions.DependencyInjection; - -using LuceneDirectory = Lucene.Net.Store.Directory; - -namespace Kentico.Xperience.Lucene.Core.Search; - -internal class DefaultLuceneSearchService : ILuceneSearchService -{ - private readonly ILuceneIndexService indexService; - private readonly IServiceProvider serviceProvider; - - public DefaultLuceneSearchService(ILuceneIndexService indexService, IServiceProvider serviceProvider) - { - this.indexService = indexService; - this.serviceProvider = serviceProvider; - } - - public TResult UseSearcher(LuceneIndex index, Func useIndexSearcher) - { - var storage = index.StorageContext.GetPublishedIndex(); - if (!System.IO.Directory.Exists(storage.Path)) - { - // ensure index - indexService.UseWriter(index, (writer) => - { - writer.Commit(); - return true; - }, storage); - } - - using LuceneDirectory indexDir = FSDirectory.Open(storage.Path); - using var reader = DirectoryReader.Open(indexDir); - var searcher = new IndexSearcher(reader); - return useIndexSearcher(searcher); - } - - public TResult UseSearcherWithFacets(LuceneIndex index, Query query, int n, Func useIndexSearcher) - { - var storage = index.StorageContext.GetPublishedIndex(); - if (!System.IO.Directory.Exists(storage.Path)) - { - // ensure index - indexService.UseIndexAndTaxonomyWriter(index, (writer, tw) => - { - writer.Commit(); - tw.Commit(); - return true; - }, storage); - } - - using LuceneDirectory indexDir = FSDirectory.Open(storage.Path); - using var reader = DirectoryReader.Open(indexDir); - var searcher = new IndexSearcher(reader); - - using var taxonomyDir = FSDirectory.Open(storage.TaxonomyPath); - - using var taxonomyReader = new DirectoryTaxonomyReader(taxonomyDir); - var facetsCollector = new FacetsCollector(); - Dictionary facetsMap = []; - FacetsCollector.Search(searcher, query, n, facetsCollector); - var strategy = serviceProvider.GetRequiredStrategy(index); - var config = strategy?.FacetsConfigFactory() ?? new FacetsConfig(); - OrdinalsReader ordinalsReader = new DocValuesOrdinalsReader(FacetsConfig.DEFAULT_INDEX_FIELD_NAME); - var facetCounts = new TaxonomyFacetCounts(ordinalsReader, taxonomyReader, config, facetsCollector); - var facets = new MultiFacets(facetsMap, facetCounts); - - var results = useIndexSearcher(searcher, facets); - - return results; - - } -} +using Kentico.Xperience.Lucene.Core.Indexing; + +using Lucene.Net.Facet; +using Lucene.Net.Facet.Taxonomy; +using Lucene.Net.Facet.Taxonomy.Directory; +using Lucene.Net.Index; +using Lucene.Net.Search; +using Lucene.Net.Store; + +using Microsoft.Extensions.DependencyInjection; + +using LuceneDirectory = Lucene.Net.Store.Directory; + +namespace Kentico.Xperience.Lucene.Core.Search; + +internal class DefaultLuceneSearchService : ILuceneSearchService +{ + private readonly ILuceneIndexService indexService; + private readonly IServiceProvider serviceProvider; + + public DefaultLuceneSearchService(ILuceneIndexService indexService, IServiceProvider serviceProvider) + { + this.indexService = indexService; + this.serviceProvider = serviceProvider; + } + + public TResult UseSearcher(LuceneIndex index, Func useIndexSearcher) + { + var storage = index.StorageContext.GetPublishedIndex(); + if (!System.IO.Directory.Exists(storage.Path)) + { + // ensure index + indexService.UseWriter(index, (writer) => + { + writer.Commit(); + return true; + }, storage); + } + + using LuceneDirectory indexDir = FSDirectory.Open(storage.Path); + using var reader = DirectoryReader.Open(indexDir); + var searcher = new IndexSearcher(reader); + return useIndexSearcher(searcher); + } + + public TResult UseSearcherWithFacets(LuceneIndex index, Query query, int n, Func useIndexSearcher) + { + var storage = index.StorageContext.GetPublishedIndex(); + if (!System.IO.Directory.Exists(storage.Path)) + { + // ensure index + indexService.UseIndexAndTaxonomyWriter(index, (writer, tw) => + { + writer.Commit(); + tw.Commit(); + return true; + }, storage); + } + + using LuceneDirectory indexDir = FSDirectory.Open(storage.Path); + using var reader = DirectoryReader.Open(indexDir); + var searcher = new IndexSearcher(reader); + + using var taxonomyDir = FSDirectory.Open(storage.TaxonomyPath); + + using var taxonomyReader = new DirectoryTaxonomyReader(taxonomyDir); + var facetsCollector = new FacetsCollector(); + Dictionary facetsMap = []; + FacetsCollector.Search(searcher, query, n, facetsCollector); + var strategy = serviceProvider.GetRequiredStrategy(index); + var config = strategy?.FacetsConfigFactory() ?? new FacetsConfig(); + OrdinalsReader ordinalsReader = new DocValuesOrdinalsReader(FacetsConfig.DEFAULT_INDEX_FIELD_NAME); + var facetCounts = new TaxonomyFacetCounts(ordinalsReader, taxonomyReader, config, facetsCollector); + var facets = new MultiFacets(facetsMap, facetCounts); + + var results = useIndexSearcher(searcher, facets); + + return results; + + } +} diff --git a/src/Kentico.Xperience.Lucene.Core/Search/ILuceneSearchService.cs b/src/Kentico.Xperience.Lucene.Core/Search/ILuceneSearchService.cs index 7246870b..777da0bf 100644 --- a/src/Kentico.Xperience.Lucene.Core/Search/ILuceneSearchService.cs +++ b/src/Kentico.Xperience.Lucene.Core/Search/ILuceneSearchService.cs @@ -1,16 +1,16 @@ -using Kentico.Xperience.Lucene.Core.Indexing; - -using Lucene.Net.Facet; -using Lucene.Net.Search; - -namespace Kentico.Xperience.Lucene.Core.Search; - -/// -/// Primary service used for querying lucene indexes -/// -public interface ILuceneSearchService -{ - TResult UseSearcher(LuceneIndex index, Func useIndexSearcher); - - TResult UseSearcherWithFacets(LuceneIndex index, Query query, int n, Func useIndexSearcher); -} +using Kentico.Xperience.Lucene.Core.Indexing; + +using Lucene.Net.Facet; +using Lucene.Net.Search; + +namespace Kentico.Xperience.Lucene.Core.Search; + +/// +/// Primary service used for querying lucene indexes +/// +public interface ILuceneSearchService +{ + TResult UseSearcher(LuceneIndex index, Func useIndexSearcher); + + TResult UseSearcherWithFacets(LuceneIndex index, Query query, int n, Func useIndexSearcher); +} diff --git a/src/Kentico.Xperience.Lucene.Core/Search/LuceneSearchResultModel.cs b/src/Kentico.Xperience.Lucene.Core/Search/LuceneSearchResultModel.cs index b417e2c3..1d084dc5 100644 --- a/src/Kentico.Xperience.Lucene.Core/Search/LuceneSearchResultModel.cs +++ b/src/Kentico.Xperience.Lucene.Core/Search/LuceneSearchResultModel.cs @@ -1,16 +1,16 @@ -using Lucene.Net.Facet; - -namespace Kentico.Xperience.Lucene.Core.Search; - -public class LuceneSearchResultModel -{ - public string Query { get; set; } = ""; - public IEnumerable Hits { get; set; } = []; - public int TotalHits { get; set; } - public int TotalPages { get; set; } - public int PageSize { get; set; } - public int Page { get; set; } - - public string? Facet { get; set; } - public LabelAndValue[]? Facets { get; set; } -} +using Lucene.Net.Facet; + +namespace Kentico.Xperience.Lucene.Core.Search; + +public class LuceneSearchResultModel +{ + public string Query { get; set; } = ""; + public IEnumerable Hits { get; set; } = []; + public int TotalHits { get; set; } + public int TotalPages { get; set; } + public int PageSize { get; set; } + public int Page { get; set; } + + public string? Facet { get; set; } + public LabelAndValue[]? Facets { get; set; } +} diff --git a/src/Kentico.Xperience.Lucene.Core/ServiceProviderExtensions.cs b/src/Kentico.Xperience.Lucene.Core/ServiceProviderExtensions.cs index 23f21cf5..3ec0eecb 100644 --- a/src/Kentico.Xperience.Lucene.Core/ServiceProviderExtensions.cs +++ b/src/Kentico.Xperience.Lucene.Core/ServiceProviderExtensions.cs @@ -1,24 +1,24 @@ -using Kentico.Xperience.Lucene.Core.Indexing; - -namespace Microsoft.Extensions.DependencyInjection; - -public static class ServiceProviderExtensions -{ - /// - /// 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 no custom strategy is specified. - /// However, incorrect dependency management in user-code could cause issues. - /// - /// Returns an instance of the assigned to the given . - internal static ILuceneIndexingStrategy GetRequiredStrategy(this IServiceProvider serviceProvider, LuceneIndex index) - { - var strategy = serviceProvider.GetRequiredService(index.LuceneIndexingStrategyType) as ILuceneIndexingStrategy; - - return strategy!; - } -} +using Kentico.Xperience.Lucene.Core.Indexing; + +namespace Microsoft.Extensions.DependencyInjection; + +public static class ServiceProviderExtensions +{ + /// + /// 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 no custom strategy is specified. + /// However, incorrect dependency management in user-code could cause issues. + /// + /// Returns an instance of the assigned to the given . + internal static ILuceneIndexingStrategy GetRequiredStrategy(this IServiceProvider serviceProvider, LuceneIndex index) + { + var strategy = serviceProvider.GetRequiredService(index.LuceneIndexingStrategyType) as ILuceneIndexingStrategy; + + return strategy!; + } +} diff --git a/tests/Kentico.Xperience.Lucene.Tests/Data/MockDataProvider.cs b/tests/Kentico.Xperience.Lucene.Tests/Data/MockDataProvider.cs index 1d7a4be9..8336f03b 100644 --- a/tests/Kentico.Xperience.Lucene.Tests/Data/MockDataProvider.cs +++ b/tests/Kentico.Xperience.Lucene.Tests/Data/MockDataProvider.cs @@ -1,75 +1,75 @@ -using CMS.DataEngine; - -using DancingGoat.Models; - -using Kentico.Xperience.Lucene.Core.Indexing; - -using Lucene.Net.Util; - -namespace Kentico.Xperience.Lucene.Tests.Base; - -internal static class MockDataProvider -{ - public static IndexEventWebPageItemModel WebModel => new( - itemID: 0, - itemGuid: Guid.NewGuid(), - languageName: CzechLanguageName, - contentTypeName: ArticlePage.CONTENT_TYPE_NAME, - name: "Name", - isSecured: false, - contentTypeID: 1, - contentLanguageID: 1, - websiteChannelName: DefaultChannel, - webPageItemTreePath: "/", - order: 0 - ); - - public static LuceneIndexIncludedPath Path => new("/%") - { - ContentTypes = [new LuceneIndexContentType(ArticlePage.CONTENT_TYPE_NAME, - DataClassInfoProvider.ProviderObject - .Get() - .WhereEquals(nameof(DataClassInfo.ClassName), ArticlePage.CONTENT_TYPE_NAME) - .FirstOrDefault()? - .ClassDisplayName ?? "", 0 - ) - ] - }; - - - public static LuceneIndex Index => new( - new LuceneIndexModel() - { - IndexName = DefaultIndex, - ChannelName = DefaultChannel, - LanguageNames = [EnglishLanguageName, CzechLanguageName], - Paths = [Path], - AnalyzerName = DefaultAnalyzer - }, - [], - [], - LuceneVersion.LUCENE_48 - ); - - public static readonly string DefaultIndex = "SimpleIndex"; - public static readonly string DefaultChannel = "DefaultChannel"; - public static readonly string DefaultAnalyzer = "StandardAnalyzer"; - 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 LuceneIndex GetIndex(string indexName, int id) => new( - new LuceneIndexModel() - { - Id = id, - IndexName = indexName, - ChannelName = DefaultChannel, - LanguageNames = [EnglishLanguageName, CzechLanguageName], - Paths = [Path], - }, - [], - [], - LuceneVersion.LUCENE_48 - ); -} +using CMS.DataEngine; + +using DancingGoat.Models; + +using Kentico.Xperience.Lucene.Core.Indexing; + +using Lucene.Net.Util; + +namespace Kentico.Xperience.Lucene.Tests.Base; + +internal static class MockDataProvider +{ + public static IndexEventWebPageItemModel WebModel => new( + itemID: 0, + itemGuid: Guid.NewGuid(), + languageName: CzechLanguageName, + contentTypeName: ArticlePage.CONTENT_TYPE_NAME, + name: "Name", + isSecured: false, + contentTypeID: 1, + contentLanguageID: 1, + websiteChannelName: DefaultChannel, + webPageItemTreePath: "/", + order: 0 + ); + + public static LuceneIndexIncludedPath Path => new("/%") + { + ContentTypes = [new LuceneIndexContentType(ArticlePage.CONTENT_TYPE_NAME, + DataClassInfoProvider.ProviderObject + .Get() + .WhereEquals(nameof(DataClassInfo.ClassName), ArticlePage.CONTENT_TYPE_NAME) + .FirstOrDefault()? + .ClassDisplayName ?? "", 0 + ) + ] + }; + + + public static LuceneIndex Index => new( + new LuceneIndexModel() + { + IndexName = DefaultIndex, + ChannelName = DefaultChannel, + LanguageNames = [EnglishLanguageName, CzechLanguageName], + Paths = [Path], + AnalyzerName = DefaultAnalyzer + }, + [], + [], + LuceneVersion.LUCENE_48 + ); + + public static readonly string DefaultIndex = "SimpleIndex"; + public static readonly string DefaultChannel = "DefaultChannel"; + public static readonly string DefaultAnalyzer = "StandardAnalyzer"; + 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 LuceneIndex GetIndex(string indexName, int id) => new( + new LuceneIndexModel() + { + Id = id, + IndexName = indexName, + ChannelName = DefaultChannel, + LanguageNames = [EnglishLanguageName, CzechLanguageName], + Paths = [Path], + }, + [], + [], + LuceneVersion.LUCENE_48 + ); +}