公衆號閱讀
Lucene
[TOC]
什麼是Lucene ???
The Apache LuceneTM project develops open-source search software, including:
Lucene Core, our flagship sub-project, provides Java-based indexing and search technology, as well as spellchecking, hit highlighting and advanced analysis/tokenization capabilities.
lucene官網(http://lucene.apache.org/)
Lucene是apache軟件基金會4 jakarta項目組的一個子項目,是一個開放源代碼的全文檢索引擎工具包,但它不是一個完整的全文檢索引擎,而是一個全文檢索引擎的架構,提供了完整的查詢引擎和索引引擎,部分文本分析引擎(英文與德文兩種西方語言)。Lucene的目的是爲軟件開發人員提供一個簡單易用的工具包,以方便的在目標系統中實現全文檢索的功能,或者是以此爲基礎建立起完整的全文檢索引擎。Lucene是一套用於全文檢索和搜尋的開源程式庫,由Apache軟件基金會支持和提供。Lucene提供了一個簡單卻強大的應用程式接口,能夠做全文索引和搜尋。在Java開發環境裏Lucene是一個成熟的免費開源工具。就其本身而言,Lucene是當前以及最近幾年最受歡迎的免費Java信息檢索程序庫。人們經常提到信息檢索程序庫,雖然與搜索引擎有關,但不應該將信息檢索程序庫與搜索引擎相混淆。
爲什麼使用Lucene
現在Lucene在互聯網行業的用的非常廣泛,尤其是大數據時代的今天,那麼根據自己的理解給大家簡單的介紹一下爲什麼要學習Lucene。
傳統的sql查詢方式,數據量過多時,數據庫的壓力就會變得很大,查詢速度會變得非常慢。我們需要使用更好的解決方案來分擔數據庫的壓力。爲了解決數據庫壓力和速度的問題,我們的數據庫就變成了索引庫,我們使用Lucene的API的來操作服務器上的索引庫。這樣完全和數據庫進行了隔離。
一、快速入門
現在我們已經瞭解了Lucene。
- Lucene是一套用於全文檢索和搜尋的開源程序庫,由Apache軟件基金會支持和提供
- Lucene提供了一個簡單卻強大的應用程序接口(API),能夠做全文索引和搜尋,在Java開發環境裏Lucene是一個成熟的免費開放源代碼工具
- Lucene並不是現成的搜索引擎產品,但可以用來製作搜索引擎產品
總結:Lucene全文檢索就是對文檔中全部內容進行分詞,然後對所有單詞建立倒排索引的過程。
- 目前最新的版本是7.x系列,但是在企業中還是用4.x比較多,所以我們學習4.x的版本。
檢索數據需要我們先分詞
,存入索引庫
。
- 文檔Document:數據庫中一條具體的記錄
- 字段Field:數據庫中的每個字段
- 目錄對象Directory:物理存儲位置
- 寫出器的配置對象:需要分詞器和lucene的版本
開發需要的jar包
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>1.7</maven.compiler.source>
<maven.compiler.target>1.7</maven.compiler.target>
<lunece.version>4.10.2</lunece.version>
</properties>
<dependencies>
<!-- 分詞器 -->
<dependency>
<groupId>com.janeluo</groupId>
<artifactId>ikanalyzer</artifactId>
<version>2012_u6</version>
</dependency>
<!-- lucene核心庫 -->
<dependency>
<groupId>org.apache.lucene</groupId>
<artifactId>lucene-core</artifactId>
<version>${lunece.version}</version>
</dependency>
<!-- Lucene的查詢解析器 -->
<dependency>
<groupId>org.apache.lucene</groupId>
<artifactId>lucene-queryparser</artifactId>
<version>${lunece.version}</version>
</dependency>
<!-- lucene的默認分詞器庫 -->
<dependency>
<groupId>org.apache.lucene</groupId>
<artifactId>lucene-analyzers-common</artifactId>
<version>${lunece.version}</version>
</dependency>
<!-- lucene的高亮顯示 -->
<dependency>
<groupId>org.apache.lucene</groupId>
<artifactId>lucene-highlighter</artifactId>
<version>${lunece.version}</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.11</version>
<scope>test</scope>
</dependency>
</dependencies>
- 創建文檔對象
- 創建存儲目錄
- 創建分詞器
- 創建索引寫入器的配置對象
- 創建索引寫入器對象
- 將文檔交給索引寫入器
- 提交
- 關閉
// 創建索引
@Test
public void testCreate() throws Exception {
// 1 創建文檔對象
Document document = new Document();
// 創建並添加字段信息。參數:字段的名稱、字段的值、是否存儲,這裏選Store.YES代表存儲到文檔列表。Store.NO代表不存儲
document.add(new StringField("id", "1", Store.YES));
// 這裏我們title字段需要用TextField,即創建索引又會被分詞。StringField會創建索引,但是不會被分詞
document.add(new TextField("title", "谷歌地圖之父跳槽facebook", Store.YES));
// 2 索引目錄類,指定索引在硬盤中的位置
Directory directory = FSDirectory.open(new File("E:\\luceneTest"));
// 3 創建分詞器對象
// Analyzer analyzer = new StandardAnalyzer();
Analyzer analyzer = new IKAnalyzer();
// 4 索引寫出工具的配置對象
IndexWriterConfig conf = new IndexWriterConfig(Version.LATEST, analyzer);
// 是否清空索引庫;設置打開方式:OpenMode.APPEND
// 會在索引庫的基礎上追加新索引。OpenMode.CREATE會先清空原來數據,再提交新的索引
conf.setOpenMode(OpenMode.CREATE);
// 5 創建索引的寫出工具類。參數:索引的目錄和配置信息
IndexWriter indexWriter = new IndexWriter(directory, conf);
// 6 把文檔交給IndexWriter
indexWriter.addDocument(document);
// 7 提交
indexWriter.commit();
// 8 關閉
indexWriter.close();
}
索引查看工具
啓動run.bat
***
@Test
public void testSearch() throws Exception {
// 索引目錄對象
Directory directory = FSDirectory.open(new File("E:\\luceneTest"));
// 索引讀取工具
IndexReader reader = DirectoryReader.open(directory);
// 索引搜索工具
IndexSearcher searcher = new IndexSearcher(reader);
// 創建查詢解析器,兩個參數:默認要查詢的字段的名稱,分詞器
QueryParser parser = new QueryParser("title", new IKAnalyzer());
// 創建查詢解析器,倆個參數:默認要查詢的字段名稱,分詞器
// MultiFieldQueryParser parser2 = new MultiFieldQueryParser(new
// String[] {
// "id", "title" }, new IKAnalyzer());
// Query query2 = parser2.parse("1");
// 創建查詢對象
Query query = parser.parse("谷歌之父");
// 搜索數據,兩個參數:查詢條件對象要查詢的最大結果條數
// 返回的結果是 按照匹配度排名得分前N名的文檔信息(包含查詢到的總條數信息、所有符合條件的文檔的編號信息)。
TopDocs topDocs = searcher.search(query, 10);
// 獲取總條數
System.out.println("本次搜索共找到" + topDocs.totalHits + "條數據");
// 獲取得分文檔對象(ScoreDoc)數組.SocreDoc中包含:文檔的編號、文檔的得分
ScoreDoc[] scoreDocs = topDocs.scoreDocs;
for (ScoreDoc scoreDoc : scoreDocs) {
// 取出文檔編號
int docID = scoreDoc.doc;
// 根據編號去找文檔
Document doc = reader.document(docID);
System.out.println("id: " + doc.get("id"));
System.out.println("title: " + doc.get("title"));
// 取出文檔得分
System.out.println("得分: " + scoreDoc.score);
}
}
二、工具類
查詢
注:代碼中加了必要註釋
public void search(Query query) throws Exception {
// 索引目錄對象
Directory directory = FSDirectory.open(new File("E:\\luceneTest"));
// 索引讀取工具
IndexReader reader = DirectoryReader.open(directory);
// 索引搜索工具
IndexSearcher searcher = new IndexSearcher(reader);
// 搜索數據,兩個參數:查詢條件對象要查詢的最大結果條數
// 返回的結果是 按照匹配度排名得分前N名的文檔信息(包含查詢到的總條數信息、所有符合條件的文檔的編號信息)。
TopDocs topDocs = searcher.search(query, 10);
// 獲取總條數
System.out.println("本次搜索共找到" + topDocs.totalHits + "條數據");
// 獲取得分文檔對象(ScoreDoc)數組.SocreDoc中包含:文檔的編號、文檔的得分
ScoreDoc[] scoreDocs = topDocs.scoreDocs;
for (ScoreDoc scoreDoc : scoreDocs) {
// 取出文檔編號
int docID = scoreDoc.doc;
// 根據編號去找文檔
Document doc = reader.document(docID);
System.out.println("id: " + doc.get("id"));
System.out.println("title: " + doc.get("title"));
// 取出文檔得分
System.out.println("得分: " + scoreDoc.score);
}
}
注:普通詞條查詢
/*
* 測試普通詞條查詢 注意:Term(詞條)是搜索的最小單位,不可再分詞。值必須是字符串!
*/
@Test
public void testTermQuery() throws Exception {
// 創建詞條查詢對象
Query query = new TermQuery(new Term("title", "谷歌地圖"));
search(query);
}
注:通配符查詢
/*
* 測試通配符查詢 ? 可以代表任意一個字符 * 可以任意多個任意字符
*/
@Test
public void testWildCardQuery() throws Exception {
// 創建查詢對象
Query query = new WildcardQuery(new Term("title", "*歌*"));
search(query);
}
注:模糊查詢;數組範圍查詢;布爾查詢
/*
* 測試模糊查詢
*/
@Test
public void testFuzzyQuery() throws Exception {
// 創建模糊查詢對象:允許用戶輸錯。但是要求錯誤的最大編輯距離不能超過2
// 編輯距離:一個單詞到另一個單詞最少要修改的次數 facebool --> facebook 需要編輯1次,編輯距離就是1
// Query query = new FuzzyQuery(new Term("title","fscevool"));
// 可以手動指定編輯距離,但是參數必須在0~2之間
Query query = new FuzzyQuery(new Term("title", "facevool"), 2);
search(query);
}
/***************************************************************
* 測試:數值範圍查詢 注意:數值範圍查詢,可以用來對非String類型的ID進行精確的查找
*/
@Test
public void testNumericRangeQuery() throws Exception {
// 數值範圍查詢對象,參數:字段名稱,最小值、最大值、是否包含最小值、是否包含最大值
Query query = NumericRangeQuery.newLongRange("id", 2L, 2L, true, true);
search(query);
}
/*****************************************************************
* 布爾查詢: 布爾查詢本身沒有查詢條件,可以把其它查詢通過邏輯運算進行組合! 交集:Occur.MUST + Occur.MUST
* 並集:Occur.SHOULD + Occur.SHOULD 非:Occur.MUST_NOT
*/
// @Test
// public void testBooleanQuery() throws Exception {
//
// Query query1 = NumericRangeQuery.newLongRange("id", 1L, 3L, true, true);
// Query query2 = NumericRangeQuery.newLongRange("id", 2L, 4L, true, true);
// // 創建布爾查詢的對象
// BooleanQuery query = new BooleanQuery();
// // 組合其它查詢
// query.add(query1, BooleanClause.Occur.MUST_NOT);
// query.add(query2, BooleanClause.Occur.SHOULD);
//
// search(query);
// }
修改-刪除
注:修改和刪除操作
/*
* 測試:修改索引 注意: A:Lucene修改功能底層會先刪除,再把新的文檔添加。
* B:修改功能會根據Term進行匹配,所有匹配到的都會被刪除。這樣不好 C:因此,一般我們修改時,都會根據一個唯一不重複字段進行匹配修改。例如ID
* D:但是詞條搜索,要求ID必須是字符串。如果不是,這個方法就不能用。
* 如果ID是數值類型,我們不能直接去修改。可以先手動刪除deleteDocuments(數值範圍查詢鎖定ID),再添加。
*/
@Test
public void testUpdate() throws Exception {
// 創建目錄對象
Directory directory = FSDirectory.open(new File("E:\\luceneTest"));
// 創建配置對象
IndexWriterConfig conf = new IndexWriterConfig(Version.LATEST,
new IKAnalyzer());
// 創建索引寫出工具
IndexWriter writer = new IndexWriter(directory, conf);
// 創建新的文檔數據
Document doc = new Document();
doc.add(new StringField("id", "1", Store.YES));
doc.add(new TextField("title", "谷歌地圖之父跳槽facebook ", Store.YES));
/*
* 修改索引。參數: 詞條:根據這個詞條匹配到的所有文檔都會被修改 文檔信息:要修改的新的文檔數據
*/
writer.updateDocument(new Term("id", "1"), doc);
// 提交
writer.commit();
// 關閉
writer.close();
}
/*
* 演示:刪除索引 注意: 一般,爲了進行精確刪除,我們會根據唯一字段來刪除。比如ID 如果是用Term刪除,要求ID也必須是字符串類型!
*/
@Test
public void testDelete() throws Exception {
// 創建目錄對象
Directory directory = FSDirectory.open(new File("E:\\luceneTest"));
// 創建配置對象
IndexWriterConfig conf = new IndexWriterConfig(Version.LATEST,
new IKAnalyzer());
// 創建索引寫出工具
IndexWriter writer = new IndexWriter(directory, conf);
// 根據詞條進行刪除
// writer.deleteDocuments(new Term("id", "1"));
// 根據query對象刪除,如果ID是數值類型,那麼我們可以用數值範圍查詢鎖定一個具體的ID
// Query query = NumericRangeQuery.newLongRange("id", 2L, 2L, true,
// true);
// writer.deleteDocuments(query);
// 刪除所有
writer.deleteAll();
// 提交
writer.commit();
// 關閉
writer.close();
}
高亮顯示
- 創建目錄 對象
- 創建索引讀取工具
- 創建索引搜索工具
- 創建查詢解析器
- 創建查詢對象
- 創建格式化器
- 創建查詢分數工具
- 準備高亮工具
- 搜索
- 獲取結果
- 用高亮工具處理普通的查詢結果
- 關鍵字增加css樣式
// 高亮顯示
@Test
public void testHighlighter() throws Exception {
// 目錄對象
Directory directory = FSDirectory.open(new File("indexDir"));
// 創建讀取工具
IndexReader reader = DirectoryReader.open(directory);
// 創建搜索工具
IndexSearcher searcher = new IndexSearcher(reader);
QueryParser parser = new QueryParser("title", new IKAnalyzer());
Query query = parser.parse("谷歌地圖");
// 格式化器
Formatter formatter = new SimpleHTMLFormatter("<em>", "</em>");
QueryScorer scorer = new QueryScorer(query);
// 準備高亮工具
Highlighter highlighter = new Highlighter(formatter, scorer);
// 搜索
TopDocs topDocs = searcher.search(query, 10);
System.out.println("本次搜索共" + topDocs.totalHits + "條數據");
ScoreDoc[] scoreDocs = topDocs.scoreDocs;
for (ScoreDoc scoreDoc : scoreDocs) {
// 獲取文檔編號
int docID = scoreDoc.doc;
Document doc = reader.document(docID);
System.out.println("id: " + doc.get("id"));
String title = doc.get("title");
// 用高亮工具處理普通的查詢結果,參數:分詞器,要高亮的字段的名稱,高亮字段的原始值
String hTitle = highlighter.getBestFragment(new IKAnalyzer(), "title", title);
System.out.println("title: " + hTitle);
// 獲取文檔的得分
System.out.println("得分:" + scoreDoc.score);
}
}
排序
// 排序
@Test
public void testSortQuery() throws Exception {
// 目錄對象
Directory directory = FSDirectory.open(new File("indexDir"));
// 創建讀取工具
IndexReader reader = DirectoryReader.open(directory);
// 創建搜索工具
IndexSearcher searcher = new IndexSearcher(reader);
QueryParser parser = new QueryParser("title", new IKAnalyzer());
Query query = parser.parse("谷歌地圖");
// 創建排序對象,需要排序字段SortField,參數:字段的名稱、字段的類型、是否反轉如果是false,升序。true降序
Sort sort = new Sort(new SortField("id", SortField.Type.LONG, true));
// 搜索
TopDocs topDocs = searcher.search(query, 10,sort);
System.out.println("本次搜索共" + topDocs.totalHits + "條數據");
ScoreDoc[] scoreDocs = topDocs.scoreDocs;
for (ScoreDoc scoreDoc : scoreDocs) {
// 獲取文檔編號
int docID = scoreDoc.doc;
Document doc = reader.document(docID);
System.out.println("id: " + doc.get("id"));
System.out.println("title: " + doc.get("title"));
}
}
分頁
// 分頁
@Test
public void testPageQuery() throws Exception {
// 實際上Lucene本身不支持分頁。因此我們需要自己進行邏輯分頁。我們要準備分頁參數:
int pageSize = 2;// 每頁條數
int pageNum = 3;// 當前頁碼
int start = (pageNum - 1) * pageSize;// 當前頁的起始條數
int end = start + pageSize;// 當前頁的結束條數(不能包含)
// 目錄對象
Directory directory = FSDirectory.open(new File("indexDir"));
// 創建讀取工具
IndexReader reader = DirectoryReader.open(directory);
// 創建搜索工具
IndexSearcher searcher = new IndexSearcher(reader);
QueryParser parser = new QueryParser("title", new IKAnalyzer());
Query query = parser.parse("谷歌地圖");
// 創建排序對象,需要排序字段SortField,參數:字段的名稱、字段的類型、是否反轉如果是false,升序。true降序
Sort sort = new Sort(new SortField("id", Type.LONG, false));
// 搜索數據,查詢0~end條
TopDocs topDocs = searcher.search(query, end,sort);
System.out.println("本次搜索共" + topDocs.totalHits + "條數據");
ScoreDoc[] scoreDocs = topDocs.scoreDocs;
for (int i = start; i < end; i++) {
ScoreDoc scoreDoc = scoreDocs[i];
// 獲取文檔編號
int docID = scoreDoc.doc;
Document doc = reader.document(docID);
System.out.println("id: " + doc.get("id"));
System.out.println("title: " + doc.get("title"));
}
}
三、優化
Lucene打分算法
- 當談論到查詢的相關性,很重要的一件事就是對於給定的查詢語句,如何計算文檔得分。文檔得分是一個用來描述查詢語句和文檔之間匹配程度的變量。如果你希望通過干預Lucene查詢來改變查詢結果的排序,你就需要對Lucene的得分計算有所理解。
- 當一個文檔出現在了搜索結果中,這就意味着該文檔與用戶給定的查詢語句是相匹配的。Lucene會對匹配成功的文檔給定一個分數。至少從Lucene這個層面,從打分公式的結果來看,分數值越高,代表文檔相關性越高。 自然而然,我們可以得出:兩個不同的查詢語句對同一個文檔的打分將會有所不同,但是比較這兩個得分是沒有意義的。用戶需要記住的是:我們不僅要避免去比較不同查詢語句對同一個文檔的打分結果,還要避免比較不同查詢語句對文檔打分結果的最大值。這是因爲文檔的得分是多個因素共同影響的結果,不僅有權重(boosts)和查詢語句的結構起作用,還有匹配關鍵詞的個數,關鍵詞所在的域,查詢歸一化因子中用到的匹配類型……。在極端情況下,只是因爲我們用了自定義打分的查詢對象或者由於倒排索引中詞的動態變化,相似的查詢表達式對於同一個文檔都會產生截然不同的打分。
計算文檔得分,考慮因素如下:
- 文檔權重(Document boost):在索引時給某個文檔設置的權重值。
- 域權重(Field boost):在查詢的時候給某個域設置的權重值。
- 調整因子(Coord):基於文檔中包含查詢關鍵詞個數計算出來的調整因子。一般而言,如果一個文檔中相比其它的文檔出現了更多的查詢關鍵詞,那麼其值越大。
- 逆文檔頻率(Inerse document frequency):基於Term的一個因子,存在的意義是告訴打分公式一個詞的稀有程度。其值越低,詞越稀有(這裏的值是指單純的頻率,即多少個文檔中出現了該詞;而非指Lucene中idf的計算公式)。打分公式利用這個因子提升包含稀有詞文檔的權重。
- 長度歸一化(Length norm):基於域的一個歸一化因子。其值由給定域中Term的個數決定(在索引文檔的時候已經計算出來了,並且存儲到了索引中)。域越的文本越長,因子的權重越低。這表明Lucene打分公式偏向於域包含Term少的文檔。
- 詞頻(Term frequency):基於Term的一個因子。用來描述給定Term在一個文檔中出現的次數,詞頻越大,文檔的得分越大。
- 查詢歸一化因子(Query norm):基於查詢語句的歸一化因子。其值爲查詢語句中每一個查詢詞權重的平方和。查詢歸一化因子使得比較不同查詢語句的得分變得可行,當然比較不同查詢語句得分並不總是那麼易於實現和可行的。
Lucene打分公式
Lucene概念上的打分公式是這樣的:(TF/IDF公式的概念版)
上面的公式展示了布爾信息檢索模型和向量空間信息檢索模型的組合。我們暫時不去討論它,直接見識下Lucene實際應用的打分公式:
可以看到,文檔的分數實際上是由查詢語句q和文檔d作爲變量的一個函數值。打分公式中有兩部分不直接依賴於查詢詞,它們是coord和queryNorm。公式的值是這樣計算的,coord和queryNorm兩大部分直接乘以查詢語句中每個查詢詞計算值的總和。另一方面,這個總和也是由每個查詢詞的詞頻(tf),逆文檔頻率(idf),查詢詞的權重,還有norm,也就是前面說的length norm相乘而得的結果。聽上去有些複雜吧?不用擔心,這些東西不需要全部記住。你只需要知道在進行文檔打分的時候,哪些因素是起決定作用的就可以了。基本上,從前面的公式中可以提煉出以下的幾個規則:
- 匹配到的關鍵詞越稀有,文檔的得分就越高。
- 文檔的域越小(包含比較少的Term),文檔的得分就越高。
- 設置的權重(索引和搜索時設置的都可以)越大,文檔得分越高。
正如我們所看到的那樣,Lucene會給具有這些特徵的文檔打最高分:文檔內容能夠匹配到較多的稀有的搜索關鍵詞,文檔的域包含較少的Term,並且域中的Term多是稀有的。簡而言之
停用詞和擴展詞加載
將IKAnalyzer.cfg.xml和stopword.dic和xxx.dic文件複製到MyEclipse的src目錄下,再進行配置
IK Analyzer默認的停用詞詞典爲IKAnalyzer2012_u6/stopword.dic,這個停用詞詞典並不完整,只有30多個英文停用詞。可以擴展停用詞字典,新增ext_stopword.dic,文件和IKAnalyzer.cfg.xml在同一目錄,編輯IKAnalyzer.cfg.xml把新增的停用詞字典寫入配置文件,多個停用詞字典用逗號隔開,如下所示。
<entry key="ext_stopwords">stopword.dic;ext_stopword.dic</entry>
接下來就可以構建自己的搜索引擎了。
上面展示了lucene一些基本操作,更詳細的的工具類可以訪問https://github.com/wangshiyu777/usefulDemo,分享了詳細Demo。