diff --git a/Directory.Build.props b/Directory.Build.props
index e01037c..7db8251 100644
--- a/Directory.Build.props
+++ b/Directory.Build.props
@@ -5,7 +5,7 @@
$(Company)
Copyright © $(Company) $([System.DateTime]::Now.Year)
$(Company)™
- 4.1.0
+ 4.2.0
MIT
diff --git a/src/Kentico.Xperience.Lucene/Indexing/DefaultLuceneClient.cs b/src/Kentico.Xperience.Lucene/Indexing/DefaultLuceneClient.cs
index 91a9239..6b4db27 100644
--- a/src/Kentico.Xperience.Lucene/Indexing/DefaultLuceneClient.cs
+++ b/src/Kentico.Xperience.Lucene/Indexing/DefaultLuceneClient.cs
@@ -1,325 +1,331 @@
-using CMS.ContentEngine;
-using CMS.Core;
-using CMS.DataEngine;
-using CMS.Helpers;
-using CMS.Helpers.Caching.Abstractions;
-using CMS.Websites;
-using Kentico.Xperience.Lucene.Admin;
-using Kentico.Xperience.Lucene.Search;
-using Lucene.Net.Documents;
-using Lucene.Net.Index;
-using Lucene.Net.Search;
-using Microsoft.Extensions.DependencyInjection;
-
-namespace Kentico.Xperience.Lucene.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;
+using CMS.ContentEngine;
+using CMS.Core;
+using CMS.DataEngine;
+using CMS.Helpers;
+using CMS.Helpers.Caching.Abstractions;
+using CMS.Websites;
+using Kentico.Xperience.Lucene.Admin;
+using Kentico.Xperience.Lucene.Search;
+using Lucene.Net.Documents;
+using Lucene.Net.Index;
+using Lucene.Net.Search;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace Kentico.Xperience.Lucene.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;
- }
-
- ///
- 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 LuceneIndexStatisticsViewModel()
- {
- 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)
- {
- foreach (string language in luceneIndex.LanguageNames)
- {
- var queryBuilder = new ContentItemQueryBuilder();
-
- if (includedPathAttribute.ContentTypes != null && includedPathAttribute.ContentTypes.Count > 0)
- {
- foreach (string contentType in includedPathAttribute.ContentTypes)
- {
- queryBuilder.ForContentType(contentType, config => config.ForWebsite(luceneIndex.WebSiteChannelName, includeUrlPath: true));
- }
- }
- 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));
- if (id is not null)
- {
- writer.DeleteDocuments(new Term(nameof(IIndexEventItemModel.ItemGuid), id));
- }
-
- // 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));
- if (id 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
- writer.DeleteDocuments(new Term(nameof(IIndexEventItemModel.ItemGuid), id));
- }
- // 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)));
-}
+ 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 LuceneIndexStatisticsViewModel()
+ {
+ 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 (string contentType in includedPathAttribute.ContentTypes)
+ {
+ queryBuilder.ForContentType(contentType, 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));
+ if (id is not null)
+ {
+ writer.DeleteDocuments(new Term(nameof(IIndexEventItemModel.ItemGuid), id));
+ }
+
+ // 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));
+ if (id 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
+ writer.DeleteDocuments(new Term(nameof(IIndexEventItemModel.ItemGuid), id));
+ }
+ // 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)));
+}