Files
Umbraco-CMS/src/Umbraco.Examine.Lucene/UmbracoContentIndex.cs
Shannon e34b593b79 Updates to latest Examine version
The latest Examine version has some API breaking changes - namely it shouldn't have been relying on IOptionsSnapshot and instead IOptionsMonitor.
2021-08-26 11:36:24 -06:00

155 lines
6.5 KiB
C#

// Copyright (c) Umbraco.
// See LICENSE for more details.
using System;
using System.Collections.Generic;
using System.Linq;
using Examine;
using Examine.Lucene;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Umbraco.Cms.Core.Hosting;
using Umbraco.Cms.Core.Services;
namespace Umbraco.Cms.Infrastructure.Examine
{
/// <summary>
/// An indexer for Umbraco content and media
/// </summary>
public class UmbracoContentIndex : UmbracoExamineIndex, IUmbracoContentIndex, IDisposable
{
private readonly ILogger<UmbracoContentIndex> _logger;
private readonly ISet<string> _idOnlyFieldSet = new HashSet<string> { "id" };
public UmbracoContentIndex(
ILoggerFactory loggerFactory,
string name,
IOptionsMonitor<LuceneDirectoryIndexOptions> indexOptions,
IHostingEnvironment hostingEnvironment,
IRuntimeState runtimeState,
ILocalizationService languageService = null)
: base(loggerFactory, name, indexOptions, hostingEnvironment, runtimeState)
{
LanguageService = languageService;
_logger = loggerFactory.CreateLogger<UmbracoContentIndex>();
LuceneDirectoryIndexOptions namedOptions = indexOptions.Get(name);
if (namedOptions == null)
{
throw new InvalidOperationException($"No named {typeof(LuceneDirectoryIndexOptions)} options with name {name}");
}
if (namedOptions.Validator is IContentValueSetValidator contentValueSetValidator)
{
PublishedValuesOnly = contentValueSetValidator.PublishedValuesOnly;
}
}
protected ILocalizationService LanguageService { get; }
/// <summary>
/// Explicitly override because we need to do validation differently than the underlying logic
/// </summary>
/// <param name="values"></param>
void IIndex.IndexItems(IEnumerable<ValueSet> values) => PerformIndexItems(values, OnIndexOperationComplete);
/// <summary>
/// Special check for invalid paths
/// </summary>
/// <param name="values"></param>
/// <param name="onComplete"></param>
protected override void PerformIndexItems(IEnumerable<ValueSet> values, Action<IndexOperationEventArgs> onComplete)
{
// We don't want to re-enumerate this list, but we need to split it into 2x enumerables: invalid and valid items.
// The Invalid items will be deleted, these are items that have invalid paths (i.e. moved to the recycle bin, etc...)
// Then we'll index the Value group all together.
var invalidOrValid = values.GroupBy(v =>
{
if (!v.Values.TryGetValue("path", out List<object> paths) || paths.Count <= 0 || paths[0] == null)
{
return ValueSetValidationResult.Failed;
}
ValueSetValidationResult validationResult = ValueSetValidator.Validate(v);
return validationResult;
}).ToList();
var hasDeletes = false;
var hasUpdates = false;
// ordering by descending so that Filtered/Failed processes first
foreach (IGrouping<ValueSetValidationResult, ValueSet> group in invalidOrValid.OrderByDescending(x => x.Key))
{
switch (group.Key)
{
case ValueSetValidationResult.Valid:
hasUpdates = true;
//these are the valid ones, so just index them all at once
base.PerformIndexItems(group.ToList(), onComplete);
break;
case ValueSetValidationResult.Failed:
// don't index anything that is invalid
break;
case ValueSetValidationResult.Filtered:
hasDeletes = true;
// these are the invalid/filtered items so we'll delete them
// since the path is not valid we need to delete this item in
// case it exists in the index already and has now
// been moved to an invalid parent.
base.PerformDeleteFromIndex(group.Select(x => x.Id), null);
break;
}
}
if ((hasDeletes && !hasUpdates) || (!hasDeletes && !hasUpdates))
{
//we need to manually call the completed method
onComplete(new IndexOperationEventArgs(this, 0));
}
}
/// <inheritdoc />
/// <summary>
/// Deletes a node from the index.
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
/// <param name="itemIds">ID of the node to delete</param>
/// <param name="onComplete"></param>
protected override void PerformDeleteFromIndex(IEnumerable<string> itemIds, Action<IndexOperationEventArgs> onComplete)
{
var idsAsList = itemIds.ToList();
for (int i = 0; i < idsAsList.Count; i++)
{
string nodeId = idsAsList[i];
//find all descendants based on path
var descendantPath = $@"\-1\,*{nodeId}\,*";
var rawQuery = $"{UmbracoExamineFieldNames.IndexPathFieldName}:{descendantPath}";
var c = Searcher.CreateQuery();
var filtered = c.NativeQuery(rawQuery);
var selectedFields = filtered.SelectFields(_idOnlyFieldSet);
var 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);
}
}
}