C#編寫了一個基於Lucene.Net的搜索引擎查詢通用工具類:SearchEngineUtil

来源:https://www.cnblogs.com/zuowj/archive/2019/10/17/11689563.html
-Advertisement-
Play Games

最近由於工作原因,一直忙於公司的各種項目(大部份都是基於spring cloud的微服務項目),故有一段時間沒有與大家分享總結最近的技術研究成果的,其實最近我一直在不斷的深入研究學習Spring、Spring Boot、Spring Cloud的各種框架原理,同時也隨時關註著.NET CORE的發展 ...


  最近由於工作原因,一直忙於公司的各種項目(大部份都是基於spring cloud的微服務項目),故有一段時間沒有與大家分享總結最近的技術研究成果的,其實最近我一直在不斷的深入研究學習Spring、Spring Boot、Spring Cloud的各種框架原理,同時也隨時關註著.NET CORE的發展情況及最新技術點,也在極客時間上訂閱相關的專欄,只要下班有空我都會去認真閱讀觀看,紙質書箱也買了一些,總之近一年都是在通過:微信技術公眾號(.NET、JAVA、演算法、前端等技術方向)、極客時間、技術書箱 不斷的吸取、借鑒他人之精華,從而不斷的充實提高自己的技術水平,所謂:學如逆水行舟,不進則退,工作中學習,學習後工作中運用,當然寫文章分享是一種總結,同時也是“溫故而知新”的最佳應用。

  前面廢話說得有點多了,就直奔本文的主題內容,編寫一個基於Lucene.Net的搜索引擎查詢通用工具類:SearchEngineUtil,Lucene是什麼,見百度百科 ,重點是:Lucene是一個全文檢索引擎的架構,提供了完整的查詢引擎和索引引擎,Lucene.NET是C#及.NET運行時下的另一種語言的實現,官網地址:http://lucenenet.apache.org/  ,具體用法就不多說了,官網以及網上都有很多,但由於Lucene.Net的原生SDK中的API比較複雜,用起來不太方便,故我進行了適當的封裝,把常用的增、刪、改、查(分頁查)在保證靈活度的情況下進行了封裝,使得操作Lucene.Net變得相對簡單一些,代碼本身也不複雜,貼出完整的SearchEngineUtil代碼如下:

using Lucene.Net.Analysis.PanGu;
using Lucene.Net.Documents;
using Lucene.Net.Index;
using Lucene.Net.QueryParsers;
using Lucene.Net.Search;
using Lucene.Net.Store;
using NLog;
using PanGu;
using PanGu.HighLight;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;

namespace CN.Zuowenjun.Blog.Common
{
    /// <summary>
    /// Lucene 搜索引擎實用工具類
    /// Author:zuowenjun
    /// </summary>
    public class SearchEngineUtil
    {

        /// <summary>
        /// 創建並添加索引記錄
        /// </summary>
        /// <typeparam name="TIndex"></typeparam>
        /// <param name="indexDir"></param>
        /// <param name="indexData"></param>
        /// <param name="setDocFiledsAction"></param>
        public static void AddIndex<TIndex>(string indexDir, TIndex indexData, Action<Document, TIndex> setDocFiledsAction)
        {
            //創建索引目錄
            if (!System.IO.Directory.Exists(indexDir))
            {
                System.IO.Directory.CreateDirectory(indexDir);
            }
            FSDirectory directory = FSDirectory.Open(new DirectoryInfo(indexDir), new NativeFSLockFactory());
            bool isUpdate = IndexReader.IndexExists(directory);
            if (isUpdate)
            {
                //如果索引目錄被鎖定(比如索引過程中程式異常退出),則首先解鎖
                if (IndexWriter.IsLocked(directory))
                {
                    IndexWriter.Unlock(directory);
                }
            }
            using (IndexWriter writer = new IndexWriter(directory, new PanGuAnalyzer(), !isUpdate, IndexWriter.MaxFieldLength.UNLIMITED))
            {
                Document document = new Document();

                setDocFiledsAction(document, indexData);

                writer.AddDocument(document);

                writer.Optimize();//優化索引
            }
        }

        /// <summary>
        /// 刪除索引記錄
        /// </summary>
        /// <param name="indexDir"></param>
        /// <param name="keyFiledName"></param>
        /// <param name="keyFileValue"></param>
        public static void DeleteIndex(string indexDir, string keyFiledName, object keyFileValue)
        {
            FSDirectory directory = FSDirectory.Open(new DirectoryInfo(indexDir), new NativeFSLockFactory());
            if (!IndexReader.IndexExists(directory))
            {
                return;
            }

            using (IndexWriter iw = new IndexWriter(directory, new PanGuAnalyzer(), IndexWriter.MaxFieldLength.UNLIMITED))
            {
                iw.DeleteDocuments(new Term(keyFiledName, keyFileValue.ToString()));
                iw.Optimize();//刪除文件後並非從磁碟中移除,而是生成一個.del的文件,需要調用Optimize方法來清除。在清除文件前可以使用UndeleteAll方法恢復
            }
        }

        /// <summary>
        /// 更新索引記錄
        /// </summary>
        /// <param name="indexDir"></param>
        /// <param name="keyFiledName"></param>
        /// <param name="keyFileValue"></param>
        /// <param name="doc"></param>
        public static void UpdateIndex(string indexDir, string keyFiledName, object keyFileValue, Document doc)
        {
            FSDirectory directory = FSDirectory.Open(new DirectoryInfo(indexDir), new NativeFSLockFactory());
            if (!IndexReader.IndexExists(directory))
            {
                return;
            }

            using (IndexWriter iw = new IndexWriter(directory, new PanGuAnalyzer(), IndexWriter.MaxFieldLength.UNLIMITED))
            {
                iw.UpdateDocument(new Term(keyFiledName, keyFileValue.ToString()), doc);
                iw.Optimize();
            }
        }

        /// <summary>
        /// 是否存在指定的索引文檔
        /// </summary>
        /// <param name="indexDir"></param>
        /// <param name="keyFiledName"></param>
        /// <param name="keyFileValue"></param>
        /// <returns></returns>
        public static bool ExistsDocument(string indexDir, string keyFiledName, object keyFileValue)
        {
            FSDirectory directory = FSDirectory.Open(new DirectoryInfo(indexDir), new NativeFSLockFactory());
            if (!IndexReader.IndexExists(directory))
            {
                return false;
            }

            var reader = IndexReader.Open(directory, true);

            return reader.DocFreq(new Term(keyFiledName, keyFileValue.ToString())) > 0;
        }

        /// <summary>
        /// 查詢索引匹配到的記錄
        /// </summary>
        /// <typeparam name="TResult"></typeparam>
        /// <param name="indexDir"></param>
        /// <param name="buildQueryAction"></param>
        /// <param name="getSortFieldsFunc"></param>
        /// <param name="buildResultFunc"></param>
        /// <param name="topCount"></param>
        /// <param name="needHighlight"></param>
        /// <returns></returns>
        public static List<TResult> SearchIndex<TResult>(string indexDir, Func<BooleanQuery, IDictionary<string, string>> buildQueryAction,
            Func<IEnumerable<SortField>> getSortFieldsFunc, Func<Document, TResult> buildResultFunc, bool needHighlight = true, int topCount = 0)
        {
            FSDirectory directory = FSDirectory.Open(new DirectoryInfo(indexDir), new NoLockFactory());

            if (!IndexReader.IndexExists(directory))
            {
                return new List<TResult>();
            }

            IndexReader reader = IndexReader.Open(directory, true);
            IndexSearcher searcher = new IndexSearcher(reader);

            BooleanQuery bQuery = new BooleanQuery();
            var keyWords = buildQueryAction(bQuery);

            Sort sort = null;
            var sortFields = getSortFieldsFunc();
            if (sortFields != null)
            {
                sort = new Sort();
                sort.SetSort(sortFields.ToArray());
            }

            topCount = topCount > 0 ? topCount : int.MaxValue;//當未指定TOP值,則設置最大值以表示獲取全部
            TopDocs resultDocs = null;
            if (sort != null)
            {
                resultDocs = searcher.Search(bQuery, null, topCount, sort);
            }
            else
            {
                resultDocs = searcher.Search(bQuery, null, topCount);
            }

            if (topCount > resultDocs.TotalHits)
            {
                topCount = resultDocs.TotalHits;
            }

            Dictionary<string, PropertyInfo> highlightProps = null;
            List<TResult> results = new List<TResult>();
            if (resultDocs != null)
            {
                for (int i = 0; i < topCount; i++)
                {
                    Document doc = searcher.Doc(resultDocs.ScoreDocs[i].Doc);
                    var model = buildResultFunc(doc);
                    if (needHighlight)
                    {
                        model = SetHighlighter(keyWords, model, ref highlightProps);
                    }

                    results.Add(model);
                }
            }

            return results;

        }

        /// <summary>
        /// 分頁查詢索引匹配到的記錄
        /// </summary>
        /// <typeparam name="TResult"></typeparam>
        /// <param name="indexDir"></param>
        /// <param name="buildQueryAction"></param>
        /// <param name="getSortFieldsFunc"></param>
        /// <param name="buildResultFunc"></param>
        /// <param name="pageSize"></param>
        /// <param name="page"></param>
        /// <param name="totalCount"></param>
        /// <param name="needHighlight"></param>
        /// <returns></returns>
        public static List<TResult> SearchIndexByPage<TResult>(string indexDir, Func<BooleanQuery, IDictionary<string, string>> buildQueryAction,
            Func<IEnumerable<SortField>> getSortFieldsFunc, Func<Document, TResult> buildResultFunc, int pageSize, int page, out int totalCount, bool needHighlight = true)
        {
            FSDirectory directory = FSDirectory.Open(new DirectoryInfo(indexDir), new NoLockFactory());

            if (!IndexReader.IndexExists(directory))
            {
                totalCount = 0;
                return new List<TResult>();
            }

            IndexReader reader = IndexReader.Open(directory, true);
            IndexSearcher searcher = new IndexSearcher(reader);

            BooleanQuery bQuery = new BooleanQuery();
            var keyWords = buildQueryAction(bQuery);

            Sort sort = null;
            var sortFields = getSortFieldsFunc();
            if (sortFields != null)
            {
                sort = new Sort();
                sort.SetSort(sortFields.ToArray());
            }

            TopScoreDocCollector docCollector = TopScoreDocCollector.Create(1, true);
            searcher.Search(bQuery, docCollector);
            totalCount = docCollector.TotalHits;

            if (totalCount <= 0) return null;

            TopDocs resultDocs = searcher.Search(bQuery, null, pageSize * page, sort);

            Dictionary<string, PropertyInfo> highlightProps = null;
            List<TResult> results = new List<TResult>();
            int indexStart = (page - 1) * pageSize;
            int indexEnd = indexStart + pageSize;
            if (totalCount < indexEnd) indexEnd = totalCount;

            if (resultDocs != null)
            {
                for (int i = indexStart; i < indexEnd; i++)
                {
                    Document doc = searcher.Doc(resultDocs.ScoreDocs[i].Doc);
                    var model = buildResultFunc(doc);
                    if (needHighlight)
                    {
                        model = SetHighlighter(keyWords, model, ref highlightProps);
                    }

                    results.Add(model);
                }
            }

            return results;
        }



        /// <summary>
        /// 設置結果高亮
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="dicKeywords"></param>
        /// <param name="model"></param>
        /// <param name="props"></param>
        /// <returns></returns>
        private static T SetHighlighter<T>(IDictionary<string, string> dicKeywords, T model, ref Dictionary<string, PropertyInfo> props)
        {
            SimpleHTMLFormatter simpleHTMLFormatter = new SimpleHTMLFormatter("<font color=\"red\">", "</font>");
            Highlighter highlighter = new Highlighter(simpleHTMLFormatter, new Segment());
            highlighter.FragmentSize = 250;

            Type modelType = typeof(T);
            foreach (var item in dicKeywords)
            {
                if (!string.IsNullOrWhiteSpace(item.Value))
                {
                    if (props == null)
                    {
                        props = new Dictionary<string, PropertyInfo>();
                    }

                    if (!props.ContainsKey(item.Key))
                    {
                        props[item.Key] = modelType.GetProperty(item.Key, BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.Instance);
                    }

                    var modelProp = props[item.Key];
                    if (modelProp.PropertyType == typeof(string))
                    {
                        string newValue = highlighter.GetBestFragment(item.Value, modelProp.GetValue(model).ToString());
                        if (!string.IsNullOrEmpty(newValue))
                        {
                            modelProp.SetValue(model, newValue);
                        }
                    }
                }
            }

            return model;
        }


        /// <summary>
        /// 拆分關鍵詞
        /// </summary>
        /// <param name="keywords"></param>
        /// <returns></returns>
        public static string GetKeyWordsSplitBySpace(string keyword)
        {
            PanGuTokenizer ktTokenizer = new PanGuTokenizer();
            StringBuilder result = new StringBuilder();
            ICollection<WordInfo> words = ktTokenizer.SegmentToWordInfos(keyword);
            foreach (WordInfo word in words)
            {
                if (word == null)
                {
                    continue;
                }
                result.AppendFormat("{0}^{1}.0 ", word.Word, (int)Math.Pow(3, word.Rank));
            }
            return result.ToString().Trim();
        }

        /// <summary>
        /// 【輔助方法】創建盤古查詢對象
        /// </summary>
        /// <param name="field"></param>
        /// <param name="keyword"></param>
        /// <returns></returns>
        public static Query CreatePanGuQuery(string field, string keyword, bool needSplit = true)
        {
            if (needSplit)
            {
                keyword = GetKeyWordsSplitBySpace(keyword);
            }

            QueryParser parse = new QueryParser(Lucene.Net.Util.Version.LUCENE_30, field, new PanGuAnalyzer());
            parse.DefaultOperator = QueryParser.Operator.OR;
            Query query = parse.Parse(keyword);
            return query;
        }

        /// <summary>
        /// 【輔助方法】創建盤古多欄位查詢對象
        /// </summary>
        /// <param name="keyword"></param>
        /// <param name="fields"></param>
        /// <returns></returns>
        public static Query CreatePanGuMultiFieldQuery(string keyword, bool needSplit, params string[] fields)
        {
            if (needSplit)
            {
                keyword = GetKeyWordsSplitBySpace(keyword);
            }

            QueryParser parse = new MultiFieldQueryParser(Lucene.Net.Util.Version.LUCENE_30, fields, new PanGuAnalyzer());
            parse.DefaultOperator = QueryParser.Operator.OR;
            Query query = parse.Parse(keyword);
            return query;
        }

    }
}

 裡面除了使用了Lucene.Net nuget包,還單獨引用了PanGu分詞器及其相關組件,因為大多數情況下我們的內容會包含中文。如上代碼就不再細講了,註釋得比較清楚了。下麵貼出一些實際的用法:

創建索引:

 SearchEngineUtil.AddIndex(GetSearchIndexDir(), post, (doc, data) => BuildPostSearchDocument(data, doc));


        private Document BuildPostSearchDocument(Post post, Document doc = null)
        {

            if (doc == null)
            {
                doc = new Document();//創建Document
            }

            doc.Add(new Field("Id", post.Id.ToString(), Field.Store.YES, Field.Index.NOT_ANALYZED));
            doc.Add(new Field("Title", post.Title, Field.Store.YES, Field.Index.ANALYZED));
            doc.Add(new Field("Summary", post.Summary, Field.Store.YES, Field.Index.ANALYZED));
            doc.Add(new Field("CreateTime", post.CreateTime.ToString("yyyy/MM/dd HH:mm"), Field.Store.YES, Field.Index.NO));
            doc.Add(new Field("Author", post.IsOriginal ? (post.Creator ?? userQueryService.FindByName(post.CreateBy)).NickName : post.SourceBy, Field.Store.YES, Field.Index.NO));

            return doc;
        }

 刪除索引:

 SearchEngineUtil.DeleteIndex(GetSearchIndexDir(), "Id", post.Id);

 更新索引:

SearchEngineUtil.UpdateIndex(GetSearchIndexDir(), "Id", post.Id, BuildPostSearchDocument(post));

 分頁查詢:

               var keyword = SearchEngineUtil.GetKeyWordsSplitBySpace("夢在旅途 中國夢");
                var searchResult = SearchEngineUtil.SearchIndexByPage(indexDir, (bQuery) =>
                {
                    var query = SearchEngineUtil.CreatePanGuMultiFieldQuery(keyword, false, "Title", "Summary");
                    bQuery.Add(query, Occur.SHOULD);
                    return new Dictionary<string, string> {
                    { "Title",keyword},{"Summary",keyword}
                    };
                }, () =>
                {
                    return new[] { new SortField("Id", SortField.INT, true) };
                }, doc =>
                {
                    return new PostSearchInfoDto
                    {
                        Id = doc.Get("Id"),
                        Title = doc.Get("Title"),
                        Summary = doc.Get("Summary"),
                        Author = doc.Get("Author"),
                        CreateTime = doc.Get("CreateTime")
                    };

                }, pageSize, pageNo, out totalCount);

其它的還有:判斷索引中的指定文檔記錄存不存在、查詢符合條件的索引文檔等在此沒有列出,大家有興趣的可以COPY到自己的項目中測試一下。

這裡可以看一下我在自己的項目中(個人全新改版的自己博客,還在開發中)應用搜索場景的效果:

 

最後說明的是:Lucene並不是一個完整的全文檢索引擎,但瞭解它對於學習elasticsearch、solr還是有一定的幫助,目前一般應用於實際的生產項目中,多半是使用更高層的elasticsearch、solr。

 (本文中的代碼我是今年很早前就寫好了,只是今天才分享出來)

 

我喜歡對一些常用的組件進行封裝,比如過往封裝有:

基於MongoDb官方C#驅動封裝MongoDbCsharpHelper類(CRUD類)

基於RabbitMQ.Client組件實現RabbitMQ可復用的 ConnectionPool(連接池)

 


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • pycharm中.py文件模板應用方法: 設置->文件和代碼模板->文件->Python Script->右側輸入模板內容->應用->確定註釋: #開頭為單行註釋(快捷鍵為CTRL+/),成對的'''中間的為多行註釋多行代碼連接符:\ print("hello world") 等於print("he ...
  • 1. 統計字元(可以在jieba分詞之後使用) 2. 多次覆蓋,迴圈寫入文件 比如,迴圈兩次的結果是: 3. 一次性寫入文件,中間不會覆蓋和多次寫入;但是如果重覆運行代碼,則會覆蓋之前的全部內容,一次性重新寫入所有新內容 ...
  • IntelliJ快捷鍵 導入包 alt + enter 刪除游標所在行 ctrl + y 複製游標所在行 ctrl + d 格式代碼 ctrl + alt + l 單行註釋 ctrl + / 多行註釋 ctrl + shift + / 自動生成代碼 alt + ins 移動代碼 alt + shif ...
  • "Python3 多進程編程(Multiprocess programming)" "為什麼使用多進程" "具體用法" "Python多線程的通信" "進程對列Queue" "生產者消費者問題" "JoinableQueue" "Queue實例" "管道Pipe" Python3 多進程編程(Mul ...
  • Java開發過程中的常用工具類庫 [TOC] Apache Commons類庫 Apache Commons是一個非常有用的工具包,為解決各種實際的問題提供了通用現成的代碼,不需要我們程式員再重覆造輪子。關於這個類庫的詳細介紹可以訪問 "官網介紹" 。下麵表格列出了部分的工具包。我們平時開發過程中可 ...
  • 今天被一個很簡單的坑到了,還想了很長時間,insert 函數,真的知道它內部執行的操作嗎? 開始其實是在看一本演算法的書,書裡面給了兩段工作內容差不多的偽代碼 第一段如下: 第二段如下: 最開始感覺第二中代碼中計算量不是應該比第一段多了一個計算長度的部分嗎?應該是最二種時間花費更多,事實上len(da ...
  • 前言 學習路線圖往往是學習一樣技術的入門指南。網上搜到的Java學習路線圖也是一抓一大把。 今天我只選一張圖,僅此一圖,足以包羅Java後端技術的知識點。所謂不求最好,但求最全,學習Java後端的同學完全可以參考這張圖進行學習路線安排。 當然,有一些知識點是可選的,並不是說上面有的你都要會啦。我在復 ...
  • 阿裡面經 "阿裡中間件研發麵經" "螞蟻金服研發麵經" 崗位是研發工程師,直接找螞蟻金服的大佬進行內推。 我參與了阿裡巴巴中間件部門的提前批面試,一共經歷了四次面試,拿到了口頭offer。 然後我也參加了螞蟻金服中間件部門的面試,經歷了三次面試,但是沒有走流程,所以面試中止了。 最後我走的是螞蟻金服 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...