Lucene的使用與重構

突然一想很久不寫博客了,工做緣由我的緣由,這些天一直但願一天假如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中讀取的)。

顯然,須要對搜索這個模塊進行單獨的維護了,這樣修改方便,而且也能夠供其餘項目使用,也是軟件設計原則吧!

IndexerAttribute

    [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;
        }
    }

 

表示對模型具體字段的索引方式

LuceneDB

    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應用。

ILunceneDBPathProvider

    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

LuceneDBIndexer

    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標籤

LuceneDBSearcher

    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文件夾並將數據轉爲對象

LuceneEntityBase

    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。

結下來介紹一個比較重要的接口。

ICondition

    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的一些其餘複雜的查詢也沒有加進去,可是從這個過程來講對本身來講是值得的,依稀記得實習的時候覺得老師講過的一句話,貌似高三語文老師也講過哈,信達雅,可是這個過程是不易的,還須要更多的學習和挑戰!

相關文章
相關標籤/搜索