LuceneSearchEngine.cs 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310
  1. using System;
  2. using System.Collections.Generic;
  3. using System.IO;
  4. using System.Linq;
  5. using System.Threading.Tasks;
  6. using Lucene.Net.Analysis.Standard;
  7. using Lucene.Net.Documents;
  8. using Lucene.Net.Index;
  9. using Lucene.Net.QueryParsers;
  10. using Lucene.Net.Search;
  11. using Lucene.Net.Store;
  12. using MediaBrowser.Controller;
  13. using MediaBrowser.Controller.Entities;
  14. using MediaBrowser.Controller.Library;
  15. using MediaBrowser.Model.Logging;
  16. namespace MediaBrowser.Server.Implementations.Library
  17. {
  18. /// <summary>
  19. /// Class LuceneSearchEngine
  20. /// http://www.codeproject.com/Articles/320219/Lucene-Net-ultra-fast-search-for-MVC-or-WebForms
  21. /// </summary>
  22. public class LuceneSearchEngine : ILibrarySearchEngine, IDisposable
  23. {
  24. public LuceneSearchEngine(IServerApplicationPaths serverPaths, ILogManager logManager)
  25. {
  26. //string luceneDbPath = serverPaths.DataPath + "\\SearchIndexDB";
  27. //if (!System.IO.Directory.Exists(luceneDbPath))
  28. // System.IO.Directory.CreateDirectory(luceneDbPath);
  29. //else if(File.Exists(luceneDbPath + "\\write.lock"))
  30. // File.Delete(luceneDbPath + "\\write.lock");
  31. //LuceneSearch.Init(luceneDbPath, logManager.GetLogger("Lucene"));
  32. //BaseItem.LibraryManager.LibraryChanged += LibraryChanged;
  33. }
  34. public void LibraryChanged(object source, ChildrenChangedEventArgs changeInformation)
  35. {
  36. Task.Run(() =>
  37. {
  38. if (changeInformation.ItemsAdded.Count + changeInformation.ItemsUpdated.Count > 0)
  39. {
  40. LuceneSearch.AddUpdateLuceneIndex(changeInformation.ItemsAdded.Concat(changeInformation.ItemsUpdated));
  41. }
  42. if (changeInformation.ItemsRemoved.Count > 0)
  43. {
  44. LuceneSearch.RemoveFromLuceneIndex(changeInformation.ItemsRemoved);
  45. }
  46. });
  47. }
  48. public void AddItemsToIndex(IEnumerable<BaseItem> items)
  49. {
  50. LuceneSearch.AddUpdateLuceneIndex(items);
  51. }
  52. /// <summary>
  53. /// Searches items and returns them in order of relevance.
  54. /// </summary>
  55. /// <param name="items">The items.</param>
  56. /// <param name="searchTerm">The search term.</param>
  57. /// <returns>IEnumerable{BaseItem}.</returns>
  58. /// <exception cref="System.ArgumentNullException">searchTerm</exception>
  59. public IEnumerable<BaseItem> Search(IEnumerable<BaseItem> items, string searchTerm)
  60. {
  61. if (string.IsNullOrEmpty(searchTerm))
  62. {
  63. throw new ArgumentNullException("searchTerm");
  64. }
  65. return LuceneSearch.Search(searchTerm);
  66. }
  67. public void Dispose()
  68. {
  69. //BaseItem.LibraryManager.LibraryChanged -= LibraryChanged;
  70. //LuceneSearch.CloseAll();
  71. }
  72. }
  73. public static class LuceneSearch
  74. {
  75. private static ILogger logger;
  76. private static string path;
  77. private static object lockOb = new object();
  78. private static FSDirectory _directory;
  79. private static FSDirectory directory
  80. {
  81. get
  82. {
  83. if (_directory == null)
  84. {
  85. logger.Info("Opening new Directory: " + path);
  86. _directory = FSDirectory.Open(path);
  87. }
  88. return _directory;
  89. }
  90. set
  91. {
  92. _directory = value;
  93. }
  94. }
  95. private static IndexWriter _writer;
  96. private static IndexWriter writer
  97. {
  98. get
  99. {
  100. if (_writer == null)
  101. {
  102. logger.Info("Opening new IndexWriter");
  103. _writer = new IndexWriter(directory, analyzer, IndexWriter.MaxFieldLength.UNLIMITED);
  104. }
  105. return _writer;
  106. }
  107. set
  108. {
  109. _writer = value;
  110. }
  111. }
  112. private static Dictionary<string, float> bonusTerms;
  113. public static void Init(string path, ILogger logger)
  114. {
  115. logger.Info("Lucene: Init");
  116. bonusTerms = new Dictionary<string, float>();
  117. bonusTerms.Add("Name", 2);
  118. bonusTerms.Add("Overview", 1);
  119. // Optimize the DB on initialization
  120. // TODO: Test whether this has..
  121. // Any effect what-so-ever (apart from initializing the indexwriter on the mainthread context, which makes things a whole lot easier)
  122. // Costs too much time
  123. // Is heavy on the CPU / Memory
  124. LuceneSearch.logger = logger;
  125. LuceneSearch.path = path;
  126. writer.Optimize();
  127. }
  128. private static StandardAnalyzer analyzer = new StandardAnalyzer(Lucene.Net.Util.Version.LUCENE_30);
  129. private static Searcher searcher = null;
  130. private static Document createDocument(BaseItem data)
  131. {
  132. Document doc = new Document();
  133. doc.Add(new Field("Id", data.Id.ToString(), Field.Store.YES, Field.Index.NO));
  134. doc.Add(new Field("Name", data.Name, Field.Store.YES, Field.Index.ANALYZED) { Boost = 2 });
  135. doc.Add(new Field("Overview", data.Overview != null ? data.Overview : "", Field.Store.YES, Field.Index.ANALYZED));
  136. return doc;
  137. }
  138. private static void Create(BaseItem item)
  139. {
  140. lock (lockOb)
  141. {
  142. try
  143. {
  144. if (searcher != null)
  145. {
  146. try
  147. {
  148. searcher.Dispose();
  149. }
  150. catch (Exception e)
  151. {
  152. logger.ErrorException("Error in Lucene while creating index (disposing alive searcher)", e, item);
  153. }
  154. searcher = null;
  155. }
  156. _removeFromLuceneIndex(item);
  157. _addToLuceneIndex(item);
  158. }
  159. catch (Exception e)
  160. {
  161. logger.ErrorException("Error in Lucene while creating index", e, item);
  162. }
  163. }
  164. }
  165. private static void _addToLuceneIndex(BaseItem data)
  166. {
  167. // Prevent double entries
  168. var doc = createDocument(data);
  169. writer.AddDocument(doc);
  170. }
  171. private static void _removeFromLuceneIndex(BaseItem data)
  172. {
  173. var query = new TermQuery(new Term("Id", data.Id.ToString()));
  174. writer.DeleteDocuments(query);
  175. }
  176. public static void AddUpdateLuceneIndex(IEnumerable<BaseItem> items)
  177. {
  178. foreach (var item in items)
  179. {
  180. logger.Info("Adding/Updating BaseItem " + item.Name + "(" + item.Id.ToString() + ") to/on Lucene Index");
  181. Create(item);
  182. }
  183. writer.Commit();
  184. writer.Flush(true, true, true);
  185. }
  186. public static void RemoveFromLuceneIndex(IEnumerable<BaseItem> items)
  187. {
  188. foreach (var item in items)
  189. {
  190. logger.Info("Removing BaseItem " + item.Name + "(" + item.Id.ToString() + ") from Lucene Index");
  191. _removeFromLuceneIndex(item);
  192. }
  193. writer.Commit();
  194. writer.Flush(true, true, true);
  195. }
  196. public static IEnumerable<BaseItem> Search(string searchQuery)
  197. {
  198. var results = new List<BaseItem>();
  199. lock (lockOb)
  200. {
  201. try
  202. {
  203. if (searcher == null)
  204. {
  205. searcher = new IndexSearcher(directory, true);
  206. }
  207. BooleanQuery finalQuery = new BooleanQuery();
  208. MultiFieldQueryParser parser = new MultiFieldQueryParser(Lucene.Net.Util.Version.LUCENE_30, new string[] { "Name", "Overview" }, analyzer, bonusTerms);
  209. string[] terms = searchQuery.Split(new[] { " " }, StringSplitOptions.RemoveEmptyEntries);
  210. foreach (string term in terms)
  211. finalQuery.Add(parser.Parse(term.Replace("~", "") + "~0.75"), Occur.SHOULD);
  212. foreach (string term in terms)
  213. finalQuery.Add(parser.Parse(term.Replace("*", "") + "*"), Occur.SHOULD);
  214. logger.Debug("Querying Lucene with query: " + finalQuery.ToString());
  215. long start = DateTime.Now.Ticks;
  216. var searchResult = searcher.Search(finalQuery, 20);
  217. foreach (var searchHit in searchResult.ScoreDocs)
  218. {
  219. Document hit = searcher.Doc(searchHit.Doc);
  220. results.Add(BaseItem.LibraryManager.GetItemById(Guid.Parse(hit.Get("Id"))));
  221. }
  222. long total = DateTime.Now.Ticks - start;
  223. float msTotal = (float)total / TimeSpan.TicksPerMillisecond;
  224. logger.Debug(searchResult.ScoreDocs.Length + " result" + (searchResult.ScoreDocs.Length == 1 ? "" : "s") + " in " + msTotal + " ms.");
  225. }
  226. catch (Exception e)
  227. {
  228. logger.ErrorException("Error while searching Lucene index", e);
  229. }
  230. }
  231. return results;
  232. }
  233. public static void CloseAll()
  234. {
  235. logger.Debug("Lucene: CloseAll");
  236. if (writer != null)
  237. {
  238. logger.Debug("Lucene: CloseAll - Writer is alive");
  239. writer.Flush(true, true, true);
  240. writer.Commit();
  241. writer.WaitForMerges();
  242. writer.Dispose();
  243. writer = null;
  244. }
  245. if (analyzer != null)
  246. {
  247. logger.Debug("Lucene: CloseAll - Analyzer is alive");
  248. analyzer.Close();
  249. analyzer.Dispose();
  250. analyzer = null;
  251. }
  252. if (searcher != null)
  253. {
  254. logger.Debug("Lucene: CloseAll - Searcher is alive");
  255. searcher.Dispose();
  256. searcher = null;
  257. }
  258. if (directory != null)
  259. {
  260. logger.Debug("Lucene: CloseAll - Directory is alive");
  261. directory.Dispose();
  262. directory = null;
  263. }
  264. }
  265. }
  266. }