using System; using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Text; using System.Text.RegularExpressions; using AutoMapper; using Examine; using Umbraco.Core; using Umbraco.Core.Composing; using Umbraco.Core.Models; using Umbraco.Core.Services; using Umbraco.Web.Models.ContentEditing; namespace Umbraco.Web.Search { internal class UmbracoTreeSearcher { /// /// Searches for results based on the entity type /// /// /// /// /// /// /// A starting point for the search, generally a node id, but for members this is a member type alias /// /// /// /// public IEnumerable ExamineSearch( UmbracoHelper umbracoHelper, string query, UmbracoEntityTypes entityType, int pageSize, long pageIndex, out long totalFound, string searchFrom = null) { var sb = new StringBuilder(); string type; var searcher = Constants.Examine.InternalSearcher; var fields = new[] { "id", "__NodeId" }; var umbracoContext = umbracoHelper.UmbracoContext; //TODO: WE should really just allow passing in a lucene raw query switch (entityType) { case UmbracoEntityTypes.Member: searcher = Constants.Examine.InternalMemberSearcher; type = "member"; fields = new[] { "id", "__NodeId", "email", "loginName" }; 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"; var allMediaStartNodes = umbracoContext.Security.CurrentUser.CalculateMediaStartNodeIds(Current.Services.EntityService); AppendPath(sb, UmbracoObjectTypes.Media, allMediaStartNodes, searchFrom, Current.Services.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); break; default: throw new NotSupportedException("The " + typeof(UmbracoTreeSearcher) + " currently does not support searching against object type " + entityType); } var internalSearcher = ExamineManager.Instance.SearchProviderCollection[searcher]; //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 //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(new[] { '\"', '\'' }); query = Lucene.Net.QueryParsers.QueryParser.Escape(query); //nothing to search if (searchFrom.IsNullOrWhiteSpace() && query.IsNullOrWhiteSpace()) { totalFound = 0; return new List(); } //update the query with the query term if (query.IsNullOrWhiteSpace() == false) { //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 "); foreach (var f in fields) { //additional fields normally sb.Append(f); sb.Append(": ("); sb.Append(query); sb.Append(") "); } sb.Append(") "); } } else { var trimmed = query.Trim(new[] { '\"', '\'' }); //nothing to search if (searchFrom.IsNullOrWhiteSpace() && trimmed.IsNullOrWhiteSpace()) { totalFound = 0; return new List(); } //update the query with the query term if (trimmed.IsNullOrWhiteSpace() == false) { query = Lucene.Net.QueryParsers.QueryParser.Escape(query); 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(") "); foreach (var f in fields) { //additional fields normally sb.Append(f); sb.Append(":"); sb.Append("("); foreach (var w in querywords) { sb.Append(w.ToLower()); sb.Append("* "); } sb.Append(")"); sb.Append(" "); } sb.Append(") "); } } //must match index type sb.Append("+__IndexType:"); sb.Append(type); var raw = internalSearcher.CreateSearchCriteria().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(umbracoHelper, pagedResult); default: throw new NotSupportedException("The " + typeof(UmbracoTreeSearcher) + " currently does not support searching against object type " + entityType); } } private void AppendPath(StringBuilder sb, UmbracoObjectTypes objectType, int[] startNodeIds, string searchFrom, IEntityService entityService) { if (sb == null) throw new ArgumentNullException("sb"); if (entityService == null) throw new ArgumentNullException("entityService"); int searchFromId; var entityPath = int.TryParse(searchFrom, out 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) // -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("\\,*"); } /// /// Returns a collection of entities for media based on search results /// /// /// private IEnumerable MemberFromSearchResults(SearchResult[] results) { var mapped = Mapper.Map>(results).ToArray(); //add additional data foreach (var m in mapped) { //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.ToInvariantString() == m.Id.ToString()); if (searchResult.Fields.ContainsKey("email") && searchResult.Fields["email"] != null) { m.AdditionalData["Email"] = results.First(x => x.Id.ToInvariantString() == m.Id.ToString()).Fields["email"]; } if (searchResult.Fields.ContainsKey("__key") && searchResult.Fields["__key"] != null) { Guid key; if (Guid.TryParse(searchResult.Fields["__key"], out key)) { m.Key = key; } } } return mapped; } /// /// Returns a collection of entities for media based on search results /// /// /// private IEnumerable MediaFromSearchResults(IEnumerable results) { var mapped = Mapper.Map>(results).ToArray(); //add additional data foreach (var m in mapped) { //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"; } } return mapped; } /// /// Returns a collection of entities for content based on search results /// /// /// /// private IEnumerable ContentFromSearchResults(UmbracoHelper umbracoHelper, IEnumerable results) { var mapped = Mapper.Map>(results).ToArray(); //add additional data foreach (var m in mapped) { var intId = m.Id.TryConvertTo(); if (intId.Success) { m.AdditionalData["Url"] = umbracoHelper.NiceUrl(intId.Result); } } return mapped; } } }