作過站內搜索的朋友應該對Lucene.Net不陌生,沒作過的也許會問:就不是個查詢嘛!爲何不能使用Like模糊查找呢?算法
緣由很簡單--模糊查詢的契合度過低,匹配關鍵字之間不能含有其餘內容。最重要的是它會形成數據庫全表掃描,效率底下,即便使用視圖,也會形成數據庫服務器"亞歷山大",那LuceneNet又是一個神馬東西?如何使用?如下給出詳細的介紹包括Demo數據庫
首先說明的是--Lucene.Net只是一個全文檢索開發包,不是一個成型的搜索引擎,服務器
它的功能就是負責將文本數據按照某種分詞算法進行切詞,分詞後的結果存儲在索引庫中,從索引庫檢索數據的速度灰常快.併發
對以上加粗的詞彙稍做下闡述:函數
文本數據:Lucene.Net只能對文本信息進行檢索,因此非文本信息要麼轉換成爲文本信息,要麼你就死了這條心吧!post
分詞算法:將一句完整的話分解成若干詞彙的算法 常見的一元分詞(Lucene.Net內置就是一元分詞,效率高,契合度低),二元分詞,基於詞庫的分詞算法(契合度高,效率低)...測試
切詞:將一句完整的話,按分詞算法切成若干詞語網站
好比:"不是全部痞子都叫一毛" 這句話,若是根據一元分詞算法則被切成: 不 是 所 有 痞 子 都 叫 一 毛 搜索引擎
若是二元分詞算法則切成: 不是 是所 全部 有痞 痞子 子都 都叫 叫一 一毛
若是基於詞庫的算法有可能:不是 全部 痞子 都叫 一毛 具體看詞庫
索引庫:簡單的理解成一個提供了全文檢索功能的數據庫
若是文字難以理解 見Demo文件說明中的右側圖吧
首先展現效果圖,避免各位觀衆不知偶所云.
這裏有三張圖:
圖1 簡單使用頁面效果圖
圖2 對數據庫新增數據後 索引庫更新效果圖
圖3 將圖2中的新增數據修改後 索引庫更新效果圖
圖1中的BookList.aspx 頁面
<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="BookList.aspx.cs" Inherits="Web.LuceneNet.BookList" %> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns="http://www.w3.org/1999/xhtml"> <head runat="server"> <title></title> </head> <body> <form id="form1" method="get" action="BookList.aspx"> <div> 請輸入搜索關鍵字:<input type="text" name="SearchKey" value="" /> <input type="submit" name="btnSearch" value="一哈哈" /> <input type="submit" name="btnCreate" value="建立索引" /> <br /> <ul> <asp:Repeater ID="Repeater1" runat="server"> <ItemTemplate> <li><a href='#'> <%# Eval("Title") %></a></li> <li><span> <%# Eval("ContentDescription") %></span></li> </ItemTemplate> </asp:Repeater> </ul> </div> </form> </body> </html>
BookList.aspx.cs 後臺的處理操做
using System; using System.Collections.Generic; using System.IO; using Lucene.Net.Analysis.PanGu; using Lucene.Net.Documents; using Lucene.Net.Index; using Lucene.Net.Search; using Lucene.Net.Store; using PZYM.Shop.BLL; namespace Web.LuceneNet { public partial class BookList : System.Web.UI.Page { protected void Page_Load(object sender, EventArgs e) { string btnCreate = Request.QueryString["btnCreate"]; string btnSearch = Request.QueryString["btnSearch"]; if(!string.IsNullOrEmpty(btnCreate)) { //建立索引庫 CreateIndexByData(); } if(!string.IsNullOrEmpty(btnSearch)) { //搜索 SearchFromIndexData(); } } /// <summary> /// 建立索引 /// </summary> private void CreateIndexByData() { string indexPath = Context.Server.MapPath("~/IndexData");//索引文檔保存位置 FSDirectory directory = FSDirectory.Open(new DirectoryInfo(indexPath), new NativeFSLockFactory()); //IndexReader:對索引庫進行讀取的類 bool isExist = IndexReader.IndexExists(directory); //是否存在索引庫文件夾以及索引庫特徵文件 if(isExist) { //若是索引目錄被鎖定(好比索引過程當中程序異常退出或另外一進程在操做索引庫),則解鎖 //Q:存在問題 若是一個用戶正在對索引庫寫操做 此時是上鎖的 而另外一個用戶過來操做時 將鎖解開了 因而產生衝突 --解決方法後續 if(IndexWriter.IsLocked(directory)) { IndexWriter.Unlock(directory); } } //建立向索引庫寫操做對象 IndexWriter(索引目錄,指定使用盤古分詞進行切詞,最大寫入長度限制) //補充:使用IndexWriter打開directory時會自動對索引庫文件上鎖 IndexWriter writer = new IndexWriter(directory, new PanGuAnalyzer(), !isExist, IndexWriter.MaxFieldLength.UNLIMITED); BooksManager bookManager = new BooksManager(); List<PZYM.Shop.Model.Books> bookList = bookManager.GetModelList(""); //--------------------------------遍歷數據源 將數據轉換成爲文檔對象 存入索引庫 foreach(var book in bookList) { Document document = new Document(); //new一篇文檔對象 --一條記錄對應索引庫中的一個文檔 //向文檔中添加字段 Add(字段,值,是否保存字段原始值,是否針對該列建立索引) document.Add(new Field("id", book.Id.ToString(), Field.Store.YES, Field.Index.NOT_ANALYZED));//--全部字段的值都將以字符串類型保存 由於索引庫只存儲字符串類型數據 //Field.Store:表示是否保存字段原值。指定Field.Store.YES的字段在檢索時才能用document.Get取出原值 //Field.Index.NOT_ANALYZED:指定不按照分詞後的結果保存--是否按分詞後結果保存取決因而否對該列內容進行模糊查詢 document.Add(new Field("title", book.Title, Field.Store.YES, Field.Index.ANALYZED, Field.TermVector.WITH_POSITIONS_OFFSETS)); //Field.Index.ANALYZED:指定文章內容按照分詞後結果保存 不然沒法實現後續的模糊查詢 //WITH_POSITIONS_OFFSETS:指示不只保存分割後的詞 還保存詞之間的距離 document.Add(new Field("content", book.ContentDescription, Field.Store.YES, Field.Index.ANALYZED, Field.TermVector.WITH_POSITIONS_OFFSETS)); writer.AddDocument(document); //文檔寫入索引庫 } writer.Close();//會自動解鎖 directory.Close(); //不要忘了Close,不然索引結果搜不到 } /// <summary> /// 從索引庫中檢索關鍵字 /// </summary> private void SearchFromIndexData() { string indexPath = Context.Server.MapPath("~/IndexData"); FSDirectory directory = FSDirectory.Open(new DirectoryInfo(indexPath), new NoLockFactory()); IndexReader reader = IndexReader.Open(directory, true); IndexSearcher searcher = new IndexSearcher(reader); //搜索條件 PhraseQuery query = new PhraseQuery(); //把用戶輸入的關鍵字進行分詞 foreach(string word in Common.SplitContent.SplitWords(Request.QueryString["SearchKey"])) { query.Add(new Term("content", word)); } //query.Add(new Term("content", "C#"));//多個查詢條件時 爲且的關係 query.SetSlop(100); //指定關鍵詞相隔最大距離 //TopScoreDocCollector盛放查詢結果的容器 TopScoreDocCollector collector = TopScoreDocCollector.create(1000, true); searcher.Search(query, null, collector);//根據query查詢條件進行查詢,查詢結果放入collector容器 //TopDocs 指定0到GetTotalHits() 即全部查詢結果中的文檔 若是TopDocs(20,10)則意味着獲取第20-30之間文檔內容 達到分頁的效果 ScoreDoc[] docs = collector.TopDocs(0, collector.GetTotalHits()).scoreDocs; //展現數據實體對象集合 List<PZYM.Shop.Model.Books> bookResult = new List<PZYM.Shop.Model.Books>(); for(int i = 0; i < docs.Length; i++) { int docId = docs[i].doc;//獲得查詢結果文檔的id(Lucene內部分配的id) Document doc = searcher.Doc(docId);//根據文檔id來得到文檔對象Document PZYM.Shop.Model.Books book = new PZYM.Shop.Model.Books(); book.Title = doc.Get("title"); //book.ContentDescription = doc.Get("content");//未使用高亮 //搜索關鍵字高亮顯示 使用盤古提供高亮插件 book.ContentDescription = Common.SplitContent.HightLight(Request.QueryString["SearchKey"], doc.Get("content")); book.Id = Convert.ToInt32(doc.Get("id")); bookResult.Add(book); } Repeater1.DataSource = bookResult; Repeater1.DataBind(); } } }
使用的分詞方法與關鍵字變紅 SplitContent.cs
using System.Collections.Generic; using System.IO; using Lucene.Net.Analysis; using Lucene.Net.Analysis.PanGu; using PanGu; namespace Web.Common { public class SplitContent { public static string[] SplitWords(string content) { List<string> strList = new List<string>(); Analyzer analyzer = new PanGuAnalyzer();//指定使用盤古 PanGuAnalyzer 分詞算法 TokenStream tokenStream = analyzer.TokenStream("", new StringReader(content)); Lucene.Net.Analysis.Token token = null; while((token = tokenStream.Next()) != null) { //Next繼續分詞 直至返回null strList.Add(token.TermText()); //獲得分詞後結果 } return strList.ToArray(); } //須要添加PanGu.HighLight.dll的引用 /// <summary> /// 搜索結果高亮顯示 /// </summary> /// <param name="keyword"> 關鍵字 </param> /// <param name="content"> 搜索結果 </param> /// <returns> 高亮後結果 </returns> public static string HightLight(string keyword, string content) { //建立HTMLFormatter,參數爲高亮單詞的先後綴 PanGu.HighLight.SimpleHTMLFormatter simpleHTMLFormatter = new PanGu.HighLight.SimpleHTMLFormatter("<font style=\"font-style:normal;color:#cc0000;\"><b>", "</b></font>"); //建立 Highlighter ,輸入HTMLFormatter 和 盤古分詞對象Semgent PanGu.HighLight.Highlighter highlighter = new PanGu.HighLight.Highlighter(simpleHTMLFormatter, new Segment()); //設置每一個摘要段的字符數 highlighter.FragmentSize = 1000; //獲取最匹配的摘要段 return highlighter.GetBestFragment(keyword, content); } } }
Analyzer類:LuceneNet中分詞算法的基類 任何自定義算法都需繼承它
FSDirectory類: 指定索引庫文件存放文件位置 是Directory的子類(它有兩個子類 還有一個RAMDirecory,它用來指定將索引庫文件存放在內存中)
IndexReader:對索引進行讀取的類
靜態方法bool IndexExists(Directory directory)--判斷目錄directory是不是一個索引目錄
IndexWriter:對索引進行寫的類
靜態方法bool IsLocked(Directory directory)--判斷目錄是否鎖定
它在對索引目錄寫以前會把目錄鎖定,兩個IndexWrite沒法同時操做一個索引文件
IndexWrite在進行寫操做的時候會自動加鎖
Close自動解鎖
Unlock手動解鎖(一般用在程序異常退出 IndexWrite還沒來得及close)
Document類:要檢索的文檔 至關於一條記錄
Add(Field field)向文檔中添加字段
Filed類:構造函數(字段名,字段值,是否存儲原文,是否對該字段建立索引,存儲索引詞間距)
是否存儲原文:Field.Store.YES 存儲原值(如顯示原內容必須爲YES) Field.Store.NO不存儲原值 Field.Store.YES壓縮存儲
是否建立索引:Field.Index.NOT_ANALYZED不建立索引 Field.Index.ANALYZED建立索引(利於檢索)
IndexSearcher:搜索類 Searcher類的子類
Search(查詢條件Query,過濾條件Filter,檢索見過存放容器Collector)
Query類:全部查詢條件父類(子類都具備Add方法)
子類PhraseQuery:多個關鍵詞的拼接類 關鍵詞間是且的關係
query.Add(new Term("字段名", 關鍵詞))
query.Add(new Term("字段名2", 關鍵詞2))
相似於:where 字段名 contains 關鍵詞 and 字段名2 contains 關鍵詞2
子類BooleanQuery:相似PharseQuery 經過它實現關鍵詞間的或關係(MUST必須有 Should無關緊要 MUST_NOT必須沒有 詳見BookList2.aspx.cs代碼)
上述只是Lucene.Net的簡單使用
接下來咱們深刻探討上述使用過程當中存在的一些問題以及指的改進的地方:
Q1:建立索引事件耗時的操做,尤爲是在數據量很大的狀況下,索引庫生成耗時是個問題
Q2:真實項目中確定不可能存在建立索引按鈕,那建立索引的事件何時觸發,由誰觸發呢?
Q3:如代碼中的Q同樣 多個用戶共同操做索引庫時的併發問題
解答上述三個問題
A1.耗時的操做固然另起一個後臺線程來完成撒
A2.在網站Application_Start的時,利用A1中的後臺線程循環監聽Books表的增刪改操做,在對Books表的增刪改爲功以後,對索引庫相對應的數據作出增刪改操做
A3.併發問題最好的解決方式--創建請求隊列,單線程處理隊列,相似於操做系統的中的生產者消費者模式
IndexManager.cs類中定義後臺線程 循環監聽請求隊列 負責對索引庫的更新操做
using System.Collections.Generic; using System.IO; using System.Threading; using System.Web; using Lucene.Net.Analysis.PanGu; using Lucene.Net.Documents; using Lucene.Net.Index; using Lucene.Net.Store; using PZYM.Shop.Model; namespace Web.LuceneNet { public class IndexManager { public static readonly IndexManager bookIndex = new IndexManager(); public static readonly string indexPath = HttpContext.Current.Server.MapPath("~/IndexData"); private IndexManager() { } //請求隊列 解決索引目錄同時操做的併發問題 private Queue<BookViewMode> bookQueue = new Queue<BookViewMode>(); /// <summary> /// 新增Books表信息時 添加邢增索引請求至隊列 /// </summary> /// <param name="books"></param> public void Add(Books books) { BookViewMode bvm = new BookViewMode(); bvm.Id = books.Id; bvm.Title = books.Title; bvm.IT = IndexType.Insert; bvm.Content = books.ContentDescription; bookQueue.Enqueue(bvm); } /// <summary> /// 刪除Books表信息時 添加刪除索引請求至隊列 /// </summary> /// <param name="bid"></param> public void Del(int bid) { BookViewMode bvm = new BookViewMode(); bvm.Id = bid; bvm.IT = IndexType.Delete; bookQueue.Enqueue(bvm); } /// <summary> /// 修改Books表信息時 添加修改索引(實質上是先刪除原有索引 再新增修改後索引)請求至隊列 /// </summary> /// <param name="books"></param> public void Mod(Books books) { BookViewMode bvm = new BookViewMode(); bvm.Id = books.Id; bvm.Title = books.Title; bvm.IT = IndexType.Modify; bvm.Content = books.ContentDescription; bookQueue.Enqueue(bvm); } public void StartNewThread() { ThreadPool.QueueUserWorkItem(new WaitCallback(QueueToIndex)); } //定義一個線程 將隊列中的數據取出來 插入索引庫中 private void QueueToIndex(object para) { while(true) { if(bookQueue.Count > 0) { CRUDIndex(); } else { Thread.Sleep(3000); } } } /// <summary> /// 更新索引庫操做 /// </summary> private void CRUDIndex() { FSDirectory directory = FSDirectory.Open(new DirectoryInfo(indexPath), new NativeFSLockFactory()); bool isExist = IndexReader.IndexExists(directory); if(isExist) { if(IndexWriter.IsLocked(directory)) { IndexWriter.Unlock(directory); } } IndexWriter writer = new IndexWriter(directory, new PanGuAnalyzer(), !isExist, IndexWriter.MaxFieldLength.UNLIMITED); while(bookQueue.Count > 0) { Document document = new Document(); BookViewMode book = bookQueue.Dequeue(); if(book.IT == IndexType.Insert) { document.Add(new Field("id", book.Id.ToString(), Field.Store.YES, Field.Index.NOT_ANALYZED)); document.Add(new Field("title", book.Title, Field.Store.YES, Field.Index.ANALYZED, Field.TermVector.WITH_POSITIONS_OFFSETS)); document.Add(new Field("content", book.Content, Field.Store.YES, Field.Index.ANALYZED, Field.TermVector.WITH_POSITIONS_OFFSETS)); writer.AddDocument(document); } else if(book.IT == IndexType.Delete) { writer.DeleteDocuments(new Term("id", book.Id.ToString())); } else if(book.IT == IndexType.Modify) { //先刪除 再新增 writer.DeleteDocuments(new Term("id", book.Id.ToString())); document.Add(new Field("id", book.Id.ToString(), Field.Store.YES, Field.Index.NOT_ANALYZED)); document.Add(new Field("title", book.Title, Field.Store.YES, Field.Index.ANALYZED, Field.TermVector.WITH_POSITIONS_OFFSETS)); document.Add(new Field("content", book.Content, Field.Store.YES, Field.Index.ANALYZED, Field.TermVector.WITH_POSITIONS_OFFSETS)); writer.AddDocument(document); } } writer.Close(); directory.Close(); } } public class BookViewMode { public int Id { get; set; } public string Title { get; set; } public string Content { get; set; } public IndexType IT { get; set; } } //操做類型枚舉 public enum IndexType { Insert, Modify, Delete } }
BookList2.aspx與BookList.aspx大同小異 這裏不給出了
BookList2.aspx.cs 對數據庫數據新增 修改 刪除等操做
using System; using System.Collections.Generic; using System.IO; using Lucene.Net.Documents; using Lucene.Net.Index; using Lucene.Net.Search; using Lucene.Net.Store; using PZYM.Shop.BLL; using PZYM.Shop.Model; namespace Web.LuceneNet { public partial class BookList2 : System.Web.UI.Page { protected void Page_Load(object sender, EventArgs e) { string btnInsert = Request.QueryString["btnInsert"]; string btnSearch = Request.QueryString["btnSearch"]; if(!string.IsNullOrEmpty(btnInsert)) { //數據新增至索引庫 InsertToIndex(); } if(!string.IsNullOrEmpty(btnSearch)) { //搜索 SearchFromIndexData(); } } //臨時數據代替表單提交 private void InsertToIndex() { //建立一條臨時數據 Books book = new Books(); book.Author = "痞子一毛"; book.Title = "piziyimao"; book.CategoryId = 1; book.ContentDescription = "不是全部痞子都叫一毛不是全部痞子都叫一毛不是全部痞子都叫一毛不是全部痞子都叫一毛"; book.PublisherId = 1; book.ISBN = "124365"; book.WordsCount = 1000000; book.UnitPrice = 88; book.CategoryId = 1; book.Clicks = 10; book.PublishDate = DateTime.Now; BooksManager bm = new BooksManager(); //IndexManager.bookIndex.Add()數據新增 索引庫更新測試 //int insertId; //if((insertId = bm.Add(book)) > 0) { // book.Id = insertId; // IndexManager.bookIndex.Add(book); //} //IndexManager.bookIndex.Mod()數據修改 索引庫更新測試 book.Id = 10001;//數據庫生成主鍵ID book.ContentDescription = "儂好哇, 記住不是全部痞子都叫一毛喲"; bm.Update(book); IndexManager.bookIndex.Mod(book); } /// <summary> /// 從索引庫中檢索關鍵字 /// </summary> private void SearchFromIndexData() { string indexPath = Context.Server.MapPath("~/IndexData"); FSDirectory directory = FSDirectory.Open(new DirectoryInfo(indexPath), new NoLockFactory()); IndexReader reader = IndexReader.Open(directory, true); IndexSearcher searcher = new IndexSearcher(reader); //--------------------------------------這裏配置搜索條件 //PhraseQuery query = new PhraseQuery(); //foreach(string word in Common.SplitContent.SplitWords(Request.QueryString["SearchKey"])) { // query.Add(new Term("content", word));//這裏是 and關係 //} //query.SetSlop(100); //關鍵詞Or關係設置 BooleanQuery queryOr = new BooleanQuery(); TermQuery query = null; foreach(string word in Common.SplitContent.SplitWords(Request.QueryString["SearchKey"])) { query = new TermQuery(new Term("content", word)); queryOr.Add(query, BooleanClause.Occur.SHOULD);//這裏設置 條件爲Or關係 } //-------------------------------------- TopScoreDocCollector collector = TopScoreDocCollector.create(1000, true); //searcher.Search(query, null, collector); searcher.Search(queryOr, null, collector); ScoreDoc[] docs = collector.TopDocs(0, 10).scoreDocs;//取前十條數據 能夠經過它實現LuceneNet搜索結果分頁 List<PZYM.Shop.Model.Books> bookResult = new List<PZYM.Shop.Model.Books>(); for(int i = 0; i < docs.Length; i++) { int docId = docs[i].doc; Document doc = searcher.Doc(docId); PZYM.Shop.Model.Books book = new PZYM.Shop.Model.Books(); book.Title = doc.Get("title"); book.ContentDescription = Common.SplitContent.HightLight(Request.QueryString["SearchKey"], doc.Get("content")); book.Id = Convert.ToInt32(doc.Get("id")); bookResult.Add(book); } Repeater1.DataSource = bookResult; Repeater1.DataBind(); } } }
Global.ascx中設置
protected void Application_Start(object sender, EventArgs e) { IndexManager.bookIndex.StartNewThread(); }
原文地址http://www.cnblogs.com/piziyimao/archive/2013/01/31/2887072.html