diff --git a/src/Umbraco.Cms.Api.Delivery/Indexing/Selectors/AncestorsSelectorIndexer.cs b/src/Umbraco.Cms.Api.Delivery/Indexing/Selectors/AncestorsSelectorIndexer.cs new file mode 100644 index 0000000000..d6b8b86c8a --- /dev/null +++ b/src/Umbraco.Cms.Api.Delivery/Indexing/Selectors/AncestorsSelectorIndexer.cs @@ -0,0 +1,17 @@ +using Umbraco.Cms.Core.DeliveryApi; +using Umbraco.Cms.Core.Models; + +namespace Umbraco.Cms.Api.Delivery.Indexing.Selectors; + +public sealed class AncestorsSelectorIndexer : IContentIndexHandler +{ + // NOTE: "id" is a reserved field name + internal const string FieldName = "itemId"; + + public IEnumerable GetFieldValues(IContent content) + => new[] { new IndexFieldValue { FieldName = FieldName, Value = content.Key } }; + + public IEnumerable GetFields() + => new[] { new IndexField { FieldName = FieldName, FieldType = FieldType.String } }; +} + diff --git a/src/Umbraco.Cms.Api.Delivery/Querying/Selectors/AncestorsSelector.cs b/src/Umbraco.Cms.Api.Delivery/Querying/Selectors/AncestorsSelector.cs index dc89d0cdad..542567bf0d 100644 --- a/src/Umbraco.Cms.Api.Delivery/Querying/Selectors/AncestorsSelector.cs +++ b/src/Umbraco.Cms.Api.Delivery/Querying/Selectors/AncestorsSelector.cs @@ -1,3 +1,4 @@ +using Umbraco.Cms.Api.Delivery.Indexing.Selectors; using Umbraco.Cms.Core.DeliveryApi; using Umbraco.Cms.Core.Models.PublishedContent; using Umbraco.Cms.Core.PublishedCache; @@ -33,7 +34,7 @@ public sealed class AncestorsSelector : QueryOptionBase, ISelectorHandler // it means that CanHandle() returned true, meaning that this Selector should be able to handle the selector value return new SelectorOption { - FieldName = "id", + FieldName = AncestorsSelectorIndexer.FieldName, Value = string.Empty }; } @@ -44,7 +45,7 @@ public sealed class AncestorsSelector : QueryOptionBase, ISelectorHandler return new SelectorOption { - FieldName = "id", + FieldName = AncestorsSelectorIndexer.FieldName, Value = string.Join(" ", ancestorKeys) }; } diff --git a/src/Umbraco.Cms.Api.Delivery/Services/ApiContentQueryService.cs b/src/Umbraco.Cms.Api.Delivery/Services/ApiContentQueryService.cs index b8cbd71b19..7a6a0181e8 100644 --- a/src/Umbraco.Cms.Api.Delivery/Services/ApiContentQueryService.cs +++ b/src/Umbraco.Cms.Api.Delivery/Services/ApiContentQueryService.cs @@ -18,6 +18,7 @@ internal sealed class ApiContentQueryService : IApiContentQueryService // Examin private readonly FilterHandlerCollection _filterHandlers; private readonly SortHandlerCollection _sortHandlers; private readonly string _fallbackGuidValue; + private readonly ISet _itemIdOnlyFieldSet = new HashSet { "itemId" }; public ApiContentQueryService( IExamineManager examineManager, @@ -62,19 +63,22 @@ internal sealed class ApiContentQueryService : IApiContentQueryService // Examin HandleFiltering(filters, queryOperation); // Handle Sorting - IOrdering sortQuery = HandleSorting(sorts, queryOperation); + IOrdering sortQuery = HandleSorting(sorts, queryOperation).SelectFields(_itemIdOnlyFieldSet); - ISearchResults? results = sortQuery.Execute(QueryOptions.SkipTake(skip, take)); + ISearchResults? results = sortQuery + .SelectFields(_itemIdOnlyFieldSet) + .Execute(QueryOptions.SkipTake(skip, take)); if (results is null) { return emptyResult; } - else - { - Guid[] items = results.Select(x => Guid.Parse(x.Id)).ToArray(); - return new PagedModel(results.TotalItemCount, items); - } + + Guid[] items = results + .Where(r => r.Values.ContainsKey("itemId")) + .Select(r => Guid.Parse(r.Values["itemId"])) + .ToArray(); + return new PagedModel(results.TotalItemCount, items); } private IBooleanOperation? HandleSelector(string? fetch, IQuery baseQuery) diff --git a/src/Umbraco.Examine.Lucene/DeliveryApiContentIndex.cs b/src/Umbraco.Examine.Lucene/DeliveryApiContentIndex.cs index a9b4ae3bd6..3a7f31f32c 100644 --- a/src/Umbraco.Examine.Lucene/DeliveryApiContentIndex.cs +++ b/src/Umbraco.Examine.Lucene/DeliveryApiContentIndex.cs @@ -1,3 +1,4 @@ +using Examine; using Examine.Lucene; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; @@ -6,7 +7,7 @@ using Umbraco.Cms.Core.Services; namespace Umbraco.Cms.Infrastructure.Examine; -public class DeliveryApiContentIndex : UmbracoExamineIndex +public class DeliveryApiContentIndex : UmbracoContentIndexBase { public DeliveryApiContentIndex( ILoggerFactory loggerFactory, @@ -16,5 +17,13 @@ public class DeliveryApiContentIndex : UmbracoExamineIndex IRuntimeState runtimeState) : base(loggerFactory, name, indexOptions, hostingEnvironment, runtimeState) { + PublishedValuesOnly = true; + EnableDefaultEventHandler = true; + } + + protected override void OnTransformingIndexValues(IndexingItemEventArgs e) + { + // UmbracoExamineIndex (base class down the hierarchy) performs some magic transformations here for paths and icons; + // we don't want that for the Delivery API, so we'll have to override this method and simply do nothing. } } diff --git a/src/Umbraco.Examine.Lucene/UmbracoContentIndex.cs b/src/Umbraco.Examine.Lucene/UmbracoContentIndex.cs index 7aa2eea5fc..9bd6b76dc7 100644 --- a/src/Umbraco.Examine.Lucene/UmbracoContentIndex.cs +++ b/src/Umbraco.Examine.Lucene/UmbracoContentIndex.cs @@ -3,7 +3,6 @@ using Examine; using Examine.Lucene; -using Examine.Search; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Umbraco.Cms.Core.Hosting; @@ -14,11 +13,8 @@ namespace Umbraco.Cms.Infrastructure.Examine; /// /// An indexer for Umbraco content and media /// -public class UmbracoContentIndex : UmbracoExamineIndex, IUmbracoContentIndex +public class UmbracoContentIndex : UmbracoContentIndexBase, IUmbracoContentIndex { - private readonly ISet _idOnlyFieldSet = new HashSet { "id" }; - private readonly ILogger _logger; - public UmbracoContentIndex( ILoggerFactory loggerFactory, string name, @@ -29,7 +25,6 @@ public class UmbracoContentIndex : UmbracoExamineIndex, IUmbracoContentIndex : base(loggerFactory, name, indexOptions, hostingEnvironment, runtimeState) { LanguageService = languageService; - _logger = loggerFactory.CreateLogger(); LuceneDirectoryIndexOptions namedOptions = indexOptions.Get(name); if (namedOptions == null) @@ -109,43 +104,4 @@ public class UmbracoContentIndex : UmbracoExamineIndex, IUmbracoContentIndex onComplete(new IndexOperationEventArgs(this, 0)); } } - - /// - /// - /// Deletes a node from the index. - /// - /// - /// When a content node is deleted, we also need to delete it's children from the index so we need to perform a - /// custom Lucene search to find all decendents and create Delete item queues for them too. - /// - /// ID of the node to delete - /// - protected override void PerformDeleteFromIndex(IEnumerable itemIds, Action? onComplete) - { - var idsAsList = itemIds.ToList(); - - for (var i = 0; i < idsAsList.Count; i++) - { - var nodeId = idsAsList[i]; - - //find all descendants based on path - var descendantPath = $@"\-1\,*{nodeId}\,*"; - var rawQuery = $"{UmbracoExamineFieldNames.IndexPathFieldName}:{descendantPath}"; - IQuery? c = Searcher.CreateQuery(); - IBooleanOperation? filtered = c.NativeQuery(rawQuery); - IOrdering? selectedFields = filtered.SelectFields(_idOnlyFieldSet); - ISearchResults? results = selectedFields.Execute(); - - _logger.LogDebug("DeleteFromIndex with query: {Query} (found {TotalItems} results)", rawQuery, results.TotalItemCount); - - var toRemove = results.Select(x => x.Id).ToList(); - // delete those descendants (ensure base. is used here so we aren't calling ourselves!) - base.PerformDeleteFromIndex(toRemove, null); - - // remove any ids from our list that were part of the descendants - idsAsList.RemoveAll(x => toRemove.Contains(x)); - } - - base.PerformDeleteFromIndex(idsAsList, onComplete); - } } diff --git a/src/Umbraco.Examine.Lucene/UmbracoContentIndexBase.cs b/src/Umbraco.Examine.Lucene/UmbracoContentIndexBase.cs new file mode 100644 index 0000000000..813d4cc8f6 --- /dev/null +++ b/src/Umbraco.Examine.Lucene/UmbracoContentIndexBase.cs @@ -0,0 +1,63 @@ +using Examine; +using Examine.Lucene; +using Examine.Search; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Umbraco.Cms.Core.Hosting; +using Umbraco.Cms.Core.Services; + +namespace Umbraco.Cms.Infrastructure.Examine; + +public abstract class UmbracoContentIndexBase : UmbracoExamineIndex +{ + private readonly ISet _idOnlyFieldSet = new HashSet { "id" }; + private readonly ILogger _logger; + + protected UmbracoContentIndexBase( + ILoggerFactory loggerFactory, + string name, + IOptionsMonitor indexOptions, + IHostingEnvironment hostingEnvironment, + IRuntimeState runtimeState) + : base(loggerFactory, name, indexOptions, hostingEnvironment, runtimeState) => + _logger = loggerFactory.CreateLogger(); + + /// + /// + /// Deletes a node from the index. + /// + /// + /// When a content node is deleted, we also need to delete it's children from the index so we need to perform a + /// custom Lucene search to find all decendents and create Delete item queues for them too. + /// + /// ID of the node to delete + /// + protected override void PerformDeleteFromIndex(IEnumerable itemIds, Action? onComplete) + { + var idsAsList = itemIds.ToList(); + + for (var i = 0; i < idsAsList.Count; i++) + { + var nodeId = idsAsList[i]; + + //find all descendants based on path + var descendantPath = $@"\-1\,*{nodeId}\,*"; + var rawQuery = $"{UmbracoExamineFieldNames.IndexPathFieldName}:{descendantPath}"; + IQuery? c = Searcher.CreateQuery(); + IBooleanOperation? filtered = c.NativeQuery(rawQuery); + IOrdering? selectedFields = filtered.SelectFields(_idOnlyFieldSet); + ISearchResults? results = selectedFields.Execute(); + + _logger.LogDebug("DeleteFromIndex with query: {Query} (found {TotalItems} results)", rawQuery, results.TotalItemCount); + + var toRemove = results.Select(x => x.Id).ToList(); + // delete those descendants (ensure base. is used here so we aren't calling ourselves!) + base.PerformDeleteFromIndex(toRemove, null); + + // remove any ids from our list that were part of the descendants + idsAsList.RemoveAll(x => toRemove.Contains(x)); + } + + base.PerformDeleteFromIndex(idsAsList, onComplete); + } +} diff --git a/src/Umbraco.Infrastructure/Examine/DeliveryApiContentIndexFieldDefinitionBuilder.cs b/src/Umbraco.Infrastructure/Examine/DeliveryApiContentIndexFieldDefinitionBuilder.cs index 6179cd1d80..8b4094f493 100644 --- a/src/Umbraco.Infrastructure/Examine/DeliveryApiContentIndexFieldDefinitionBuilder.cs +++ b/src/Umbraco.Infrastructure/Examine/DeliveryApiContentIndexFieldDefinitionBuilder.cs @@ -14,9 +14,12 @@ public class DeliveryApiContentIndexFieldDefinitionBuilder : IDeliveryApiContent public FieldDefinitionCollection Build() { // mandatory field definitions go here + // see also the field definitions in the Delivery API content index value set builder var fieldDefinitions = new List { - new("id", FieldDefinitionTypes.FullText) + new("id", FieldDefinitionTypes.Integer), + new(UmbracoExamineFieldNames.IndexPathFieldName, FieldDefinitionTypes.Raw), + new(UmbracoExamineFieldNames.NodeNameFieldName, FieldDefinitionTypes.Raw) }; // add custom fields from index handlers (selectors, filters, sorts) diff --git a/src/Umbraco.Infrastructure/Examine/DeliveryApiContentIndexValueSetBuilder.cs b/src/Umbraco.Infrastructure/Examine/DeliveryApiContentIndexValueSetBuilder.cs index bceb825898..8b62a8507d 100644 --- a/src/Umbraco.Infrastructure/Examine/DeliveryApiContentIndexValueSetBuilder.cs +++ b/src/Umbraco.Infrastructure/Examine/DeliveryApiContentIndexValueSetBuilder.cs @@ -37,8 +37,9 @@ public class DeliveryApiContentIndexValueSetBuilder : IDeliveryApiContentIndexVa // mandatory index values go here var indexValues = new Dictionary { - ["id"] = content.Key, - [UmbracoExamineFieldNames.NodeNameFieldName] = content.PublishName ?? string.Empty + ["id"] = content.Id, // required for unpublishing/deletion handling + [UmbracoExamineFieldNames.IndexPathFieldName] = content.Path, // required for unpublishing/deletion handling + [UmbracoExamineFieldNames.NodeNameFieldName] = content.PublishName ?? string.Empty, // primarily needed for backoffice index browsing }; // add custom field values from index handlers (selectors, filters, sorts) @@ -52,7 +53,8 @@ public class DeliveryApiContentIndexValueSetBuilder : IDeliveryApiContentIndexVa indexValues[fieldValue.FieldName] = fieldValue.Value; } - yield return new ValueSet(content.Key.ToString(), IndexTypes.Content, content.ContentType.Alias, indexValues); + // NOTE: must use content.Id here, not content.Key - otherwise automatic clean-up i.e. on deletion or unpublishing will not work + yield return new ValueSet(content.Id.ToString(), IndexTypes.Content, content.ContentType.Alias, indexValues); } } diff --git a/src/Umbraco.Infrastructure/Examine/ExamineUmbracoIndexingHandler.cs b/src/Umbraco.Infrastructure/Examine/ExamineUmbracoIndexingHandler.cs index 0bd6a56c3d..78fa0c7417 100644 --- a/src/Umbraco.Infrastructure/Examine/ExamineUmbracoIndexingHandler.cs +++ b/src/Umbraco.Infrastructure/Examine/ExamineUmbracoIndexingHandler.cs @@ -31,6 +31,7 @@ internal class ExamineUmbracoIndexingHandler : IUmbracoIndexingHandler private readonly IValueSetBuilder _memberValueSetBuilder; private readonly IProfilingLogger _profilingLogger; private readonly IPublishedContentValueSetBuilder _publishedContentValueSetBuilder; + private readonly IDeliveryApiContentIndexValueSetBuilder _deliveryApiContentIndexValueSetBuilder; private readonly ICoreScopeProvider _scopeProvider; public ExamineUmbracoIndexingHandler( @@ -43,7 +44,8 @@ internal class ExamineUmbracoIndexingHandler : IUmbracoIndexingHandler IContentValueSetBuilder contentValueSetBuilder, IPublishedContentValueSetBuilder publishedContentValueSetBuilder, IValueSetBuilder mediaValueSetBuilder, - IValueSetBuilder memberValueSetBuilder) + IValueSetBuilder memberValueSetBuilder, + IDeliveryApiContentIndexValueSetBuilder deliveryApiContentIndexValueSetBuilder) { _mainDom = mainDom; _logger = logger; @@ -55,6 +57,7 @@ internal class ExamineUmbracoIndexingHandler : IUmbracoIndexingHandler _publishedContentValueSetBuilder = publishedContentValueSetBuilder; _mediaValueSetBuilder = mediaValueSetBuilder; _memberValueSetBuilder = memberValueSetBuilder; + _deliveryApiContentIndexValueSetBuilder = deliveryApiContentIndexValueSetBuilder; _enabled = new Lazy(IsEnabled); } @@ -273,7 +276,6 @@ internal class ExamineUmbracoIndexingHandler : IUmbracoIndexingHandler public override void Execute() => Execute(_backgroundTaskQueue, _examineUmbracoIndexingHandler, _content, _isPublished); - // TODO: Delivery API index needs updating here public static void Execute(IBackgroundTaskQueue backgroundTaskQueue, ExamineUmbracoIndexingHandler examineUmbracoIndexingHandler, IContent content, bool isPublished) => backgroundTaskQueue.QueueBackgroundWorkItem(cancellationToken => @@ -305,6 +307,19 @@ internal class ExamineUmbracoIndexingHandler : IUmbracoIndexingHandler index.IndexItems(valueSet); } + if (cancellationToken.IsCancellationRequested) + { + return Task.CompletedTask; + } + + if (isPublished && examineUmbracoIndexingHandler._examineManager.TryGetIndex( + Core.Constants.UmbracoIndexes.DeliveryApiContentIndexName, + out IIndex deliveryApiContentIndex)) + { + IEnumerable valueSets = examineUmbracoIndexingHandler._deliveryApiContentIndexValueSetBuilder.GetValueSets(content); + deliveryApiContentIndex.IndexItems(valueSets); + } + return Task.CompletedTask; }); }