本篇將剖析全文檢索的原理(基於Lucene),分析它如何建立索引,如何查詢索引,以及它為什麼這麼快!是不是比你快?
全文檢索原理描述
對非結構化資料也即對全文資料的搜尋主要有兩種方法:
一種是順序掃描法(Serial Scanning):所謂順序掃描,比如要找內容包含某一個字串的檔案,就是一個文件一個文件的看,對於每一個文件,從頭看到尾,如果此文件包含此字串,則此文件為我們要找的檔案,接著看下一個檔案,直到掃描完所有的檔案。
如利用windows的搜尋也可以搜尋檔案內容,只是相當的慢。如果你有一個80G硬碟,如果想在上面找到一個內容包含某字串的檔案,不花他幾個小時,怕是做不到。
Linux下的grep命令也是這一種方式。大家可能覺得這種方法比較原始,但對於小資料量的檔案,這種方法還是最直接,最方便的。但是對於大量的檔案,這種方法就很慢了。
有人可能會說,對非結構化資料順序掃描很慢,對結構化資料的搜尋卻相對較快(由於結構化資料有一定的結構可以採取一定的搜尋演算法加快速度),那麼把我們的非結構化資料想辦法弄得有一定結構不就行了嗎?
這種想法很天然,卻構成了全文檢索的基本思路,也即將非結構化資料中的一部分資訊提取出來,重新組織,使其變得有一定結構,然後對此有一定結構的資料進行搜尋,從而達到搜尋相對較快的目的。
這部分從非結構化資料中提取出的然後重新組織的資訊,我們稱之索引。
這種說法比較抽象,舉幾個例子就很容易明白,比如字典,字典的拼音表和部首檢字表就相當於字典的索引,對每一個字的解釋是非結構化的,如果字典沒有音節表和部首檢字表,在茫茫辭海中找一個字只能順序掃描。然而字的某些資訊可以提取出來進行結構化處理,比如讀音,就比較結構化,分聲母和韻母,分別只有幾種可以一一列舉,於是將讀音拿出來按一定的順序排列,每一項讀音都指向此字的詳細解釋的頁數。我們搜尋時按結構化的拼音搜到讀音,然後按其指向的頁數,便可找到我們的非結構化資料——也即對字的解釋。
這種先建立索引,再對索引進行搜尋的過程就叫全文檢索(Full-text Search)。
全文檢索大體分兩個過程,索引建立(Indexing)和搜尋索引(Search)。
索引建立:將現實世界中所有的結構化和非結構化資料提取資訊,建立索引的過程。
搜尋索引:就是得到使用者的查詢請求,搜尋建立的索引,然後返回結果的過程。
於是全文檢索就存在三個重要問題:
-
索引裡面究竟存些什麼?(Index)
-
如何建立索引?(Indexing)
-
如何對索引進行搜尋?(Search)
下面我們順序對每個問題進行研究。
索引裡面有什麼?
索引裡面究竟需要存些什麼呢?
首先我們來看為什麼順序掃描的速度慢: 其實是由於我們想要搜尋的資訊和非結構化資料中所儲存的資訊不一致造成的。 非結構化資料中所儲存的資訊是每個檔案包含哪些字串,也即已知檔案,欲求字串相對容易,也即是從檔案到字串的對映。而我們想搜尋的資訊是哪些檔案包含此字串,也即已知字串,欲求檔案,也即從字串到檔案的對映。兩者恰恰相反。於是如果索引總能夠儲存從字串到檔案的對映,則會大大提高搜尋速度。 由於從字串到檔案的對映是檔案到字串對映的反向過程,於是儲存這種資訊的索引稱為反向索引。
反向索引的所儲存的資訊一般如下: 假設我的文件集合裡面有100篇文件,為了方便表示,我們為文件編號從1到100,得到下面的結構:
左邊儲存的是一系列字串,稱為詞典。
每個字串都指向包含此字串的文件(Document)連結串列,此文件連結串列稱為倒排表(Posting List)。 有了索引,便使儲存的資訊和要搜尋的資訊一致,可以大大加快搜尋的速度。
比如說,我們要尋找既包含字串“lucene”又包含字串“solr”的文件,我們只需要以下幾步:
- 取出包含字串“lucene”的文件連結串列。
- 取出包含字串“solr”的文件連結串列。
- 通過合併連結串列,找出既包含“lucene”又包含“solr”的檔案。
看到這個地方,有人可能會說,全文檢索的確加快了搜尋的速度,但是多了索引的過程,兩者加起來不一定比順序掃描快多少。的確,加上索引的過程,全文檢索不一定比順序掃描快,尤其是在資料量小的時候更是如此。而對一個很大量的資料建立索引也是一個很慢的過程。
然而兩者還是有區別的,順序掃描是每次都要掃描,而建立索引的過程僅僅需要一次,以後便是一勞永逸的了,每次搜尋,建立索引的過程不必經過,僅僅搜尋建立好的索引就可以了。
這也是全文搜尋相對於順序掃描的優勢之一:一次索引,多次使用。
如何建立索引?
全文檢索的索引建立過程一般有以下幾步:
第一步:一些要索引的原文件(Document)。
為了方便說明索引建立過程,這裡特意用兩個檔案為例:
檔案一:Students should be allowed to go out with their friends, but not allowed to drink beer.
檔案二:My friend Jerry went to school to see his students but found them drunk which is not allowed.
複製程式碼
第二步:將原文件傳給分次元件(Tokenizer)。
分片語件(Tokenizer)會做以下幾件事情(此過程稱為Tokenize):
-
將文件分成一個一個單獨的單詞。
-
去除標點符號。
-
去除停詞(Stop word)。
所謂停詞(Stop word)就是一種語言中最普通的一些單詞,由於沒有特別的意義,因而大多數情況下不能成為搜尋的關鍵詞,因而建立索引時,這種詞會被去掉而減少索引的大小。
英語中停詞(Stop word)如:“the”,“a”,“this”等。
對於每一種語言的分片語件(Tokenizer),都有一個停詞(stop word)集合。
經過分詞(Tokenizer)後得到的結果稱為詞元(Token)。
在我們的例子中,便得到以下詞元(Token):
“Students”,“allowed”,“go”,“their”,“friends”,“allowed”,“drink”,“beer”,“My”,“friend”,“Jerry”,“went”,“school”,“see”,“his”,“students”,“found”,“them”,“drunk”,“allowed”。
複製程式碼
第三步:將得到的詞元(Token)傳給語言處理元件(Linguistic Processor)。
語言處理元件(linguistic processor)主要是對得到的詞元(Token)做一些同語言相關的處理。
對於英語,語言處理元件(Linguistic Processor)一般做以下幾點:
-
變為小寫(Lowercase)。
-
將單詞縮減為詞根形式,如“cars”到“car”等。這種操作稱為:stemming。
-
將單詞轉變為詞根形式,如“drove”到“drive”等。這種操作稱為:lemmatization。
Stemming 和 lemmatization的異同: 相同之處:Stemming和lemmatization都要使詞彙成為詞根形式。 兩者的方式不同: Stemming採用的是“縮減”的方式:“cars”到“car”,“driving”到“drive”。 Lemmatization採用的是“轉變”的方式:“drove”到“drove”,“driving”到“drive”。 兩者的演算法不同: Stemming主要是採取某種固定的演算法來做這種縮減,如去除“s”,去除“ing”加“e”,將“ational”變為“ate”,將“tional”變為“tion”。 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也能被搜尋出來。
第四步:將得到的詞(Term)傳給索引元件(Indexer)。
索引元件(Indexer)主要做以下幾件事情:
-
利用得到的詞(Term)建立一個字典。
在我們的例子中字典如下: 複製程式碼
-
對字典按字母順序進行排序。
3.合併相同的詞(Term)成為文件倒排(Posting List)連結串列。
在此表中,有幾個定義:
Document Frequency 即文件頻次,表示總共有多少檔案包含此詞(Term)。
Frequency 即詞頻率,表示此檔案中包含了幾個此詞(Term)。
所以對詞(Term) “allow”來講,總共有兩篇文件包含此詞(Term),從而詞(Term)後面的文件連結串列總共有兩項,第一項表示包含“allow”的第一篇文件,即1號文件,此文件中,“allow”出現了2次,第二項表示包含“allow”的第二個文件,是2號文件,此文件中,“allow”出現了1次。
到此為止,索引已經建立好了,我們可以通過它很快的找到我們想要的文件。
而且在此過程中,我們驚喜地發現,搜尋“drive”,“driving”,“drove”,“driven”也能夠被搜到。因為在我們的索引中,“driving”,“drove”,“driven”都會經過語言處理而變成“drive”,在搜尋時,如果您輸入“driving”,輸入的查詢語句同樣經過我們這裡的一到三步,從而變為查詢“drive”,從而可以搜尋到想要的文件。
如何對索引進行搜尋?
到這裡似乎我們可以宣佈“我們找到想要的文件了”。
然而事情並沒有結束,找到了僅僅是全文檢索的一個方面。不是嗎?
如果僅僅只有一個或十個文件包含我們查詢的字串,我們的確找到了。然而如果結果有一千個,甚至成千上萬個呢?那個又是您最想要的檔案呢?
第一步:使用者輸入查詢語句。
查詢語句同我們普通的語言一樣,也是有一定語法的。
舉個例子,使用者輸入語句:lucene AND learned NOT hadoop。
說明使用者想找一個包含lucene和learned然而不包括hadoop的文件。
複製程式碼
第二步:對查詢語句進行詞法分析,語法分析,及語言處理。
由於查詢語句有語法,因而也要進行語法分析,語法分析及語言處理。
第三步:搜尋索引,得到符合語法樹的文件。
此步驟有分幾小步:
1.首先,在反向索引表中,分別找出包含lucene,learn,hadoop的文件連結串列。
2.其次,對包含lucene,learn的連結串列進行合併操作,得到既包含lucene又包含learn的文件連結串列。
3.然後,將此連結串列與hadoop的文件連結串列進行差操作,去除包含hadoop的文件,從而得到既包含lucene又包含learn而且不包含hadoop的文件連結串列。
4.此文件連結串列就是我們要找的文件。
第四步:根據得到的文件和查詢語句的相關性,對結果進行排序。
雖然在上一步,我們得到了想要的文件,然而對於查詢結果應該按照與查詢語句的相關性進行排序,越相關者越靠前。
總結
1. 索引過程:1) 有一系列被索引檔案。
2) 被索引檔案經過語法分析和語言處理形成一系列詞(Term)。
3) 經過索引建立形成詞典和反向索引表。
4) 通過索引儲存將索引寫入硬碟。
2. 搜尋過程:
a) 使用者輸入查詢語句。
b) 對查詢語句經過語法分析和語言分析得到一系列詞(Term)。
c) 通過語法分析得到一個查詢樹。
d) 通過索引儲存將索引讀入到記憶體。
e) 利用查詢樹搜尋索引,從而得到每個詞(Term)的文件連結串列,對文件連結串列進行交,差,並得到結果文件。
f) 將搜尋到的結果文件對查詢的相關性進行排序。
g) 返回查詢結果給使用者。
下一篇將介紹solr與elasticsearch選型。