Lucene實現索引和查詢
0引言
隨著萬維網的發展和大數據時代的到來,每天都有大量的數字化信息在生產、存儲、傳遞和轉化,如何從大量的信息中以一定的方式找到滿足自己需求的信息,使之有序化並加以利用成為一大難題。全文檢索技術是現如今最普遍的信息查詢應用,生活中利用搜索引擎,在博客論壇中查找信息,這些搜索的核心原理就是本文要實現的全文檢索技術。隨著文檔信息數字化的實現,將信息有效存儲並及時準確的提取是每一個公司、企業和單位要做好的基礎。針對英文的全文檢索已經有很多成熟的理論和方法,開放源代碼的全文檢索引擎Lucene 是Apache 軟件基金會Jakarta 項目組的一個子項目,它的目的是為軟件開發人員提供一個簡單易用的工具包,方便在目標系統中實現全文檢索的功能。Lucene不支持中文,但是目前已有很多開源的中文分詞器可以對中文內容進行索引,本文在研究Lucene核心原理的基礎上,分別實現了對中英文網頁的爬取和檢索。
1 Lucene介紹
1.1 lucene簡介
Lucene是一個用Java寫的全文檢索引擎工具包,實現構造了索引和搜索兩大核心功能,並且兩者相互獨立,這使得開發人員可以方便擴展,Lucene提供了豐富的API , 可以與存儲在索引中的信息方便的交互。需要說明的是它並不是一個完整的全文檢索應用, 而是為應用程序提供索引和搜索功能。即若想讓Lucene 真正起作用, 還需在其基礎上做一些必要的二次開發。
Lucene的結構設計與數據庫的設計較為相似,但Lucene的索引與數據庫有著極大的不同。數據庫和Lucene建立索引都是為了查找方便,但是數據庫僅僅針對部分字段進行建立,且需要把數據轉化為格式化信息,並予以保存。而全文檢索是將全部信息按照一定方式進行索引。兩種檢索的不同和相似如表1-1所示。
表1-1:數據庫檢索與Lucene檢索對比
比較項 |
Lucene檢索 |
數據庫檢索 |
數據檢索 |
從Lucene的索引文件中檢出 |
由數據庫索引檢索記錄 |
索引結構 |
Document(文檔) |
Record(記錄) |
查詢結果 |
Hit:滿足關系的文檔組成 |
查詢結果集:包含關鍵字的記錄組成 |
全文檢索 |
支持 |
不支持 |
模糊查詢 |
支持 |
不支持 |
結果排序 |
設置權重,進行相關性排序 |
不能排序 |
1.2 lucene總體結構
Lucene軟件包的發布形式是一個JAR文件,版本更新較快且版本差距較大,本文使用的是5.3.1的版本,主要使用的子包如表1-2所示。
表1-2:子包和功能
包名 |
功能 |
Org .apache.lucene .analysis |
分詞 |
Org .apache.lucene .document |
對索引管理的文檔 |
Org .apache.lucene .index |
索引操作,包括增加、刪除等 |
Org .apache.lucene .queryParser |
查詢器,構造檢索表達式 |
Org .apache.lucene .search |
檢索管理 |
Org .apache.lucene .store |
數據存儲管理 |
Org .apache.lucene .util |
公共類 |
1.3 lucene架構設計
Lucene功能非常強大,但從根本上來說,主要包括兩塊:一是從文本內容切分詞後索引入庫;二是根據查詢條件返回結果,即建立索引和進行查詢兩部分。
如圖1-1所示,本文拋出外部接口以及信息來源,重點對網頁爬取的文本內容進行索引和查詢 。
圖1-1:Lucene的架構設計
2 JDK的安裝和環境變量的配置
1.jdk的下載:
在oracle官網下載符合系統版本的壓縮包,網址如下。點擊安裝,根據提示進行安裝,在安裝過程中會提示是否安裝jre,點擊是。 http://www.oracle.com/technetwork/java/javase/downloads/index.html
2.設置環境變量:
(1)右鍵計算機=》屬性=》高級系統設置=》環境變量=》系統變量=》新建=》JAVA_HOME:安裝路徑
(2)Path中新增=》%JAVA_HOME%\bin
3.測試是否成功:
開始=》運行=》CMD 回車 在彈出的 DOS 窗口內
輸入:java -version 會出現版本信息,
輸入: javac出現 javac 的用法信息
出現如圖2-1所示為成功。
圖2-1:cmd命令框測試java配置
3 編寫Java代碼實現對網頁內容的獲取
因為Lucene針對不同語言要使用不同的分詞器,英文使用標準分詞器,中文選擇使用smartcn分詞器。在獲取網頁的時候,先獲取網頁存為html文件,在html中由於標簽 的幹擾,會對檢索效果產生影響,因此需要對html標簽進行剔除,並將文本內容轉為txt文件進行保存。中英文除了分詞器不同,其他基本一致,因此之後的代碼和實驗結果演 示會選擇任一。本文選取五十篇中文故事和英文故事的網頁為例。
具體代碼設計如下圖:Url2Html.java將輸入網址的網頁轉存為html文件,Html2Txt.java文件實現html文檔標簽的去除,轉存為txt文檔。具體代碼如圖3-1和3-2。
public void way(String filePath,String url) throws Exception{
File dest = new File(filePath);//建立文件
InputStream is;//接收字節輸入流
FileOutputStream fos = new FileOutputStream(dest);//字節輸出流
URL wangzhi = new URL(url);//設定網址URL
is = wangzhi.openStream();
BufferedInputStream bis = new BufferedInputStream(is);//為字節輸入流加緩沖
BufferedOutputStream bos = new BufferedOutputStream(fos);//為字節輸出流加緩沖
/*
* 對字節進行讀取
*/
int length;
byte[] bytes = new byte[1024*20];
while((length = bis.read(bytes, 0, bytes.length)) != -1){
fos.write(bytes, 0, length);
}
/*
* 關閉緩沖流和輸入輸出流
*/
bos.close();
fos.close();
bis.close();
is.close();
}
public String getBody(String val){
String zyf = val.replaceAll("</?[^>]+>", ""); //剔出<html>的標簽
return zyf;
}
public void writeTxt(String Str,String writePath) {
File writename = new File(writePath);
try {
writename.createNewFile();
BufferedWriter out = new BufferedWriter(new FileWriter(writename));
out.write(Str);
out.flush();
out.close();
} catch (IOException e) {
e.printStackTrace();
}
}
以童話故事《笨狼上學》的網頁為例,文檔路徑設為”E:\work \lucene \test \data \html”和”E:\work\lucene\test\data\txt”,在每一次讀取網頁的時候需要設定的兩個參數為文件命名filename和獲取目標網址url。新建一個main函數,實現對兩個方法的調用。具體實現如圖3-3所示:
public static void main(String[] args) {
String filename = "jingdizhi";//文件名字
String url = "http://www.51test.net/show/8072125.html";//需要爬取的網頁url
String filePath = "E:\\work\\lucene\\test\\data\\html\\"+filename+".html";//寫出html的文件路徑+文件名
String writePath = "E:\\work\\lucene\\test\\data\\txt\\"+filename+".txt";//寫出txt的文件路徑+文件名
Url2Html url2html = new Url2Html();
try {
url2html.way(filePath,url);
} catch (Exception e) {
e.printStackTrace();
}
Html2Txt html2txt = new Html2Txt();
String read=html2txt.readfile(filePath);//讀取html文件
String txt = html2txt.getBody(read);//去除html標簽
System.out.println(txt);
try {
html2txt.writeTxt(txt,writePath);
} catch (Exception e) {
e.printStackTrace();
}
}
執行程序後,分別在兩個文件夾中建立”笨狼上學.html”和”笨狼上學.txt”。
4 建立索引
索引和查詢的基本原理如下:
建立索引:搜索引擎的索引其實就是實現“單詞-文檔矩陣”的具體數據結構。也是進行全文檢索的第一步,lucene提供IndexWriter類進行索引的管理,主要包括add()、delete()、update()。還有對權值的設定,通過不同索引權值的設定,可以在搜索的時候根據相關性大小進行返回。
進行搜索:原本的直接搜索是針對文檔進行順序檢索,在建立索引之後,可以通過對索引的查找以找到索引詞在文檔中出現的位置,然後返回索引項所對的文檔中的位置和詞。Lucene提供IndexSearcher類進行對文檔的檢索,檢索形式主要分為兩類,第一類是Term,針對單個詞項的檢索;第二類是Parser,可以自定義構造檢索表達式,有較多的檢索形式,具體的方法會在之後進行實現的演示。
4.1 實驗環境
本PC機采用windows 10x64系統,8G內存,256G固態硬盤。開發環境為Myeclipse 10,jdk版本為1.8。在實驗過程中,因為部分語法的轉變,若幹Class采用1.6版本實現。
4.2 建立索引
建立索引庫就是往索引庫添加一條條索引記錄,Lucene為添加一條索引記錄提供了接口,添加索引。
主要用到了“寫索引器”、“文檔”、“域”這3 個類。要建立索引,首先要構造一個Document 文檔對象,確定Document的各個域,這類似於關系型數據庫中表結構的建立,Document相當於表中的一個記錄行,域相當於一行中的列,在Lucene 中針對不同域的屬性和數據輸出的需求,對域還可以選擇不同的索引/存儲字段規則,在本實驗中,文件名fileName、文件路徑fullPath和文本內容content作為Document 的域。
IndexWriter 負責接收新加入的文檔,並寫入索引庫中。在創建“寫索引器”IndexWriter 時需要指定所使用的語言分析器。建立索引分為兩個類別,第一:不加權索引;第二:加權索引。
public Indexer(String indexDir)throws Exception{ Directory dir=FSDirectory.open(Paths.get(indexDir)); Analyzer analyzer=new StandardAnalyzer(); // 標準分詞器 //SmartChineseAnalyzer analyzer = new SmartChineseAnalyzer(); IndexWriterConfig iwc=new IndexWriterConfig(analyzer); writer=new IndexWriter(dir, iwc); }
設置索引字段,Store表示是否對索引內容存儲:fileName和fullPath占用內存較少可以進行存儲,以方便查詢返回。
private Document getDocument(File f)throws Exception { Document doc=new Document(); doc.add(new TextField("contents", new FileReader(f))); doc.add(new TextField("fileName", f.getName(),Store.YES)); doc.add(new TextField("fullPath",f.getCanonicalPath(),Store.YES));//路徑索引 return doc; }
執行主代碼後結果如圖:設計在索引某個文件的時候返回文件“索引文件:+文件路徑”,且計算輸出索引全部文件花費的時間。
4.3 對索引的刪除和修改
一般對數據庫的操作包括CRUD(增加、刪除、更改、查詢),增加就是對索引項的選擇和建立,查詢作為較為核心的功能會在之後展開論述,這裏主要記錄一下在刪除、更新索引時用到的方法。
刪除分為兩種類型,包括普通的刪除和徹底刪除,因為索引的刪除影響到整個數據庫,而且對於大型的系統而言,刪除索引意味著對系統的底層進行更改,耗時耗力而且無法返回,前面索引的時候看到建立索引後生成若幹小文件,當進行查找的時候會將各個文件進行合並然後查找。普通刪除僅僅是對之前建立的索引做個簡單的標記,致使無法進行查找返回。徹底刪除則是對索引進行銷毀,無法撤銷。以刪除索引項“id”為1的索引為例:
普通的刪除(在合並前刪除):
writer.deleteDocuments(new Term("id","1")); writer.commit();
徹底的刪除(在合並後刪除):
writer.deleteDocuments(new Term("id","1")); writer.forceMergeDeletes(); // 強制刪除 writer.commit();
對索引的修改原理比較簡單,就是在原有索引的基礎上實現覆蓋,實現代碼跟上文的增加索引一樣,在此不多做闡述。
4.4 對索引的加權
Lucene默認按照相關度排序,Lucene對Field提供了一個可以設置的Boosting參數,這個參數用來表示記錄的重要性,在滿足搜索條件是,會優先考慮重要性高的記錄,返回結果靠前,如果記錄較多,權值低的記錄會排到首頁之後,因此,對索引的加權操作是影響返回結果滿意度的重要因素,在實際設計信息系統的時候,應該有嚴格的權值計算公式,方便對Field權值的更改,更好的滿足用戶的需求。
例如搜索引擎將點擊率高,鏈入鏈出的網頁給定較高的權重,在返回的時候排到第一頁。實現代碼如圖4-1所示,不加權和加權結果對比如圖4-2所示。
TextField field = new TextField("fullPath", f.getCanonicalPath(), Store.YES); if("A GREAT GRIEF.txt".equals(f.getName())){ field.setBoost(2.0f);//對文件名為secondry story.txt的fullPath路徑加權; } //默認權重為1.0,改為1.2即增加權重。 doc.add(field);
圖4-1:索引加權
圖4-2:加權之前
圖4-2:加權之後
由圖4-2結果可以看出,不加權時,按照字典順序排列返回,因此first在secondry之前,在對secondry命名的文件路徑加權後,返回的時候順序發生變化,實現對權重的測試。
5 進行查詢
Lucene 的檢索接口主要由QueryParser、IndexSearcher、Hits這3 個類構成,QueryParser 是查詢解析器,負責解析用戶提交的查詢關鍵字,在新建一個解析器時需要指定要解析的域和使用什麽語言分析器,這裏使用的語言分析器必須與索引庫建立時使用的解析器相同,否則查詢結果不正確。IndexSearcher是索引搜索器,在實例化IndexSearcher時需要指定索引庫所在的目錄,IndexSearcher有一個search 方法執行索引的檢索,這個方法接受Query 作為參數,返回Hits,Hists 是一系列排好序的查詢結果的集合,集合的元素是Document。通過Document的get 方法可以得到與這個文檔對應文件的信息,比如:文件名、文件路徑、文件內容等。
5.1 基本查詢
如圖查詢主要有兩種方式,但是推薦使用第一種構造QueryParser表達式,它可以有靈活的組合方式,包括布爾邏輯表達、模糊匹配等,但是第二種Term只能針對詞匯查詢。
1.構造QueryParser查詢式:
QueryParser parser=new QueryParser("fullPath", analyzer); Query query=parser.parse(q);
2.對特定項的查詢:
Term t = new Term("fileName", q); Query query = new TermQuery(t);
查詢結果如圖5-1所示:以查詢文件名fileName包含“大”為例。
圖5-1:“大”查詢結果
5.2 模糊查詢
在構造QueryParser時,通過對詞項q的修改可以實現精確匹配和模糊匹配。模糊匹配通過在“q”之後加“~”進行修改。如圖5-2所示:
圖5-2:模糊匹配
5.3 限定條件查詢
布爾邏輯查詢和模糊查詢只需要對查詢詞q進行更改,而限定條件查詢需要對query表達式進行設定,主要分為以下幾類:
分別為指定項範圍搜索、指定數字範圍、指定字符串開頭和多條件查詢,分別列出應用的查詢,true參數指的:是否包含上限和下限在內。
指定項範圍:
TermRangeQuery query=new TermRangeQuery("desc", new BytesRef("b".getBytes()), new BytesRef("c".getBytes()), true, true);
指定數字範圍:
NumericRangeQuery<Integer> query=NumericRangeQuery.newIntRange("id", 1, 2, true, true);
指定字符串開頭:
PrefixQuery query=new PrefixQuery(new Term("city","a"));
多條件查詢:
NumericRangeQuery<Integer>query1=NumericRangeQuery.newIntRange("id", 1, 2, true, true); PrefixQuery query2=new PrefixQuery(new Term("city","a")); BooleanQuery.Builder booleanQuery=new BooleanQuery.Builder(); booleanQuery.add(query1,BooleanClause.Occur.MUST); booleanQuery.add(query2,BooleanClause.Occur.MUST);
5.4 高亮查詢
在百度、谷歌等搜索引擎中,進行查詢時,返回的網頁包含查詢關鍵字的時候會顯示為紅色,且進行摘要顯示,即對包含關鍵字的部分內容進行截取並返回。高亮查詢即為實現對關鍵字的樣式更改,本實驗在myeclipse中進行,返回結果並不會有樣式的改變,只會對返回內容的關鍵字添加html標簽,如果顯示到網頁即產生樣式的變化。
高亮的設置代碼如圖5-3所示,結果如圖5-4所示,會對南京匹配詞添加<b>和<font>標簽,顯示到網頁上為加粗和變紅。
QueryScorer scorer=new QueryScorer(query); Fragmenter fragmenter=new SimpleSpanFragmenter(scorer); SimpleHTMLFormatter simpleHTMLFormatter=new SimpleHTMLFormatter("<b><font color=‘red‘>","</font></b>"); Highlighter highlighter=new Highlighter(simpleHTMLFormatter, scorer); highlighter.setTextFragmenter(fragmenter);
圖5-3:高亮設置
圖5-4:高亮顯示結果
6 實驗過程中遇到的問題和不足
Lucene版本更新較快,在jdk版本、eclipse版本和lucene版本之間需要一個良好的銜接,否則會造成很多的不兼容,在調試版本以及jdk1.6和jdk1.8的選擇上出現很多困難,比如網頁抓取中的append方法在1.8版本已經刪除,不能使用。但是對文檔路勁的讀取FSDirectory.open()則需要jdk1.8才支持。
本實驗的不足之處主要表現在:
代碼的靈活性較低,在爬取網頁的時候需要手工進行,且需要對中文和英文分別進行,應該完善代碼使得對網頁的語言有個判定,然後自動選擇執行不同的分詞器。
代碼的復用性較低,沒有較為合理的分類和方法的構建,為了簡便,基本在幾個核心代碼中進行註釋和標記而實現效果,有待改進。
代碼的可移植性較低,對網頁的爬取使用的是jdk1.6的版本,Lucene的實現使用的是jdk1.8的版本,在導出到其他機器上,需要對環境稍加修改和配置,無法實現一鍵式操作。
7 總結
本文從Lucene的原理出發,了解了全文檢索的思路和方法,並對常用的功能進行了實驗和測試。在實驗的過程中,了解了搜索引擎的原理,基於信息檢索課程的內容上,有了一個更好的實操體驗。Lucene 是一個優秀的開源全文本搜索技術框架,通過對它的深入研究,對其實現機制更加熟悉,在研究它的過程中學習了很多面向對象的編程方法和思想,它良好的系統框架和擴展性值得學習借鑒。
Lucene實現索引和查詢