diff --git a/src/Umbraco.Examine.Lucene/BackOfficeExamineSearcher.cs b/src/Umbraco.Examine.Lucene/BackOfficeExamineSearcher.cs index 9cb667ad44..71c0929e39 100644 --- a/src/Umbraco.Examine.Lucene/BackOfficeExamineSearcher.cs +++ b/src/Umbraco.Examine.Lucene/BackOfficeExamineSearcher.cs @@ -1,384 +1,414 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System; -using System.Collections.Generic; using System.Globalization; -using System.Linq; using System.Text; using System.Text.RegularExpressions; using Examine; using Examine.Search; -using Examine.Search; using Lucene.Net.QueryParsers.Classic; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Mapping; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.ContentEditing; +using Umbraco.Cms.Core.Models.Entities; +using Umbraco.Cms.Core.Models.Membership; using Umbraco.Cms.Core.Routing; using Umbraco.Cms.Core.Security; using Umbraco.Cms.Core.Services; -using Umbraco.Cms.Core.Web; using Umbraco.Extensions; -using Constants = Umbraco.Cms.Core.Constants; -namespace Umbraco.Cms.Infrastructure.Examine +namespace Umbraco.Cms.Infrastructure.Examine; + +public class BackOfficeExamineSearcher : IBackOfficeExamineSearcher { - public class BackOfficeExamineSearcher : IBackOfficeExamineSearcher + private readonly AppCaches _appCaches; + private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor; + private readonly IEntityService _entityService; + private readonly IExamineManager _examineManager; + private readonly ILocalizationService _languageService; + private readonly IPublishedUrlProvider _publishedUrlProvider; + private readonly IUmbracoTreeSearcherFields _treeSearcherFields; + private readonly IUmbracoMapper _umbracoMapper; + + public BackOfficeExamineSearcher( + IExamineManager examineManager, + ILocalizationService languageService, + IBackOfficeSecurityAccessor backOfficeSecurityAccessor, + IEntityService entityService, + IUmbracoTreeSearcherFields treeSearcherFields, + AppCaches appCaches, + IUmbracoMapper umbracoMapper, + IPublishedUrlProvider publishedUrlProvider) { - private readonly IExamineManager _examineManager; - private readonly ILocalizationService _languageService; - private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor; - private readonly IEntityService _entityService; - private readonly IUmbracoTreeSearcherFields _treeSearcherFields; - private readonly AppCaches _appCaches; - private readonly IUmbracoMapper _umbracoMapper; - private readonly IPublishedUrlProvider _publishedUrlProvider; + _examineManager = examineManager; + _languageService = languageService; + _backOfficeSecurityAccessor = backOfficeSecurityAccessor; + _entityService = entityService; + _treeSearcherFields = treeSearcherFields; + _appCaches = appCaches; + _umbracoMapper = umbracoMapper; + _publishedUrlProvider = publishedUrlProvider; + } - public BackOfficeExamineSearcher(IExamineManager examineManager, - ILocalizationService languageService, - IBackOfficeSecurityAccessor backOfficeSecurityAccessor, - IEntityService entityService, - IUmbracoTreeSearcherFields treeSearcherFields, - AppCaches appCaches, - IUmbracoMapper umbracoMapper, - IPublishedUrlProvider publishedUrlProvider) + public IEnumerable Search( + string query, + UmbracoEntityTypes entityType, + int pageSize, + long pageIndex, + out long totalFound, + string? searchFrom = null, + bool ignoreUserStartNodes = false) + { + var sb = new StringBuilder(); + + string type; + var indexName = Constants.UmbracoIndexes.InternalIndexName; + var fields = _treeSearcherFields.GetBackOfficeFields().ToList(); + + ISet fieldsToLoad = new HashSet(_treeSearcherFields.GetBackOfficeFieldsToLoad()); + + // TODO: WE should try to allow passing in a lucene raw query, however we will still need to do some manual string + // manipulation for things like start paths, member types, etc... + //if (Examine.ExamineExtensions.TryParseLuceneQuery(query)) + //{ + + //} + + //special GUID check since if a user searches on one specifically we need to escape it + if (Guid.TryParse(query, out Guid g)) { - _examineManager = examineManager; - _languageService = languageService; - _backOfficeSecurityAccessor = backOfficeSecurityAccessor; - _entityService = entityService; - _treeSearcherFields = treeSearcherFields; - _appCaches = appCaches; - _umbracoMapper = umbracoMapper; - _publishedUrlProvider = publishedUrlProvider; + query = "\"" + g + "\""; } - public IEnumerable Search(string query, UmbracoEntityTypes entityType, int pageSize, long pageIndex, out long totalFound, string? searchFrom = null, bool ignoreUserStartNodes = false) + IUser? currentUser = _backOfficeSecurityAccessor?.BackOfficeSecurity?.CurrentUser; + + switch (entityType) { - var sb = new StringBuilder(); - - string type; - var indexName = Constants.UmbracoIndexes.InternalIndexName; - var fields = _treeSearcherFields.GetBackOfficeFields().ToList(); - - ISet fieldsToLoad = new HashSet(_treeSearcherFields.GetBackOfficeFieldsToLoad()); - - // TODO: WE should try to allow passing in a lucene raw query, however we will still need to do some manual string - // manipulation for things like start paths, member types, etc... - //if (Examine.ExamineExtensions.TryParseLuceneQuery(query)) - //{ - - //} - - //special GUID check since if a user searches on one specifically we need to escape it - if (Guid.TryParse(query, out var g)) - { - query = "\"" + g.ToString() + "\""; - } - - var currentUser = _backOfficeSecurityAccessor?.BackOfficeSecurity?.CurrentUser; - - switch (entityType) - { - case UmbracoEntityTypes.Member: - indexName = Constants.UmbracoIndexes.MembersIndexName; - type = "member"; - fields.AddRange(_treeSearcherFields.GetBackOfficeMembersFields()); - foreach(var field in _treeSearcherFields.GetBackOfficeMembersFieldsToLoad()) - { - fieldsToLoad.Add(field); - } - - if (searchFrom != null && searchFrom != Constants.Conventions.MemberTypes.AllMembersListId && searchFrom.Trim() != "-1") - { - sb.Append("+__NodeTypeAlias:"); - sb.Append(searchFrom); - sb.Append(" "); - } - break; - case UmbracoEntityTypes.Media: - type = "media"; - fields.AddRange(_treeSearcherFields.GetBackOfficeMediaFields()); - foreach (var field in _treeSearcherFields.GetBackOfficeMediaFieldsToLoad()) - { - fieldsToLoad.Add(field); - } - - var allMediaStartNodes = currentUser != null - ? currentUser.CalculateMediaStartNodeIds(_entityService, _appCaches) - : Array.Empty(); - AppendPath(sb, UmbracoObjectTypes.Media, allMediaStartNodes, searchFrom, ignoreUserStartNodes, _entityService); - break; - case UmbracoEntityTypes.Document: - type = "content"; - fields.AddRange(_treeSearcherFields.GetBackOfficeDocumentFields()); - foreach (var field in _treeSearcherFields.GetBackOfficeDocumentFieldsToLoad()) - { - fieldsToLoad.Add(field); - } - var allContentStartNodes = currentUser != null - ? currentUser.CalculateContentStartNodeIds(_entityService, _appCaches) - : Array.Empty(); - AppendPath(sb, UmbracoObjectTypes.Document, allContentStartNodes, searchFrom, ignoreUserStartNodes, _entityService); - break; - default: - throw new NotSupportedException("The " + typeof(BackOfficeExamineSearcher) + " currently does not support searching against object type " + entityType); - } - - if (!_examineManager.TryGetIndex(indexName, out var index)) - throw new InvalidOperationException("No index found by name " + indexName); - - if (!BuildQuery(sb, query, searchFrom, fields, type)) - { - totalFound = 0; - return Enumerable.Empty(); - } - - var result = index.Searcher - .CreateQuery() - .NativeQuery(sb.ToString()) - .SelectFields(fieldsToLoad) - //only return the number of items specified to read up to the amount of records to fill from 0 -> the number of items on the page requested - .Execute(QueryOptions.SkipTake(Convert.ToInt32(pageSize * pageIndex), pageSize)); - - totalFound = result.TotalItemCount; - - return result; - } - - private bool BuildQuery(StringBuilder sb, string query, string? searchFrom, List fields, string type) - { - //build a lucene query: - // the nodeName will be boosted 10x without wildcards - // then nodeName will be matched normally with wildcards - // the rest will be normal without wildcards - - var allLangs = _languageService.GetAllLanguages().Select(x => x.IsoCode.ToLowerInvariant()).ToList(); - - // the chars [*-_] in the query will mess everything up so let's remove those - query = Regex.Replace(query, "[\\*\\-_]", ""); - - //check if text is surrounded by single or double quotes, if so, then exact match - var surroundedByQuotes = Regex.IsMatch(query, "^\".*?\"$") - || Regex.IsMatch(query, "^\'.*?\'$"); - - if (surroundedByQuotes) - { - //strip quotes, escape string, the replace again - query = query.Trim(Constants.CharArrays.DoubleQuoteSingleQuote); - - query = QueryParser.Escape(query); - - //nothing to search - if (searchFrom.IsNullOrWhiteSpace() && query.IsNullOrWhiteSpace()) + case UmbracoEntityTypes.Member: + indexName = Constants.UmbracoIndexes.MembersIndexName; + type = "member"; + fields.AddRange(_treeSearcherFields.GetBackOfficeMembersFields()); + foreach (var field in _treeSearcherFields.GetBackOfficeMembersFieldsToLoad()) { - return false; + fieldsToLoad.Add(field); } - //update the query with the query term - if (query.IsNullOrWhiteSpace() == false) + if (searchFrom != null && searchFrom != Constants.Conventions.MemberTypes.AllMembersListId && + searchFrom.Trim() != "-1") { - //add back the surrounding quotes - query = string.Format("{0}{1}{0}", "\"", query); + sb.Append("+__NodeTypeAlias:"); + sb.Append(searchFrom); + sb.Append(" "); + } - sb.Append("+("); + break; + case UmbracoEntityTypes.Media: + type = "media"; + fields.AddRange(_treeSearcherFields.GetBackOfficeMediaFields()); + foreach (var field in _treeSearcherFields.GetBackOfficeMediaFieldsToLoad()) + { + fieldsToLoad.Add(field); + } - AppendNodeNamePhraseWithBoost(sb, query, allLangs); + var allMediaStartNodes = currentUser != null + ? currentUser.CalculateMediaStartNodeIds(_entityService, _appCaches) + : Array.Empty(); + AppendPath(sb, UmbracoObjectTypes.Media, allMediaStartNodes, searchFrom, ignoreUserStartNodes, _entityService); + break; + case UmbracoEntityTypes.Document: + type = "content"; + fields.AddRange(_treeSearcherFields.GetBackOfficeDocumentFields()); + foreach (var field in _treeSearcherFields.GetBackOfficeDocumentFieldsToLoad()) + { + fieldsToLoad.Add(field); + } - foreach (var f in fields) - { - //additional fields normally - sb.Append(f); - sb.Append(": ("); - sb.Append(query); - sb.Append(") "); - } + var allContentStartNodes = currentUser != null + ? currentUser.CalculateContentStartNodeIds(_entityService, _appCaches) + : Array.Empty(); + AppendPath(sb, UmbracoObjectTypes.Document, allContentStartNodes, searchFrom, ignoreUserStartNodes, _entityService); + break; + default: + throw new NotSupportedException("The " + typeof(BackOfficeExamineSearcher) + + " currently does not support searching against object type " + + entityType); + } + if (!_examineManager.TryGetIndex(indexName, out IIndex? index)) + { + throw new InvalidOperationException("No index found by name " + indexName); + } + + if (!BuildQuery(sb, query, searchFrom, fields, type)) + { + totalFound = 0; + return Enumerable.Empty(); + } + + ISearchResults? result = index.Searcher + .CreateQuery() + .NativeQuery(sb.ToString()) + .SelectFields(fieldsToLoad) + //only return the number of items specified to read up to the amount of records to fill from 0 -> the number of items on the page requested + .Execute(QueryOptions.SkipTake(Convert.ToInt32(pageSize * pageIndex), pageSize)); + + totalFound = result.TotalItemCount; + + return result; + } + + private bool BuildQuery(StringBuilder sb, string query, string? searchFrom, List fields, string type) + { + //build a lucene query: + // the nodeName will be boosted 10x without wildcards + // then nodeName will be matched normally with wildcards + // the rest will be normal without wildcards + + var allLangs = _languageService.GetAllLanguages().Select(x => x.IsoCode.ToLowerInvariant()).ToList(); + + // the chars [*-_] in the query will mess everything up so let's remove those + query = Regex.Replace(query, "[\\*\\-_]", string.Empty); + + //check if text is surrounded by single or double quotes, if so, then exact match + var surroundedByQuotes = Regex.IsMatch(query, "^\".*?\"$") + || Regex.IsMatch(query, "^\'.*?\'$"); + + if (surroundedByQuotes) + { + //strip quotes, escape string, the replace again + query = query.Trim(Constants.CharArrays.DoubleQuoteSingleQuote); + + query = QueryParserBase.Escape(query); + + //nothing to search + if (searchFrom.IsNullOrWhiteSpace() && query.IsNullOrWhiteSpace()) + { + return false; + } + + //update the query with the query term + if (query.IsNullOrWhiteSpace() == false) + { + //add back the surrounding quotes + query = string.Format("{0}{1}{0}", "\"", query); + + sb.Append("+("); + + AppendNodeNamePhraseWithBoost(sb, query, allLangs); + + foreach (var f in fields) + { + //additional fields normally + sb.Append(f); + sb.Append(": ("); + sb.Append(query); sb.Append(") "); } + + sb.Append(") "); } - else + } + else + { + var trimmed = query.Trim(Constants.CharArrays.DoubleQuoteSingleQuote); + + //nothing to search + if (searchFrom.IsNullOrWhiteSpace() && trimmed.IsNullOrWhiteSpace()) { - var trimmed = query.Trim(Constants.CharArrays.DoubleQuoteSingleQuote); + return false; + } - //nothing to search - if (searchFrom.IsNullOrWhiteSpace() && trimmed.IsNullOrWhiteSpace()) + //update the query with the query term + if (trimmed.IsNullOrWhiteSpace() == false) + { + query = QueryParserBase.Escape(query); + + var querywords = query.Split(Constants.CharArrays.Space, StringSplitOptions.RemoveEmptyEntries); + + sb.Append("+("); + + AppendNodeNameExactWithBoost(sb, query, allLangs); + + AppendNodeNameWithWildcards(sb, querywords, allLangs); + + foreach (var f in fields) { - return false; - } + var queryWordsReplaced = new string[querywords.Length]; - //update the query with the query term - if (trimmed.IsNullOrWhiteSpace() == false) - { - query = QueryParser.Escape(query); - - var querywords = query.Split(Constants.CharArrays.Space, StringSplitOptions.RemoveEmptyEntries); - - sb.Append("+("); - - AppendNodeNameExactWithBoost(sb, query, allLangs); - - AppendNodeNameWithWildcards(sb, querywords, allLangs); - - foreach (var f in fields) + // when searching file names containing hyphens we need to replace the hyphens with spaces + if (f.Equals(UmbracoExamineFieldNames.UmbracoFileFieldName)) { - var queryWordsReplaced = new string[querywords.Length]; - - // when searching file names containing hyphens we need to replace the hyphens with spaces - if (f.Equals(UmbracoExamineFieldNames.UmbracoFileFieldName)) + for (var index = 0; index < querywords.Length; index++) { - for (var index = 0; index < querywords.Length; index++) - { - queryWordsReplaced[index] = querywords[index].Replace("\\-", " ").Replace("_", " ").Trim(" "); - } + queryWordsReplaced[index] = + querywords[index].Replace("\\-", " ").Replace("_", " ").Trim(" "); } - else - { - queryWordsReplaced = querywords; - } - - //additional fields normally - sb.Append(f); - sb.Append(":"); - sb.Append("("); - foreach (var w in queryWordsReplaced) - { - sb.Append(w.ToLower()); - sb.Append("* "); - } - sb.Append(")"); - sb.Append(" "); + } + else + { + queryWordsReplaced = querywords; } - sb.Append(") "); + //additional fields normally + sb.Append(f); + sb.Append(":"); + sb.Append("("); + foreach (var w in queryWordsReplaced) + { + sb.Append(w.ToLower()); + sb.Append("* "); + } + + sb.Append(")"); + sb.Append(" "); } + + sb.Append(") "); } - - //must match index type - sb.Append("+__IndexType:"); - sb.Append(type); - - return true; } - private void AppendNodeNamePhraseWithBoost(StringBuilder sb, string query, IEnumerable allLangs) + //must match index type + sb.Append("+__IndexType:"); + sb.Append(type); + + return true; + } + + private void AppendNodeNamePhraseWithBoost(StringBuilder sb, string query, IEnumerable allLangs) + { + //node name exactly boost x 10 + sb.Append("nodeName: ("); + sb.Append(query.ToLower()); + sb.Append(")^10.0 "); + + //also search on all variant node names + foreach (var lang in allLangs) { //node name exactly boost x 10 - sb.Append("nodeName: ("); + sb.Append($"nodeName_{lang}: ("); sb.Append(query.ToLower()); sb.Append(")^10.0 "); - - //also search on all variant node names - foreach (var lang in allLangs) - { - //node name exactly boost x 10 - sb.Append($"nodeName_{lang}: ("); - sb.Append(query.ToLower()); - sb.Append(")^10.0 "); - } } + } - private void AppendNodeNameExactWithBoost(StringBuilder sb, string query, IEnumerable allLangs) + private void AppendNodeNameExactWithBoost(StringBuilder sb, string query, IEnumerable allLangs) + { + //node name exactly boost x 10 + sb.Append("nodeName:"); + sb.Append("\""); + sb.Append(query.ToLower()); + sb.Append("\""); + sb.Append("^10.0 "); + //also search on all variant node names + foreach (var lang in allLangs) { //node name exactly boost x 10 - sb.Append("nodeName:"); + sb.Append($"nodeName_{lang}:"); sb.Append("\""); sb.Append(query.ToLower()); sb.Append("\""); sb.Append("^10.0 "); - //also search on all variant node names - foreach (var lang in allLangs) - { - //node name exactly boost x 10 - sb.Append($"nodeName_{lang}:"); - sb.Append("\""); - sb.Append(query.ToLower()); - sb.Append("\""); - sb.Append("^10.0 "); - } + } + } + + private void AppendNodeNameWithWildcards(StringBuilder sb, string[] querywords, IEnumerable allLangs) + { + //node name normally with wildcards + sb.Append("nodeName:"); + sb.Append("("); + foreach (var w in querywords) + { + sb.Append(w.ToLower()); + sb.Append("* "); } - private void AppendNodeNameWithWildcards(StringBuilder sb, string[] querywords, IEnumerable allLangs) + sb.Append(") "); + //also search on all variant node names + foreach (var lang in allLangs) { //node name normally with wildcards - sb.Append("nodeName:"); + sb.Append($"nodeName_{lang}:"); sb.Append("("); foreach (var w in querywords) { sb.Append(w.ToLower()); sb.Append("* "); } + sb.Append(") "); - //also search on all variant node names - foreach (var lang in allLangs) - { - //node name normally with wildcards - sb.Append($"nodeName_{lang}:"); - sb.Append("("); - foreach (var w in querywords) - { - sb.Append(w.ToLower()); - sb.Append("* "); - } - sb.Append(") "); - } - } - - private void AppendPath(StringBuilder sb, UmbracoObjectTypes objectType, int[]? startNodeIds, string? searchFrom, bool ignoreUserStartNodes, IEntityService entityService) - { - if (sb == null) throw new ArgumentNullException(nameof(sb)); - if (entityService == null) throw new ArgumentNullException(nameof(entityService)); - - UdiParser.TryParse(searchFrom, true, out var udi); - searchFrom = udi == null ? searchFrom : entityService.GetId(udi).Result.ToString(); - - var entityPath = int.TryParse(searchFrom, NumberStyles.Integer, CultureInfo.InvariantCulture, out var searchFromId) && searchFromId > 0 - ? entityService.GetAllPaths(objectType, searchFromId).FirstOrDefault() - : null; - if (entityPath != null) - { - // find... only what's underneath - sb.Append("+__Path:"); - AppendPath(sb, entityPath.Path, false); - sb.Append(" "); - } - else if (startNodeIds?.Length == 0) - { - // make sure we don't find anything - sb.Append("+__Path:none "); - } - else if (startNodeIds?.Contains(-1) == false && ignoreUserStartNodes == false) // -1 = no restriction - { - var entityPaths = entityService.GetAllPaths(objectType, startNodeIds); - - // for each start node, find the start node, and what's underneath - // +__Path:(-1*,1234 -1*,1234,* -1*,5678 -1*,5678,* ...) - sb.Append("+__Path:("); - var first = true; - foreach (var ep in entityPaths) - { - if (first) - first = false; - else - sb.Append(" "); - AppendPath(sb, ep.Path, true); - } - sb.Append(") "); - } - } - - private void AppendPath(StringBuilder sb, string path, bool includeThisNode) - { - path = path.Replace("-", "\\-").Replace(",", "\\,"); - if (includeThisNode) - { - sb.Append(path); - sb.Append(" "); - } - sb.Append(path); - sb.Append("\\,*"); } } + + private void AppendPath(StringBuilder sb, UmbracoObjectTypes objectType, int[]? startNodeIds, string? searchFrom, bool ignoreUserStartNodes, IEntityService entityService) + { + if (sb == null) + { + throw new ArgumentNullException(nameof(sb)); + } + + if (entityService == null) + { + throw new ArgumentNullException(nameof(entityService)); + } + + UdiParser.TryParse(searchFrom, true, out Udi? udi); + searchFrom = udi == null ? searchFrom : entityService.GetId(udi).Result.ToString(); + + TreeEntityPath? entityPath = + int.TryParse(searchFrom, NumberStyles.Integer, CultureInfo.InvariantCulture, out var searchFromId) && + searchFromId > 0 + ? entityService.GetAllPaths(objectType, searchFromId).FirstOrDefault() + : null; + if (entityPath != null) + { + // find... only what's underneath + sb.Append("+__Path:"); + AppendPath(sb, entityPath.Path, false); + sb.Append(" "); + } + else if (startNodeIds?.Length == 0) + { + // make sure we don't find anything + sb.Append("+__Path:none "); + } + else if (startNodeIds?.Contains(-1) == false && ignoreUserStartNodes == false) // -1 = no restriction + { + IEnumerable entityPaths = entityService.GetAllPaths(objectType, startNodeIds); + + // for each start node, find the start node, and what's underneath + // +__Path:(-1*,1234 -1*,1234,* -1*,5678 -1*,5678,* ...) + sb.Append("+__Path:("); + var first = true; + foreach (TreeEntityPath ep in entityPaths) + { + if (first) + { + first = false; + } + else + { + sb.Append(" "); + } + + AppendPath(sb, ep.Path, true); + } + + sb.Append(") "); + } + } + + private void AppendPath(StringBuilder sb, string path, bool includeThisNode) + { + path = path.Replace("-", "\\-").Replace(",", "\\,"); + if (includeThisNode) + { + sb.Append(path); + sb.Append(" "); + } + + sb.Append(path); + sb.Append("\\,*"); + } } diff --git a/src/Umbraco.Examine.Lucene/ConfigurationEnabledDirectoryFactory.cs b/src/Umbraco.Examine.Lucene/ConfigurationEnabledDirectoryFactory.cs index 108aad06bc..f58dffacac 100644 --- a/src/Umbraco.Examine.Lucene/ConfigurationEnabledDirectoryFactory.cs +++ b/src/Umbraco.Examine.Lucene/ConfigurationEnabledDirectoryFactory.cs @@ -1,65 +1,63 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System; -using System.IO; using Examine; using Examine.Lucene.Directories; using Examine.Lucene.Providers; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; using Umbraco.Cms.Core.Configuration.Models; +using Directory = Lucene.Net.Store.Directory; -namespace Umbraco.Cms.Infrastructure.Examine +namespace Umbraco.Cms.Infrastructure.Examine; + +/// +/// An Examine directory factory implementation based on configured values +/// +public class ConfigurationEnabledDirectoryFactory : DirectoryFactoryBase { - /// - /// An Examine directory factory implementation based on configured values - /// - public class ConfigurationEnabledDirectoryFactory : DirectoryFactoryBase + private readonly IApplicationRoot _applicationRoot; + private readonly IServiceProvider _services; + private readonly IndexCreatorSettings _settings; + private IDirectoryFactory? _directoryFactory; + + public ConfigurationEnabledDirectoryFactory( + IServiceProvider services, + IOptions settings, + IApplicationRoot applicationRoot) { - private readonly IServiceProvider _services; - private readonly IApplicationRoot _applicationRoot; - private readonly IndexCreatorSettings _settings; - private IDirectoryFactory? _directoryFactory; + _services = services; + _applicationRoot = applicationRoot; + _settings = settings.Value; + } - public ConfigurationEnabledDirectoryFactory( - IServiceProvider services, - IOptions settings, - IApplicationRoot applicationRoot) + protected override Directory CreateDirectory(LuceneIndex luceneIndex, bool forceUnlock) + { + _directoryFactory = CreateFactory(); + return _directoryFactory.CreateDirectory(luceneIndex, forceUnlock); + } + + /// + /// Creates a directory factory based on the configured value and ensures that + /// + private IDirectoryFactory CreateFactory() + { + DirectoryInfo dirInfo = _applicationRoot.ApplicationRoot; + + if (!dirInfo.Exists) { - _services = services; - _applicationRoot = applicationRoot; - _settings = settings.Value; + System.IO.Directory.CreateDirectory(dirInfo.FullName); } - protected override Lucene.Net.Store.Directory CreateDirectory(LuceneIndex luceneIndex, bool forceUnlock) + switch (_settings.LuceneDirectoryFactory) { - _directoryFactory = CreateFactory(); - return _directoryFactory.CreateDirectory(luceneIndex, forceUnlock); - } - - /// - /// Creates a directory factory based on the configured value and ensures that - /// - private IDirectoryFactory CreateFactory() - { - DirectoryInfo dirInfo = _applicationRoot.ApplicationRoot; - - if (!dirInfo.Exists) - { - Directory.CreateDirectory(dirInfo.FullName); - } - - switch (_settings.LuceneDirectoryFactory) - { - case LuceneDirectoryFactory.SyncedTempFileSystemDirectoryFactory: - return _services.GetRequiredService(); - case LuceneDirectoryFactory.TempFileSystemDirectoryFactory: - return _services.GetRequiredService(); - case LuceneDirectoryFactory.Default: - default: - return _services.GetRequiredService(); - } + case LuceneDirectoryFactory.SyncedTempFileSystemDirectoryFactory: + return _services.GetRequiredService(); + case LuceneDirectoryFactory.TempFileSystemDirectoryFactory: + return _services.GetRequiredService(); + case LuceneDirectoryFactory.Default: + default: + return _services.GetRequiredService(); } } } diff --git a/src/Umbraco.Examine.Lucene/DependencyInjection/ConfigureIndexOptions.cs b/src/Umbraco.Examine.Lucene/DependencyInjection/ConfigureIndexOptions.cs index 48c9fbb5ff..e6306ab444 100644 --- a/src/Umbraco.Examine.Lucene/DependencyInjection/ConfigureIndexOptions.cs +++ b/src/Umbraco.Examine.Lucene/DependencyInjection/ConfigureIndexOptions.cs @@ -1,4 +1,3 @@ -using System; using Examine; using Examine.Lucene; using Examine.Lucene.Analyzers; @@ -8,58 +7,55 @@ using Microsoft.Extensions.Options; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Configuration.Models; -namespace Umbraco.Cms.Infrastructure.Examine.DependencyInjection +namespace Umbraco.Cms.Infrastructure.Examine.DependencyInjection; + +/// +/// Configures the index options to construct the Examine indexes +/// +public sealed class ConfigureIndexOptions : IConfigureNamedOptions { - /// - /// Configures the index options to construct the Examine indexes - /// - public sealed class ConfigureIndexOptions : IConfigureNamedOptions + private readonly IndexCreatorSettings _settings; + private readonly IUmbracoIndexConfig _umbracoIndexConfig; + + public ConfigureIndexOptions( + IUmbracoIndexConfig umbracoIndexConfig, + IOptions settings) { - private readonly IUmbracoIndexConfig _umbracoIndexConfig; - private readonly IndexCreatorSettings _settings; - - public ConfigureIndexOptions( - IUmbracoIndexConfig umbracoIndexConfig, - IOptions settings) - { - _umbracoIndexConfig = umbracoIndexConfig; - _settings = settings.Value; - } - - public void Configure(string name, LuceneDirectoryIndexOptions options) - { - switch (name) - { - case Constants.UmbracoIndexes.InternalIndexName: - options.Analyzer = new CultureInvariantWhitespaceAnalyzer(); - options.Validator = _umbracoIndexConfig.GetContentValueSetValidator(); - options.FieldDefinitions = new UmbracoFieldDefinitionCollection(); - break; - case Constants.UmbracoIndexes.ExternalIndexName: - options.Analyzer = new StandardAnalyzer(LuceneInfo.CurrentVersion); - options.Validator = _umbracoIndexConfig.GetPublishedContentValueSetValidator(); - options.FieldDefinitions = new UmbracoFieldDefinitionCollection(); - break; - case Constants.UmbracoIndexes.MembersIndexName: - options.Analyzer = new CultureInvariantWhitespaceAnalyzer(); - options.Validator = _umbracoIndexConfig.GetMemberValueSetValidator(); - options.FieldDefinitions = new UmbracoFieldDefinitionCollection(); - break; - } - - // ensure indexes are unlocked on startup - options.UnlockIndex = true; - - if (_settings.LuceneDirectoryFactory == LuceneDirectoryFactory.SyncedTempFileSystemDirectoryFactory) - { - // if this directory factory is enabled then a snapshot deletion policy is required - options.IndexDeletionPolicy = new SnapshotDeletionPolicy(new KeepOnlyLastCommitDeletionPolicy()); - } - - - } - - public void Configure(LuceneDirectoryIndexOptions options) - => throw new NotImplementedException("This is never called and is just part of the interface"); + _umbracoIndexConfig = umbracoIndexConfig; + _settings = settings.Value; } + + public void Configure(string name, LuceneDirectoryIndexOptions options) + { + switch (name) + { + case Constants.UmbracoIndexes.InternalIndexName: + options.Analyzer = new CultureInvariantWhitespaceAnalyzer(); + options.Validator = _umbracoIndexConfig.GetContentValueSetValidator(); + options.FieldDefinitions = new UmbracoFieldDefinitionCollection(); + break; + case Constants.UmbracoIndexes.ExternalIndexName: + options.Analyzer = new StandardAnalyzer(LuceneInfo.CurrentVersion); + options.Validator = _umbracoIndexConfig.GetPublishedContentValueSetValidator(); + options.FieldDefinitions = new UmbracoFieldDefinitionCollection(); + break; + case Constants.UmbracoIndexes.MembersIndexName: + options.Analyzer = new CultureInvariantWhitespaceAnalyzer(); + options.Validator = _umbracoIndexConfig.GetMemberValueSetValidator(); + options.FieldDefinitions = new UmbracoFieldDefinitionCollection(); + break; + } + + // ensure indexes are unlocked on startup + options.UnlockIndex = true; + + if (_settings.LuceneDirectoryFactory == LuceneDirectoryFactory.SyncedTempFileSystemDirectoryFactory) + { + // if this directory factory is enabled then a snapshot deletion policy is required + options.IndexDeletionPolicy = new SnapshotDeletionPolicy(new KeepOnlyLastCommitDeletionPolicy()); + } + } + + public void Configure(LuceneDirectoryIndexOptions options) + => throw new NotImplementedException("This is never called and is just part of the interface"); } diff --git a/src/Umbraco.Examine.Lucene/DependencyInjection/UmbracoBuilderExtensions.cs b/src/Umbraco.Examine.Lucene/DependencyInjection/UmbracoBuilderExtensions.cs index 8eafde1a38..dac930964e 100644 --- a/src/Umbraco.Examine.Lucene/DependencyInjection/UmbracoBuilderExtensions.cs +++ b/src/Umbraco.Examine.Lucene/DependencyInjection/UmbracoBuilderExtensions.cs @@ -3,38 +3,39 @@ using Examine.Lucene.Directories; using Microsoft.Extensions.DependencyInjection; using Umbraco.Cms.Core; using Umbraco.Cms.Core.DependencyInjection; -using Umbraco.Cms.Infrastructure.DependencyInjection; -namespace Umbraco.Cms.Infrastructure.Examine.DependencyInjection +namespace Umbraco.Cms.Infrastructure.Examine.DependencyInjection; + +public static class UmbracoBuilderExtensions { - public static class UmbracoBuilderExtensions + /// + /// Adds the Examine indexes for Umbraco + /// + /// + /// + public static IUmbracoBuilder AddExamineIndexes(this IUmbracoBuilder umbracoBuilder) { - /// - /// Adds the Examine indexes for Umbraco - /// - /// - /// - public static IUmbracoBuilder AddExamineIndexes(this IUmbracoBuilder umbracoBuilder) - { - IServiceCollection services = umbracoBuilder.Services; + IServiceCollection services = umbracoBuilder.Services; - services.AddSingleton(); - services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); - services.AddExamine(); + services.AddExamine(); - // Create the indexes - services - .AddExamineLuceneIndex(Constants.UmbracoIndexes.InternalIndexName) - .AddExamineLuceneIndex(Constants.UmbracoIndexes.ExternalIndexName) - .AddExamineLuceneIndex(Constants.UmbracoIndexes.MembersIndexName) - .ConfigureOptions(); + // Create the indexes + services + .AddExamineLuceneIndex(Constants.UmbracoIndexes + .InternalIndexName) + .AddExamineLuceneIndex(Constants.UmbracoIndexes + .ExternalIndexName) + .AddExamineLuceneIndex(Constants.UmbracoIndexes + .MembersIndexName) + .ConfigureOptions(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); - return umbracoBuilder; - } + return umbracoBuilder; } } diff --git a/src/Umbraco.Examine.Lucene/Extensions/ExamineExtensions.cs b/src/Umbraco.Examine.Lucene/Extensions/ExamineExtensions.cs index 02a9ea85e0..99a31a0513 100644 --- a/src/Umbraco.Examine.Lucene/Extensions/ExamineExtensions.cs +++ b/src/Umbraco.Examine.Lucene/Extensions/ExamineExtensions.cs @@ -1,75 +1,66 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System; using System.Diagnostics.CodeAnalysis; -using System.Linq; -using System.Threading; using Examine; using Examine.Lucene.Providers; using Lucene.Net.Analysis.Core; -using Lucene.Net.Index; using Lucene.Net.QueryParsers.Classic; -using Microsoft.Extensions.Logging; -using Umbraco.Cms.Core.Composing; -using Umbraco.Cms.Core.Configuration.Models; -using Umbraco.Cms.Core.Runtime; +using Lucene.Net.Search; using Umbraco.Cms.Infrastructure.Examine; -namespace Umbraco.Extensions -{ - /// - /// Extension methods for the LuceneIndex - /// - public static class ExamineExtensions - { - internal static bool TryParseLuceneQuery(string query) - { - // TODO: I'd assume there would be a more strict way to parse the query but not that i can find yet, for now we'll - // also do this rudimentary check - if (!query.Contains(":")) - { - return false; - } +namespace Umbraco.Extensions; - try +/// +/// Extension methods for the LuceneIndex +/// +public static class ExamineExtensions +{ + internal static bool TryParseLuceneQuery(string query) + { + // TODO: I'd assume there would be a more strict way to parse the query but not that i can find yet, for now we'll + // also do this rudimentary check + if (!query.Contains(":")) + { + return false; + } + + try + { + //This will pass with a plain old string without any fields, need to figure out a way to have it properly parse + Query? parsed = new QueryParser(LuceneInfo.CurrentVersion, UmbracoExamineFieldNames.NodeNameFieldName, new KeywordAnalyzer()).Parse(query); + return true; + } + catch (ParseException) + { + return false; + } + catch (Exception) + { + return false; + } + } + + /// + /// Checks if the index can be read/opened + /// + /// + /// The exception returned if there was an error + /// + public static bool IsHealthy(this LuceneIndex indexer, [MaybeNullWhen(true)] out Exception ex) + { + try + { + using (indexer.IndexWriter.IndexWriter.GetReader(false)) { - //This will pass with a plain old string without any fields, need to figure out a way to have it properly parse - var parsed = new QueryParser(LuceneInfo.CurrentVersion, UmbracoExamineFieldNames.NodeNameFieldName, new KeywordAnalyzer()).Parse(query); + ex = null; return true; } - catch (ParseException) - { - return false; - } - catch (Exception) - { - return false; - } } - - /// - /// Checks if the index can be read/opened - /// - /// - /// The exception returned if there was an error - /// - public static bool IsHealthy(this LuceneIndex indexer, [MaybeNullWhen(true)] out Exception ex) + catch (Exception e) { - try - { - using (indexer.IndexWriter.IndexWriter.GetReader(false)) - { - ex = null; - return true; - } - } - catch (Exception e) - { - ex = e; - return false; - } + ex = e; + return false; } - } } diff --git a/src/Umbraco.Examine.Lucene/LuceneIndexDiagnostics.cs b/src/Umbraco.Examine.Lucene/LuceneIndexDiagnostics.cs index 4d7ec2f23d..00f5be31a3 100644 --- a/src/Umbraco.Examine.Lucene/LuceneIndexDiagnostics.cs +++ b/src/Umbraco.Examine.Lucene/LuceneIndexDiagnostics.cs @@ -1,9 +1,6 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System; -using System.Collections.Generic; -using System.Threading.Tasks; using Examine.Lucene; using Examine.Lucene.Providers; using Lucene.Net.Store; @@ -14,81 +11,77 @@ using Umbraco.Cms.Core.Hosting; using Umbraco.Extensions; using Directory = Lucene.Net.Store.Directory; -namespace Umbraco.Cms.Infrastructure.Examine +namespace Umbraco.Cms.Infrastructure.Examine; + +public class LuceneIndexDiagnostics : IIndexDiagnostics { - public class LuceneIndexDiagnostics : IIndexDiagnostics + private readonly IHostingEnvironment _hostingEnvironment; + private readonly LuceneDirectoryIndexOptions? _indexOptions; + + public LuceneIndexDiagnostics( + LuceneIndex index, + ILogger logger, + IHostingEnvironment hostingEnvironment, + IOptionsMonitor? indexOptions) { - private readonly IHostingEnvironment _hostingEnvironment; - private readonly LuceneDirectoryIndexOptions? _indexOptions; - - public LuceneIndexDiagnostics( - LuceneIndex index, - ILogger logger, - IHostingEnvironment hostingEnvironment, - IOptionsMonitor? indexOptions) + _hostingEnvironment = hostingEnvironment; + if (indexOptions != null) { - _hostingEnvironment = hostingEnvironment; - if (indexOptions != null) - { - _indexOptions = indexOptions.Get(index.Name); + _indexOptions = indexOptions.Get(index.Name); + } + Index = index; + Logger = logger; + } + + public LuceneIndex Index { get; } + public ILogger Logger { get; } + + + public Attempt IsHealthy() + { + var isHealthy = Index.IsHealthy(out Exception? indexError); + return isHealthy ? Attempt.Succeed() : Attempt.Fail(indexError?.Message); + } + + public long GetDocumentCount() => Index.GetDocumentCount(); + + public IEnumerable GetFieldNames() => Index.GetFieldNames(); + + public virtual IReadOnlyDictionary Metadata + { + get + { + Directory luceneDir = Index.GetLuceneDirectory(); + var d = new Dictionary + { + [nameof(UmbracoExamineIndex.CommitCount)] = Index.CommitCount, + [nameof(UmbracoExamineIndex.DefaultAnalyzer)] = Index.DefaultAnalyzer.GetType().Name, + ["LuceneDirectory"] = luceneDir.GetType().Name + }; + + if (luceneDir is FSDirectory fsDir) + { + var rootDir = _hostingEnvironment.ApplicationPhysicalPath; + d["LuceneIndexFolder"] = fsDir.Directory.ToString().ToLowerInvariant() + .TrimStart(rootDir.ToLowerInvariant()).Replace("\\", " /").EnsureStartsWith('/'); } - Index = index; - Logger = logger; - } - public LuceneIndex Index { get; } - public ILogger Logger { get; } - - - - public Attempt IsHealthy() - { - var isHealthy = Index.IsHealthy(out var indexError); - return isHealthy ? Attempt.Succeed() : Attempt.Fail(indexError?.Message); - } - - public long GetDocumentCount() => Index.GetDocumentCount(); - - public IEnumerable GetFieldNames() => Index.GetFieldNames(); - - public virtual IReadOnlyDictionary Metadata - { - get + if (_indexOptions != null) { - Directory luceneDir = Index.GetLuceneDirectory(); - var d = new Dictionary + if (_indexOptions.DirectoryFactory != null) { - [nameof(UmbracoExamineIndex.CommitCount)] = Index.CommitCount, - [nameof(UmbracoExamineIndex.DefaultAnalyzer)] = Index.DefaultAnalyzer.GetType().Name, - ["LuceneDirectory"] = luceneDir.GetType().Name - }; - - if (luceneDir is FSDirectory fsDir) - { - - var rootDir = _hostingEnvironment.ApplicationPhysicalPath; - d["LuceneIndexFolder"] = fsDir.Directory.ToString().ToLowerInvariant().TrimStart(rootDir.ToLowerInvariant()).Replace("\\", " /").EnsureStartsWith('/'); + d[nameof(LuceneDirectoryIndexOptions.DirectoryFactory)] = _indexOptions.DirectoryFactory.GetType(); } - if (_indexOptions != null) + if (_indexOptions.IndexDeletionPolicy != null) { - if (_indexOptions.DirectoryFactory != null) - { - d[nameof(LuceneDirectoryIndexOptions.DirectoryFactory)] = _indexOptions.DirectoryFactory.GetType(); - } - - if (_indexOptions.IndexDeletionPolicy != null) - { - d[nameof(LuceneDirectoryIndexOptions.IndexDeletionPolicy)] = _indexOptions.IndexDeletionPolicy.GetType(); - } - + d[nameof(LuceneDirectoryIndexOptions.IndexDeletionPolicy)] = + _indexOptions.IndexDeletionPolicy.GetType(); } - - return d; } + + return d; } - - } } diff --git a/src/Umbraco.Examine.Lucene/LuceneIndexDiagnosticsFactory.cs b/src/Umbraco.Examine.Lucene/LuceneIndexDiagnosticsFactory.cs index fb8b082d15..971e515228 100644 --- a/src/Umbraco.Examine.Lucene/LuceneIndexDiagnosticsFactory.cs +++ b/src/Umbraco.Examine.Lucene/LuceneIndexDiagnosticsFactory.cs @@ -1,52 +1,49 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System; using Examine; -using Examine.Lucene; using Examine.Lucene.Providers; -using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; using Umbraco.Cms.Core.Hosting; -namespace Umbraco.Cms.Infrastructure.Examine +namespace Umbraco.Cms.Infrastructure.Examine; + +/// +/// Implementation of which returns +/// for lucene based indexes that don't have an implementation else fallsback to the default +/// implementation. +/// +public class LuceneIndexDiagnosticsFactory : IndexDiagnosticsFactory { - /// - /// Implementation of which returns - /// for lucene based indexes that don't have an implementation else fallsback to the default implementation. - /// - public class LuceneIndexDiagnosticsFactory : IndexDiagnosticsFactory + private readonly IHostingEnvironment _hostingEnvironment; + private readonly ILoggerFactory _loggerFactory; + + public LuceneIndexDiagnosticsFactory( + ILoggerFactory loggerFactory, + IHostingEnvironment hostingEnvironment) { - private readonly ILoggerFactory _loggerFactory; - private readonly IHostingEnvironment _hostingEnvironment; + _loggerFactory = loggerFactory; + _hostingEnvironment = hostingEnvironment; + } - public LuceneIndexDiagnosticsFactory( - ILoggerFactory loggerFactory, - IHostingEnvironment hostingEnvironment) + public override IIndexDiagnostics Create(IIndex index) + { + if (!(index is IIndexDiagnostics indexDiag)) { - _loggerFactory = loggerFactory; - _hostingEnvironment = hostingEnvironment; - } - - public override IIndexDiagnostics Create(IIndex index) - { - if (!(index is IIndexDiagnostics indexDiag)) + if (index is LuceneIndex luceneIndex) { - if (index is LuceneIndex luceneIndex) - { - indexDiag = new LuceneIndexDiagnostics( - luceneIndex, - _loggerFactory.CreateLogger(), - _hostingEnvironment, - null); - } - else - { - indexDiag = base.Create(index); - } + indexDiag = new LuceneIndexDiagnostics( + luceneIndex, + _loggerFactory.CreateLogger(), + _hostingEnvironment, + null); + } + else + { + indexDiag = base.Create(index); } - return indexDiag; } + + return indexDiag; } } diff --git a/src/Umbraco.Examine.Lucene/LuceneRAMDirectoryFactory.cs b/src/Umbraco.Examine.Lucene/LuceneRAMDirectoryFactory.cs index 1c7127b9d5..84c1b3c4e4 100644 --- a/src/Umbraco.Examine.Lucene/LuceneRAMDirectoryFactory.cs +++ b/src/Umbraco.Examine.Lucene/LuceneRAMDirectoryFactory.cs @@ -1,30 +1,21 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System; -using System.IO; -using System.Threading; using Examine.Lucene.Directories; using Examine.Lucene.Providers; using Lucene.Net.Store; using Directory = Lucene.Net.Store.Directory; -namespace Umbraco.Cms.Infrastructure.Examine +namespace Umbraco.Cms.Infrastructure.Examine; + +public class LuceneRAMDirectoryFactory : DirectoryFactoryBase { - public class LuceneRAMDirectoryFactory : DirectoryFactoryBase + protected override Directory CreateDirectory(LuceneIndex luceneIndex, bool forceUnlock) + => new RandomIdRAMDirectory(); + + private class RandomIdRAMDirectory : RAMDirectory { - - public LuceneRAMDirectoryFactory() - { - } - - protected override Directory CreateDirectory(LuceneIndex luceneIndex, bool forceUnlock) - => new RandomIdRAMDirectory(); - - private class RandomIdRAMDirectory : RAMDirectory - { - private readonly string _lockId = Guid.NewGuid().ToString(); - public override string GetLockID() => _lockId; - } + private readonly string _lockId = Guid.NewGuid().ToString(); + public override string GetLockID() => _lockId; } } diff --git a/src/Umbraco.Examine.Lucene/NoPrefixSimpleFsLockFactory.cs b/src/Umbraco.Examine.Lucene/NoPrefixSimpleFsLockFactory.cs index ed6f47c882..e10374387e 100644 --- a/src/Umbraco.Examine.Lucene/NoPrefixSimpleFsLockFactory.cs +++ b/src/Umbraco.Examine.Lucene/NoPrefixSimpleFsLockFactory.cs @@ -1,29 +1,26 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System.IO; using Lucene.Net.Store; -namespace Umbraco.Cms.Infrastructure.Examine +namespace Umbraco.Cms.Infrastructure.Examine; + +/// +/// A custom that ensures a prefixless lock prefix +/// +/// +/// This is a work around for the Lucene APIs. By default Lucene will use a null prefix however when we set a custom +/// lock factory the null prefix is overwritten. +/// +public class NoPrefixSimpleFsLockFactory : SimpleFSLockFactory { - - /// - /// A custom that ensures a prefixless lock prefix - /// - /// - /// This is a work around for the Lucene APIs. By default Lucene will use a null prefix however when we set a custom - /// lock factory the null prefix is overwritten. - /// - public class NoPrefixSimpleFsLockFactory : SimpleFSLockFactory + public NoPrefixSimpleFsLockFactory(DirectoryInfo lockDir) : base(lockDir) { - public NoPrefixSimpleFsLockFactory(DirectoryInfo lockDir) : base(lockDir) - { - } + } - public override string LockPrefix - { - get => base.LockPrefix; - set => base.LockPrefix = null; //always set to null - } + public override string LockPrefix + { + get => base.LockPrefix; + set => base.LockPrefix = null; //always set to null } } diff --git a/src/Umbraco.Examine.Lucene/UmbracoApplicationRoot.cs b/src/Umbraco.Examine.Lucene/UmbracoApplicationRoot.cs index e99f986176..d27d673a1f 100644 --- a/src/Umbraco.Examine.Lucene/UmbracoApplicationRoot.cs +++ b/src/Umbraco.Examine.Lucene/UmbracoApplicationRoot.cs @@ -1,23 +1,22 @@ -using System.IO; using Examine; +using Umbraco.Cms.Core; using Umbraco.Cms.Core.Hosting; -namespace Umbraco.Cms.Infrastructure.Examine +namespace Umbraco.Cms.Infrastructure.Examine; + +/// +/// Sets the Examine to be ExamineIndexes sub directory of the Umbraco TEMP folder +/// +public class UmbracoApplicationRoot : IApplicationRoot { - /// - /// Sets the Examine to be ExamineIndexes sub directory of the Umbraco TEMP folder - /// - public class UmbracoApplicationRoot : IApplicationRoot - { - private readonly IHostingEnvironment _hostingEnvironment; + private readonly IHostingEnvironment _hostingEnvironment; - public UmbracoApplicationRoot(IHostingEnvironment hostingEnvironment) - => _hostingEnvironment = hostingEnvironment; + public UmbracoApplicationRoot(IHostingEnvironment hostingEnvironment) + => _hostingEnvironment = hostingEnvironment; - public DirectoryInfo ApplicationRoot - => new DirectoryInfo( - Path.Combine( - _hostingEnvironment.MapPathContentRoot(Core.Constants.SystemDirectories.TempData), - "ExamineIndexes")); - } + public DirectoryInfo ApplicationRoot + => new( + Path.Combine( + _hostingEnvironment.MapPathContentRoot(Constants.SystemDirectories.TempData), + "ExamineIndexes")); } diff --git a/src/Umbraco.Examine.Lucene/UmbracoContentIndex.cs b/src/Umbraco.Examine.Lucene/UmbracoContentIndex.cs index ec6c8e35a4..a2c4dedafe 100644 --- a/src/Umbraco.Examine.Lucene/UmbracoContentIndex.cs +++ b/src/Umbraco.Examine.Lucene/UmbracoContentIndex.cs @@ -1,154 +1,151 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System; -using System.Collections.Generic; -using System.Linq; 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 +namespace Umbraco.Cms.Infrastructure.Examine; + +/// +/// An indexer for Umbraco content and media +/// +public class UmbracoContentIndex : UmbracoExamineIndex, IUmbracoContentIndex { - /// - /// An indexer for Umbraco content and media - /// - public class UmbracoContentIndex : UmbracoExamineIndex, IUmbracoContentIndex, IDisposable + private readonly ISet _idOnlyFieldSet = new HashSet { "id" }; + private readonly ILogger _logger; + + public UmbracoContentIndex( + ILoggerFactory loggerFactory, + string name, + IOptionsMonitor indexOptions, + IHostingEnvironment hostingEnvironment, + IRuntimeState runtimeState, + ILocalizationService? languageService = null) + : base(loggerFactory, name, indexOptions, hostingEnvironment, runtimeState) { - private readonly ILogger _logger; - private readonly ISet _idOnlyFieldSet = new HashSet { "id" }; - public UmbracoContentIndex( - ILoggerFactory loggerFactory, - string name, - IOptionsMonitor indexOptions, - IHostingEnvironment hostingEnvironment, - IRuntimeState runtimeState, - ILocalizationService? languageService = null) - : base(loggerFactory, name, indexOptions, hostingEnvironment, runtimeState) - { - LanguageService = languageService; - _logger = loggerFactory.CreateLogger(); + LanguageService = languageService; + _logger = loggerFactory.CreateLogger(); - LuceneDirectoryIndexOptions namedOptions = indexOptions.Get(name); - if (namedOptions == null) + 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; } + + /// + /// Explicitly override because we need to do validation differently than the underlying logic + /// + /// + void IIndex.IndexItems(IEnumerable values) => PerformIndexItems(values, OnIndexOperationComplete); + + /// + /// Special check for invalid paths + /// + /// + /// + protected override void PerformIndexItems(IEnumerable values, Action 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? paths) || paths.Count <= 0 || paths[0] == null) { - throw new InvalidOperationException($"No named {typeof(LuceneDirectoryIndexOptions)} options with name {name}"); + return ValueSetValidationStatus.Failed; } - if (namedOptions.Validator is IContentValueSetValidator contentValueSetValidator) + ValueSetValidationResult validationResult = ValueSetValidator.Validate(v); + + return validationResult.Status; + }).ToList(); + + var hasDeletes = false; + var hasUpdates = false; + + // ordering by descending so that Filtered/Failed processes first + foreach (IGrouping group in invalidOrValid.OrderByDescending(x => x.Key)) + { + switch (group.Key) { - PublishedValuesOnly = contentValueSetValidator.PublishedValuesOnly; + case ValueSetValidationStatus.Valid: + hasUpdates = true; + + //these are the valid ones, so just index them all at once + base.PerformIndexItems(group.ToList(), 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; } } - protected ILocalizationService? LanguageService { get; } - - /// - /// Explicitly override because we need to do validation differently than the underlying logic - /// - /// - void IIndex.IndexItems(IEnumerable values) => PerformIndexItems(values, OnIndexOperationComplete); - - /// - /// Special check for invalid paths - /// - /// - /// - protected override void PerformIndexItems(IEnumerable values, Action onComplete) + if ((hasDeletes && !hasUpdates) || (!hasDeletes && !hasUpdates)) { - // 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? paths) || paths.Count <= 0 || paths[0] == null) - { - return ValueSetValidationStatus.Failed; - } + //we need to manually call the completed method + onComplete(new IndexOperationEventArgs(this, 0)); + } + } - ValueSetValidationResult validationResult = ValueSetValidator.Validate(v); + /// + /// + /// 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(); - return validationResult.Status; - }).ToList(); + for (var i = 0; i < idsAsList.Count; i++) + { + var nodeId = idsAsList[i]; - var hasDeletes = false; - var hasUpdates = false; + //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(); - // ordering by descending so that Filtered/Failed processes first - foreach (IGrouping group in invalidOrValid.OrderByDescending(x => x.Key)) - { - switch (group.Key) - { - case ValueSetValidationStatus.Valid: - hasUpdates = true; + _logger.LogDebug("DeleteFromIndex with query: {Query} (found {TotalItems} results)", rawQuery, results.TotalItemCount); - //these are the valid ones, so just index them all at once - base.PerformIndexItems(group.ToList(), onComplete); - break; - case ValueSetValidationStatus.Failed: - // don't index anything that is invalid - break; - case ValueSetValidationStatus.Filtered: - hasDeletes = true; + 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); - // 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)); - } - } - - /// - /// - /// 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 (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); + // 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/UmbracoExamineIndex.cs b/src/Umbraco.Examine.Lucene/UmbracoExamineIndex.cs index 9b94482a74..f2b94215a5 100644 --- a/src/Umbraco.Examine.Lucene/UmbracoExamineIndex.cs +++ b/src/Umbraco.Examine.Lucene/UmbracoExamineIndex.cs @@ -1,9 +1,6 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System; -using System.Collections.Generic; -using System.Linq; using Examine; using Examine.Lucene; using Examine.Lucene.Providers; @@ -15,123 +12,123 @@ using Umbraco.Cms.Core.Hosting; using Umbraco.Cms.Core.Services; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Examine +namespace Umbraco.Cms.Infrastructure.Examine; + +/// +/// An abstract provider containing the basic functionality to be able to query against Umbraco data. +/// +public abstract class UmbracoExamineIndex : LuceneIndex, IUmbracoIndex, IIndexDiagnostics { + private readonly UmbracoExamineIndexDiagnostics _diagnostics; + private readonly ILogger _logger; + private readonly IRuntimeState _runtimeState; + private bool _hasLoggedInitLog; + + protected UmbracoExamineIndex( + ILoggerFactory loggerFactory, + string name, + IOptionsMonitor indexOptions, + IHostingEnvironment hostingEnvironment, + IRuntimeState runtimeState) + : base(loggerFactory, name, indexOptions) + { + _runtimeState = runtimeState; + _diagnostics = new UmbracoExamineIndexDiagnostics(this, loggerFactory.CreateLogger(), hostingEnvironment, indexOptions); + _logger = loggerFactory.CreateLogger(); + } + + public Attempt IsHealthy() => _diagnostics.IsHealthy(); + public virtual IReadOnlyDictionary Metadata => _diagnostics.Metadata; /// - /// An abstract provider containing the basic functionality to be able to query against Umbraco data. + /// When set to true Umbraco will keep the index in sync with Umbraco data automatically /// - public abstract class UmbracoExamineIndex : LuceneIndex, IUmbracoIndex, IIndexDiagnostics + public bool EnableDefaultEventHandler { get; set; } = true; + + public bool PublishedValuesOnly { get; protected set; } = false; + + /// + /// override to check if we can actually initialize. + /// + /// + /// This check is required since the base examine lib will try to rebuild on startup + /// + protected override void PerformDeleteFromIndex(IEnumerable itemIds, Action? onComplete) { - private readonly UmbracoExamineIndexDiagnostics _diagnostics; - private readonly IRuntimeState _runtimeState; - private bool _hasLoggedInitLog = false; - private readonly ILogger _logger; - - protected UmbracoExamineIndex( - ILoggerFactory loggerFactory, - string name, - IOptionsMonitor indexOptions, - IHostingEnvironment hostingEnvironment, - IRuntimeState runtimeState) - : base(loggerFactory, name, indexOptions) + if (CanInitialize()) { - _runtimeState = runtimeState; - _diagnostics = new UmbracoExamineIndexDiagnostics(this, loggerFactory.CreateLogger(), hostingEnvironment, indexOptions); - _logger = loggerFactory.CreateLogger(); + base.PerformDeleteFromIndex(itemIds, onComplete); + } + } + + protected override void PerformIndexItems(IEnumerable values, Action onComplete) + { + if (CanInitialize()) + { + base.PerformIndexItems(values, onComplete); + } + } + + /// + /// Returns true if the Umbraco application is in a state that we can initialize the examine indexes + /// + /// + protected bool CanInitialize() + { + var canInit = _runtimeState.Level == RuntimeLevel.Run; + + if (!canInit && !_hasLoggedInitLog) + { + _hasLoggedInitLog = true; + _logger.LogWarning("Runtime state is not " + RuntimeLevel.Run + ", no indexing will occur"); } - /// - /// When set to true Umbraco will keep the index in sync with Umbraco data automatically - /// - public bool EnableDefaultEventHandler { get; set; } = true; + return canInit; + } - public bool PublishedValuesOnly { get; protected set; } = false; + /// + /// This ensures that the special __Raw_ fields are indexed correctly + /// + /// + protected override void OnDocumentWriting(DocumentWritingEventArgs docArgs) + { + Document? d = docArgs.Document; - /// - /// override to check if we can actually initialize. - /// - /// - /// This check is required since the base examine lib will try to rebuild on startup - /// - protected override void PerformDeleteFromIndex(IEnumerable itemIds, Action? onComplete) + foreach (KeyValuePair> f in docArgs.ValueSet.Values + .Where(x => x.Key.StartsWith(UmbracoExamineFieldNames.RawFieldPrefix)).ToList()) { - if (CanInitialize()) + if (f.Value.Count > 0) { - base.PerformDeleteFromIndex(itemIds, onComplete); + //remove the original value so we can store it the correct way + d.RemoveField(f.Key); + + d.Add(new StoredField(f.Key, f.Value[0].ToString())); } } - protected override void PerformIndexItems(IEnumerable values, Action onComplete) + base.OnDocumentWriting(docArgs); + } + + protected override void OnTransformingIndexValues(IndexingItemEventArgs e) + { + base.OnTransformingIndexValues(e); + + var updatedValues = e.ValueSet.Values.ToDictionary(x => x.Key, x => (IEnumerable)x.Value); + + //ensure special __Path field + var path = e.ValueSet.GetValue("path"); + if (path != null) { - if (CanInitialize()) - { - base.PerformIndexItems(values, onComplete); - } + updatedValues[UmbracoExamineFieldNames.IndexPathFieldName] = path.Yield(); } - /// - /// Returns true if the Umbraco application is in a state that we can initialize the examine indexes - /// - /// - protected bool CanInitialize() + //icon + if (e.ValueSet.Values.TryGetValue("icon", out IReadOnlyList? icon) && + e.ValueSet.Values.ContainsKey(UmbracoExamineFieldNames.IconFieldName) == false) { - var canInit = _runtimeState.Level == RuntimeLevel.Run; - - if (!canInit && !_hasLoggedInitLog) - { - _hasLoggedInitLog = true; - _logger.LogWarning("Runtime state is not " + RuntimeLevel.Run + ", no indexing will occur"); - } - - return canInit; + updatedValues[UmbracoExamineFieldNames.IconFieldName] = icon; } - /// - /// This ensures that the special __Raw_ fields are indexed correctly - /// - /// - protected override void OnDocumentWriting(DocumentWritingEventArgs docArgs) - { - var d = docArgs.Document; - - foreach (var f in docArgs.ValueSet.Values.Where(x => x.Key.StartsWith(UmbracoExamineFieldNames.RawFieldPrefix)).ToList()) - { - if (f.Value.Count > 0) - { - //remove the original value so we can store it the correct way - d.RemoveField(f.Key); - - d.Add(new StoredField(f.Key, f.Value[0].ToString())); - } - } - - base.OnDocumentWriting(docArgs); - } - - protected override void OnTransformingIndexValues(IndexingItemEventArgs e) - { - base.OnTransformingIndexValues(e); - - var updatedValues = e.ValueSet.Values.ToDictionary(x => x.Key, x => (IEnumerable) x.Value); - - //ensure special __Path field - var path = e.ValueSet.GetValue("path"); - if (path != null) - { - updatedValues[UmbracoExamineFieldNames.IndexPathFieldName] = path.Yield(); - } - - //icon - if (e.ValueSet.Values.TryGetValue("icon", out var icon) && e.ValueSet.Values.ContainsKey(UmbracoExamineFieldNames.IconFieldName) == false) - { - updatedValues[UmbracoExamineFieldNames.IconFieldName] = icon; - } - - e.SetValues(updatedValues); - } - - public Attempt IsHealthy() => _diagnostics.IsHealthy(); - public virtual IReadOnlyDictionary Metadata => _diagnostics.Metadata; + e.SetValues(updatedValues); } } diff --git a/src/Umbraco.Examine.Lucene/UmbracoExamineIndexDiagnostics.cs b/src/Umbraco.Examine.Lucene/UmbracoExamineIndexDiagnostics.cs index 6a18884dc2..9e06d1c98e 100644 --- a/src/Umbraco.Examine.Lucene/UmbracoExamineIndexDiagnostics.cs +++ b/src/Umbraco.Examine.Lucene/UmbracoExamineIndexDiagnostics.cs @@ -1,55 +1,50 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System.Collections.Generic; -using System.Linq; using Examine.Lucene; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Umbraco.Cms.Core.Hosting; -namespace Umbraco.Cms.Infrastructure.Examine +namespace Umbraco.Cms.Infrastructure.Examine; + +public class UmbracoExamineIndexDiagnostics : LuceneIndexDiagnostics { - public class UmbracoExamineIndexDiagnostics : LuceneIndexDiagnostics + private readonly UmbracoExamineIndex _index; + + public UmbracoExamineIndexDiagnostics( + UmbracoExamineIndex index, + ILogger logger, + IHostingEnvironment hostingEnvironment, + IOptionsMonitor indexOptions) + : base(index, logger, hostingEnvironment, indexOptions) => + _index = index; + + public override IReadOnlyDictionary Metadata { - private readonly UmbracoExamineIndex _index; - - public UmbracoExamineIndexDiagnostics( - UmbracoExamineIndex index, - ILogger logger, - IHostingEnvironment hostingEnvironment, - IOptionsMonitor indexOptions) - : base(index, logger, hostingEnvironment, indexOptions) + get { - _index = index; - } + var d = base.Metadata.ToDictionary(x => x.Key, x => x.Value); - public override IReadOnlyDictionary Metadata - { - get + d[nameof(UmbracoExamineIndex.EnableDefaultEventHandler)] = _index.EnableDefaultEventHandler; + d[nameof(UmbracoExamineIndex.PublishedValuesOnly)] = _index.PublishedValuesOnly; + + if (_index.ValueSetValidator is ValueSetValidator vsv) { - var d = base.Metadata.ToDictionary(x => x.Key, x => x.Value); - - d[nameof(UmbracoExamineIndex.EnableDefaultEventHandler)] = _index.EnableDefaultEventHandler; - d[nameof(UmbracoExamineIndex.PublishedValuesOnly)] = _index.PublishedValuesOnly; - - if (_index.ValueSetValidator is ValueSetValidator vsv) - { - d[nameof(ValueSetValidator.IncludeItemTypes)] = vsv.IncludeItemTypes; - d[nameof(ContentValueSetValidator.ExcludeItemTypes)] = vsv.ExcludeItemTypes; - d[nameof(ContentValueSetValidator.IncludeFields)] = vsv.IncludeFields; - d[nameof(ContentValueSetValidator.ExcludeFields)] = vsv.ExcludeFields; - } - - if (_index.ValueSetValidator is ContentValueSetValidator cvsv) - { - d[nameof(ContentValueSetValidator.PublishedValuesOnly)] = cvsv.PublishedValuesOnly; - d[nameof(ContentValueSetValidator.SupportProtectedContent)] = cvsv.SupportProtectedContent; - d[nameof(ContentValueSetValidator.ParentId)] = cvsv.ParentId; - } - - return d.Where(x => x.Value != null).ToDictionary(x => x.Key, x => x.Value); + d[nameof(ValueSetValidator.IncludeItemTypes)] = vsv.IncludeItemTypes; + d[nameof(ContentValueSetValidator.ExcludeItemTypes)] = vsv.ExcludeItemTypes; + d[nameof(ContentValueSetValidator.IncludeFields)] = vsv.IncludeFields; + d[nameof(ContentValueSetValidator.ExcludeFields)] = vsv.ExcludeFields; } + + if (_index.ValueSetValidator is ContentValueSetValidator cvsv) + { + d[nameof(ContentValueSetValidator.PublishedValuesOnly)] = cvsv.PublishedValuesOnly; + d[nameof(ContentValueSetValidator.SupportProtectedContent)] = cvsv.SupportProtectedContent; + d[nameof(ContentValueSetValidator.ParentId)] = cvsv.ParentId; + } + + return d.Where(x => x.Value != null).ToDictionary(x => x.Key, x => x.Value); } } } diff --git a/src/Umbraco.Examine.Lucene/UmbracoLockFactory.cs b/src/Umbraco.Examine.Lucene/UmbracoLockFactory.cs index 89f61c1e53..4f45e3513e 100644 --- a/src/Umbraco.Examine.Lucene/UmbracoLockFactory.cs +++ b/src/Umbraco.Examine.Lucene/UmbracoLockFactory.cs @@ -1,15 +1,13 @@ -// Copyright (c) Umbraco. +// Copyright (c) Umbraco. // See LICENSE for more details. -using System.IO; using Examine.Lucene.Directories; using Lucene.Net.Store; -namespace Umbraco.Cms.Infrastructure.Examine +namespace Umbraco.Cms.Infrastructure.Examine; + +public class UmbracoLockFactory : ILockFactory { - public class UmbracoLockFactory : ILockFactory - { - public LockFactory GetLockFactory(DirectoryInfo directory) - => new NoPrefixSimpleFsLockFactory(directory); - } + public LockFactory GetLockFactory(DirectoryInfo directory) + => new NoPrefixSimpleFsLockFactory(directory); } diff --git a/src/Umbraco.Examine.Lucene/UmbracoMemberIndex.cs b/src/Umbraco.Examine.Lucene/UmbracoMemberIndex.cs index a6e000ff43..f40ca76e84 100644 --- a/src/Umbraco.Examine.Lucene/UmbracoMemberIndex.cs +++ b/src/Umbraco.Examine.Lucene/UmbracoMemberIndex.cs @@ -7,21 +7,20 @@ using Microsoft.Extensions.Options; using Umbraco.Cms.Core.Hosting; using Umbraco.Cms.Core.Services; -namespace Umbraco.Cms.Infrastructure.Examine +namespace Umbraco.Cms.Infrastructure.Examine; + +/// +/// Custom indexer for members +/// +public class UmbracoMemberIndex : UmbracoExamineIndex, IUmbracoMemberIndex { - /// - /// Custom indexer for members - /// - public class UmbracoMemberIndex : UmbracoExamineIndex, IUmbracoMemberIndex + public UmbracoMemberIndex( + ILoggerFactory loggerFactory, + string name, + IOptionsMonitor indexOptions, + IHostingEnvironment hostingEnvironment, + IRuntimeState runtimeState) + : base(loggerFactory, name, indexOptions, hostingEnvironment, runtimeState) { - public UmbracoMemberIndex( - ILoggerFactory loggerFactory, - string name, - IOptionsMonitor indexOptions, - IHostingEnvironment hostingEnvironment, - IRuntimeState runtimeState) - : base(loggerFactory, name, indexOptions, hostingEnvironment, runtimeState) - { - } } }