Lucene 是一個高效的,基于Java 的全文檢索庫。
說起查找,我們首先想起的就是順序查找,比如我們有10個文檔,要查找含有l(wèi)ucene單詞,我們會依次去遍歷所有的文檔進行查找,直到找到含有這個單詞的文檔。 這就是一種是順序掃描法。
小數據量下這種還是很方便,但是大數據量下就很麻煩。
對于大數據下的結構數據,我們可以創(chuàng)建索引(數據庫中采用B+樹),那么非結構數據進行結構化,然后創(chuàng)建索引就是我們的快速查找的思路。
全文檢索大體分兩個過程,索引創(chuàng)建 (Indexing) 和搜索索引 (Search) 。
索引創(chuàng)建:將現實世界中所有的結構化和非結構化數據提取信息,創(chuàng)建索引的過程。
搜索索引:就是得到用戶的查詢請求,搜索創(chuàng)建的索引,然后返回結果的過程。
創(chuàng)建索引的流程
1. 將要索引的文檔/數據庫/字符串導入到lucene中
//創(chuàng)建文檔1
Document document = new Document();
//向文檔中添加域
document.add(new TextField(FIELD,"Students should be allowed to go out with their friends, but not allowed to drink beer.", Field.Store.YES));
//創(chuàng)建文檔2
Document document1 = new Document();
//向文檔中添加域
document1.add(new TextField(FIELD,"My friend Jerry went to school to see his students but found them drunk which is not allowed.", Field.Store.YES));
//將文檔添加到本地索引中
indexWriter.addDocument(document);
indexWriter.addDocument(document1);
2. 將原文檔傳給分詞器(Tokenizer)
當然這里傳給分詞器的操作在addDocument后會自行調用,因為這里選擇的是TextField,默認是告訴lucene要進行分詞,如果為StoredField則不會進行分詞操作。
對文檔的分解操作都是IndexChain的工作。
// 構建索引
//注意這里一定要進行配置分詞器,默認的分詞器是按照英文設置的,這里采用的版本號默認是8.0.0
IndexWriterConfig config = new IndexWriterConfig(new StandardAnalyzer());
IndexWriter indexWriter = new IndexWriter(dirLucene,config);
......
//將文檔添加到本地索引中
indexWriter.addDocument(document);
indexWriter.addDocument(document1);
分詞器(Tokenizer)會做以下幾件事情( 此過程稱為Tokenize) :
將文檔分成一個一個單獨的單詞。
去除標點符號。
去除停詞(Stop word) 。
停詞(Stop word)就是一種語言中最普通的一些單詞,由于沒有特別的意義,因而大多數情況下不能成為搜索的關鍵詞,因而創(chuàng)建索引時,這種詞會被去掉而減少索引的大小。英語中停詞(Stop word)如:“the”,“a”,“this”等。
經過分詞(Tokenizer) 后得到的結果稱為詞元(Token) 。
在我們的例子中,便得到以下詞元(Token):
“Students”,“allowed”,“go”,“their”,“friends”,“allowed”,“drink”,“beer”,“My”,“friend”,“Jerry”,“went”,“school”,“see”,“his”,“students”,“found”,“them”,“drunk”,“allowed”。
注:這里的代碼細節(jié)是由IndexChain 完成的,IndexChain的起點是 DocFieldProcessor,它會分別調用 DocInverter類(倒排信息處理)和 TowStoredFieldsConsumer類(正向信息處理)。
3. 將得到的詞元(Token)傳給語言處理組件(Linguistic Processor)。
語言處理組件(linguistic processor)主要是對得到的詞元(Token)做一些同語言相關的處理。
對于英語,語言處理組件(Linguistic Processor) 一般做以下幾點:
變?yōu)樾?Lowercase) 。
將單詞縮減為詞根形式,如“cars ”到“car ”等。這種操作稱為:stemming 。
將單詞轉變?yōu)樵~根形式,如“drove ”到“drive ”等。這種操作稱為:lemmatization 。
Stemming 和 lemmatization的異同:
相同之處:Stemming和lemmatization都要使詞匯成為詞根形式。
兩者的方式不同:
Stemming采用的是“縮減”的方式:“cars”到“car”,“driving”到“drive”。
Lemmatization采用的是“轉變”的方式:“drove”到“drove”,“driving”到“drive”。
兩者的算法不同:
Stemming主要是采取某種固定的算法來做這種縮減,如去除“s”,去除“ing”加“e”,將“ational”變?yōu)椤癮te”,將“tional”變?yōu)椤皌ion”。
Lemmatization主要是采用保存某種字典的方式做這種轉變。比如字典中有“driving”到“drive”,“drove”到“drive”,“am, is, are”到“be”的映射,做轉變時,只要查字典就可以了。
Stemming和lemmatization不是互斥關系,是有交集的,有的詞利用這兩種方式都能達到相同的轉換。
語言處理組件(linguistic processor)的結果稱為詞(Term) 。
在我們的例子中,經過語言處理,得到的詞(Term)如下:
“student”,“allow”,“go”,“their”,“friend”,“allow”,“drink”,“beer”,“my”,“friend”,“jerry”,“go”,“school”,“see”,“his”,“student”,“find”,“them”,“drink”,“allow”。
也正是因為有語言處理的步驟,才能使搜索drove,而drive也能被搜索出來。
4. 將得到的詞(Term)傳給索引組件(Indexer)。
利用得到的詞(Term)創(chuàng)建一個字典。
| Term | Document ID |
|---|---|
| student | 1 |
| allow | 1 |
| go | 1 |
| their | 1 |
| friend | 1 |
| allow | 1 |
| drink | 1 |
| beer | 1 |
| my | 2 |
| friend | 2 |
| jerry | 2 |
| go | 2 |
| school | 2 |
| see | 2 |
| his | 2 |
| student | 2 |
| find | 2 |
| them | 2 |
| drink | 2 |
| allow | 2 |
然后將字典進行排序,相同的字典詞進行合并,如go 存在兩個。構建成鏈表

在詞表中還保存了其他的定義信息:
Document Frequency 即文檔頻次,表示總共有多少文件包含此詞(Term)。
Frequency 即詞頻率,表示此文件中包含了幾個此詞(Term)。
所以對詞(Term) “allow”來講,總共有兩篇文檔包含此詞(Term),從而詞(Term)后面的文檔鏈表總共有兩項,第一項表示包含“allow”的第一篇文檔,即1號文檔,此文檔中,“allow”出現了2次,第二項表示包含“allow”的第二個文檔,是2號文檔,此文檔中,“allow”出。
最終倒排索引構成完畢,我們發(fā)現搜索“drive”,“driving”,“drove”,“driven”也能夠被搜到。因為在我們的索引中,“driving”,“drove”,“driven”都會經過語言處理而變成“drive”,在搜索時,如果您輸入“driving”,輸入的查詢語句同樣經過我們這里的一到三步,從而變?yōu)椴樵儭癲rive”,從而可以搜索到想要的文檔。
其次是,Lucene 的 IndexWriter 是線程安全的,即它支持多線程索引。每
個 IndexChain 都有一個獨立的索引內存空間,每一個indexChain是互不干擾的各負責各自部分,索引效率很高。
其次是,在寫入內存階段, Lucene 通過 IndexChain 把 document 分解并把相關信息存儲到內存中,等到滿足 flush 條件(內存容量或者文檔個數積累到臨界值),就通過 IndexChain 把內存中的數據flush 到硬盤。
5.lucene索引數據存儲本地
在我們進行 Field.Store.YES 或者 StoredField 則會將索引存儲在本地。
Lucene的索引結構是有層次結構的,主要分以下幾個層次:
索引(Index):
一個目錄一個索引,在Lucene中一個索引是放在一個文件夾中的。
如左圖,同一文件夾中的所有的文件構成一個Lucene索引。
段(Segment):
一個索引可以包含多個段,段與段之間是獨立的,添加新文檔可以生成新的段,不同的段可以合并。
在建立索引的時候對性能影響最大的地方就是在將索引寫入文件的時候, 所以在具體應用的時候就需要對此加以控制,段(Segment) 就是實現這種控制的。稍后詳細描述段(Segment) 的控制策略。
如上圖,具有相同前綴文件的屬同一個段,圖中共兩個段 "_0" 和 "_1"。
segments.gen和segments_5是段的元數據文件,也即它們保存了段的屬性信息。
文檔(Document):
文檔是我們建索引的基本單位,不同的文檔是保存在不同的段中的,一個段可以包含多篇文檔。
新添加的文檔是單獨保存在一個新生成的段中,隨著段的合并,不同的文檔合并到同一個段中。
域(Field):
一篇文檔包含不同類型的信息,可以分開索引,比如標題,時間,正文,作者等,都可以保存在不同的域里。
不同域的索引方式可以不同。
詞(Term):
詞是索引的最小單位,是經過詞法分析和語言處理后的字符串。
Lucene的索引結構中,即保存了正向信息,也保存了反向信息。
所謂正向信息:
按層次保存了從索引,一直到詞的包含關系:索引(Index) –> 段(segment) –> 文檔(Document) –> 域(Field) –> 詞(Term)
也即此索引包含了那些段,每個段包含了那些文檔,每個文檔包含了那些域,每個域包含了那些詞。
既然是層次結構,則每個層次都保存了本層次的信息以及下一層次的元信息,也即屬性信息,比如一本介紹中國地理的書,應該首先介紹中國地理的概況,以及中國包含多少個省,每個省介紹本省的基本概況及包含多少個市,每個市介紹本市的基本概況及包含多少個縣,每個縣具體介紹每個縣的具體情況。
如上圖,包含正向信息的文件有:
segments_N保存了此索引包含多少個段,每個段包含多少篇文檔。
XXX.fnm保存了此段包含了多少個域,每個域的名稱及索引方式。
XXX.fdx,XXX.fdt保存了此段包含的所有文檔,每篇文檔包含了多少域,每個域保存了那些信息。
XXX.tvx,XXX.tvd,XXX.tvf保存了此段包含多少文檔,每篇文檔包含了多少域,每個域包含了多少詞,每個詞的字符串,位置等信息。
所謂反向信息:
保存了詞典到倒排表的映射:詞(Term) –> 文檔(Document)
如上圖,包含反向信息的文件有:
XXX.tis,XXX.tii保存了詞典(Term Dictionary),也即此段包含的所有的詞按字典順序的排序。
XXX.frq保存了倒排表,也即包含每個詞的文檔ID列表。
XXX.prx保存了倒排表中每個詞在包含此詞的文檔中的位置。
段(Segment) 的控制策略
在建立索引的時候對性能影響最大的地方就是在將索引寫入文件的時候, 所以在具體應用的時候就需要對此加以控制:
Lucene默認情況是每加入10份文檔(Document)就從內存往index文件寫入并生成一個段(Segment) ,然后每10個段(Segment)就合并成一個段(Segment). 這些控制的變量如下:
| IndexWriter屬性 | 默認值 | 描述 |
|---|---|---|
| MergeFactory | 10 | 控制segment合并的頻率和大小 |
| MaxMergeDocs | Int32.MaxValue | 限制每個segment中包含的文檔數 |
| MinMergeDocs | 10 | 當內存中的文檔達到多少的時候再寫入segment |
MaxMergeDocs用于控制一個segment文件中最多包含的Document數.比如限制為100的話,即使當前有10個segment也不會合并,因為合并后的segment將包含1000個文檔,超過了限制。
MinMergeDocs用于確定一個當內存中文檔達到多少的時候才寫入文件,該項對segment的數量和大小不會有什么影響,它僅僅影響內存的使用,進一步影響寫索引的效率。
進行搜索的流程
根據我們創(chuàng)建的倒排索引,搜索不就是對構建鏈表的遍歷,合并嗎?
其實并沒有那么簡單,google搜索引擎,當輸入關鍵詞,返回大量結果,那么你最想要的是那個,總不能在一個個找吧。
1. 用戶輸入查詢語句
查詢語句的語法根據全文檢索系統(tǒng)的實現而不同。最基本的有比如:AND, OR, NOT等。
舉個例子,用戶輸入語句:lucene AND learned NOT hadoop。
說明用戶想找一個包含lucene和learned然而不包括hadoop的文檔。
當然這些分析,是基于詞法和語法分析
2. 對查詢語句進行詞法分析,語法分析,及語言處理
詞法分析主要是識別單詞或詞組
語法分析主要是根據查詢語句的語法規(guī)則來形成一棵語法樹
現查詢語句不滿足語法規(guī)則,則會報錯。如lucene NOT AND learned,則會出錯。
- 語言處理同索引過程中的語言處理幾乎相同。
如learned變成learn等。
經過第二步,我們得到一棵經過語言處理的語法樹。
3. 搜索索引,得到符合語法樹的文檔
首先,在反向索引表中,分別找出包含lucene,learn,hadoop的文檔鏈表。
其次,對包含lucene,learn的鏈表進行合并操作,得到既包含lucene又包含learn的文檔鏈表。
然后,將此鏈表與hadoop的文檔鏈表進行差操作,去除包含hadoop的文檔,從而得到既包含lucene又包含learn而且不包含hadoop的文檔鏈表。
4. 根據得到的文檔和查詢語句的相關性,對結果進行排序
對于查詢結果應該按照與查詢語句的相關性進行排序,越相關者越靠前。
如何計算相關性呢?
不如我們把查詢語句看作一片短小的文檔,對文檔與文檔之間的相關性(relevance)進行打分(scoring),分數高的相關性好,就應該排在前面。
那么又怎么對文檔之間的關系進行打分呢?
首先,一個文檔有很多詞(Term)組成 ,如search, lucene, full-text, this, a, what等。
其次對于文檔之間的關系,不同的Term重要性不同 ,比如對于本篇文檔,search, Lucene, full-text就相對重要一些,this, a , what可能相對不重要一些。所以如果兩篇文檔都包含search, Lucene,fulltext,這兩篇文檔的相關性好一些,然而就算一篇文檔包含this, a, what,另一篇文檔不包含this, a, what,也不能影響兩篇文檔的相關性。
因而判斷文檔之間的關系,首先找出哪些詞(Term)對文檔之間的關系最重要,如search, Lucene, fulltext。然后判斷這些詞(Term)之間的關系。
找出詞(Term) 對文檔的重要性的過程稱為計算詞的權重(Term weight) 的過程。
計算詞的權重(term weight)有兩個參數,第一個是詞(Term),第二個是文檔(Document)。
詞的權重(Term weight)表示此詞(Term)在此文檔中的重要程度,越重要的詞(Term)有越大的權重(Term weight),因而在計算文檔之間的相關性中將發(fā)揮更大的作用。
判斷詞(Term) 之間的關系從而得到文檔相關性的過程應用一種叫做向量空間模型的算法(Vector Space Model) 。
1. 計算權重(Term weight)的過程。
影響一個詞(Term)在一篇文檔中的重要性主要有兩個因素:
Term Frequency (tf):即此Term在此文檔中出現了多少次。tf 越大說明越重要。
Document Frequency (df):即有多少文檔包含次Term。df 越大說明越不重要。
例如,然而在一篇英語文檔中,the出現的次數更多,就說明越重要嗎?the并不能代表這篇文檔。
Wt,d = tft,d x log(n/dft)
wt,d 為詞t在文檔d中的重要程度。
2. 判斷Term之間的關系從而得到文檔相關性的過程,也即向量空間模型的算法(VSM)
我們把文檔看作一系列詞(Term),每一個詞(Term)都有一個權重(Term weight),不同的詞(Term)根據自己在文檔中的權重來影響文檔相關性的打分計算。
于是我們把所有此文檔中詞(term)的權重(term weight) 看作一個向量。
Document = {term1, term2, …… ,term N}
Document Vector = {weight1, weight2, …… ,weight N}
同樣我們把查詢語句看作一個簡單的文檔,也用向量來表示。
Query = {term1, term 2, …… , term N}
Query Vector = {weight1, weight2, …… , weight N}
我們把所有搜索出的文檔向量及查詢向量放到一個N維空間中,每個詞(term)是一維。

我們認為兩個向量之間的夾角越小,相關性越大。
所以我們計算夾角的余弦值作為相關性的打分,夾角越小,余弦值越大,打分越高,相關性越大。
VSM是基于詞與詞之間是相互獨立的詞袋模型,N代表的是整個文檔集的詞匯量,其中每一篇文檔都是一個N維向量詞匯表中的每一個詞的 ID 對應著向量中的一個位置,詞的權重為向量位置上的值。如果文檔中沒有該詞,那么該位置上的值為 0。
舉個例子,整個文檔集有11個Term,共有三篇文檔搜索出來。其中各自的權重(Term weight),如下表格。

于是文檔二相關性最高,先返回,其次是文檔一,最后是文檔三。
最后總結下lucene的查詢結果流程:
