前言:Lucene是apache老爹開源的一款全文搜索引擎,雖然目前已被市面上一些更好用的搜索引擎逐步替代,但作爲搜索引擎的鼻祖,仍然有必要學習一番,而且有了Lucene的基礎之後,學習solr,elastichSearch也會更容易理解。
看完本篇,你將瞭解到Lucene是什麼,Lucene的使用場景,原理,什麼是倒排索引,如何分詞,以及如何使用lucene實戰構建簡易的搜索引擎等。
目錄
1.Lucene可以用來做什麼?
我們知道,傳統關係型數據庫在數據量大到一定量級後(一般是單表超過500W行或者2G左右),查詢速度會變得非常緩慢,也比較喫資源,加上需求中經常會有一些模糊查詢,模糊查詢可能會導致傳統數據庫的索引失效,這時候如果再用傳統數據庫就呵呵了。所以在好的電商系統中,搜素引擎都是標配。通過搜素引擎,可以很好的減輕數據庫的讀壓力。除此之外,像我們熟知的百度,谷歌等搜索大佬,也都有用到搜索引擎,搜索引擎還可以用於搭建文庫,總之很強大,可以做很多事情。
2.Lucene的原理
Lucene的原理我說白話一點,就是先把被搜索內容按照一定規則進行分詞並存儲,生成一個目錄,然後把你輸入的搜索關鍵詞也進行分詞,然後與前面生成的目錄中的關鍵詞進行匹配,如果匹配到,就可以快速定位到該被搜索內容的存儲位置,從而實現搜索。說簡單點就是這樣,當然裏面有很多細節,先不着急,等看完倒排索引之後你會對Lucene的原理有更深的瞭解。
如圖,比如我搜索了程序員禿頭,搜索引擎一般會根據一定的拆分規則,把我輸入的“程序員禿頭”拆分成:程序員,禿頭,頭等關鍵字。然後與爬蟲爬取到的大量文檔裏的關鍵詞進行匹配(這些大量文檔會按照一定規則預先分詞,建立索引,然後存儲起來),如果匹配到了就可以返回給用戶了,返回的結果可以對關鍵詞添加一些“高亮”,標紅等特效,也可以按照一定規則對搜索結果進行排序。
3.Lucene的倒排索引
這裏我援引別人已經畫好的圖來解釋下倒排索引,描述也力求簡潔,幫助快速理解:
下面第一張圖是文檔的原始內容和編號,第二張圖是建立的倒排索引。
倒排索引的建立步驟:
①先對原始文檔中的內容按照一定規則進行分詞,至於如何分先不用管,我後面會講到。
②然後對分詞後的單詞在原始文檔中進行定位,比如“谷歌”在原始文檔中的1-5號文檔都有出現過,所以谷歌的倒排列表就是:1,2,3,4,5. 其它倒排列表以此類推。
有了這樣一套對應列表之後,下次用戶如果輸入了“網站”這個關鍵詞,系統就可以通過倒排列表快速定位到原始文檔中的第5項數據,類似於生成了一本書的一個目錄,告訴你幾個關鍵詞,可以在目錄中快速定位具體的文檔在多少頁,不用再一頁一頁翻了。
看到這裏你應該明白倒排索引的原理了,可以先思考下爲啥叫“倒排”索引,而不是別的名字?
回答這個問題前,先看下什麼是正向索引:正向索引建立的關係是由文檔->關鍵詞的,也就是給定一篇文檔,記錄關鍵詞在文檔中出現的位置,通過文檔來關聯關鍵詞的,這種方式在數據量小的情況下完全OK,但互聯網這片大海中,文檔的量及是宇宙級的,這種方式要掃描的文檔驢輩子都掃不完。倒排索引正好是逆向思維,把文檔的關鍵詞提取,然後通過關鍵詞取關聯文檔,這樣就極大的減輕了掃描的量級。
關於倒排索引就提這麼多,實際上要比這個複雜一些,感興趣的建議讀完之後可以回頭來深入研究,以免打斷思路,這裏直接給出我援引的博主圖片的博客地址:https://blog.csdn.net/csdnliuxin123524/article/details/91581209
4.Lucene分詞
Lucene提供了幾種分詞的實現,在4.0+的版本後還提供了對中文分詞的支持,據說效果不太好,於是後來有人開源了更好用的中文分詞器:IK Analysis,詞庫可以自定義,不太友好的地方就是這款基於Lucene的中文分詞器好像因爲Lucene的落寞而年久失修了,版本還停留在2012年的版本,對新版本的Lucene無法兼容,據說當年創造了每秒160萬分詞的神話。
5.Lucene索引建立與窺探
這裏我直接通過代碼來實現Lucene的分詞,分詞器我採用lucene提供的代碼其實很簡單,不要被其篇幅所蠱惑。
第一步,引入依賴,這裏我建立一個Maven工程爲例:
pom.xml中引入如下依賴:
<dependencies>
<dependency>
<groupId>org.apache.lucene</groupId>
<artifactId>lucene-core</artifactId>
<version>${org.apache.lucene}</version>
</dependency>
<dependency>
<groupId>org.apache.lucene</groupId>
<artifactId>lucene-analyzers-common</artifactId>
<version>${org.apache.lucene}</version>
</dependency>
<dependency>
<groupId>org.apache.lucene</groupId>
<artifactId>lucene-highlighter</artifactId>
<version>${org.apache.lucene}</version>
</dependency>
<dependency>
<groupId>org.apache.lucene</groupId>
<artifactId>lucene-queries</artifactId>
<version>${org.apache.lucene}</version>
</dependency>
<dependency>
<groupId>org.apache.lucene</groupId>
<artifactId>lucene-queryparser</artifactId>
<version>${org.apache.lucene}</version>
</dependency>
<dependency>
<groupId>org.apache.lucene</groupId>
<artifactId>lucene-memory</artifactId>
<version>${org.apache.lucene}</version>
</dependency>
<dependency>
<groupId>org.apache.lucene</groupId>
<artifactId>lucene-analyzers-smartcn</artifactId>
<version>${org.apache.lucene}</version>
</dependency>
<dependency>
<groupId>org.apache.lucene</groupId>
<artifactId>lucene-join</artifactId>
<version>${org.apache.lucene}</version>
</dependency>
</dependencies>
版本號我這裏使用截至目前2019年11月18日最新版本:8.3.0
<properties>
<org.apache.lucene>8.3.0</org.apache.lucene>
</properties>
實現代碼如下:
public class Test {
//生成索引的存放位置
private final static String IDX_DIR = "D:\\lucene";
public static void main(String[] args) {
IndexWriter indexWriter = null;
try {
//創建索引庫
Directory dir = FSDirectory.open(Paths.get(IDX_DIR));
//新建分析器對象
Analyzer analyzer = new SmartChineseAnalyzer();
//新建配置對象
IndexWriterConfig config = new IndexWriterConfig(analyzer);
//創建一個IndexWriter對象
indexWriter = new IndexWriter(dir, config);
//創建文檔對象
Document document1 = new Document();
//添加文檔字段,字段名可以自己指定,Store.YES 說明該字段要被存儲,false則不需要被存儲到文檔列表
document1.add(new StringField("id", "1L", Store.YES));
document1.add(new TextField("title", "三旬老漢隔人暴扣", Store.YES));
document1.add(new TextField("content", "某23號三旬老漢一記搶斷之後運球過人後隔人劈扣拿下2分!", Store.YES));
Document document2 = new Document();
document2.add(new StringField("id", "2L", Store.YES));
document2.add(new TextField("title", "濃眉哥怒砍3雙", Store.YES));
document2.add(new TextField("content", "濃眉哥本場狀態佳,僅前三節比賽就拿下3雙,將比賽勝局牢牢鎖定!", Store.YES));
//寫出索引數據
indexWriter.addDocument(document1);
indexWriter.addDocument(document2);
indexWriter.commit();
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
indexWriter.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
點擊main方法運行後,我們可以在對應的目錄(我這裏是D盤的lucene目錄)下發現被新建的幾個文件
到這裏說明索引文件已被創建,那麼索引裏到底有啥玩意,想窺探一下但發現根本打不開,這個時候lukeall大哥就登場了:下載地址:https://github.com/DmitryKey/luke/releases
一定要匹配你使用的lucene版本,否則可能解析報錯。
下載後解壓打開,然後選擇你索引所在目錄(我這裏是D盤的lucene目錄),就可以看到分詞詳情了:
點擊OK後,在show top terms可以看到被分詞的內容:
藉助Luke我們可以看到被Lucene分詞後建立的倒排索引,再結合3中提到的倒排索引原理,應該可以很好的理解倒排索引和Lucene的索引建立.
6.Lucene的實際使用
有了索引之後,接下來就是要藉助索引去實現搜索了.Luncene提供了多種搜索方式,以此來滿足不同的業務需求,我們先從最普通的關鍵詞搜索來看,在上面已經建好的Maven工程下新增TestQuery類:
public class TestQuery {
private final static String IDX_DIR = "D:\\lucene";
public static void main(String[] args) {
try {
//創建索引庫對象
Directory directory = FSDirectory.open(Paths.get(IDX_DIR));
//索引讀取工具
IndexReader indexReader = DirectoryReader.open(directory);
//索引搜索工具
IndexSearcher indexSearcher = new IndexSearcher(indexReader);
//創建查詢解析器
QueryParser parser = new QueryParser("content", new SmartChineseAnalyzer());
//創建查詢對象
Query query = parser.parse("濃眉");
//獲取搜索結果 第二個參數n是返回多少條,可以根據情況限制
TopDocs docs = indexSearcher.search(query, 10);
//獲取總條數
System.out.println("本次查詢共搜索到:" + docs.totalHits + " 條相關數據");
//獲取得分對象
ScoreDoc[] scoreDocs = docs.scoreDocs;
for (ScoreDoc scoreDoc : scoreDocs) {
//獲取文檔編號
int docId = scoreDoc.doc;
//根據文檔編號獲取文檔內容
Document document = indexReader.document(docId);
System.out.println("id: " + docId);
System.out.println("title: " + document.getField("title"));
System.out.println("content: " + document.getField("content"));
System.out.println("搜索得分: " + scoreDoc.score);
}
} catch (IOException e) {
e.printStackTrace();
} catch (ParseException e) {
e.printStackTrace();
}
}
}
運行上述代碼,效果如圖:
以上便是最普通的根據關鍵詞查詢,查詢時Lucene會根據匹配度算出一個得分,得分越高排名越靠前.
Lucene還提供了:
WildcardQuery(通配符查詢)
// 創建查詢對象
Query query = new WildcardQuery(new Term("title", "*三*"));
FuzzyQuery(模糊查詢)
// 創建模糊查詢對象:允許用戶輸錯。但是要求錯誤的最大編輯距離不能超過2
// 編輯距離:一個單詞到另一個單詞最少要修改的次數 loohan --> laohan 需要編輯1次,編輯距離就是1
// 可以手動指定編輯距離,區間[0,2]
Query query = new FuzzyQuery(new Term("title","laohan"),1);
NumericRangeQuery(數值範圍查詢)
// 針對數值字段的範圍查詢,參數:字段名稱,最小值、最大值、是否包含最小值、是否包含最大值
Query query = NumericRangeQuery.newLongRange("id", 2L, 2L, true, true);
BooleanQuery(組合查詢)
/**
* 布爾查詢:
* 布爾查詢本身沒有查詢條件,可以把其它查詢通過邏輯運算進行組合
* 交集:Occur.MUST + Occur.MUST
* 並集:Occur.SHOULD + Occur.SHOULD
* 非:Occur.MUST_NOT
*/
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);
7.索引編輯(更新)
Lucene提供的索引是可以編輯的,否則一旦數據發生變化,搜出來的東西可能並不是我們想要的,特別是在電商場景下,商品的列表可能會經常發生變化,如果不能及時的更新索引,會給用戶和商家帶來很大不便.
public class TestUpdate {
private final static String IDX_DIR = "D:\\lucene";
public static void main(String[] args) {
IndexWriter indexWriter = null;
try {
//創建目錄
Directory directory = FSDirectory.open(Paths.get(IDX_DIR));
//創建配置對象
IndexWriterConfig config = new IndexWriterConfig(new SmartChineseAnalyzer());
//創建索引寫出工具
indexWriter = new IndexWriter(directory, config);
//創建新的文檔數據
Document document = new Document();
document.add(new StringField("id","1L", Store.YES));
document.add(new TextField("title","湖人三大核心全面爆發,僅兩節比賽就領先對手三十分!",Store.YES));
//修改索引,修改指定文檔中所有的匹配字段的索引,一般選id,因爲id唯一
indexWriter.updateDocument(new Term("id","1L"),document);
indexWriter.commit();
} catch (IOException e) {
e.printStackTrace();
}finally {
try {
indexWriter.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
Lucene也支持索引的刪除:
//根據匹配關鍵詞來刪除
writer.deleteDocuments(new Term("id", "1L"));
//根據匹配數字範圍來刪除
Query query = NumericRangeQuery.newLongRange("id", 1L, 2L, true, true);
writer.deleteDocuments(query);
//刪除全部
write.deleteAll();
8.拓展功能
Lucene除了可以對建立的索引進行增刪改查之外,還可以讓查詢結果更"用戶體驗友好",內置提供了查詢關鍵詞高亮顯示功能,查詢結果排序功能等。
高亮功能:
// 設置HTML格式化樣式
Formatter formatter = new SimpleHTMLFormatter("<em>", "</em>");
QueryScorer scorer = new QueryScorer(query);
// 創建高亮工具
Highlighter highlighter = new Highlighter(formatter, scorer);
//省略對socre[]循環部分...
//通過分詞器確定需要高亮顯示的關鍵詞
String highlight = highlighter.getBestFragment(new SmartChineseAnalyzer(), "content", "濃眉哥");
//展示效果
System.out.println("高亮展示的關鍵詞:" + highlight);
排序功能:
//老版本的Lucene可以採用這種方式排序
//設置排序字段
Sort sort = new Sort(new SortField("id", Type.LONG,true));
//獲取搜索結果
TopDocs docs = indexSearcher.search(query, 10,sort);
如果你是在新版本下按照這種方式排序會拋下面這種異常:
網上的解決方法是爲需要排序的字段設置Numeric值,確實可以解決:
document.add(new NumericDocValuesField("title",10L));
但又發現了新的問題:
一用排序字段得到的搜索得分就變成NAN了.
效果如下圖:
網上說是BUG,於是我嘗試着把Lucene的版本從7一直升到8,發現這個問題依舊存在,而且網友說是從4開始就有這個問題,考慮到Apache的影響力,我覺得這樣的bug肯定活不過這麼多版本,於是看了下Sort的源碼,最終找到了解決方案,原來是因爲搜索得分算法是比較耗性能的,特別是自定義排序字段後,所以在高版本中,使用自定義排序字段後,搜索得分是默認被關閉的,如果需要計算得分需要手動開啓:
//設置排序字段
Sort sort = new Sort(new SortField("title", Type.LONG,true));
//獲取搜索結果 兩個布爾類型參數,第一個是:doDocScores,第二個是doMaxScore
TopDocs docs = indexSearcher.search(query,10,sort,true,true);
第一個參數doDocScores的意思就是開啓搜索得分. 第二個參數是在搜索過程中,如果搜到了搜索得分最高的那條數據,就自動終止不再繼續搜了,直接返回,以此來提高性能.,對於這個功能背後的算法,感興趣的可以繼續研究這篇博主總結的:
《Lucene之MaxScorer算法簡介》https://blog.csdn.net/wzhg0508/article/details/12953119
遺憾的時,Lucene並沒提供分頁功能,需要我們手動實現,思路與在Mysql下手動分頁查詢差不多,這裏不再贅述。
另外對得分算法感興趣的同學可以參考這篇:
《深入理解Lucene默認打分算法》https://blog.csdn.net/huaishu/article/details/77648377
本人學識淺薄,就不班門弄斧自己總結了,魯迅老爹的拿來主義真香!
9.總結
本篇主要介紹了Lucene的使用場景,原理,以及實操,適合新手入門,如果要喫透Lucene的話沒個幾個月還真搞不定,裏面的源碼和算法都比較厲害,現在市面上有很多基於Lucene二次開發或拓展的搜索引擎,比如solr,elasticsearch等,正是因爲有了Lucene底層強有力的支持,才讓我們今天在互聯網上獲取信息變得如此便捷與輕鬆,最後,感謝你的閱讀,文中若有不正之處歡迎留言斧正。