Lucene 篇
參考位址:《Lucene介紹與使用》
1. Lucene 簡介
Lucene 是一套用于全文檢索和搜尋的開源程式庫,提供了一個簡單卻強大的 API,能夠做全文索引和搜尋。在 Java 開發環境裡,Lucene 是一個成熟的免費開放源代碼工具,它并不是現成的搜尋引擎産品,但可以用來制作搜尋引擎産品。Solr 和 ElasticSearch 都是基于 Lucene 開發的企業級的搜尋引擎産品。 Lucene 的 API 來實作對索引的增(建立索引)、删(删除索引)、改(修改索引)、查(搜尋資料)。
2. 全文檢索
計算機索引程式通過掃描文章中的每一個詞,對每一個詞建立一個索引,指明該詞在文章中出現的次數和位置。當使用者查詢時,檢索程式就根據實作建立的索引進行查找,并将查找的結果回報給使用者的檢索方式。總結起來,就是 Lucene 全文檢索對文檔中全部内容進行分詞,然後對單詞建立反向索引的過程。
3. 建立索引
與關系資料庫 Mysql 對比,Lucene 資料架構主要概念如下:
MySQL | Lucene |
---|---|
Database | Index |
Table | Type |
Row | Document |
Column | Field |
Schema | Mapping |
Index | Everything is indexed |
SQL | Query DSL |

代碼實作流程如下:
// 建立索引
@Test
public void testCreate() throws Exception{
//1 建立文檔對象
Document document = new Document();
// 建立并添加字段資訊。
// 參數:字段的名稱、字段的值、是否存儲;
// 這裡選Store.YES代表存儲到文檔清單。Store.NO 代表不存儲
document.add(new StringField("id", "1", Field.Store.YES));
// 這裡我們 title 字段需要用 TextField,即建立索引又會被分詞;
// StringField 會建立索引,但是不會被分詞
document.add(new TextField("title", "谷歌地圖之父跳槽facebook",
Field.Store.YES));
//2 索引目錄類,指定索引在硬碟中的位置
Directory directory = FSDirectory.open(new File("d:\\indexDir"));
//3 建立分詞器對象
Analyzer analyzer = new StandardAnalyzer();
//4 索引寫出工具的配置對象
IndexWriterConfig conf = new IndexWriterConfig(Version.LATEST, analyzer);
//5 建立索引的寫出工具類。參數:索引的目錄和配置資訊
IndexWriter indexWriter = new IndexWriter(directory, conf);
//6 把文檔交給IndexWriter
indexWriter.addDocument(document);
//7 送出
indexWriter.commit();
//8 關閉
indexWriter.close();
}
複制
4. 建立索引的 API 詳解
4.1 Document
文檔對象 Document 對應資料庫中的一行,是一條原始的資料;如下圖所示;
4.2 Field
字段類 Field 對應資料庫中的一列,有不同的資料類型。一個 Document 中可以有很多個不同的字段,每一個字段都是一個 Field 類的對象。由于一個 Document 中的字段的類型是不确定的,是以 Field 類就提供了各種不同的子類,來對應這些不同類型的字段。這些子類有一些不同的特性:
- DoubleField、FloatField、IntField、LongField、StringField、TextField:
- 這些子類一定會被建立索引,但是不會被分詞,而且不一定會被存儲到文檔清單。
- 是否存儲要通過構造函數中的參數 Store 來指定:如果Store.YES代表存儲,Store.NO代表不存儲;
- TextField:既建立索引,又會被分詞;
- 注:StringField 會建立索引,但不會被分詞;如果不分詞,會造成整個字段作為一個詞條,除非使用者完全比對,否則搜尋不到:
- StoreField:一定會被存儲,但不一定會建立索引;
- 注:StoredField 可以建立各種基礎資料類型的字段;
注:相關問題:
- 問題1:如何确定一個字段是否需要存儲?
- 如果一個字段要顯示到最終的結果中,那麼一定要存儲,否則就不存儲。
- 問題2:如何确定一個字段是否需要建立索引?
- 如果要根據這個字段進行搜尋,那麼這個字段就必須建立索引。
- 問題3:如何确定一個字段是否需要分詞?
- 前提是這個字段首先要建立索引;
- 然後如果這個字段的值是不可分割的,那麼就不需要分詞。例如:ID;
4.3 Directory
目錄類 Directory 指定索引要存儲的位置。有兩種主要類型:
- FSDirectory:檔案系統目錄,會把索引庫指向本地磁盤;
- 特點:速度略慢,但是整體比較安全;
- RAMDirecotry:記憶體目錄,會把索引庫儲存在記憶體;
- 特點:速度快,但是不安全;
4.4 Analyzer
分詞器類 Analyzer 提供分詞算法,可以把文檔中的資料按照算法分詞。通常官方的分詞器并沒有合适的中文分詞器,是以一般會用到第三方提供的分詞器。比如 IK 分詞器。
IK 分詞器的詞庫有限,新增加的詞條可以通過配置檔案添加到 IK 的詞庫中(即擴充詞典),同時也可以把一些不用的詞條(停止詞典)去除。
4.5 IndexWriterConfig
索引寫出器配置類 IndexWriterConfig,設定 Lucene 的版本與分詞器類型,用來配置索引寫出器。例如:
//3 建立分詞器對象
Analyzer analyzer = new StandardAnalyzer();
//4 索引寫出工具的配置對象
IndexWriterConfig conf = new IndexWriterConfig(Version.LATEST, analyzer);
複制
4.6 IndexWriter
IndexWriter 索引寫出器類,用來實作對索引的增删改,即建立索引、删除索引、修改索引。
5. 查詢索引資料
代碼實作如下:
@Test
public void testSearch() throws Exception {
// 1. 建立索引目錄對象
Directory directory = FSDirectory.open(new File("d:\\indexDir"));
// 2. 建立索引讀取工具
IndexReader reader = DirectoryReader.open(directory);
// 3. 建立索引搜尋工具
IndexSearcher searcher = new IndexSearcher(reader);
// 4. 建立查詢解析器
// 兩個參數:預設要查詢的字段的名稱,分詞器
QueryParser parser = new QueryParser("title", new IKAnalyzer());
// 5. 建立查詢對象
Query query = parser.parse("谷歌");
// 6. 搜尋資料
// 兩個參數:查詢條件對象,以及要查詢的最大結果條數
// 傳回的結果按照比對度排名得分前 N 名的文檔資訊(包含查詢到的總條數資訊、所有符合條件的文檔的編号資訊)。
TopDocs topDocs = searcher.search(query, 10);
// 擷取總條數
System.out.println("本次搜尋共找到" + topDocs.totalHits + "條資料");
// 擷取得分文檔對象(ScoreDoc)數組
// ScoreDoc中包含:文檔的編号、文檔的得分
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);
}
}
複制
5.1 Query
Query 是查詢對象,包含要查詢的關鍵詞資訊;在上面的代碼中,通過 QueryParser 解析關鍵字,得到查詢對象。
5.2 進階查詢
除了使用 QueryParser 解析之外,也可以通過自定義查詢對象(進階查詢),即通過 Query 的子類,直接建立查詢對象,實作進階查詢。實作進階查詢的測試代碼如下:
// 傳入 Query 對象,實作進階查詢
public void search(Query query) throws Exception {
// 1. 建立索引目錄對象
Directory directory = FSDirectory.open(new File("indexDir"));
// 2. 建立索引讀取工具
IndexReader reader = DirectoryReader.open(directory);
// 3. 建立索引搜尋工具
IndexSearcher searcher = new IndexSearcher(reader);
// 4. 搜尋資料
// 兩個參數:查詢條件對象,以及要查詢的最大結果條數
// 傳回的結果是按照比對度排名得分前 N 名的文檔資訊(包含查詢到的總條數資訊、所有符合條件的文檔的編号資訊)。
TopDocs topDocs = searcher.search(query, 10);
// 5. 擷取總條數
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);
}
}
複制
5.2.1 TermQuery
TermQuery 詞條查詢,詞條 Term 是搜尋的最小機關,不可以再被分詞,而且值必須是字元串。
@Test
public void testTermQuery() throws Exception {
// 建立詞條查詢對象
Query query = new TermQuery(new Term("title", "谷歌地圖"));
search(query);
}
複制
5.2.2 WildcardQuery
WildcardQuery 通配符查詢,類似于用資料庫中
like ‘%谷歌%’
的通配符用法。
- ? 字元可以代表任意一個字元;
- * 字元可以代表任意多個任意字元;
@Test
public void testWildCardQuery() throws Exception {
// 建立查詢對象
Query query = new WildcardQuery(new Term("title", "*歌*"));
search(query);
}
複制
5.2.3 FuzzyQuery
FuzzyQuery 模糊查詢, 允許使用者輸錯,但是要求錯誤的最大編輯距離不能超過 2。編輯距離就是一個單詞到另一個單詞最少要修改的次數,比如 facebool --> facebook 需要編輯1次,編輯距離就是1。
@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"),1);
search(query);
}
複制
5.2.4 NumericRangeQuery
數值範圍查詢 NumericRangeQuery 可以對非 String 類型的 ID 進行精确查找。
@Test
public void testNumericRangeQuery() throws Exception{
// 數值範圍查詢對象
// 參數:字段名稱,最小值、最大值、是否包含最小值、是否包含最大值
Query query = NumericRangeQuery.newLongRange("id", 2L, 2L, true, true);
search(query);
}
複制
6. 全文檢索
參考位址:《全文檢索原理及實作方式》
6.1 全文檢索簡介
我們生活中的資料總體分為兩種:結構化資料和非結構化資料。其中結構化資料指具有固定格式或有限長度的資料,如資料庫,中繼資料等。非結構化資料指不定長或無固定格式的資料,如郵件,word 文檔等。
對于非結構化資料(即對全文資料)進行搜尋主要有兩種方法。一是順序掃描,比如要找内容包含某一個字元串的檔案,就是一個文檔一個文檔的看,對于每一個文檔,從頭看到尾,如果此文檔包含此字元串,則此文檔為我們要找的檔案,接着看下一個檔案,直到掃描完所有的檔案。當然這是一種特别慢的搜尋方法。
另外一種方法就是全文檢索。全文檢索的思路類似于資料庫的索引,它将非結構化資料中的一部分資訊提取出來,重新組織,使其變得有一定結構,然後對此有一定結構的資料進行搜尋,進而達到搜尋相對較快的目的。這部分從非結構化資料中提取出的,然後重新組織的資訊,我們稱之索引。
比如字典,字典的拼音表和部首檢字表就相當于字典的索引,對每一個字的解釋是非結構化的,如果字典沒有音節表和部首檢字表,在茫茫辭海中找一個字隻能順序掃描。然而字的某些資訊可以提取出來進行結構化處理,比如讀音,就比較結構化,分聲母和韻母,分别隻有幾種可以一一列舉,于是将讀音拿出來按一定的順序排列,每一項讀音都指向此字的詳細解釋的頁數。我們搜尋時按結構化的拼音搜到讀音,然後按其指向的頁數,便可找到我們的非結構化資料——也即對字的解釋。這種先建立索引,再對索引進行搜尋的過程就叫**全文檢索 (Full-text Search) **。
全文檢索大體分兩個過程,索引建立 (Indexing) 和搜尋索引 (Search)。
- 索引建立:将現實世界中所有的結構化和非結構化資料提取資訊,建立索引的過程。
- 有一系列待索引檔案;
- 被索引檔案經過文法分析和語言處理形成一系列詞 (Term) 。
- 經過索引建立形成詞典和反向索引表。
- 通過索引存儲,将索引寫入硬碟。
- 搜尋索引:得到使用者的查詢請求,搜尋建立的索引,然後傳回結果的過程。
- 使用者輸入查詢語句。
- 對查詢語句經過文法分析和語言分析,得到一系列詞(Term) 。
- 通過文法分析,得到一個查詢樹;
- 通過索引存儲,将索引讀入到記憶體。
- 利用查詢樹搜尋索引,進而得到每個詞 (Term) 的文檔連結清單;根據查詢樹邏輯運算,對文檔連結清單進行交集、差集、非運算,并得到結果文檔。
- 将搜尋到的結果文檔進行查詢的相關性排序。
- 傳回查詢結果給使用者。
6.2 索引建立
非結構化資料中所存儲的資訊是每個檔案包含哪些字元串,也即已知檔案,欲求字元串相對容易,也即是從檔案到字元串的映射。而我們想搜尋的資訊是哪些檔案包含此字元串,即已知字元串,欲求檔案,也就是從字元串到檔案的映射。兩者恰恰相反。于是如果索引總能夠儲存從字元串到檔案的映射,則會大大提高搜尋速度。
6.2.1 索引建立簡述
假設我的檔案集合裡面有100篇文檔,為了友善表示,我們為文檔編号從1到100
左邊儲存的是一系列字元串,稱為詞典;右面表示每個字元串都指向包含此字元串的文檔 (Document) 連結清單,此文檔連結清單稱為倒排表 (Posting List)。
有了索引,便使儲存的資訊和要搜尋的資訊一緻,可以大大加快搜尋的速度。
注:比如說,我們要尋找既包含字元串“lucene”又包含字元串“solr”的文檔,我們隻需要以下幾步:
- 取出包含字元串“lucene”的文檔連結清單。
- 取出包含字元串“solr”的文檔連結清單。
- 通過合并連結清單,找出既包含“lucene”又包含“solr”的檔案。
[外鍊圖檔轉存失敗,源站可能有防盜鍊機制,建議将圖檔儲存下來直接上傳(img-oPMFyyvK-1594915230287)(./pic/全文檢索_并集.jpg)]
順序掃描是每次都要掃描,而建立索引的過程僅僅需要一次,以後便是一勞永逸的了,每次搜尋,建立索引的過程不必經過,僅僅搜尋建立好的索引就可以了。這也是全文搜尋相對于順序掃描的優勢之一:一次索引,多次使用。
6.2.2 索引建立原理
建立原理在文章《全文檢索原理及實作方式》有詳細的說明,這裡隻進行總結。
- 準備一些要索引的原文檔 (Document);例如有文檔:
- 文檔 1:Students should be allowed to go out with their friends, but not allowed to drink beer;
- 文檔 2:My friend Jerry went to school to see his students but found them drunk which is not allowed.
- 文檔分詞:做分詞、去除标點符号、去除無效詞 (a, the, this) 等,獲得詞元;
- 詞元處理:如變為小寫、去除複數、轉為一般現在時等操作;
- 建構索引:将處理後的詞元傳給索引元件,建立得到一個字典。按照字母順序排序後,可以得到每個詞元在每個文檔中出現的頻率。将每個詞資訊合并,并按照頻率倒序排序,可以得到倒排連結清單。
- [外鍊圖檔轉存失敗,源站可能有防盜鍊機制,建議将圖檔儲存下來直接上傳(img-IUIDhhfy-1594915230290)(./pic/全文檢索_倒排表.jpg)]
- 文檔頻次 (Document Frequency):總共有多少檔案包含此詞 (Term)。
- 詞頻率 (Frequency):檔案中包含了幾個此詞 (Term)。
6.3 搜尋索引
問題:如何像 Google 一樣在成千上萬的搜尋結果中,找到和查詢語句最相關的呢?如何判斷搜尋出的文檔和查詢語句的相關性呢?
6.3.1 輸入查詢語句
查詢語句也是有一定文法的,比如最基本的 AND, OR, NOT 等。
6.3.2 查詢語句建構文法樹
- 詞法分析:識别單詞和關鍵字;比如提取查詢語句的 AND, NOT 等;
- 文法分析:形成文法樹;
- 語言處理:同詞元處理;
[外鍊圖檔轉存失敗,源站可能有防盜鍊機制,建議将圖檔儲存下來直接上傳(img-Ss8iDcGY-1594915230292)(./pic/全文檢索_構成文法樹.jpg)]
6.3.3 搜尋索引
按照文法樹,對索引進行搜尋。類似于 6.2.1 的過程。
6.3.4 計算相關性排序
計算文檔和查詢語句的相關性,我們可以把查詢語句看作一片短小的文檔,對文檔與文檔之間的相關性 (relevance) 進行打分 (scoring),分數高的相關性好,排在前面。
文檔由很多詞組成,找出詞對文檔重要性的過程,又稱為計算詞的權重 (Term weight)。影響一個詞在一片文檔中重要性的關鍵因素:
- Term Frequency (tf):某個詞在某篇文檔中出現的次數;TF 值越大,說明該詞越重要;
- 可以了解為:一個詞在某篇文檔中出現的次數很多,說明該文檔就是講這方面的問題的;是以說明這個詞在這篇文章很重要。
- Document Frequency (df):所有文檔中,某個詞在多少文檔中出現過;DF 值越大,說明該詞越不重要;
- 例如:this 在很多文檔中出現,但它并不重要。
判斷 Term 之間關系進而得到文檔相關性的過程,就是向量空間模型算法。該算法把文檔看作一系列詞 (Term),每一個詞 (Term) 都有一個權重 (Term weight)。不同的詞 (Term) 根據自己在文檔中的權重來影響文檔相關性的打分計算。計算方法在前面所述的文檔中可以計算。
比如計算一個共有 11 個詞的查詢語句,共有三篇文檔搜尋出來,首先計算所有詞的權重,然後根據打分公式分别計算查詢語句與三篇文檔的相關性。最後按照相關性進行排序,即可得到最想要的文檔。