Lucene構建我的搜索引擎解析

Lucene是什麼?

Lucene是apache軟件基金會4 jakarta項目組的一個子項目,是一個 開放源代碼的全文檢索引擎工具包,但它不是一個完整的全文檢索引擎,而是一個全文檢索引擎的架構,提供了完整的查詢引擎和索引引擎,部分 文本分析引擎(英文與德文兩種西方語言)。Lucene的目的是爲軟件開發人員提供一個簡單易用的工具包,以方便的在目標系統中實現全文檢索的功能,或者是以此爲基礎創建起完整的全文檢索引擎。 Lucene是一套用於 全文檢索和搜尋的開源程式庫,由 Apache軟件基金會支持和提供。Lucene提供了一個簡單卻強大的應用程式接口,可以作全文索引和搜尋。在Java開發環境裏Lucene是一個成熟的免費 開源工具。就其自己而言,Lucene是當前以及最近幾年最受歡迎的免費Java信息檢索程序庫。人們常常提到信息檢索程序庫,雖然與搜索引擎有關,但不該該將信息檢索程序庫與 搜索引擎相混淆。

簡單來講,Lucene提供了一套完整的工具來幫助開發者構建本身的搜索引擎,開發者只須要import Lucene對應的package便可快速地開發構建本身的業務搜索引擎。php

Lucene中的基本概念:

  • 索引(Index):文檔的集合組成索引。和通常的數據庫不同,Lucene不支持定義主鍵,但Solr支持。
  • 爲了方便索引大量的文檔,Lucene中的一個索引分紅若干個子索引,叫作段(segment)。段中包含了一些可搜索的文檔。
  • 文檔(Document):表明索引庫中的一條記錄。一個文檔能夠包含多個列(Field)。和通常的數據庫不同,一個文檔的一個列能夠有多個值。例如一篇文檔既能夠屬於互聯網類,又能夠屬於科技類。
  • 列(Field):命名的詞的集合。
  • 詞(Term) :由兩個值定義——詞語和這個詞語所出現的列。
  • 倒排索引是基於詞(Term)的搜索。

關於倒排索引

要學習搜索引擎,就須要瞭解倒排索引,要更加深入地理解倒排索引,就要先了解什麼是正排索引(表)。java

正排索引(正向索引)

正排表是以文檔的ID爲關鍵字,表中記錄文檔中每一個字的位置信息,查找時掃描表中每一個文檔中字的信息直到找出全部包含查詢關鍵字的文檔。

正排表結構如圖1所示,這種組織方法在創建索引的時候結構比較簡單,創建比較方便且易於維護;由於索引是基於文檔創建的,如果有新的文檔加入,直接爲該文檔創建一個新的索引塊,掛接在原來索引文件的後面。如果有文檔刪除,則直接找到該文檔號文檔對應的索引信息,將其直接刪除。可是在查詢的時候需對全部的文檔進行掃描以確保沒有遺漏,這樣就使得檢索時間大大延長,檢索效率低下。算法

儘管正排表的工做原理很是的簡單,可是因爲其檢索效率過低,除非在特定狀況下,不然實用性價值不大。數據庫

倒排索引(反向索引)

倒排表以字或詞爲關鍵字進行索引,表中關鍵字所對應的記錄表項記錄了出現這個字或詞的全部文檔,一個表項就是一個字表段,它記錄該文檔的ID和字符在該文檔中出現的位置狀況。

因爲每一個字或詞對應的文檔數量在動態變化,因此倒排表的創建和維護都較爲複雜,可是在查詢的時候因爲能夠一次獲得查詢關鍵字所對應的全部文檔,因此效率高於正排表。在全文檢索中,檢索的快速響應是一個最爲關鍵的性能,而索引創建因爲在後臺進行,儘管效率相對低一些,但不會影響整個搜索引擎的效率。apache

搜索引擎一般檢索的場景是:給定幾個關鍵詞,找出包含關鍵詞的文檔。怎麼快速找到包含某個關鍵詞的文檔就成爲搜索的關鍵。這裏咱們藉助單詞——文檔矩陣模型,經過這個模型咱們能夠很方便知道某篇文檔包含哪些關鍵詞,某個關鍵詞被哪些文檔所包含。單詞-文檔矩陣的具體數據結構能夠是倒排索引、簽名文件、後綴樹等。數組

倒排索引源於實際應用中須要根據屬性的值來查找記錄,lucene是基於倒排索引實現的。這種索引表中的每一項都包括一個屬性值和具備該屬性值的各記錄的地址。因爲不是由記錄來肯定屬性值,而是由屬性值來肯定記錄的位置,於是稱爲倒排索引(inverted index)。帶有倒排索引的文件咱們稱爲倒排索引文件,簡稱倒排文件(inverted file)。網絡

倒排索引通常表示爲一個關鍵詞,而後是它的頻度(出現的次數),位置(出如今哪一篇文章或網頁中,及有關的日期,做者等信息),它至關於爲互聯網上幾千億頁網頁作了一個索引,比如一本書的目錄、標籤通常。讀者想看哪個主題相關的章節,直接根據目錄便可找到相關的頁面。沒必要再從書的第一頁到最後一頁,一頁一頁的查找。數據結構

依據上面的原理,假設有這麼兩篇文檔:架構

  • 文檔1: When in Rome, do as the Romans do.
  • 文檔2: When do you come back from Rome?

停用詞: in, as, the, from。(在信息檢索中,爲節省存儲空間和提升搜索效率,在處理天然語言數據(或文本)以前或以後會自動過濾掉某些字或詞,這些字或詞即被稱爲Stop Words(停用詞)。)app

把這兩篇文檔拆解轉化爲倒排索引以下:

如今檢索的時候就能夠利用倒排索引的優點大大提升效率:假如查詢back這個單詞,經過上面的倒排索引,能夠直接定位到它出如今文檔2中,且出現了1次(頻率),出現的位置是文檔的第5個單詞,一目瞭然,相較於正排索引,也便是以文檔爲基本查詢單位的結構,倒排索引可以更快地定位到keyword的所在,極大提升檢索響應速度。

Lucene的索引檢索流程

首先把信息創建索引庫(原始信息通常由網絡爬蟲得到),經過Lucene的IndexWriter寫入倒排索引創建索引庫,當有query請求時,經過IndexSearcher解析、匹配,從索引庫得到結果返回並排序。

1.索引

索引相關類

  • 一個Document表明索引庫中的一條記錄。一個Document能夠包含多個列。例如一篇文章能夠包含「標題」、「正文」、「修改時間」等field,建立這些列對象之後,能夠經過Document的add方法增長這些列到Document實例。
  • 一段有意義的文字經過Analyzer分割成一個個的詞語後寫入到索引庫。

建立索引

//建立新的索引庫
IndexWriter index = new IndexWriter(indexDirectory,//索引庫存放的路徑
              new StandardAnalyzer(Version.LUCENE_CURRENT),
              true,//新建索引庫
              IndexWriter.MaxFieldLength.UNLIMITED);//不限制列的長度

File dir = new File(sourceDir);
indexDir(dir); //索引sourceDir路徑下的文件
index.optimize();//索引優化
index.close();//關閉索引庫

向索引增長文檔

一個索引和一個數據庫表相似,可是數據庫中是先定義表結構後使用。但Lucene在放數據的時候定義字段結構。

Document doc = new Document();
//建立網址列
Field f = new Field(「url」, news.URL , //news.URL 存放url地址的值
                Field.Store.YES, Field.Index. NOT_ANALYZED,//不分詞
                Field.TermVector.NO);
doc.add(f);
//建立標題列
f = new Field(「title」, news.title , //news.title 存放標題的值
                Field.Store.YES, Field.Index.ANALYZED,//分詞
                Field.TermVector.WITH_POSITIONS_OFFSETS);//存Token位置信息
doc.add(f);
//建立內容列
f = new Field(「body」, news.body , //news.body 存放內容列的值
                Field.Store.YES, Field.Index. ANALYZED, //分詞
                Field.TermVector.WITH_POSITIONS_OFFSETS); //存Token位置信息
doc.add(f);
index.addDocument(doc); //把一個文檔加入索引

2.檢索

查詢語法

  • 加權: "dog^4 cat",^表示加權
  • 修飾符: + - NOT, 例如, "+dog cat"
  • 布爾操做符: OR AND, 例如, "(dog OR cat) AND mankind"
  • 按域查詢: title:apple, 一個字段名後跟冒號,再加上要搜索的詞語或者短句,就能夠把搜索條件限制在該字段。

QueryParser

  • QueryParser將輸入查詢字串解析爲Lucene Query對象。
  • QueryParser是使用JavaCC(Java Compiler Compiler )工具生成的詞法解析器。
  • QueryParser.jj中定義了查詢語法。

分析器(Analyzer)

全文索引是按詞組織的,因此在一長串keyword輸入以後須要對其進行切分,Lucene中把索引中的詞稱爲token,Analyzer會經過內部的Tokenizer把keyword解析成詞序列,也就是token流,以供檢索使用,可使用Filter來過濾最後的查詢結果。Lucene在兩個地方使用到Analyzer:索引文檔的時候和按keyword檢索文檔的時候。索引文檔的時候Analyzer解析出的token(詞)即爲倒排表中的詞。

// 分析公司名的流程
Analyzer analyzer = new CompanyAnalyzer(); 
TokenStream ts = analyzer.tokenStream("title", new StringReader("北京xxx科技發展有限公司"));
while (ts.incrementToken()) {
    System.out.println("token: "+ts));
}

搜索

IndexSearcher isearcher = new IndexSearcher(directory,//索引路徑
true); //只讀
//搜索標題列
QueryParser parser = new QueryParser(Version.LUCENE_CURRENT,"title", analyzer);
Query query = parser.parse(「NBA」); //搜索NBA這個詞
//返回前1000條搜索結果
ScoreDoc[] hits = isearcher.search(query, 1000).scoreDocs;
//遍歷結果
for (int i = 0; i < hits.length; i++) {
  Document hitDoc = isearcher.doc(hits[i].doc);
  System.out.println(hitDoc.get("title"));
}
isearcher.close();
directory.close();

經常使用的查詢類型:

1. 最基本的詞條查詢-TermQuery: 通常用於查詢不切分的字段或者基本詞,即全匹配。

IndexSearcher isearcher = new IndexSearcher(directory, true);
//查詢url地址列
Termterm = new Term("url","http://www.lietu.com");
TermQuery query = new TermQuery(term);
//返回前1000條結果
ScoreDoc[] hits = isearcher.search(query, 1000).scoreDocs;

2. 布爾邏輯查詢-BooleanQuery: 同時查詢標題列和內容列。

QueryParser parser = new QueryParser(Version.LUCENE_CURRENT, "body", analyzer);
QuerybodyQuery =  parser.parse("NBA");//查詢內容列
parser = new QueryParser(Version.LUCENE_CURRENT, "title", analyzer);
QuerytitleQuery = parser.parse("NBA");//查詢標題列
BooleanQuery bodyOrTitleQuery = new BooleanQuery();
//用OR條件合併兩個查詢
bodyOrTitleQuery.add(bodyQuery, BooleanClause.Occur.SHOULD);
bodyOrTitleQuery.add(titleQuery, BooleanClause.Occur.SHOULD);
//返回前1000條結果
ScoreDoc[] hits = isearcher.search(bodyOrTitleQuery, 1000).scoreDocs;

布爾查詢的實現過程以下:

3. RangeQuery-區間查找: 例如日期列time按區間查詢的語法, time:[2007-08-13T00:00:00Z TO 2008-08-13T00:00:00Z]

後臺實現代碼:

ConstantScoreRangeQuery dateQuery = new ConstantScoreRangeQuery("time", t1, t2, true,
true);

舊版本區間查詢的問題

RangeQuery採用擴展成TermQuery來實現,若是查詢區間範圍太大,RangeQuery會致使TooManyClausesException ConstantScoreRangeQuery 內部採用Filter來實現,當索引很大的時候,查詢速度會很慢

Trie結構實現的區間查詢

在Lucene2.9之後的版本中,用Trie結構索引日期和數字等類型。例如:把521這個整數索引成爲:百位是五、十位是5二、個位是521。這樣重複索引的好處是能夠用最低的精度搜索匹配區域的中心地帶,用較高的精度匹配邊界。這樣減小了要搜索的Term數量。

Trie結構區間查詢

‍例如:TrieRange:[423 TO 642] 分解爲5個子條件來執行: handreds:5 OR tens:[43 TO 49] OR ones:[423 TO 429] OR tens:[60 TO 63] OR ones:[640 TO 642]‍

使用Trie結構實現的區間查詢

  • 索引時,增長一個浮點數列到索引:

    document.add(new NumericField("weight").setFloatValue(value));

  • 搜索時,使用NumericRangeQuery來查詢這樣的數字列。例如:

    Query q = NumericRangeQuery.newFloatRange(「weight」, new Float(0.3f), new Float(0.10f), true, true);

    weight:列名稱,new Float(0.3f):最小值從它開始,new Float(0.10f):最大值到它結束,true:是否包含最小/大值。

用壓縮來改進搜索性能

壓縮的原理

由於存在冗餘,因此能夠壓縮。壓縮的原理:使用預測編碼,對先後類似的內容壓縮。 壓縮的對象

  • 字符串數組(Term List)
  • 整數數組(DocId)

字符串數組排序後使用前綴壓縮,整數數組排序後使用差分編碼壓縮 。壓縮算法的兩個過程:編碼(壓縮)過程和解碼(解壓縮)過程。編碼過程能夠時間稍長,解碼過程須要速度快。相似ADSL上網機制:下載速度快,而上傳速度慢。由於在索引數據階段執行編碼過程,而在搜索階段執行解碼過程。索引數據速度能夠稍慢,可是搜索速度不能慢。

前綴編碼(Front Encoding)

由於索引詞是排序後寫入索引的,因此先後兩個索引詞詞形差異每每不大。前綴壓縮算法省略存儲相鄰兩個單詞的共同前綴。每一個詞的存儲格式是: <相同前綴的字符長度,不一樣的字符長度,不一樣的字符>。

例如:順序存儲以下三個詞:term、termagancy、termagant。不用壓縮算法的存儲方式是<詞長,詞>,例如: <4,term> <10,termagancy> <9,termagant>;應用前綴壓縮算法後,實際存儲的內容以下: <4,term> <4,6, agancy> <8,1,t>。

差分編碼(Differential Encoding)

變長壓縮算法對於較小的數字有較好的壓縮比。差分編碼能夠把數組中較大的數值用較小的數來表示,因此能夠和變長壓縮算法聯合使用來實現數組的壓縮。

編碼過程

解碼過程


例如,排好序的DocId序列:
編碼前:345, 777, 11437, …
編碼後:345, 432, 10660, …

變長壓縮(Variable byte encoding)

VInt是一個變長的正整數表示格式,是一種整數的壓縮格式表示方法。每字節分紅兩部分:最高位和低7位。最高位代表是否有更多的字節在後面,0表示這個字節是尾字節,1表示還有後續字節,低7位表示數值。按以下的規則編碼正整數x:

  • if (x < 128),則使用一個字節(最高位置0,低7位表示數值);
  • if (x< 128*128),則使用2個字節(第一個字節最高位置1,低7位表示低位數值,第二個字節最高位置0 ,低7位表示高位數值);
  • if (x<128^3),則使用3個字節,以此類推,把VInt當作是128進制的表示方法,低位優先,隨着數值的增大,向後面的字節進位。

Lucene源碼結構

這是Lucene的用法和原理,構建本身的搜索引擎可使用Lucene這個強大的工具包,將大大縮減開發週期,實現一個高性能的業務搜索引擎。

參考

[1] Michael McCandless, Erik Hatcher, Otis Gospodnetic 著;Lucene in Action(Second Edition);電子工業出版社,2011

[2] (美)W Bruce Croft 著,劉挺 秦兵 譯;搜索引擎:信息檢索實踐 暢銷書籍 科技 正版搜索引擎 信息檢索實踐;機械工業出版社,2010

[3] (日)山田浩之 (日)末永匡 著,胡屹 譯;自制搜索引擎;人民郵電出版社,2016

相關文章
相關標籤/搜索