布尔检索模型
最近在看《Introduction to Information Retrieval》(中文版為《信息檢索導論》,下文簡稱為“IR”),是最經典的信息檢索書籍之一了。由于淞姐要求我細讀這本書然后跟同事分享,就有了這個版塊,之后會陸續添加后續章節內容。即使是站在巨人的肩膀上了(看了中文版和英文版IR,也從網上搜集了不少內容),但很多細節往往還是需要自己用心體會。從一個讀者到一個講解人,在第一次做分享的時候已經感覺很不容易了,有些東西原來只是一知半解,能自己想清楚但卻很難表述。這些內容就是個人的一些讀書筆記,希望能給剛好想了解搜索引擎的你帶來一些啟發。文中會盡量備注英文原版中的術語以免丟失本意。英文電子書可以從官網獲取 Introduction to Information Retrieval。
線性掃描和倒排索引
從線性掃描講起
如果需要從文檔集合DD中搜索包含某個關鍵詞kk的文檔,最直接的方法就是從頭到尾掃描文檔集DD,對每個文檔didi都查看是否包含關鍵詞kk。這種線性掃描的方式最為直觀易懂,在Unix/Linux系統中的文本掃描命令grep做的就是這種工作。然而,當需要檢索的文檔規模非常大時,這種線性掃描的方式的效率會變得非常低下。線性掃描的時間復雜度與文檔集大小成正比,在大規模文本檢索的場景下,線性掃描不再適用。大型的Web搜索引擎需要檢索千億級別數量的網頁,如果采用類似grep的線性掃描方式,就需要依次掃描這么多的文本來判斷每一個網頁是否符合查詢要求,這樣的檢索慢如蝸牛,用戶顯然無法接受。目前,搜索引擎通過事先給文檔建立索引(index)的方法來避免這種線性掃描,使得搜索過程非常快速,這種技術稱為倒排索引。
IR中,以《莎士比亞全集》為例子來說明倒排索引的基本知識。這里筆者就重新舉個例子吧。假設某文檔集中存在這樣的三篇文檔(還有其他文檔不列舉),分別是d1d1為“Apple and cat”, d2d2為“I like cat”,d3d3為“I have an apple”,用矩陣表示,當文檔d中存在詞項t時,矩陣元素(d, t)為1,否則為0:
| d1d1 | 1 | 1 | 1 | 0 | 0 | 0 | 0 | ...... |
| d2d2 | 0 | 0 | 1 | 1 | 1 | 0 | 0 | ...... |
| d3d3 | 1 | 0 | 0 | 1 | 0 | 1 | 1 | ...... |
| ...... | ...... | ...... | ...... | ...... | ...... | ...... | ...... | ...... |
倒排索引
使用類似grep命令的線性掃描方法的話,如需查詢cat相關的文檔,需要遍歷所有文檔(把每篇文檔從頭到尾掃描一遍),返回包含cat關鍵詞的文檔集。觀察上面的矩陣表格,查詢包含cat關鍵詞的文檔,其實只需要查看cat的那一列都有哪些元素大于0的文檔就行。利用這種技巧,詞項成為索引的基本單位,如果事先建立了類似上表(表1)的矩陣(或者其他形式的數據結構,如鏈表),那么就可以快速地找到與查詢的關鍵詞相關的文檔。我們把上面的矩陣轉置一下,把詞項(term)作為橫坐標,文檔(document)作為縱坐標,然后詞項按照字母表排序,得到更直觀的矩陣:
| an | 0 | 0 | 1 | ...... |
| and | 1 | 0 | 0 | ...... |
| apple | 1 | 0 | 1 | ...... |
| cat | 1 | 1 | 0 | ...... |
| have | 0 | 0 | 1 | ...... |
| I | 0 | 1 | 1 | ...... |
| like | 0 | 1 | 0 | ...... |
| ...... | ...... | ...... | ...... | ...... |
對于上表(表2-詞項-文檔關聯矩陣),從行來看,可以表示詞項在哪些文檔中出現或者不出現;從列來看,可以得到每個文檔都有哪些詞項或者沒有哪些詞項。
看到這里,倒排索引的概念已經呼之欲出了。不著急,我們先看看如何利用上面的表2來做搜索查詢。布爾檢索模型接受布爾表達式查詢,用戶可以通過使用AND,OR以及NOT等邏輯操作符來構建查詢表達式。假如用戶提交查詢“cat AND NOT apple”(表示尋找帶有cat但不含有apple的文檔),我們從表2分別取出cat和apple對應的行向量,(110)和(101),[更規范的寫法應該是(1,1,0)和(1,0,1),此處簡寫],然后根據查詢做對應的邏輯處理,如下:
(110) AND NOT (101) = (110) AND (010) = (010)
結果向量(010)中第二個元素為1,表明該查詢對應的結果是文檔d2d2。我們來確認一下,文檔d2d2的內容為“I like cat”,確實為查詢所需。
讀者可能奇怪為什么這種基于位(bit)的邏輯操作可以得到正確的結果。我們舉個簡單的例子,如果另外一個用戶提交了查詢“cat AND apple”,我們需要從表2取出cat和apple對應的行向量。讀者觀察一下表2的那些行向量都是什么形式?cat的行向量(110)的第一位的1表示文檔d1d1包含詞項cat,第二位的1表示文檔d2d2包含詞項cat,第三位是0,表示文檔d3d3不包含詞項cat。詞項對應的行向量的第ii位表明這個詞項是否包含于第ii個文檔(didi)中!對于查詢“cat AND apple”,需要返回同時包含cat和apple的文檔。我們取出cat和apple對應的行向量,分別為(110)和(101),根據“一個詞項對應的行向量的第i位表明這個詞項是否包含于第i個文檔(didi)中”,詞項cat和apple的行向量的第i位如果都是1表明didi同時包含cat和apple,否則不同時包含cat和apple。因此cat和apple對應的行向量做AND操作可以獲得同時包含cat和apple的文檔都有哪些。(110) AND (101) = (100),只有文檔d1d1符合查詢要求。我們再看一個例子,對于查詢“NOT apple”,需要返回不包含apple的文檔。我們從表2取出apple對應的行向量(101),我們已經知道對應位為1表示文檔包含這個詞項,那么,現在我們需要查詢不包含這個詞項的文檔,當然就是取反了(NOT操作)。NOT (101) = (010),結果(010)表示第一個和第三個文檔都包含apple,只有第二個文檔d2d2不包含apple。至此,通過這幾個簡單的例子,讀者應該能夠理解這種布爾表達式查詢的工作原理了。
為了向倒排索引的概念平滑過渡,現在我們考慮一個更真實的場景(使用IR中的例子)。假如我們有100萬篇文檔(document),每篇文檔大約1000個詞,那么,通常這些文檔大概會有50萬個不同的詞項。此時,回頭看看表2(詞項-文檔關聯矩陣),這個矩陣的規模會變得非常巨大,會有50萬行(因為有50萬的詞匯量)和100萬列(因為有100萬篇文檔),元素數量為5000億(50萬×100萬),存放這張表需要的內存遠遠大于一臺計算機內存的容量!此外,不難發現,這個50萬×100萬規模的矩陣,大部分元素都是0,可以從某一列來估算,文檔didi平均大約有1000個單詞,但didi那一列卻有50萬個元素(因為全局有50萬的詞匯量)。這么一算,這個詞項-文檔關聯矩陣大約有99.8%的元素都為0。顯然,只記錄原始矩陣中1的位置的話,所需的存儲空間會大大地減少,倒排索引(inverted index)正是起到這樣的作用!
我們觀察表1和表2,表1以文檔為基本考察要素,而表2以詞項為基本考察要素,表2由原始的表1通過inverted(轉置,引申為倒排)而來,這正是倒排這個概念的由來。在倒排索引中,我們只存儲那些不為0的項。倒排索引的基本結構如下圖:
圖1(IR圖1-3):倒排索引基本結構倒排索引帶有一個詞典(dictionary),詞典中的每個詞項(term)都有對應的一個list,保存了這個詞項在哪些文檔中出現過。詞項對應的list中的每一個元素我們稱之為一個倒排記錄(posting),而把一個詞項對應的整個list稱為倒排表(posting list/inverted list)。所有詞項的倒排表一起構成postings。[讀者對于這幾個定義不必太糾結,中文翻譯過來感覺怪怪的]
我們對上圖中的倒排索引做一個基本說明。以圖1中的倒排索引為例,詞項Brutus指向的list稱為Brutus的posting list,posting list中的每一個元素都稱之為posting(如Brutus指向的1,2,4,11,31,45,173,174等,這些posting都是文檔ID,表示Brutus在這些文檔中出現)。之后我們將會看到,更實用的倒排索引的posting除了存儲文檔ID,還會保存詞項在文檔中出現的位置以及詞項在該文檔中出現的頻率,這些都是后話了。
構建倒排索引
為獲得由索引(indexing)帶來的檢索速度的提升,我們需要事先構建索引。
索引構建分為四個步驟:
(1) Collect the documents to be indexed(收集構建索引的文檔集,也就是我們檢索系統需要檢索的所有文檔)
(2) Tokenize the text, turning each document into a list of tokens(把文本轉化為token,就是把文本分割成一個一個的詞,中文版的信息檢索導論中將token譯為詞條)
(3) Do linguistic preprocessing, producing a list of normalized tokens, which
are the indexing terms(語言學預處理,利用詞干還原(stemming)和詞形歸并(lemmatization)的方法,將token轉為normalized token,例如將不同時態的單詞轉為其詞根,將單復數名詞統一轉為單數形式,還原token的本來形式來獲得統一的表示[注:更專業的說法叫“歸一化”],這些還原的token就是將要索引的詞項(term))
(4) Index the documents that each term occurs in by creating an inverted index,
consisting of a dictionary and postings(對文檔中的詞項構建倒排索引,最終倒排索引的形式可以參考圖1)
步驟1~3是索引構建的基礎,我們做一個簡要的說明。
步驟(1)中收集文檔的工作,在Web搜索引擎中就是通過網絡爬蟲(web crawler)來搜集分布于Web各處的網頁。搜集的網頁還不能直接用于后面的步驟中,還需要對網頁文本做諸如去除標簽,過濾廣告等處理。
步驟(2)就是tokenization(詞條化),例如,對于英文文本而言,就是根據空格把單詞一個一個地提取出來,把原始文本分割成token。當然了,如果只是這樣簡單的分割的話,會把“New York University”分成三個token了,因此英文也會存在短語劃分問題。中文文本的話涉及的主要方法就是分詞(word
segmentation)了,因為漢語字與字之間沒有空格,并且由于文法的特殊性,詞與詞組的邊界模糊,這讓tokenization的問題更加復雜。中文分詞技術屬于自然語言處理的范疇,有興趣的讀者可以參考相關的專業文獻。
步驟(3)就是將步驟(2)產生的token轉為更加統一規范的詞項(term),例如在文本中可能出現“apple”、“apples”、“Apple”這類token,但我們知道這幾個token都是表達蘋果(apple)的意思,因此,在構建索引的時候通常會把這幾個token統一還原為“apple”,只為“apple”建立索引項,那么“apple”就是一個term(詞項)了。倒排索引里面所有的term組成的集合我們稱為詞典(dictionary,也有稱為詞匯表(vocabulary)的)
現在,假設前三步都已完成,原始的網頁已經被我們分割成一個個的詞項(term)。下面,我們體驗一下構建基本的倒排索引的過程。
給定一個文檔集,給每個文檔分配一個唯一的標識符(docID),通過步驟1~3,我們會得到很多的詞項,用二元組的形式表示為(詞項,文檔ID),表示詞項在文檔ID對應的文檔中出現。布爾檢索只考慮詞項在文檔中有沒有出現,因此,即使一個詞項在某個文檔中多次出現,我們也只記錄一個這樣的(詞項,文檔ID)。建立倒排索引的圖形化表示如下圖2所示:
圖2(IR圖1-4):通過排序和合并構建索引的過程圖2中左邊部分是由原始文檔經過步驟1~3產生的(詞項,文檔ID)二元組;將這些二元組按照詞項的字母順序進行排序,就會把詞項相同的二元組排在一起(圖2中部);最后,把同一詞項的多個二元組合并在一起,這個詞項出現的文檔ID用list存放(也就是posting list),并且根據文檔ID排序。圖中所示的倒排索引還存儲了詞項在文檔中出現的次數(在同一個文檔中多次出現算一次)。最終,我們形成了一個包含許多詞項(term)的字典(dictionary),每個詞項都有一個倒排記錄表(posting list)。
至此,倒排索引已經建立好了,那么,如何利用這個倒排索引做布爾查詢呢?
處理布爾查詢
使用IR中的例子,查詢為“Brutus AND Calpurnia”。我們分別從倒排索引表中取出詞項Brutus和Calpurnia的posting list,然后對這兩個詞項的posting list求交集,即可得到查詢需要輸出的文檔集合。如下圖,
圖3(IR圖1-3):處理布爾查詢時對兩個倒排記錄表求交集然而,求交集這個操作的時間復雜度應該認真考量!一個顯而易見的方法就是在外循環中遍歷其中一個posting list,然后在內循環中對另外一個posting list的元素進行匹配查找。然而,這種求交集的方式需要的比較次數為O(x?y)O(x?y),其中x、y分別是兩個posting list的長度。考慮前面提到過的100萬篇文檔(document),每篇文檔大約1000個詞,這些文檔大概會有50萬個不同的詞項的例子,那么每個posting list的長度大約為2000(100萬×1000÷50萬=2000100萬×1000÷50萬=2000)。那么就需要做大約400萬(2000×2000)次比較才能返回查詢結果。在Web搜索引擎中,文檔集規模比例子要大得多,查詢需要的比較次數就更多了。那么,有沒有更優的合并算法呢?
回憶我們構建倒排索引的時候,對posting list中的posting根據文檔ID排序了,這個時候就可以派上用場了,下面的算法只需要O(x?y)O(x?y)次操作!
上面的偽代碼是我根據IR書中的偽代碼重寫的,因為原書中的涉及if-else判斷語句的代碼縮進非常奇怪,附上原圖:
圖4(IR圖1-6):兩個倒排記錄表的合并算法要使用上述的合并posting list的算法,那么所有的posting list必須按照統一的標準進行排序。這里,我們使用的排序方法是根據全局統一的文檔ID(docID)來對posting list排序。
對上述代碼稍作修改就可以寫出對p1,p2求并集的代碼(最后需要把非空的list添加到answer中)。而對于NOT查詢,只需要在結果中去掉相應的postings。
查詢優化
通過恰當地組織查詢的處理過程可以進一步降低上述合并倒排記錄表過程的比較次數。考慮查詢:
Brutus AND Caesar AND Calpurnia直接的想法是,我們依次取出Brutus,Caesar和Calpurnia的倒排記錄表,然后先合并Brutus和Caesar得到一個中間結果tmp_answer(調用Intersect(Brutus.list, Caesar.list)),然后再把tmp_answer與Calpurnia合并得到最終結果(調用Intersect(tmp_answer, Calpurnia.list))。
然而,一個啟發式的算法可能取得更好的效果,降低比較次數。根據詞項的文檔頻率(亦即泡排記錄表的長度)從小到大依次進行處理。先合并兩個最短的倒排記錄表,那么所有中間結果的長度都不會超過最短的倒排記錄表,最終需要的比較次數也就很可能最少。
這種啟發式算法大部分情況下能取得很好的效果,但在某些情況下并不是最優的,最后,有興趣的讀者可以思考一下如何合并以下的三個倒排記錄表才能最優(可見這種啟發式算法并不一定能保證比較次數最少)。
apple → [5,6,10] cat → [3,5,6,10] luffy → [6,11,12,13,14]總結
- 上一篇: Unity Resource文件夹的使用
- 下一篇: 如何创建像 Quora 这样的问答网站: