【Lucene】Apache Lucene全文檢索引擎架構之入門實戰
Lucene是一套用於全文檢索和搜尋的開源程式庫,由Apache軟體基金會支援和提供。Lucene提供了一個簡單卻強大的應用程式介面,能夠做全文索引和搜尋。在Java開發環境裡Lucene是一個成熟的免費開源工具。就其本身而言,Lucene是當前以及最近幾年最受歡迎的免費Java資訊檢索程式庫。——《百度百科》
這篇博文主要從兩個方面出發,首先介紹一下Lucene中的全文搜尋原理,其次通過程式示例來展現如何使用Lucene。關於全文搜尋原理部分我上網搜尋了一下,也看了好幾篇文章,最後在寫這篇文章的時候部分參考了其中兩篇(地址我放在文章的末尾),感謝原文作者。
1. 全文檢索
何為全文檢索?舉個例子,比如現在要在一個檔案中查詢某個字串,最直接的想法就是從頭開始檢索,查到了就OK,這種對於小資料量的檔案來說,很實用,但是對於大資料量的檔案來說,就有點呵呵了。或者說找包含某個字串的檔案,也是這樣,如果在一個擁有幾十個G的硬碟中找那效率可想而知,是很低的。
檔案中的資料是屬於非結構化資料,也就是說它是沒有什麼結構可言的,要解決上面提到的效率問題,首先我們得即將非結構化資料中的一部分資訊提取出來,重新組織,使其變得有一定結構,然後對此有一定結構的資料進行搜尋,從而達到搜尋相對較快的目的。這就叫全文搜尋。即先建立索引,再對索引進行搜尋的過程。
那麼lucene中是如何建立索引的呢?假設現在有兩個文件,內容如下:
文章1的內容為:Tom lives in Guangzhou, I live in Guangzhou too.
文章2的內容為:He once lived in Shanghai.
首先第一步是將文件傳給分片語件(Tokenizer),分片語件會將文件分成一個個單詞,並去除標點符號和停詞。所謂的停詞指的是沒有特別意義的詞,比如英文中的a,the,too等。經過分詞後,得到詞元(Token) 。如下:
文章1經過分詞後的結果:[Tom] [lives] [Guangzhou] [I] [live] [Guangzhou]
文章2經過分詞後的結果:[He] [lives] [Shanghai]
然後將詞元傳給語言處理元件(Linguistic Processor),對於英語,語言處理元件一般會將字母變為小寫,將單詞縮減為詞根形式,如”lives”到”live”等,將單詞轉變為詞根形式,如”drove”到”drive”等。然後得到詞(Term)。如下:
文章1經過處理後的結果:[tom] [live] [guangzhou] [i] [live] [guangzhou]
文章2經過處理後的結果:[he] [live] [shanghai]
最後將得到的詞傳給索引元件(Indexer),索引元件經過處理,得到下面的索引結構:
關鍵詞 | 文章號[出現頻率] | 出現位置 |
---|---|---|
guangzhou | 1[2] | 3,6 |
he | 2[1] | 1 |
i | 1[1] | 4 |
live | 1[2],2[1] | 2,5,2 |
shanghai | 2[1] | 3 |
tom | 1[1] | 1 |
以上就是lucene索引結構中最核心的部分。它的關鍵字是按字元順序排列的,因此lucene可以用二元搜尋演算法快速定位關鍵詞。實現時lucene將上面三列分別作為詞典檔案(Term Dictionary)、頻率檔案(frequencies)和位置檔案(positions)儲存。其中詞典檔案不僅儲存有每個關鍵詞,還保留了指向頻率檔案和位置檔案的指標,通過指標可以找到該關鍵字的頻率資訊和位置資訊。
搜尋的過程是先對詞典二元查詢、找到該詞,通過指向頻率檔案的指標讀出所有文章號,然後返回結果,然後就可以在具體的文章中根據出現位置找到該詞了。所以lucene在第一次建立索引的時候可能會比較慢,但是以後就不需要每次都建立索引了,就快了。當然了,這是針對英文的檢索,針對中文的規則會有不同,後面我再看看相關資料。
2. 示例程式碼
根據上文的分析,全文檢索有兩個步驟,先建立索引,再檢索。所以為了測試這個過程,我寫了兩個java類,一個是測試建立索引的,另一個是測試檢索的。首先建立個maven工程,pom.xml如下:
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>demo.lucene</groupId>
<artifactId>Lucene01</artifactId>
<version>0.0.1-SNAPSHOT</version>
<build/>
<dependencies>
<!-- lucene核心包 -->
<dependency>
<groupId>org.apache.lucene</groupId>
<artifactId>lucene-core</artifactId>
<version>5.3.1</version>
</dependency>
<!-- lucene查詢解析包 -->
<dependency>
<groupId>org.apache.lucene</groupId>
<artifactId>lucene-queryparser</artifactId>
<version>5.3.1</version>
</dependency>
<!-- lucene解析器包 -->
<dependency>
<groupId>org.apache.lucene</groupId>
<artifactId>lucene-analyzers-common</artifactId>
<version>5.3.1</version>
</dependency>
</dependencies>
</project>
在寫程式之前,首先得去弄一些檔案,我隨便找了一些英文的文件(中文的後面再研究),放到了D:\lucene\data\目錄中,如下:
文件裡面都是密密麻麻的英文,我就不截圖了。
接下來開始寫建立索引的java程式:
/**
* 建立索引的類
* @author Ni Shengwu
*
*/
public class Indexer {
private IndexWriter writer; //寫索引例項
//構造方法,例項化IndexWriter
public Indexer(String indexDir) throws Exception {
Directory dir = FSDirectory.open(Paths.get(indexDir));
Analyzer analyzer = new StandardAnalyzer(); //標準分詞器,會自動去掉空格啊,is a the等單詞
IndexWriterConfig config = new IndexWriterConfig(analyzer); //將標準分詞器配到寫索引的配置中
writer = new IndexWriter(dir, config); //例項化寫索引物件
}
//關閉寫索引
public void close() throws Exception {
writer.close();
}
//索引指定目錄下的所有檔案
public int indexAll(String dataDir) throws Exception {
File[] files = new File(dataDir).listFiles(); //獲取該路徑下的所有檔案
for(File file : files) {
indexFile(file); //呼叫下面的indexFile方法,對每個檔案進行索引
}
return writer.numDocs(); //返回索引的檔案數
}
//索引指定的檔案
private void indexFile(File file) throws Exception {
System.out.println("索引檔案的路徑:" + file.getCanonicalPath());
Document doc = getDocument(file); //獲取該檔案的document
writer.addDocument(doc); //呼叫下面的getDocument方法,將doc新增到索引中
}
//獲取文件,文件裡再設定每個欄位,就類似於資料庫中的一行記錄
private Document getDocument(File file) throws Exception{
Document doc = new Document();
//新增欄位
doc.add(new TextField("contents", new FileReader(file))); //新增內容
doc.add(new TextField("fileName", file.getName(), Field.Store.YES)); //新增檔名,並把這個欄位存到索引檔案裡
doc.add(new TextField("fullPath", file.getCanonicalPath(), Field.Store.YES)); //新增檔案路徑
return doc;
}
public static void main(String[] args) {
String indexDir = "D:\\lucene"; //將索引儲存到的路徑
String dataDir = "D:\\lucene\\data"; //需要索引的檔案資料存放的目錄
Indexer indexer = null;
int indexedNum = 0;
long startTime = System.currentTimeMillis(); //記錄索引開始時間
try {
indexer = new Indexer(indexDir);
indexedNum = indexer.indexAll(dataDir);
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
indexer.close();
} catch (Exception e) {
e.printStackTrace();
}
}
long endTime = System.currentTimeMillis(); //記錄索引結束時間
System.out.println("索引耗時" + (endTime-startTime) + "毫秒");
System.out.println("共索引了" + indexedNum + "個檔案");
}
}
我是按照建立索引的過程來寫的程式,在註釋中已經解釋的很清楚了,這裡就不再贅述了。然後執行一下main方法看一下結果,如下:
共索引了7個檔案,耗時649毫秒,還是蠻快的,而且索引檔案的路徑也是對的,然後可以看一下D:\lucene\會生成一些檔案,這些就是生成的索引。
現在有了索引了,我們可以檢索想要查詢的字元了,我隨便打開了一個檔案,在裡面找了個比較醜的字串“generate-maven-artifacts”來作為檢索的物件。在檢索之前先看一下檢索的java程式碼:
public class Searcher {
public static void search(String indexDir, String q) throws Exception {
Directory dir = FSDirectory.open(Paths.get(indexDir)); //獲取要查詢的路徑,也就是索引所在的位置
IndexReader reader = DirectoryReader.open(dir);
IndexSearcher searcher = new IndexSearcher(reader);
Analyzer analyzer = new StandardAnalyzer(); //標準分詞器,會自動去掉空格啊,is a the等單詞
QueryParser parser = new QueryParser("contents", analyzer); //查詢解析器
Query query = parser.parse(q); //通過解析要查詢的String,獲取查詢物件
long startTime = System.currentTimeMillis(); //記錄索引開始時間
TopDocs docs = searcher.search(query, 10);//開始查詢,查詢前10條資料,將記錄儲存在docs中
long endTime = System.currentTimeMillis(); //記錄索引結束時間
System.out.println("匹配" + q + "共耗時" + (endTime-startTime) + "毫秒");
System.out.println("查詢到" + docs.totalHits + "條記錄");
for(ScoreDoc scoreDoc : docs.scoreDocs) { //取出每條查詢結果
Document doc = searcher.doc(scoreDoc.doc); //scoreDoc.doc相當於docID,根據這個docID來獲取文件
System.out.println(doc.get("fullPath")); //fullPath是剛剛建立索引的時候我們定義的一個欄位
}
reader.close();
}
public static void main(String[] args) {
String indexDir = "D:\\lucene";
String q = "generate-maven-artifacts"; //查詢這個字串
try {
search(indexDir, q);
} catch (Exception e) {
e.printStackTrace();
}
}
}
執行一下main方法,看一下結果:
Lucene已經正確的幫我們檢索到了,然後我把中間的“-”去掉,它也能幫我們檢索到,但是我把前面的字元都去掉,只留下“rtifacts”就檢索不到了,這也能說明Lucene中建立索引是以單詞來劃分的,但是這個問題是可以解決的,我會在後續的文章中寫到。