最近翻開了之前老楊(楊中科)的Lucene.Net站内搜尋項目的教學視訊,于是作為老楊腦殘粉的我又跟着複習了一遍,學習途中做了一些筆記也就成了接下來您看到的這篇博文,僅僅是我的個人筆記,大神請呵呵一笑而過。相信做過站内搜尋的.Net程式員應該對Lucene.Net不陌生,沒做過的也許會問:就不是個查詢嘛!為什麼不能使用Like模糊查找呢?原因很簡單:模糊查詢的契合度太低,比對關鍵字之間不能含有其他内容。最重要的是它會造成資料庫全表掃描,效率低下,即使使用視圖,也會造成資料庫伺服器"亞曆山大"!是以,有必要了解一下Lucene.Net這個神器(也許現在早已不是)!
前言:最近翻開了之前老楊(楊中科)的Lucene.Net站内搜尋項目的教學視訊,于是作為老楊腦殘粉的我又跟着複習了一遍,學習途中做了一些筆記也就成了接下來您看到的這篇博文,僅僅是我的個人筆記,大神請呵呵一笑而過。相信做過站内搜尋的.Net程式員應該對Lucene.Net不陌生,沒做過的也許會問:就不是個查詢嘛!為什麼不能使用Like模糊查找呢?原因很簡單:模糊查詢的契合度太低,比對關鍵字之間不能含有其他内容。最重要的是它會造成資料庫全表掃描,效率低下,即使使用視圖,也會造成資料庫伺服器"亞曆山大"!是以,有必要了解一下Lucene.Net這個神器(也許現在早已不是)!
一、Lucene.Net簡介
Lucene.Net隻是一個全文檢索開發包,不是一個成型的搜尋引擎。
它的功能就是負責将文本資料按照某種分詞算法進行切詞,分詞後的結果存儲在索引庫中,從索引庫檢索資料的速度灰常快。
對以上加粗的詞彙稍作下闡述:
文本資料:Lucene.Net隻能對文本資訊進行檢索,是以非文本資訊要麼轉換成為文本資訊,要麼你就死了這條心吧!
分詞算法:将一句完整的話分解成若幹詞彙的算法 常見的一進制分詞(Lucene.Net内置就是一進制分詞,效率高,契合度低),二進制分詞,基于詞庫的分詞算法(契合度高,效率低)...
切詞:将一句完整的話,按分詞算法切成若幹詞語
比如:"不是所有痞子都叫一毛" 這句話,如果根據一進制分詞算法則被切成: 不 是 所 有 痞 子 都 叫 一 毛
如果二進制分詞算法則切成: 不是 是所 所有 有痞 痞子 子都 都叫 叫一 一毛
如果基于詞庫的算法有可能:不是 所有 痞子 都叫 一毛 具體看詞庫
索引庫:簡單的了解成一個提供了全文檢索功能的資料庫,見下圖所示:
二、幾種分詞的使用
毫無疑問,Lucene.Net中最核心的内容就是分詞,下面我們來體驗一下基本的一進制分詞、二進制分詞以及基于詞庫分詞的代表:盤古分詞。首先,我們準備一個ASP.Net Web項目(這裡使用的是WebForms技術),引入Lucene.Net和PanGu的dll,以及加入CJK分詞的兩個class(均在附件下載下傳部分可以下載下傳),分詞示範Demo的項目結構如下圖所示:
2.1 一進制分詞
核心代碼
protected void btnGetSegmentation_Click(object sender, EventArgs e)
{
string words = txtWords.Text;
if (string.IsNullOrEmpty(words))
{
return;
}
Analyzer analyzer = new StandardAnalyzer(); // 标準分詞 → 一進制分詞
TokenStream tokenStream = analyzer.TokenStream("", new StringReader(words));
Token token = null;
while ((token = tokenStream.Next()) != null) // 隻要還有詞,就不傳回null
{
string word = token.TermText(); // token.TermText() 取得目前分詞
Response.Write(word + " | ");
}
}
View Code
效果示範
可以看到一進制分詞将這句話的每個字都作為一個詞組。前面提到,Lucene.Net維護着一個索引庫,如果每個字都作為一個詞組,那麼索引庫會變得尤為巨大,當然,分詞的算法很簡單,是以分詞效率上會很高。
2.2 二進制分詞
protected void btnGetSegmentation_Click(object sender, EventArgs e)
{
string words = txtWords.Text;
if (string.IsNullOrEmpty(words))
{
return;
}
Analyzer analyzer = new CJKAnalyzer(); // CJK分詞 → 二進制分詞
TokenStream tokenStream = analyzer.TokenStream("", new StringReader(words));
Token token = null;
while ((token = tokenStream.Next()) != null) // 隻要還有詞,就不傳回null
{
string word = token.TermText(); // token.TermText() 取得目前分詞
Response.Write(word + " | ");
}
}
可以看到二進制分詞通過将兩個字作為一個詞組,在詞組的數量上較一進制分詞有了一定減少,但是分詞的效果仍然不佳,比如:個來 這個分詞結果就不符合語義,加入索引庫也會是沒什麼機會會被用到。
2.3 盤古分詞
使用步驟
(1)從PanGu開發包中取得PanGu.dll 與 PanGu.Lucenet.Analyzer.dll并加入到項目中
(2)從PanGu開發包中取得Dict檔案,并在Bin目錄下建立一個Dict檔案夾将Dict檔案一起copy進去
可以看到,使用基于詞庫的盤古分詞進行分詞後的效果較前兩種好得太多,不過中間的“就跑不脫”這個詞組優點不符合語義。剛剛提到盤古分詞是基于詞庫的分詞,是以我們可以到詞庫裡邊去為跑不脫(四川方言)添加一個詞組到詞庫當中。
分詞擴充
詞庫就是我們剛剛加入到Bin/Dict目錄下的Dict檔案,借助PanGu開發包中的DictManage.exe打開Dict檔案,為跑不脫添加一個詞組吧!
(1)找到DictManage詞庫管理工具
(2)打開我們的Dict檔案并添加一個詞組
(3)在DictManage.exe中查找詞組,然後儲存,設定新版本号
(4)重新打開頁面檢視分詞結果
修改詞庫之後的分詞結果是不是更加符合我們得正常思維習慣了呢?
三、一個最簡單的搜尋引擎
3.1 搭建項目
這個Demo需要模拟的場景是一個BBS論壇,每天BBS論壇都會新增很多新的文章,每篇文章都會存入資料庫。從前面介紹可知,資料庫中的内容也會轉換為文本資訊存入索引庫,使用者在前端搜尋時會直接從索引庫中擷取查詢結果。整個流程如下圖所示:
我們仍然在之前分詞Demo的基礎上實作這個小Demo,整個項目的結構如下圖所示:
好了,準備一個Web頁面來展示吧:
<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="SearchEngineV1.aspx.cs" Inherits="Manulife.SearchEngine.LuceneNet.Views.SearchEngineV1" %>
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>最簡單的搜尋引擎</title>
</head>
<body>
<form id="mainForm" runat="server">
<div align="center">
<asp:Button ID="btnCreateIndex" runat="server" Text="Create Index" OnClick="btnCreateIndex_Click" />
<asp:Label ID="lblIndexStatus" runat="server" Visible="false" />
<hr />
<asp:TextBox ID="txtKeyWords" runat="server" Text="" Width="250"></asp:TextBox>
<asp:Button ID="btnGetSearchResult" runat="server" Text="Search" OnClick="btnGetSearchResult_Click" />
<hr />
</div>
<div>
<ul>
<asp:Repeater ID="rptSearchResult" runat="server">
<ItemTemplate>
<li>Id:<%#Eval("Id") %><br />
<%#Eval("Msg") %></li>
</ItemTemplate>
</asp:Repeater>
</ul>
</div>
</form>
</body>
</html>
頁面的結構如下圖所示:
頁面很簡單,隻有兩個button,一個textbox,以及一個repeater清單。其中:
(1)Create Index : 點選該按鈕會周遊文章/文章的文本檔案夾,對每個文章進行分詞,并将分詞後的結果存入索引庫;
(2)Search :點選該按鈕會将使用者輸入的關鍵詞與索引庫中的内容進行比對,并将比對後的結果顯示在repeater清單中;
3.2 建立索引
核心代碼:
/// <summary>
/// 建立索引
/// </summary>
protected void btnCreateIndex_Click(object sender, EventArgs e)
{
string indexPath = Context.Server.MapPath("~/Index"); // 索引文檔儲存位置
FSDirectory directory = FSDirectory.Open(new DirectoryInfo(indexPath), new NativeFSLockFactory());
bool isUpdate = IndexReader.IndexExists(directory); //判斷索引庫是否存在
if (isUpdate)
{
// 如果索引目錄被鎖定(比如索引過程中程式異常退出),則首先解鎖
// Lucene.Net在寫索引庫之前會自動加鎖,在close的時候會自動解鎖
// 不能多線程執行,隻能處理意外被永遠鎖定的情況
if (IndexWriter.IsLocked(directory))
{
IndexWriter.Unlock(directory); //unlock:強制解鎖,待優化
}
}
// 建立向索引庫寫操作對象 IndexWriter(索引目錄,指定使用盤古分詞進行切詞,最大寫入長度限制)
// 補充:使用IndexWriter打開directory時會自動對索引庫檔案上鎖
IndexWriter writer = new IndexWriter(directory, new PanGuAnalyzer(), !isUpdate,
IndexWriter.MaxFieldLength.UNLIMITED);
for (int i = 1000; i < 1100; i++)
{
string txt = File.ReadAllText(Context.Server.MapPath("~/Upload/Articles/") + i + ".txt");
// 一條Document相當于一條記錄
Document document = new Document();
// 每個Document可以有自己的屬性(字段),所有字段名都是自定義的,值都是string類型
// Field.Store.YES不僅要對文章進行分詞記錄,也要儲存原文,就不用去資料庫裡查一次了
document.Add(new Field("id", i.ToString(), Field.Store.YES, Field.Index.NOT_ANALYZED));
// 需要進行全文檢索的字段加 Field.Index. ANALYZED
// Field.Index.ANALYZED:指定文章内容按照分詞後結果儲存,否則無法實作後續的模糊查詢
// WITH_POSITIONS_OFFSETS:訓示不僅儲存分割後的詞,還儲存詞之間的距離
document.Add(new Field("msg", txt, Field.Store.YES, Field.Index.ANALYZED,
Field.TermVector.WITH_POSITIONS_OFFSETS));
// 防止重複索引,如果不存在則删除0條
writer.DeleteDocuments(new Term("id", i.ToString()));// 防止已存在的資料 => delete from t where id=i
// 把文檔寫入索引庫
writer.AddDocument(document);
Console.WriteLine("索引{0}建立完畢", i.ToString());
}
writer.Close(); // Close後自動對索引庫檔案解鎖
directory.Close(); // 不要忘了Close,否則索引結果搜不到
lblIndexStatus.Text = "索引檔案建立成功!";
lblIndexStatus.Visible = true;
btnCreateIndex.Enabled = false;
}
效果展示:
應用場景:
在BBS論壇新釋出一個文章的事件時,添加到資料庫之後,再進行建立索引的操作,儲存到索引庫,這樣文章内容就存了兩份,一份在資料庫,一份在索引庫。
3.2 擷取結果
/// <summary>
/// 擷取搜尋結果
/// </summary>
protected void btnGetSearchResult_Click(object sender, EventArgs e)
{
string keyword = txtKeyWords.Text;
string indexPath = Context.Server.MapPath("~/Index"); // 索引文檔儲存位置
FSDirectory directory = FSDirectory.Open(new DirectoryInfo(indexPath), new NoLockFactory());
IndexReader reader = IndexReader.Open(directory, true);
IndexSearcher searcher = new IndexSearcher(reader);
// 查詢條件
PhraseQuery query = new PhraseQuery();
// 等同于 where contains("msg",kw)
query.Add(new Term("msg", keyword));
// 兩個詞的距離大于100(經驗值)就不放入搜尋結果,因為距離太遠相關度就不高了
query.SetSlop(100);
// TopScoreDocCollector:盛放查詢結果的容器
TopScoreDocCollector collector = TopScoreDocCollector.create(1000, true);
// 使用query這個查詢條件進行搜尋,搜尋結果放入collector
searcher.Search(query, null, collector);
// 從查詢結果中取出第m條到第n條的資料
// collector.GetTotalHits()表示總的結果條數
ScoreDoc[] docs = collector.TopDocs(0, collector.GetTotalHits()).scoreDocs;
// 周遊查詢結果
IList<SearchResult> resultList = new List<SearchResult>();
for (int i = 0; i < docs.Length; i++)
{
// 拿到文檔的id,因為Document可能非常占記憶體(DataSet和DataReader的差別)
int docId = docs[i].doc;
// 是以查詢結果中隻有id,具體内容需要二次查詢
// 根據id查詢内容:放進去的是Document,查出來的還是Document
Document doc = searcher.Doc(docId);
SearchResult result = new SearchResult();
result.Id = Convert.ToInt32(doc.Get("id"));
result.Msg = HighlightHelper.HighLight(keyword, doc.Get("msg"));
resultList.Add(result);
}
// 綁定到Repeater
rptSearchResult.DataSource = resultList;
rptSearchResult.DataBind();
}
附件下載下傳
Lucene.Net開發包 : 點我下載下傳
PanGu盤古分詞開發包:點我下載下傳
簡單搜尋引擎Demo:點我下載下傳
參考資料
(1)楊中科,《Lucene.Net站内搜尋公開課》
(2)痞子一毛,《Lucene.Net》
(3)MeteorSeed,《使用Lucene.Net實作全文檢索》
(4)Lucene.Net官方網站:http://lucenenet.apache.org/download.html
作者:周旭龍
出處:http://edisonchou.cnblogs.com/
本文版權歸作者和部落格園共有,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁面明顯位置給出原文連結。