-
Notifications
You must be signed in to change notification settings - Fork 2.6k
/
UmbracoContentIndex.cs
158 lines (136 loc) · 6.35 KB
/
UmbracoContentIndex.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
// Copyright (c) Umbraco.
// See LICENSE for more details.
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;
/// <summary>
/// An indexer for Umbraco content and media
/// </summary>
public class UmbracoContentIndex : UmbracoExamineIndex, IUmbracoContentIndex
{
private readonly ISet<string> _idOnlyFieldSet = new HashSet<string> { "id" };
private readonly ILogger<UmbracoContentIndex> _logger;
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;
SupportProtectedContent = contentValueSetValidator.SupportProtectedContent;
}
}
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 IReadOnlyList<object>? paths) || paths.Count <= 0 || paths[0] == null)
{
return ValueSetValidationStatus.Failed;
}
if (ValueSetValidator is not null)
{
ValueSetValidationResult validationResult = ValueSetValidator.Validate(v);
return validationResult.Status;
}
return ValueSetValidationStatus.Valid;
}).ToArray();
var hasDeletes = false;
var hasUpdates = false;
// ordering by descending so that Filtered/Failed processes first
foreach (IGrouping<ValueSetValidationStatus, ValueSet> group in invalidOrValid.OrderByDescending(x => x.Key))
{
switch (group.Key)
{
case ValueSetValidationStatus.Valid:
hasUpdates = true;
//these are the valid ones, so just index them all at once
base.PerformIndexItems(group.ToArray(), onComplete);
break;
case ValueSetValidationStatus.Failed:
// don't index anything that is invalid
break;
case ValueSetValidationStatus.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 (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();
if (_logger.IsEnabled(Microsoft.Extensions.Logging.LogLevel.Debug))
{
_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);
}
}