Searches on variant nodeName, orders ISearchableTree results by the corresponding tree SortOrder, fixes assembly scanning ISearchableTree

This commit is contained in:
Shannon
2018-12-11 17:51:47 +11:00
parent 5148f34b32
commit eb3841fc0a
7 changed files with 177 additions and 90 deletions

View File

@@ -1,8 +1,11 @@
using System;
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;
namespace Umbraco.Examine
{
@@ -11,6 +14,29 @@ namespace Umbraco.Examine
/// </summary>
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;
}
}
/// <summary>
/// Checks if the index can be read/opened
/// </summary>

View File

@@ -8,6 +8,7 @@ using System.Runtime.CompilerServices;
// Umbraco Cms
[assembly: InternalsVisibleTo("Umbraco.Tests")]
[assembly: InternalsVisibleTo("Umbraco.Web")]
// code analysis
// IDE1006 is broken, wants _value syntax for consts, etc - and it's even confusing ppl at MS, kill it

View File

@@ -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;
}
}
/// <summary>
/// Check if the index has been rebuilt

View File

@@ -127,8 +127,12 @@ namespace Umbraco.Web.Runtime
composition.Container.RegisterUmbracoControllers(typeLoader, GetType().Assembly);
composition.Container.EnableWebApi(GlobalConfiguration.Configuration);
//we aren't scanning for ISearchableTree since that is not IDiscoverable, instead we'll just filter what we've
//already scanned since all of our ISearchableTree is of type UmbracoApiController and in most cases any developers'
//own trees they want searched will also be of type UmbracoApiController. If a developer wants to replace one of ours
//then they will have to manually register/replace.
composition.Container.RegisterCollectionBuilder<SearchableTreeCollectionBuilder>()
.Add(() => typeLoader.GetTypes<ISearchableTree>()); // fixme which searchable trees?!
.Add(() => typeLoader.GetTypes<UmbracoApiController>().Where(x => x.Implements<ISearchableTree>()));
composition.Container.Register<UmbracoTreeSearcher>(new PerRequestLifeTime());

View File

@@ -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);

View File

@@ -19,15 +19,17 @@ namespace Umbraco.Web.Search
private Dictionary<string, SearchableApplicationTree> CreateDictionary(IApplicationTreeService treeService)
{
var appTrees = treeService.GetAll().ToArray();
var appTrees = treeService.GetAll()
.OrderBy(x => x.SortOrder)
.ToArray();
var dictionary = new Dictionary<string, SearchableApplicationTree>();
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 == 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;

View File

@@ -25,12 +25,17 @@ 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, ILocalizationService languageService)
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;
}
/// <summary>
@@ -59,7 +64,13 @@ namespace Umbraco.Web.Search
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:
@@ -75,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);
@@ -92,11 +103,43 @@ namespace Umbraco.Web.Search
var internalSearcher = index.GetSearcher();
if (!BuildQuery(sb, query, searchFrom, fields, type))
{
totalFound = 0;
return Enumerable.Empty<SearchResultEntity>();
}
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 rest will be normal without wildcards
var allLangs = _languageService.GetAllLanguages().Select(x => x.IsoCode).ToList();
//check if text is surrounded by single or double quotes, if so, then exact match
var surroundedByQuotes = Regex.IsMatch(query, "^\".*?\"$")
@@ -105,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<SearchResultEntity>();
return false;
}
//update the query with the query term
@@ -122,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)
{
@@ -146,8 +187,7 @@ namespace Umbraco.Web.Search
//nothing to search
if (searchFrom.IsNullOrWhiteSpace() && trimmed.IsNullOrWhiteSpace())
{
totalFound = 0;
return new List<SearchResultEntity>();
return false;
}
//update the query with the query term
@@ -157,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
@@ -198,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<string> 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<string> 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<string> 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(") ");
}
}
@@ -281,32 +352,33 @@ namespace Umbraco.Web.Search
/// </summary>
/// <param name="results"></param>
/// <returns></returns>
private IEnumerable<SearchResultEntity> MemberFromSearchResults(ISearchResult[] results)
private IEnumerable<SearchResultEntity> MemberFromSearchResults(IEnumerable<ISearchResult> results)
{
var mapped = Mapper.Map<IEnumerable<SearchResultEntity>>(results).ToArray();
//add additional data
foreach (var m in mapped)
foreach (var result in results)
{
var m = Mapper.Map<SearchResultEntity>(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;
}
/// <summary>
@@ -316,17 +388,17 @@ namespace Umbraco.Web.Search
/// <returns></returns>
private IEnumerable<SearchResultEntity> MediaFromSearchResults(IEnumerable<ISearchResult> results)
{
var mapped = Mapper.Map<IEnumerable<SearchResultEntity>>(results).ToArray();
//add additional data
foreach (var m in mapped)
foreach (var result in results)
{
var m = Mapper.Map<SearchResultEntity>(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;
}
/// <summary>
@@ -345,7 +417,7 @@ namespace Umbraco.Web.Search
var intId = entity.Id.TryConvertTo<int>();
if (intId.Success)
{
//TODO: Here we need to figure out how to get the URL based on variant, etc...
//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);