diff --git a/src/Umbraco.Core/Constants-Examine.cs b/src/Umbraco.Core/Constants-Examine.cs deleted file mode 100644 index ddc3500066..0000000000 --- a/src/Umbraco.Core/Constants-Examine.cs +++ /dev/null @@ -1,24 +0,0 @@ -namespace Umbraco.Core -{ - public static partial class Constants - { - public static class Examine - { - /// - /// The alias of the internal member indexer - /// - public const string InternalMemberIndexer = "InternalMemberIndexer"; - - /// - /// The alias of the internal content indexer - /// - public const string InternalIndexer = "InternalIndexer"; - - /// - /// The alias of the external content indexer - /// - public const string ExternalIndexer = "ExternalIndexer"; - - } - } -} diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index cd5c005de7..ba0cf97bf7 100755 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -303,7 +303,6 @@ - diff --git a/src/Umbraco.Examine/ExamineExtensions.cs b/src/Umbraco.Examine/ExamineExtensions.cs index 71e3e65c21..525f0deaa1 100644 --- a/src/Umbraco.Examine/ExamineExtensions.cs +++ b/src/Umbraco.Examine/ExamineExtensions.cs @@ -2,9 +2,12 @@ using System.Linq; using Examine; using Examine.LuceneEngine.Providers; +using Lucene.Net.Analysis; using Lucene.Net.Index; +using Lucene.Net.QueryParsers; using Lucene.Net.Search; using Lucene.Net.Store; +using Version = Lucene.Net.Util.Version; using Umbraco.Core.Logging; namespace Umbraco.Examine @@ -14,6 +17,29 @@ namespace Umbraco.Examine /// internal static class ExamineExtensions { + public 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 + var parsed = new QueryParser(Version.LUCENE_30, "nodeName", new KeywordAnalyzer()).Parse(query); + return true; + } + catch (ParseException) + { + return false; + } + catch (Exception) + { + return false; + } + } + /// /// Forcibly unlocks all lucene based indexes /// diff --git a/src/Umbraco.Tests/Cache/PublishedCache/PublishedMediaCacheTests.cs b/src/Umbraco.Tests/Cache/PublishedCache/PublishedMediaCacheTests.cs index 64194ebb47..aeda2eaca2 100644 --- a/src/Umbraco.Tests/Cache/PublishedCache/PublishedMediaCacheTests.cs +++ b/src/Umbraco.Tests/Cache/PublishedCache/PublishedMediaCacheTests.cs @@ -111,7 +111,6 @@ namespace Umbraco.Tests.Cache.PublishedCache } [TestCase("id")] - [TestCase("nodeId")] [TestCase("__NodeId")] public void DictionaryDocument_Id_Keys(string key) { @@ -128,7 +127,6 @@ namespace Umbraco.Tests.Cache.PublishedCache } [TestCase("nodeName")] - [TestCase("__nodeName")] public void DictionaryDocument_NodeName_Keys(string key) { var dicDoc = GetDictionaryDocument(nodeNameKey: key); diff --git a/src/Umbraco.Web.UI.Client/src/init.js b/src/Umbraco.Web.UI.Client/src/init.js index 3f44638096..eaa2fe7b31 100644 --- a/src/Umbraco.Web.UI.Client/src/init.js +++ b/src/Umbraco.Web.UI.Client/src/init.js @@ -47,7 +47,17 @@ app.run(['userService', '$q', '$log', '$rootScope', '$route', '$location', 'urlH /** execute code on each successful route */ $rootScope.$on('$routeChangeSuccess', function (event, current, previous) { - currentRouteParams = angular.copy(current.params); //store this so we can reference it in $routeUpdate + var toRetain = currentRouteParams ? navigationService.retainQueryStrings(currentRouteParams, current.params) : null; + + //if toRetain is not null it means that there are missing query strings and we need to update the current params + if (toRetain) { + $route.updateParams(toRetain); + currentRouteParams = toRetain; + } + else { + currentRouteParams = angular.copy(current.params); + } + var deployConfig = Umbraco.Sys.ServerVariables.deploy; var deployEnv, deployEnvTitle; @@ -122,25 +132,33 @@ app.run(['userService', '$q', '$log', '$rootScope', '$route', '$location', 'urlH $route.reload(); } else { - + + var toRetain = navigationService.retainQueryStrings(currentRouteParams, next.params); + + //if toRetain is not null it means that there are missing query strings and we need to update the current params + if (toRetain) { + $route.updateParams(toRetain); + } + //check if the location being changed is only due to global/state query strings which means the location change //isn't actually going to cause a route change. - if (navigationService.isRouteChangingNavigation(currentRouteParams, next.params)) { - //The location change will cause a route change. We need to ensure that the global/state - //query strings have not been stripped out. If they have, we'll re-add them and re-route. + if (!toRetain && navigationService.isRouteChangingNavigation(currentRouteParams, next.params)) { + + //The location change will cause a route change, continue the route if the query strings haven't been updated. + $route.reload(); - var toRetain = navigationService.retainQueryStrings(currentRouteParams, next.params); - if (toRetain) { - $route.updateParams(toRetain); - } - else { - //continue the route - $route.reload(); - } } else { + //navigation is not changing but we should update the currentRouteParams to include all current parameters - currentRouteParams = angular.copy(next.params); + + if (toRetain) { + currentRouteParams = toRetain; + } + else { + currentRouteParams = angular.copy(next.params); + } + } } }); diff --git a/src/Umbraco.Web/Editors/ExamineManagementController.cs b/src/Umbraco.Web/Editors/ExamineManagementController.cs index 67209c91bd..2f96ee3d45 100644 --- a/src/Umbraco.Web/Editors/ExamineManagementController.cs +++ b/src/Umbraco.Web/Editors/ExamineManagementController.cs @@ -73,7 +73,7 @@ namespace Umbraco.Web.Editors if (!msg.IsSuccessStatusCode) throw new HttpResponseException(msg); - var results = TryParseLuceneQuery(query) + var results = Examine.ExamineExtensions.TryParseLuceneQuery(query) ? searcher.Search(searcher.CreateCriteria().RawQuery(query), maxResults: pageSize * (pageIndex + 1)) : searcher.Search(query, true, maxResults: pageSize * (pageIndex + 1)); @@ -92,28 +92,7 @@ namespace Umbraco.Web.Editors }; } - private 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 - var parsed = new QueryParser(Version.LUCENE_30, "nodeName", new KeywordAnalyzer()).Parse(query); - return true; - } - catch (ParseException) - { - return false; - } - catch (Exception) - { - return false; - } - } + /// /// Check if the index has been rebuilt diff --git a/src/Umbraco.Web/Models/Mapping/EntityMapperProfile.cs b/src/Umbraco.Web/Models/Mapping/EntityMapperProfile.cs index ea76293df5..178027857c 100644 --- a/src/Umbraco.Web/Models/Mapping/EntityMapperProfile.cs +++ b/src/Umbraco.Web/Models/Mapping/EntityMapperProfile.cs @@ -122,6 +122,8 @@ namespace Umbraco.Web.Models.Mapping .ForMember(dest => dest.AdditionalData, opt => opt.Ignore()) .AfterMap((src, dest) => { + //TODO: Properly map this (not aftermap) + //get the icon if there is one dest.Icon = src.Values.ContainsKey(UmbracoExamineIndex.IconFieldName) ? src.Values[UmbracoExamineIndex.IconFieldName] diff --git a/src/Umbraco.Web/PublishedCache/XmlPublishedCache/DictionaryPublishedContent.cs b/src/Umbraco.Web/PublishedCache/XmlPublishedCache/DictionaryPublishedContent.cs index 4453fe7321..7c311236c0 100644 --- a/src/Umbraco.Web/PublishedCache/XmlPublishedCache/DictionaryPublishedContent.cs +++ b/src/Umbraco.Web/PublishedCache/XmlPublishedCache/DictionaryPublishedContent.cs @@ -55,7 +55,7 @@ namespace Umbraco.Web.PublishedCache.XmlPublishedCache ValidateAndSetProperty(valueDictionary, val => _key = Guid.Parse(val), "key"); //ValidateAndSetProperty(valueDictionary, val => _templateId = int.Parse(val), "template", "templateId"); ValidateAndSetProperty(valueDictionary, val => _sortOrder = Int32.Parse(val), "sortOrder"); - ValidateAndSetProperty(valueDictionary, val => _name = val, "nodeName", "__nodeName"); + ValidateAndSetProperty(valueDictionary, val => _name = val, "nodeName"); ValidateAndSetProperty(valueDictionary, val => _urlName = val, "urlName"); ValidateAndSetProperty(valueDictionary, val => _documentTypeAlias = val, "nodeTypeAlias", LuceneIndex.ItemTypeFieldName); ValidateAndSetProperty(valueDictionary, val => _documentTypeId = Int32.Parse(val), "nodeType"); diff --git a/src/Umbraco.Web/PublishedCache/XmlPublishedCache/PublishedMediaCache.cs b/src/Umbraco.Web/PublishedCache/XmlPublishedCache/PublishedMediaCache.cs index ac6b425e27..f203d5d2c9 100644 --- a/src/Umbraco.Web/PublishedCache/XmlPublishedCache/PublishedMediaCache.cs +++ b/src/Umbraco.Web/PublishedCache/XmlPublishedCache/PublishedMediaCache.cs @@ -240,9 +240,9 @@ namespace Umbraco.Web.PublishedCache.XmlPublishedCache try { - if (eMgr.TryGetIndex(Constants.Examine.InternalIndexer, out var index)) + if (eMgr.TryGetIndex(Constants.UmbracoIndexes.InternalIndexName, out var index)) return index.GetSearcher(); - throw new InvalidOperationException($"No index found by name {Constants.Examine.InternalIndexer}"); + throw new InvalidOperationException($"No index found by name {Constants.UmbracoIndexes.InternalIndexName}"); } catch (FileNotFoundException) { diff --git a/src/Umbraco.Web/PublishedContentExtensions.cs b/src/Umbraco.Web/PublishedContentExtensions.cs index eb47fd2f2d..f82c20fc4b 100644 --- a/src/Umbraco.Web/PublishedContentExtensions.cs +++ b/src/Umbraco.Web/PublishedContentExtensions.cs @@ -270,7 +270,7 @@ namespace Umbraco.Web { //fixme: pass in the IExamineManager - indexName = string.IsNullOrEmpty(indexName) ? Constants.Examine.ExternalIndexer : indexName; + indexName = string.IsNullOrEmpty(indexName) ? Constants.UmbracoIndexes.ExternalIndexName : indexName; if (!ExamineManager.Instance.TryGetIndex(indexName, out var index)) throw new InvalidOperationException("No index found with name " + indexName); @@ -290,7 +290,7 @@ namespace Umbraco.Web { //fixme: pass in the IExamineManager - indexName = string.IsNullOrEmpty(indexName) ? Constants.Examine.ExternalIndexer : indexName; + indexName = string.IsNullOrEmpty(indexName) ? Constants.UmbracoIndexes.ExternalIndexName : indexName; if (!ExamineManager.Instance.TryGetIndex(indexName, out var index)) throw new InvalidOperationException("No index found with name " + indexName); @@ -312,8 +312,8 @@ namespace Umbraco.Web if (searchProvider == null) { - if (!ExamineManager.Instance.TryGetIndex(Constants.Examine.ExternalIndexer, out var index)) - throw new InvalidOperationException("No index found with name " + Constants.Examine.ExternalIndexer); + if (!ExamineManager.Instance.TryGetIndex(Constants.UmbracoIndexes.ExternalIndexName, out var index)) + throw new InvalidOperationException("No index found with name " + Constants.UmbracoIndexes.ExternalIndexName); searchProvider = index.GetSearcher(); } var results = searchProvider.Search(criteria); diff --git a/src/Umbraco.Web/PublishedContentQuery.cs b/src/Umbraco.Web/PublishedContentQuery.cs index 04611000b9..774393f3de 100644 --- a/src/Umbraco.Web/PublishedContentQuery.cs +++ b/src/Umbraco.Web/PublishedContentQuery.cs @@ -192,7 +192,7 @@ namespace Umbraco.Web //fixme: inject IExamineManager indexName = string.IsNullOrEmpty(indexName) - ? Constants.Examine.ExternalIndexer + ? Constants.UmbracoIndexes.ExternalIndexName : indexName; if (!ExamineManager.Instance.TryGetIndex(indexName, out var index)) @@ -220,8 +220,8 @@ namespace Umbraco.Web //fixme: inject IExamineManager if (searcher == null) { - if (!ExamineManager.Instance.TryGetIndex(Constants.Examine.ExternalIndexer, out var index)) - throw new InvalidOperationException($"No index found by name {Constants.Examine.ExternalIndexer}"); + if (!ExamineManager.Instance.TryGetIndex(Constants.UmbracoIndexes.ExternalIndexName, out var index)) + throw new InvalidOperationException($"No index found by name {Constants.UmbracoIndexes.ExternalIndexName}"); searcher = index.GetSearcher(); } diff --git a/src/Umbraco.Web/Runtime/WebRuntimeComponent.cs b/src/Umbraco.Web/Runtime/WebRuntimeComponent.cs index 2e1c934bc0..0d27253466 100644 --- a/src/Umbraco.Web/Runtime/WebRuntimeComponent.cs +++ b/src/Umbraco.Web/Runtime/WebRuntimeComponent.cs @@ -128,7 +128,7 @@ namespace Umbraco.Web.Runtime composition.Container.EnableWebApi(GlobalConfiguration.Configuration); composition.Container.RegisterCollectionBuilder() - .Add(() => typeLoader.GetTypes()); // fixme which searchable trees?! + .Add(() => typeLoader.GetTypes()); composition.Container.Register(new PerRequestLifeTime()); diff --git a/src/Umbraco.Web/Search/ExamineComponent.cs b/src/Umbraco.Web/Search/ExamineComponent.cs index f2d3168b45..6d67169533 100644 --- a/src/Umbraco.Web/Search/ExamineComponent.cs +++ b/src/Umbraco.Web/Search/ExamineComponent.cs @@ -26,6 +26,8 @@ using Examine.LuceneEngine.Directories; using LightInject; using Umbraco.Core.Composing; using Umbraco.Core.Strings; +using Umbraco.Web.Models.ContentEditing; +using Umbraco.Web.Trees; namespace Umbraco.Web.Search { @@ -53,6 +55,7 @@ namespace Umbraco.Web.Search // but greater that SafeXmlReaderWriter priority which is 60 private const int EnlistPriority = 80; + public override void Compose(Composition composition) { base.Compose(composition); diff --git a/src/Umbraco.Web/Search/SearchableTreeCollection.cs b/src/Umbraco.Web/Search/SearchableTreeCollection.cs index 86f4494353..38c329cafa 100644 --- a/src/Umbraco.Web/Search/SearchableTreeCollection.cs +++ b/src/Umbraco.Web/Search/SearchableTreeCollection.cs @@ -1,6 +1,8 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; +using Umbraco.Core; using Umbraco.Core.Composing; using Umbraco.Core.Services; using Umbraco.Web.Trees; @@ -19,15 +21,17 @@ namespace Umbraco.Web.Search private Dictionary CreateDictionary(IApplicationTreeService treeService) { - var appTrees = treeService.GetAll().ToArray(); - var dictionary = new Dictionary(); + var appTrees = treeService.GetAll() + .OrderBy(x => x.SortOrder) + .ToArray(); + var dictionary = new Dictionary(StringComparer.OrdinalIgnoreCase); var searchableTrees = this.ToArray(); - foreach (var searchableTree in searchableTrees) + foreach (var appTree in appTrees) { - var found = appTrees.FirstOrDefault(x => x.Alias == searchableTree.TreeAlias); + var found = searchableTrees.FirstOrDefault(x => x.TreeAlias.InvariantEquals(appTree.Alias)); if (found != null) { - dictionary[searchableTree.TreeAlias] = new SearchableApplicationTree(found.ApplicationAlias, found.Alias, searchableTree); + dictionary[found.TreeAlias] = new SearchableApplicationTree(appTree.ApplicationAlias, appTree.Alias, found); } } return dictionary; diff --git a/src/Umbraco.Web/Search/SearchableTreeCollectionBuilder.cs b/src/Umbraco.Web/Search/SearchableTreeCollectionBuilder.cs index ae83cc5eab..22db27b1fb 100644 --- a/src/Umbraco.Web/Search/SearchableTreeCollectionBuilder.cs +++ b/src/Umbraco.Web/Search/SearchableTreeCollectionBuilder.cs @@ -21,5 +21,8 @@ namespace Umbraco.Web.Search { return new SearchableTreeCollection(CreateItems(), _treeService); } + + //per request because generally an instance of ISearchableTree is a controller + protected override ILifetime CollectionLifetime => new PerRequestLifeTime(); } } diff --git a/src/Umbraco.Web/Search/UmbracoTreeSearcher.cs b/src/Umbraco.Web/Search/UmbracoTreeSearcher.cs index 9aab30edae..c3ab7318a0 100644 --- a/src/Umbraco.Web/Search/UmbracoTreeSearcher.cs +++ b/src/Umbraco.Web/Search/UmbracoTreeSearcher.cs @@ -10,6 +10,7 @@ using Umbraco.Core; using Umbraco.Core.Composing; using Umbraco.Core.Models; using Umbraco.Core.Services; +using Umbraco.Examine; using Umbraco.Web.Models.ContentEditing; using Umbraco.Web.Trees; using SearchResult = Examine.SearchResult; @@ -23,11 +24,18 @@ namespace Umbraco.Web.Search { private readonly IExamineManager _examineManager; private readonly UmbracoHelper _umbracoHelper; + private readonly ILocalizationService _languageService; + private readonly IEntityService _entityService; - public UmbracoTreeSearcher(IExamineManager examineManager, UmbracoHelper umbracoHelper) + public UmbracoTreeSearcher(IExamineManager examineManager, + UmbracoHelper umbracoHelper, + ILocalizationService languageService, + IEntityService entityService) { _examineManager = examineManager ?? throw new ArgumentNullException(nameof(examineManager)); _umbracoHelper = umbracoHelper ?? throw new ArgumentNullException(nameof(umbracoHelper)); + _languageService = languageService; + _entityService = entityService; } /// @@ -51,16 +59,22 @@ namespace Umbraco.Web.Search var sb = new StringBuilder(); string type; - var indexName = Constants.Examine.InternalIndexer; + var indexName = Constants.UmbracoIndexes.InternalIndexName; var fields = new[] { "id", "__NodeId" }; var umbracoContext = _umbracoHelper.UmbracoContext; - //TODO: WE should really just allow passing in a lucene raw query + //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)) + //{ + + //} + switch (entityType) { case UmbracoEntityTypes.Member: - indexName = Constants.Examine.InternalMemberIndexer; + indexName = Constants.UmbracoIndexes.MembersIndexName; type = "member"; fields = new[] { "id", "__NodeId", "email", "loginName" }; if (searchFrom != null && searchFrom != Constants.Conventions.MemberTypes.AllMembersListId && searchFrom.Trim() != "-1") @@ -72,13 +86,13 @@ namespace Umbraco.Web.Search break; case UmbracoEntityTypes.Media: type = "media"; - var allMediaStartNodes = umbracoContext.Security.CurrentUser.CalculateMediaStartNodeIds(Current.Services.EntityService); - AppendPath(sb, UmbracoObjectTypes.Media, allMediaStartNodes, searchFrom, Current.Services.EntityService); + var allMediaStartNodes = umbracoContext.Security.CurrentUser.CalculateMediaStartNodeIds(_entityService); + AppendPath(sb, UmbracoObjectTypes.Media, allMediaStartNodes, searchFrom, _entityService); break; case UmbracoEntityTypes.Document: type = "content"; - var allContentStartNodes = umbracoContext.Security.CurrentUser.CalculateContentStartNodeIds(Current.Services.EntityService); - AppendPath(sb, UmbracoObjectTypes.Document, allContentStartNodes, searchFrom, Current.Services.EntityService); + var allContentStartNodes = umbracoContext.Security.CurrentUser.CalculateContentStartNodeIds(_entityService); + AppendPath(sb, UmbracoObjectTypes.Document, allContentStartNodes, searchFrom, _entityService); break; default: throw new NotSupportedException("The " + typeof(UmbracoTreeSearcher) + " currently does not support searching against object type " + entityType); @@ -89,11 +103,43 @@ namespace Umbraco.Web.Search var internalSearcher = index.GetSearcher(); + if (!BuildQuery(sb, query, searchFrom, fields, type)) + { + totalFound = 0; + return Enumerable.Empty(); + } + + var raw = internalSearcher.CreateCriteria().RawQuery(sb.ToString()); + + var result = internalSearcher + //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 + .Search(raw, Convert.ToInt32(pageSize * (pageIndex + 1))); + + totalFound = result.TotalItemCount; + + var pagedResult = result.Skip(Convert.ToInt32(pageIndex)); + + switch (entityType) + { + case UmbracoEntityTypes.Member: + return MemberFromSearchResults(pagedResult.ToArray()); + case UmbracoEntityTypes.Media: + return MediaFromSearchResults(pagedResult); + case UmbracoEntityTypes.Document: + return ContentFromSearchResults(pagedResult); + default: + throw new NotSupportedException("The " + typeof(UmbracoTreeSearcher) + " currently does not support searching against object type " + entityType); + } + } + + private bool BuildQuery(StringBuilder sb, string query, string searchFrom, string[] fields, string type) + { //build a lucene query: - // the __nodeName will be boosted 10x without wildcards - // then __nodeName will be matched normally with wildcards + // 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(); //check if text is surrounded by single or double quotes, if so, then exact match var surroundedByQuotes = Regex.IsMatch(query, "^\".*?\"$") @@ -102,15 +148,14 @@ namespace Umbraco.Web.Search if (surroundedByQuotes) { //strip quotes, escape string, the replace again - query = query.Trim(new[] { '\"', '\'' }); + query = query.Trim('\"', '\''); query = Lucene.Net.QueryParsers.QueryParser.Escape(query); //nothing to search if (searchFrom.IsNullOrWhiteSpace() && query.IsNullOrWhiteSpace()) { - totalFound = 0; - return new List(); + return false; } //update the query with the query term @@ -119,10 +164,9 @@ namespace Umbraco.Web.Search //add back the surrounding quotes query = string.Format("{0}{1}{0}", "\"", query); - //node name exactly boost x 10 - sb.Append("+(__nodeName: ("); - sb.Append(query.ToLower()); - sb.Append(")^10.0 "); + sb.Append("+("); + + AppendNodeNamePhraseWithBoost(sb, query, allLangs); foreach (var f in fields) { @@ -143,8 +187,7 @@ namespace Umbraco.Web.Search //nothing to search if (searchFrom.IsNullOrWhiteSpace() && trimmed.IsNullOrWhiteSpace()) { - totalFound = 0; - return new List(); + return false; } //update the query with the query term @@ -154,24 +197,12 @@ namespace Umbraco.Web.Search var querywords = query.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries); - //node name exactly boost x 10 - sb.Append("+(__nodeName:"); - sb.Append("\""); - sb.Append(query.ToLower()); - sb.Append("\""); - sb.Append("^10.0 "); - - //node name normally with wildcards - sb.Append(" __nodeName:"); - sb.Append("("); - foreach (var w in querywords) - { - sb.Append(w.ToLower()); - sb.Append("* "); - } - sb.Append(") "); + sb.Append("+("); + AppendNodeNameExactWithBoost(sb, query, allLangs); + AppendNodeNameWithWildcards(sb, querywords, allLangs); + foreach (var f in fields) { //additional fields normally @@ -195,26 +226,69 @@ namespace Umbraco.Web.Search sb.Append("+__IndexType:"); sb.Append(type); - var raw = internalSearcher.CreateCriteria().RawQuery(sb.ToString()); + return true; + } - var result = internalSearcher - //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 - .Search(raw, Convert.ToInt32(pageSize * (pageIndex + 1))); + 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 "); - totalFound = result.TotalItemCount; - - var pagedResult = result.Skip(Convert.ToInt32(pageIndex)); - - switch (entityType) + //also search on all variant node names + foreach (var lang in allLangs) { - case UmbracoEntityTypes.Member: - return MemberFromSearchResults(pagedResult.ToArray()); - case UmbracoEntityTypes.Media: - return MediaFromSearchResults(pagedResult); - case UmbracoEntityTypes.Document: - return ContentFromSearchResults(pagedResult); - default: - throw new NotSupportedException("The " + typeof(UmbracoTreeSearcher) + " currently does not support searching against object type " + entityType); + //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) + { + //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_{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("* "); + } + 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(") "); } } @@ -278,32 +352,33 @@ namespace Umbraco.Web.Search /// /// /// - private IEnumerable MemberFromSearchResults(ISearchResult[] results) + private IEnumerable MemberFromSearchResults(IEnumerable results) { - var mapped = Mapper.Map>(results).ToArray(); //add additional data - foreach (var m in mapped) + foreach (var result in results) { + var m = Mapper.Map(result); + //if no icon could be mapped, it will be set to document, so change it to picture if (m.Icon == "icon-document") { m.Icon = "icon-user"; } - - var searchResult = results.First(x => x.Id == m.Id.ToString()); - if (searchResult.Values.ContainsKey("email") && searchResult.Values["email"] != null) + + if (result.Values.ContainsKey("email") && result.Values["email"] != null) { - m.AdditionalData["Email"] = results.First(x => x.Id == m.Id.ToString()).Values["email"]; + m.AdditionalData["Email"] = result.Values["email"]; } - if (searchResult.Values.ContainsKey("__key") && searchResult.Values["__key"] != null) + if (result.Values.ContainsKey("__key") && result.Values["__key"] != null) { - if (Guid.TryParse(searchResult.Values["__key"], out var key)) + if (Guid.TryParse(result.Values["__key"], out var key)) { m.Key = key; } } + + yield return m; } - return mapped; } /// @@ -313,17 +388,17 @@ namespace Umbraco.Web.Search /// private IEnumerable MediaFromSearchResults(IEnumerable results) { - var mapped = Mapper.Map>(results).ToArray(); //add additional data - foreach (var m in mapped) + foreach (var result in results) { + var m = Mapper.Map(result); //if no icon could be mapped, it will be set to document, so change it to picture if (m.Icon == "icon-document") { m.Icon = "icon-picture"; } + yield return m; } - return mapped; } /// @@ -333,17 +408,28 @@ namespace Umbraco.Web.Search /// private IEnumerable ContentFromSearchResults(IEnumerable results) { - var mapped = Mapper.Map>(results).ToArray(); - //add additional data - foreach (var m in mapped) + var defaultLang = _languageService.GetDefaultLanguageIsoCode(); + + foreach (var result in results) { - var intId = m.Id.TryConvertTo(); + var entity = Mapper.Map(result); + + var intId = entity.Id.TryConvertTo(); if (intId.Success) { - m.AdditionalData["Url"] = _umbracoHelper.Url(intId.Result); + //if it varies by culture, return the default language URL + if (result.Values.TryGetValue(UmbracoContentIndex.VariesByCultureFieldName, out var varies) && varies == "1") + { + entity.AdditionalData["Url"] = _umbracoHelper.Url(intId.Result, defaultLang); + } + else + { + entity.AdditionalData["Url"] = _umbracoHelper.Url(intId.Result); + } } + + yield return entity; } - return mapped; } } diff --git a/src/Umbraco.Web/Trees/ContentTreeController.cs b/src/Umbraco.Web/Trees/ContentTreeController.cs index d2b94c815b..bb38b8c578 100644 --- a/src/Umbraco.Web/Trees/ContentTreeController.cs +++ b/src/Umbraco.Web/Trees/ContentTreeController.cs @@ -36,10 +36,12 @@ namespace Umbraco.Web.Trees public class ContentTreeController : ContentTreeControllerBase, ISearchableTree { private readonly UmbracoTreeSearcher _treeSearcher; + private readonly ActionCollection _actions; - public ContentTreeController(UmbracoTreeSearcher treeSearcher) + public ContentTreeController(UmbracoTreeSearcher treeSearcher, ActionCollection actions) { _treeSearcher = treeSearcher; + _actions = actions; } protected override int RecycleBinId => Constants.System.RecycleBinContent; @@ -127,7 +129,7 @@ namespace Umbraco.Web.Trees // we need to get the default permissions as you can't set permissions on the very root node var permission = Services.UserService.GetPermissions(Security.CurrentUser, Constants.System.Root).First(); - var nodeActions = Current.Actions.FromEntityPermission(permission) + var nodeActions = _actions.FromEntityPermission(permission) .Select(x => new MenuItem(x)); //these two are the standard items @@ -313,8 +315,7 @@ namespace Umbraco.Web.Trees private void AddActionNode(IUmbracoEntity item, MenuItemCollection menu, bool hasSeparator = false, bool convert = false, bool opensDialog = false) where TAction : IAction { - //fixme: Inject - var menuItem = menu.Items.Add(Services.TextService.Localize("actions", Current.Actions.GetAction().Alias), hasSeparator); + var menuItem = menu.Items.Add(Services.TextService.Localize("actions", _actions.GetAction().Alias), hasSeparator); if (convert) menuItem.ConvertLegacyMenuItem(item, "content", "content"); menuItem.OpensDialog = opensDialog; } diff --git a/src/Umbraco.Web/Trees/ISearchableTree.cs b/src/Umbraco.Web/Trees/ISearchableTree.cs index 4146bfaf45..3d82d548c8 100644 --- a/src/Umbraco.Web/Trees/ISearchableTree.cs +++ b/src/Umbraco.Web/Trees/ISearchableTree.cs @@ -1,9 +1,10 @@ using System.Collections.Generic; +using Umbraco.Core.Composing; using Umbraco.Web.Models.ContentEditing; namespace Umbraco.Web.Trees { - public interface ISearchableTree + public interface ISearchableTree : IDiscoverable { /// /// The alias of the tree that the belongs to