123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398 |
- using System;
- using System.Collections.Concurrent;
- using System.Collections.Generic;
- using System.IO;
- using System.Linq;
- using System.Text.RegularExpressions;
- using System.Threading.Tasks;
- using JetBrains.Annotations;
- using UnityEngine;
-
- namespace UnityEditor.Searcher
- {
- [PublicAPI]
- public class SearcherDatabase : SearcherDatabaseBase
- {
- Dictionary<string, IReadOnlyList<ValueTuple<string, float>>> m_Index = new Dictionary<string, IReadOnlyList<ValueTuple<string, float>>>();
-
- class Result
- {
- public SearcherItem item;
- public float maxScore;
- }
-
- const bool k_IsParallel = true;
-
- public Func<string, SearcherItem, bool> MatchFilter { get; set; }
-
- public static SearcherDatabase Create(
- List<SearcherItem> items,
- string databaseDirectory,
- bool serializeToFile = true
- )
- {
- if (serializeToFile && databaseDirectory != null && !Directory.Exists(databaseDirectory))
- Directory.CreateDirectory(databaseDirectory);
-
- var database = new SearcherDatabase(databaseDirectory, items);
-
- if (serializeToFile)
- database.SerializeToFile();
-
- database.BuildIndex();
- return database;
- }
-
- public static SearcherDatabase Load(string databaseDirectory)
- {
- if (!Directory.Exists(databaseDirectory))
- throw new InvalidOperationException("databaseDirectory not found.");
-
- var database = new SearcherDatabase(databaseDirectory, null);
- database.LoadFromFile();
- database.BuildIndex();
-
- return database;
- }
-
- public SearcherDatabase(IReadOnlyCollection<SearcherItem> db)
- : this("", db)
- {
- }
-
- SearcherDatabase(string databaseDirectory, IReadOnlyCollection<SearcherItem> db)
- : base(databaseDirectory)
- {
- m_ItemList = new List<SearcherItem>();
- var nextId = 0;
-
- if (db != null)
- foreach (var item in db)
- AddItemToIndex(item, ref nextId, null);
- }
-
- public override List<SearcherItem> Search(string query, out float localMaxScore)
- {
- // Match assumes the query is trimmed
- query = query.Trim(' ', '\t');
- localMaxScore = 0;
-
- if (string.IsNullOrWhiteSpace(query))
- {
- if (MatchFilter == null)
- return m_ItemList;
-
- // ReSharper disable once RedundantLogicalConditionalExpressionOperand
- if (k_IsParallel && m_ItemList.Count > 100)
- return FilterMultiThreaded(query);
-
- return FilterSingleThreaded(query);
- }
-
- var finalResults = new List<SearcherItem> { null };
- var max = new Result();
- var tokenizedQuery = new List<string>();
- foreach (var token in Tokenize(query))
- {
- tokenizedQuery.Add(token.Trim().ToLower());
- }
-
- // ReSharper disable once RedundantLogicalConditionalExpressionOperand
- if (k_IsParallel && m_ItemList.Count > 100)
- SearchMultithreaded(query, tokenizedQuery, max, finalResults);
- else
- SearchSingleThreaded(query, tokenizedQuery, max, finalResults);
-
- localMaxScore = max.maxScore;
- if (max.item != null)
- finalResults[0] = max.item;
- else
- finalResults.RemoveAt(0);
-
- return finalResults;
- }
-
- protected virtual bool Match(string query, IReadOnlyList<string> tokenizedQuery, SearcherItem item, out float score)
- {
- var filter = MatchFilter?.Invoke(query, item) ?? true;
- return Match(tokenizedQuery, item.Path, out score) && filter;
- }
-
- List<SearcherItem> FilterSingleThreaded(string query)
- {
- var result = new List<SearcherItem>();
-
- foreach (var searcherItem in m_ItemList)
- {
- if (!MatchFilter.Invoke(query, searcherItem))
- continue;
-
- result.Add(searcherItem);
- }
-
- return result;
- }
-
- List<SearcherItem> FilterMultiThreaded(string query)
- {
- var result = new List<SearcherItem>();
- var count = Environment.ProcessorCount;
- var tasks = new Task[count];
- var lists = new List<SearcherItem>[count];
- var itemsPerTask = (int)Math.Ceiling(m_ItemList.Count / (float)count);
-
- for (var i = 0; i < count; i++)
- {
- var i1 = i;
- tasks[i] = Task.Run(() =>
- {
- lists[i1] = new List<SearcherItem>();
-
- for (var j = 0; j < itemsPerTask; j++)
- {
- var index = j + itemsPerTask * i1;
- if (index >= m_ItemList.Count)
- break;
-
- var item = m_ItemList[index];
- if (!MatchFilter.Invoke(query, item))
- continue;
-
- lists[i1].Add(item);
- }
- });
- }
-
- Task.WaitAll(tasks);
-
- for (var i = 0; i < count; i++)
- {
- result.AddRange(lists[i]);
- }
-
- return result;
- }
-
- readonly float k_ScoreCutOff = 0.33f;
-
- void SearchSingleThreaded(string query, IReadOnlyList<string> tokenizedQuery, Result max, ICollection<SearcherItem> finalResults)
- {
- List<Result> results = new List<Result>();
-
- foreach (var item in m_ItemList)
- {
- float score = 0;
- if (query.Length == 0 || Match(query, tokenizedQuery, item, out score))
- {
- if (score > max.maxScore)
- {
- max.item = item;
- max.maxScore = score;
- }
- results.Add(new Result() { item = item, maxScore = score});
- }
- }
-
- PostprocessResults(results, finalResults, max);
- }
-
- void SearchMultithreaded(string query, IReadOnlyList<string> tokenizedQuery, Result max, List<SearcherItem> finalResults)
- {
- var count = Environment.ProcessorCount;
- var tasks = new Task[count];
- var localResults = new Result[count];
- var queue = new ConcurrentQueue<Result>();
- var itemsPerTask = (int)Math.Ceiling(m_ItemList.Count / (float)count);
-
- for (var i = 0; i < count; i++)
- {
- var i1 = i;
- localResults[i1] = new Result();
- tasks[i] = Task.Run(() =>
- {
- var result = localResults[i1];
- for (var j = 0; j < itemsPerTask; j++)
- {
- var index = j + itemsPerTask * i1;
- if (index >= m_ItemList.Count)
- break;
- var item = m_ItemList[index];
- float score = 0;
- if (query.Length == 0 || Match(query, tokenizedQuery, item, out score))
- {
- if (score > result.maxScore)
- {
- result.maxScore = score;
- result.item = item;
- }
-
- queue.Enqueue(new Result { item = item, maxScore = score });
- }
- }
- });
- }
-
- Task.WaitAll(tasks);
-
- for (var i = 0; i < count; i++)
- {
- if (localResults[i].maxScore > max.maxScore)
- {
- max.maxScore = localResults[i].maxScore;
- max.item = localResults[i].item;
- }
- }
-
- PostprocessResults(queue, finalResults, max);
- }
-
- void PostprocessResults(IEnumerable<Result> results, ICollection<SearcherItem> items, Result max)
- {
- foreach (var result in results)
- {
- var normalizedScore = result.maxScore / max.maxScore;
- if (result.item != null && result.item != max.item && normalizedScore > k_ScoreCutOff)
- {
- items.Add(result.item);
- }
- }
- }
-
- public override void BuildIndex()
- {
- m_Index.Clear();
-
- foreach (var item in m_ItemList)
- {
- if (!m_Index.ContainsKey(item.Path))
- {
- List<ValueTuple<string, float>> terms = new List<ValueTuple<string, float>>();
-
- // If the item uses synonyms to return results for similar words/phrases, add them to the search terms
- IList<string> tokens = null;
- if (item.Synonyms == null)
- tokens = Tokenize(item.Name);
- else
- tokens = Tokenize(string.Format("{0} {1}", item.Name, string.Join(" ", item.Synonyms)));
-
- // Fixes bug: https://fogbugz.unity3d.com/f/cases/1359158/
- // Without this, node names with spaces or those with Pascal casing were not added to index
- var nodeName = item.Name.ToLower().Replace(" ", String.Empty);
- tokens.Add(nodeName);
-
- string tokenSuite = "";
- foreach (var token in tokens)
- {
- var t = token.ToLower();
- if (t.Length > 1)
- {
- terms.Add(new ValueTuple<string, float>(t, 0.8f));
- }
-
- if (tokenSuite.Length > 0)
- {
- tokenSuite += " " + t;
- terms.Add(new ValueTuple<string, float>(tokenSuite, 1f));
- }
- else
- {
- tokenSuite = t;
- }
- }
-
- // Add a term containing all the uppercase letters (CamelCase World BBox => CCWBB)
- var initialList = Regex.Split(item.Name, @"\P{Lu}+");
- var initials = string.Concat(initialList).Trim();
- if (!string.IsNullOrEmpty(initials))
- terms.Add(new ValueTuple<string, float>(initials.ToLower(), 0.5f));
-
- m_Index.Add(item.Path, terms);
- }
- }
- }
-
- static IList<string> Tokenize(string s)
- {
- var knownTokens = new HashSet<string>();
- var tokens = new List<string>();
-
- // Split on word boundaries
- foreach (var t in Regex.Split(s, @"\W"))
- {
- // Split camel case words
- var tt = Regex.Split(t, @"(\p{Lu}+\P{Lu}*)");
- foreach (var ttt in tt)
- {
- var tttt = ttt.Trim();
- if (!string.IsNullOrEmpty(tttt) && !knownTokens.Contains(tttt))
- {
- knownTokens.Add(tttt);
- tokens.Add(tttt);
- }
- }
- }
-
- return tokens;
- }
-
- bool Match(IReadOnlyList<string> tokenizedQuery, string itemPath, out float score)
- {
- itemPath = itemPath.Trim();
- if (itemPath == "")
- {
- if (tokenizedQuery.Count == 0)
- {
- score = 1;
- return true;
- }
- else
- {
- score = 0;
- return false;
- }
- }
-
- IReadOnlyList<ValueTuple<string, float>> indexTerms;
- if (!m_Index.TryGetValue(itemPath, out indexTerms))
- {
- score = 0;
- return false;
- }
-
- float maxScore = 0.0f;
- foreach (var t in indexTerms)
- {
- float scoreForTerm = 0f;
- var querySuite = "";
- var querySuiteFactor = 1.25f;
- foreach (var q in tokenizedQuery)
- {
- if (t.Item1.StartsWith(q))
- {
- scoreForTerm += t.Item2 * q.Length / t.Item1.Length;
- }
-
- if (querySuite.Length > 0)
- {
- querySuite += " " + q;
- if (t.Item1.StartsWith(querySuite))
- {
- scoreForTerm += t.Item2 * querySuiteFactor * querySuite.Length / t.Item1.Length;
- }
- }
- else
- {
- querySuite = q;
- }
-
- querySuiteFactor *= querySuiteFactor;
- }
-
- maxScore = Mathf.Max(maxScore, scoreForTerm);
- }
-
- score = maxScore;
- return score > 0;
- }
- }
- }
|