暂无描述
您最多选择25个主题 主题必须以字母或数字开头,可以包含连字符 (-),并且长度不得超过35个字符

SearcherDatabase.cs 13KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398
  1. using System;
  2. using System.Collections.Concurrent;
  3. using System.Collections.Generic;
  4. using System.IO;
  5. using System.Linq;
  6. using System.Text.RegularExpressions;
  7. using System.Threading.Tasks;
  8. using JetBrains.Annotations;
  9. using UnityEngine;
  10. namespace UnityEditor.Searcher
  11. {
  12. [PublicAPI]
  13. public class SearcherDatabase : SearcherDatabaseBase
  14. {
  15. Dictionary<string, IReadOnlyList<ValueTuple<string, float>>> m_Index = new Dictionary<string, IReadOnlyList<ValueTuple<string, float>>>();
  16. class Result
  17. {
  18. public SearcherItem item;
  19. public float maxScore;
  20. }
  21. const bool k_IsParallel = true;
  22. public Func<string, SearcherItem, bool> MatchFilter { get; set; }
  23. public static SearcherDatabase Create(
  24. List<SearcherItem> items,
  25. string databaseDirectory,
  26. bool serializeToFile = true
  27. )
  28. {
  29. if (serializeToFile && databaseDirectory != null && !Directory.Exists(databaseDirectory))
  30. Directory.CreateDirectory(databaseDirectory);
  31. var database = new SearcherDatabase(databaseDirectory, items);
  32. if (serializeToFile)
  33. database.SerializeToFile();
  34. database.BuildIndex();
  35. return database;
  36. }
  37. public static SearcherDatabase Load(string databaseDirectory)
  38. {
  39. if (!Directory.Exists(databaseDirectory))
  40. throw new InvalidOperationException("databaseDirectory not found.");
  41. var database = new SearcherDatabase(databaseDirectory, null);
  42. database.LoadFromFile();
  43. database.BuildIndex();
  44. return database;
  45. }
  46. public SearcherDatabase(IReadOnlyCollection<SearcherItem> db)
  47. : this("", db)
  48. {
  49. }
  50. SearcherDatabase(string databaseDirectory, IReadOnlyCollection<SearcherItem> db)
  51. : base(databaseDirectory)
  52. {
  53. m_ItemList = new List<SearcherItem>();
  54. var nextId = 0;
  55. if (db != null)
  56. foreach (var item in db)
  57. AddItemToIndex(item, ref nextId, null);
  58. }
  59. public override List<SearcherItem> Search(string query, out float localMaxScore)
  60. {
  61. // Match assumes the query is trimmed
  62. query = query.Trim(' ', '\t');
  63. localMaxScore = 0;
  64. if (string.IsNullOrWhiteSpace(query))
  65. {
  66. if (MatchFilter == null)
  67. return m_ItemList;
  68. // ReSharper disable once RedundantLogicalConditionalExpressionOperand
  69. if (k_IsParallel && m_ItemList.Count > 100)
  70. return FilterMultiThreaded(query);
  71. return FilterSingleThreaded(query);
  72. }
  73. var finalResults = new List<SearcherItem> { null };
  74. var max = new Result();
  75. var tokenizedQuery = new List<string>();
  76. foreach (var token in Tokenize(query))
  77. {
  78. tokenizedQuery.Add(token.Trim().ToLower());
  79. }
  80. // ReSharper disable once RedundantLogicalConditionalExpressionOperand
  81. if (k_IsParallel && m_ItemList.Count > 100)
  82. SearchMultithreaded(query, tokenizedQuery, max, finalResults);
  83. else
  84. SearchSingleThreaded(query, tokenizedQuery, max, finalResults);
  85. localMaxScore = max.maxScore;
  86. if (max.item != null)
  87. finalResults[0] = max.item;
  88. else
  89. finalResults.RemoveAt(0);
  90. return finalResults;
  91. }
  92. protected virtual bool Match(string query, IReadOnlyList<string> tokenizedQuery, SearcherItem item, out float score)
  93. {
  94. var filter = MatchFilter?.Invoke(query, item) ?? true;
  95. return Match(tokenizedQuery, item.Path, out score) && filter;
  96. }
  97. List<SearcherItem> FilterSingleThreaded(string query)
  98. {
  99. var result = new List<SearcherItem>();
  100. foreach (var searcherItem in m_ItemList)
  101. {
  102. if (!MatchFilter.Invoke(query, searcherItem))
  103. continue;
  104. result.Add(searcherItem);
  105. }
  106. return result;
  107. }
  108. List<SearcherItem> FilterMultiThreaded(string query)
  109. {
  110. var result = new List<SearcherItem>();
  111. var count = Environment.ProcessorCount;
  112. var tasks = new Task[count];
  113. var lists = new List<SearcherItem>[count];
  114. var itemsPerTask = (int)Math.Ceiling(m_ItemList.Count / (float)count);
  115. for (var i = 0; i < count; i++)
  116. {
  117. var i1 = i;
  118. tasks[i] = Task.Run(() =>
  119. {
  120. lists[i1] = new List<SearcherItem>();
  121. for (var j = 0; j < itemsPerTask; j++)
  122. {
  123. var index = j + itemsPerTask * i1;
  124. if (index >= m_ItemList.Count)
  125. break;
  126. var item = m_ItemList[index];
  127. if (!MatchFilter.Invoke(query, item))
  128. continue;
  129. lists[i1].Add(item);
  130. }
  131. });
  132. }
  133. Task.WaitAll(tasks);
  134. for (var i = 0; i < count; i++)
  135. {
  136. result.AddRange(lists[i]);
  137. }
  138. return result;
  139. }
  140. readonly float k_ScoreCutOff = 0.33f;
  141. void SearchSingleThreaded(string query, IReadOnlyList<string> tokenizedQuery, Result max, ICollection<SearcherItem> finalResults)
  142. {
  143. List<Result> results = new List<Result>();
  144. foreach (var item in m_ItemList)
  145. {
  146. float score = 0;
  147. if (query.Length == 0 || Match(query, tokenizedQuery, item, out score))
  148. {
  149. if (score > max.maxScore)
  150. {
  151. max.item = item;
  152. max.maxScore = score;
  153. }
  154. results.Add(new Result() { item = item, maxScore = score});
  155. }
  156. }
  157. PostprocessResults(results, finalResults, max);
  158. }
  159. void SearchMultithreaded(string query, IReadOnlyList<string> tokenizedQuery, Result max, List<SearcherItem> finalResults)
  160. {
  161. var count = Environment.ProcessorCount;
  162. var tasks = new Task[count];
  163. var localResults = new Result[count];
  164. var queue = new ConcurrentQueue<Result>();
  165. var itemsPerTask = (int)Math.Ceiling(m_ItemList.Count / (float)count);
  166. for (var i = 0; i < count; i++)
  167. {
  168. var i1 = i;
  169. localResults[i1] = new Result();
  170. tasks[i] = Task.Run(() =>
  171. {
  172. var result = localResults[i1];
  173. for (var j = 0; j < itemsPerTask; j++)
  174. {
  175. var index = j + itemsPerTask * i1;
  176. if (index >= m_ItemList.Count)
  177. break;
  178. var item = m_ItemList[index];
  179. float score = 0;
  180. if (query.Length == 0 || Match(query, tokenizedQuery, item, out score))
  181. {
  182. if (score > result.maxScore)
  183. {
  184. result.maxScore = score;
  185. result.item = item;
  186. }
  187. queue.Enqueue(new Result { item = item, maxScore = score });
  188. }
  189. }
  190. });
  191. }
  192. Task.WaitAll(tasks);
  193. for (var i = 0; i < count; i++)
  194. {
  195. if (localResults[i].maxScore > max.maxScore)
  196. {
  197. max.maxScore = localResults[i].maxScore;
  198. max.item = localResults[i].item;
  199. }
  200. }
  201. PostprocessResults(queue, finalResults, max);
  202. }
  203. void PostprocessResults(IEnumerable<Result> results, ICollection<SearcherItem> items, Result max)
  204. {
  205. foreach (var result in results)
  206. {
  207. var normalizedScore = result.maxScore / max.maxScore;
  208. if (result.item != null && result.item != max.item && normalizedScore > k_ScoreCutOff)
  209. {
  210. items.Add(result.item);
  211. }
  212. }
  213. }
  214. public override void BuildIndex()
  215. {
  216. m_Index.Clear();
  217. foreach (var item in m_ItemList)
  218. {
  219. if (!m_Index.ContainsKey(item.Path))
  220. {
  221. List<ValueTuple<string, float>> terms = new List<ValueTuple<string, float>>();
  222. // If the item uses synonyms to return results for similar words/phrases, add them to the search terms
  223. IList<string> tokens = null;
  224. if (item.Synonyms == null)
  225. tokens = Tokenize(item.Name);
  226. else
  227. tokens = Tokenize(string.Format("{0} {1}", item.Name, string.Join(" ", item.Synonyms)));
  228. // Fixes bug: https://fogbugz.unity3d.com/f/cases/1359158/
  229. // Without this, node names with spaces or those with Pascal casing were not added to index
  230. var nodeName = item.Name.ToLower().Replace(" ", String.Empty);
  231. tokens.Add(nodeName);
  232. string tokenSuite = "";
  233. foreach (var token in tokens)
  234. {
  235. var t = token.ToLower();
  236. if (t.Length > 1)
  237. {
  238. terms.Add(new ValueTuple<string, float>(t, 0.8f));
  239. }
  240. if (tokenSuite.Length > 0)
  241. {
  242. tokenSuite += " " + t;
  243. terms.Add(new ValueTuple<string, float>(tokenSuite, 1f));
  244. }
  245. else
  246. {
  247. tokenSuite = t;
  248. }
  249. }
  250. // Add a term containing all the uppercase letters (CamelCase World BBox => CCWBB)
  251. var initialList = Regex.Split(item.Name, @"\P{Lu}+");
  252. var initials = string.Concat(initialList).Trim();
  253. if (!string.IsNullOrEmpty(initials))
  254. terms.Add(new ValueTuple<string, float>(initials.ToLower(), 0.5f));
  255. m_Index.Add(item.Path, terms);
  256. }
  257. }
  258. }
  259. static IList<string> Tokenize(string s)
  260. {
  261. var knownTokens = new HashSet<string>();
  262. var tokens = new List<string>();
  263. // Split on word boundaries
  264. foreach (var t in Regex.Split(s, @"\W"))
  265. {
  266. // Split camel case words
  267. var tt = Regex.Split(t, @"(\p{Lu}+\P{Lu}*)");
  268. foreach (var ttt in tt)
  269. {
  270. var tttt = ttt.Trim();
  271. if (!string.IsNullOrEmpty(tttt) && !knownTokens.Contains(tttt))
  272. {
  273. knownTokens.Add(tttt);
  274. tokens.Add(tttt);
  275. }
  276. }
  277. }
  278. return tokens;
  279. }
  280. bool Match(IReadOnlyList<string> tokenizedQuery, string itemPath, out float score)
  281. {
  282. itemPath = itemPath.Trim();
  283. if (itemPath == "")
  284. {
  285. if (tokenizedQuery.Count == 0)
  286. {
  287. score = 1;
  288. return true;
  289. }
  290. else
  291. {
  292. score = 0;
  293. return false;
  294. }
  295. }
  296. IReadOnlyList<ValueTuple<string, float>> indexTerms;
  297. if (!m_Index.TryGetValue(itemPath, out indexTerms))
  298. {
  299. score = 0;
  300. return false;
  301. }
  302. float maxScore = 0.0f;
  303. foreach (var t in indexTerms)
  304. {
  305. float scoreForTerm = 0f;
  306. var querySuite = "";
  307. var querySuiteFactor = 1.25f;
  308. foreach (var q in tokenizedQuery)
  309. {
  310. if (t.Item1.StartsWith(q))
  311. {
  312. scoreForTerm += t.Item2 * q.Length / t.Item1.Length;
  313. }
  314. if (querySuite.Length > 0)
  315. {
  316. querySuite += " " + q;
  317. if (t.Item1.StartsWith(querySuite))
  318. {
  319. scoreForTerm += t.Item2 * querySuiteFactor * querySuite.Length / t.Item1.Length;
  320. }
  321. }
  322. else
  323. {
  324. querySuite = q;
  325. }
  326. querySuiteFactor *= querySuiteFactor;
  327. }
  328. maxScore = Mathf.Max(maxScore, scoreForTerm);
  329. }
  330. score = maxScore;
  331. return score > 0;
  332. }
  333. }
  334. }