突然一想很久不寫博客了,工做緣由我的緣由,這些天一直但願一天假如36個小時該多好,可是,假如不可能。html
因爲近期在項目中接觸了lucene,這個已經沒有人維護的全文搜索框架,確實踩了很多坑,爲何用lucene呢?其實我也不知道前端
關於lucene原理和全文搜索引擎的一些介紹,園子裏有這幾篇寫的仍是很好的web
http://www.cnblogs.com/skybreak/archive/2013/05/06/3063520.html數據庫
http://kb.cnblogs.com/page/52642/api
因爲徹底沒有接觸過lucene,一開始固然是從X度,bing上搜索關於lucene.net的教程,發現找不到好用的,lucene已經很久沒有維護了,若是細心閱讀源碼會發現許多匪夷所思的設計。對於lucene能夠理解爲一個數據庫,lucene提供了添加數據(建立索引),以及快速全文檢索的api。緩存
經過學習,咱們發現,lucene.net給咱們不少對象cookie
Lucene.Net.Store.Directory:lucene文檔的存放目錄,我這裏把它理解爲一張表,實際上這一張表的字段能夠不同數據結構
Lucene.Net.Documents.Documents:lucene文檔,至關於一條數據框架
Lucene.Net.Index.IndexWriter:用於向Lucene.Net.Store.Directory中添加Lucene.Net.Documents.Documentside
Lucene.Net.Analysis.Standard.StandardAnalyzer:詞法分析器
固然,還有一系列查詢的類,這裏就不列舉了,說了這麼多,我這篇隨筆主要介紹什麼呢?
既然把lucene理解爲一個數據庫了,從框架使用者的角度看,不須要拘泥於lucene到底是如何存儲和檢索數據的,固然,第一次使用時仍是要學習的,可是寫完了就會發現代碼寫的好繁瑣啊,每次都要建立Directory,Analyzer寫查詢語句建立對象,好比項目一開始對站內全部信息進行檢索,一次加入全部數據,搜索全部數據,沒有問題,後來須要對另外一個模塊的信息進行單表檢索,會發現代碼逐漸開始亂了,可是因爲時間問題和開發經驗一開始很難想到好的設計。剛開始時,項目經理指導將每一個須要全文搜索的對象先獨立出來,實現一個接口,程序運行時反射實現該接口的類(暫且成他爲LuceneModel),便可實現搜索,可是後來需求要實現兩種語言的搜索,根據當前前端提供的cookie來查詢指定語言的數據,因而我就又給每個LuceneModel添加一個Language字段,而且Language字段的搜索是一個且的關係(在知足搜索關鍵字後而且該條數據的語言必須 爲從cookie中讀取的)。
顯然,須要對搜索這個模塊進行單獨的維護了,這樣修改方便,而且也能夠供其餘項目使用,也是軟件設計原則吧!
[AttributeUsage(AttributeTargets.Property, Inherited = false, AllowMultiple = false)] public class IndexerAttribute : Attribute { public bool Index; public bool Store; /// <summary> /// /// </summary> /// <param name="index">是否爲其建立索引</param> /// <param name="store">是否存儲原始數據</param> public IndexerAttribute(bool index,bool store) { Index = index; Store = store; } }
表示對模型具體字段的索引方式
public abstract class LuceneDB:IDisposable { private ILunceneDBPathProvider_dbPathProvider = LunceneDBPathProviders.Current; private string _dbPath; protected System.IO.DirectoryInfo _sysDirectory; protected Lucene.Net.Store.Directory _luDirectory; protected Lucene.Net.Analysis.Standard.StandardAnalyzer _analyzer; protected Lucene.Net.Index.IndexWriter _indexWriter; protected bool _isOpen = false; public LuceneDB(string path) { _dbPath = path; } public LuceneDB() { } public void SetDBPath(Type type) { if (null == _dbPath) { _dbPath = _dbPathProvider.Get(type); } } public string DBPath { get { return _dbPath; } } protected abstract void Open(); public virtual void Dispose() { _analyzer.Close(); _analyzer.Dispose(); } }
首先LuceneDB,表示一個鏈接對象,有子類LuceneDBIndexer和LuceneDBSearcher,二者用到的資源不同,因此提供了一個須要實現的Open方法。一般狀況下一個Lucene文件夾下有多個索引文件夾,每一個索引文件夾有着類似的數據結構(同一個數據類型),爲了使這一個模塊可以讓別的項目使用,第一個要解決的問題就是Lucene文件夾位置的問題了,對於web項目,能夠存在APPData也能夠存在bin目錄下,這裏也照顧了下對於Console應用。
public interface ILunceneDBPathProvider { string Get(Type type); }
默認的是一個AppDataLunceneDBPathProvider
public class AppDataLunceneDBPathProvider : ILunceneDBPathProvider { private string _prePath = AppDomain.CurrentDomain.BaseDirectory; public string Get(Type type) { return _prePath + @"\App_Data\Index\" + type.Name; } }
這樣若是有須要檢索的User類,他的lucene文件夾就爲App_Data\Index\User
public class LuceneDBIndexer: LuceneDB { private Dictionary<Type,IEnumerable<PropertyInfo>> _tempProps; public LuceneDBIndexer(string path) : base(path) { } public LuceneDBIndexer() { } protected override void Open() { if (_isOpen) { Dispose(); } if (!System.IO.Directory.Exists(DBPath)) { System.IO.Directory.CreateDirectory(DBPath); } _analyzer = new Lucene.Net.Analysis.Standard.StandardAnalyzer(Lucene.Net.Util.Version.LUCENE_30); _luDirectory = Lucene.Net.Store.FSDirectory.Open(new System.IO.DirectoryInfo(DBPath)); _indexWriter = new Lucene.Net.Index.IndexWriter(_luDirectory, _analyzer, true, Lucene.Net.Index.IndexWriter.MaxFieldLength.LIMITED); _isOpen = true; } public IEnumerable<PropertyInfo> GetProps(Type type) { if(null == _tempProps) { _tempProps = new Dictionary<Type, IEnumerable<PropertyInfo>>(); } if (!_tempProps.ContainsKey(type)) { _tempProps.Add(type, type.GetProperties().Where(prop => null != prop.GetCustomAttribute(typeof(IndexerAttribute), true))); } return _tempProps[type]; } public void Add<T>(T obj) { SetDBPath(typeof(T)); Open(); Document document = new Document(); foreach (var prop in GetProps(typeof(T))) { var value = prop.GetValue(obj)?.ToString(); if(null != value) { var attr = prop.GetCustomAttribute(typeof(IndexerAttribute), true) as IndexerAttribute; var store = attr.Store ? Field.Store.YES : Field.Store.NO; var index = attr.Index ? Field.Index.ANALYZED : Field.Index.NOT_ANALYZED; document.Add(new Field(prop.Name, value, store, index)); } } _indexWriter.AddDocument(document); } public void AddRange<T>(IEnumerable<T> objs) { SetDBPath(typeof(T)); Open(); foreach (var obj in objs) { Document document = new Document(); foreach (var prop in GetProps(typeof(T))) { var value = prop.GetValue(obj)?.ToString(); if (null != value) { var attr = prop.GetCustomAttribute<IndexerAttribute>(); var store = attr.Store ? Field.Store.YES : Field.Store.NO; var index = attr.Index ? Field.Index.ANALYZED : Field.Index.NOT_ANALYZED; document.Add(new Field(prop.Name, value, store, index)); } } _indexWriter.AddDocument(document); } } public override void Dispose() { _indexWriter.Optimize(); _indexWriter.Dispose(); base.Dispose(); } }
這個類用於建立索引,會讀取用在對象上的Indexer標籤
public class LuceneDBSearcher: LuceneDB { private Type _searchType; public LuceneDBSearcher(string path) : base(path) { } public LuceneDBSearcher(Type type) { SetDBPath(type); } public LuceneDBSearcher() { } public Type SearchType { set { //判斷該類型是否實現 某 約定 _searchType = value; } get { return _searchType; } } public IEnumerable<T> Search<T>(string searchText, IEnumerable<string> fields, int page, int pageSize, Dictionary<string,string> condition= null) where T : new() { return GetModels<T>(SearchText(searchText, fields, page, pageSize, condition)); } private IEnumerable<Document> SearchText(string searchText,IEnumerable<string> fields,int page,int pageSize, Dictionary<string,string> condition) { StringBuilder conditionWhere = new StringBuilder(); foreach (var item in condition) { conditionWhere.Append(" +" + item.Key + ":" + item.Value); } Open(); var parser = new Lucene.Net.QueryParsers.MultiFieldQueryParser(Lucene.Net.Util.Version.LUCENE_30, fields.ToArray(), _analyzer); var search = new Lucene.Net.Search.IndexSearcher(_luDirectory, true); var query = parser.Parse("+" + searchText + conditionWhere.ToString()); var searchDocs = search.Search(query, 100).ScoreDocs; return searchDocs.Select(t => search.Doc(t.Doc)); } protected override void Open() { _luDirectory = Lucene.Net.Store.FSDirectory.Open(new System.IO.DirectoryInfo(DBPath)); if (Lucene.Net.Index.IndexWriter.IsLocked(_luDirectory)) { Lucene.Net.Index.IndexWriter.Unlock(_luDirectory); } var lockFilePath = System.IO.Path.Combine(DBPath, "write.lock"); if (System.IO.File.Exists(lockFilePath)) { System.IO.File.Delete(lockFilePath); } _analyzer = new Lucene.Net.Analysis.Standard.StandardAnalyzer(Lucene.Net.Util.Version.LUCENE_30); } private IEnumerable<T> GetModels<T>(IEnumerable<Document> documents) where T:new() { var type = typeof(T); var props = type.GetProperties().Where(prop => null != prop.GetCustomAttribute(typeof(IndexerAttribute), true)); var objs = new List<T>(); foreach (var document in documents) { var obj = new T(); foreach (var prop in props) { var attr = prop.GetCustomAttribute<IndexerAttribute>(); if (null != attr && attr.Store) { object v = Convert.ChangeType(document.Get(prop.Name), prop.PropertyType); prop.SetValue(obj, v); } } objs.Add(obj); } return objs; } private T GetModel<T>(Document document) where T : new() { var type = typeof(T); var props = type.GetProperties().Where(prop => null != prop.GetCustomAttribute(typeof(IndexerAttribute), true)); var obj = new T(); foreach (var prop in props) { var attr = prop.GetCustomAttribute<IndexerAttribute>(); if (null != attr && attr.Store) { object v = Convert.ChangeType(document.Get(prop.Name), prop.PropertyType); prop.SetValue(obj, v); } } return obj; } public override void Dispose() { _analyzer.Dispose(); base.Dispose(); } }
用於檢索Lucene文件夾並將數據轉爲對象
public abstract class LuceneEntityBase:ILuceneStored { #region private private Dictionary<string, PropertyInfo> _propertiesCache; #endregion #region IndexerFields #region ILuceneStored [Indexer(false, true)] public string ID { get; set; } [Indexer(true, false)] public string _Customer { get; set; } [Indexer(true, false)] public string _Category { get; set; } #endregion /// <summary> /// 圖片 /// </summary> [Indexer(false, true)] public string Picture { get; set; } /// <summary> /// 標題 /// </summary> [Indexer(true, true)] public string Title { get; set; } /// <summary> /// 簡介 /// </summary> [Indexer(true, true)] public string Synopsis { get; set; } /// <summary> /// 連接 /// </summary> [Indexer(false, true)] public string Url { get; set; } #endregion public LuceneEntityBase() { } protected IEnumerable<T> Search<T>(string searchText, int page, int pageSize, object condition = null) where T:new () { var ConditionDictionary = null != condition ? InitConditionSearchFields(condition) : new Dictionary<string, string>(); var fullTextSearchFields = from propName in PropertiesCache.Select(t => t.Key) where !ConditionDictionary.ContainsKey(propName) select propName; using (var luceneDB = new LuceneDBSearcher(GetType())) { return luceneDB.Search<T>(searchText, fullTextSearchFields, page, pageSize, ConditionDictionary); } } /// <summary> /// 屬性緩存 /// </summary> protected Dictionary<string, PropertyInfo> PropertiesCache { get { if(null == _propertiesCache) { _propertiesCache = new Dictionary<string, PropertyInfo>(); foreach (var prop in GetType().GetProperties()) { var attr = prop.GetCustomAttribute<IndexerAttribute>(true); if (null != attr && attr.Index) { _propertiesCache.Add(prop.Name, prop); } } } return _propertiesCache; } } /// <summary> /// 初始化 且 條件 /// </summary> protected virtual Dictionary<string, string> InitConditionSearchFields(object andCondition) { var _conditionDictionary = new Dictionary<string, string>(); var type = GetType(); var andConditionType = andCondition.GetType(); var conditions = type.GetInterfaces().Where(t => typeof(ICondition).IsAssignableFrom(t) && t!= typeof(ICondition)) .SelectMany(t => t.GetProperties() /*t.GetInterfaceMap(t).InterfaceMethods*/) .Select(t => t.Name); foreach (var condition in conditions) { if (!_conditionDictionary.ContainsKey(condition)) { _conditionDictionary.Add(condition, andConditionType.GetProperty(condition).GetValue(andCondition)?.ToString() ?? string.Empty); } } return _conditionDictionary; } }
須要索引的抽象基類
通常而言搜索結果有一個標題,介紹,圖片等等,介紹和標題是數據庫(這裏是db)中實際存儲的,然而對於搜索的關鍵字確實沒有的,好比(在博客園中)有一個博客表,表裏面存博客標題,博客主要內容,還有一個問道表,用戶提的問題,介紹,等等,此時若是用戶來到博客園中,鍵入博客(可能已進入首頁都是博客,可是用戶仍是鍵入了博客),然而db中殊不知道那個是博客??仔細想是否是這樣子的,這裏_Customer就是設定這些數據的,固然也能夠是_Category。
結下來介紹一個比較重要的接口。
public interface ICondition { }
做爲一個空接口,若是不能找到一個它不該該存在的理由,那它就真的不該該存在了。在上面說的LuceneEntityBase中,全部的字段都是全文搜索的,即任何一個字段數據匹配用戶鍵入的值均可能稱謂匹配的文檔,這是假如須要匹配另外一個條件該如何?寫代碼之初只考慮到一個語言條件,再添加一個參數,具體改一下搜索的方法就能夠了,這樣同樣知識須要改三處,搜索入口(控制器),給該模型添加一個語言字段,具體的搜索方法也要改一下,甚至寫索引的方法也都要改。然而有了這個接口,咱們只須要實現一個ILanguage繼承該接口,而後事具體的模型也繼承ILanguage就行。
public interface ILanguage: ICondition { string Language { get; set; } }
在看一下上面LuceneEntityBase中建立且條件的方法,會解析出ICondition中的屬性並設置爲必須條件
var _conditionDictionary = new Dictionary<string, string>(); var type = GetType(); var andConditionType = andCondition.GetType(); var conditions = type.GetInterfaces().Where(t => typeof(ICondition).IsAssignableFrom(t) && t!= typeof(ICondition)) .SelectMany(t => t.GetProperties() /*t.GetInterfaceMap(t).InterfaceMethods*/) .Select(t => t.Name); foreach (var condition in conditions) { if (!_conditionDictionary.ContainsKey(condition)) { _conditionDictionary.Add(condition, andConditionType.GetProperty(condition).GetValue(andCondition)?.ToString() ?? string.Empty); } }
用上面封裝後的類創建了一個簡易的搜索示例
核心代碼只有如下三部分,數據的話都爬蟲字自百度百科
public class Figure : Lyrewing.Search.LuceneEntityBase, ICountry { [Indexer(true,true)] public string Country { get; set; } [Indexer(true,true)] public string FigureName { get; set; } /// <summary> /// 稱謂 /// </summary> [Indexer(true,true)] public string Appellation { get; set; } /// <summary> /// 關鍵字 /// </summary> [Indexer(true,true)] public string KeyWords { get; set; } public IEnumerable<Figure> Search(string searchText, int page, int pageSize, object condition = null) { return Search<Figure>(searchText, page, pageSize, condition); } } public interface ICountry : ICondition { string Country { get; set; } }
using (var luceneDB = new LuceneDBIndexer()) { luceneDB.AddRange(Figures); }
var seacher = new Figure(); var result = seacher.Search(key, 1, 100, country != Country.默認 ? new { Country = country.ToString() } : null);
經過此次從lucene的踩坑,到lucene的重構,以及代碼的一些思考,發現過程是艱辛的,代碼如今也不是完美的,lucene的一些其餘複雜的查詢也沒有加進去,可是從這個過程來講對本身來講是值得的,依稀記得實習的時候覺得老師講過的一句話,貌似高三語文老師也講過哈,信達雅,可是這個過程是不易的,還須要更多的學習和挑戰!