搜索引擎核心技术与算法 —— 词项词典与倒排索引优化
一只小狐貍帶你解鎖NLP/ML/DL秘籍
作者:QvQ
老板~我會寫倒排索引啦!我要把它放進咱們自研搜索引擎啦!
我呸!你這種demo級代碼,都不夠當單元測試的!
嚶嚶嚶,課本上就是這樣講的呀?!
來來,帶你見識一下工業級搜索引擎里的倒排索引是怎么優化的!
前言
首先回顧一下構建倒排索引的幾個主要步驟:
(1)?收集待建索引的文檔;
(2)?對這些文檔中的文本進行詞條化;
(3)?對第2步產生的詞條進行語言學預處理,得到詞項;
(4)?根據詞項對所有文檔建立索引。?可以看到,上訴過程中非常重要的一步就是獲得詞項,那么詞項是什么,又是怎么獲得的呢?
詞項集合的確定
在確定詞項前,我們需要明確三個概念:
詞條:一段文本中有效詞的子序列,其中每個子序列稱為一個詞條。
詞條類:相同詞條構成的集合。
詞項:一個詞項指的是在信息檢索系統詞典中所包含的某個可能經過歸一化處理的詞條類。(詞項集合和詞條集合可以完全不同,比如可以采用某一個分類體系中的類別標簽作為詞項。當然,在實際的信息檢索系統中,詞項往往和詞條密切相關)
三者關系如下:
下面,讓我們一起學習這幾者是如何一步步變化得來的。
1.1 詞條化
詞條化過程詞條化的主要任務就是確定哪些才是正確的詞條。比如,對于簡單的句子將字符串進行拆分并去掉標點符號即可。
然而,上面的例子僅僅代表的是一種最簡單的情況。實際上即使對于單詞之間存在空格的英文來說也存在很多難以處理的問題。比如,英文中的上撇號“’”既可以代表所有關系也可以代表縮寫,應當在詞條化過程中究竟應該如何對它進行處理?參考下面的例子:
對其中的“ O’Neill” 來說,詞條化的結果可能有如下幾種形式可以選擇,那么到底哪一種才正確??
對于可能的各種拆分策略來說,最后的選擇結果會決定哪些布爾查詢會被匹配上、哪些不會被匹配上。給定查詢neill AND capital,上述五種拆分策略中有3種會被匹配上(即第1、4、5種情況)。而如果給定查詢o’neill AND capital,則在沒有對查詢進行任何預處理的情況下,上述策略中只有一種能匹配上。不管是輸入布爾查詢或者自由文本查詢,人們總是希望對文檔和查詢進行同樣的詞條化處理,這往往通過采用相同的詞條化工具來實現。這樣做能夠確保文本與查詢中的同一字符串序列的處理結果相一致。
在詞條化的過程中,需要注意以下幾個問題:
(1)對大多數語言特別是一些特定領域的語言來說,往往有一些特定的詞條需要被識別成詞項,如編程語言“C++”和“C#”、“B-52”之類的飛行器名字或者叫“M*A*S*H”的電視秀節目等等,這時候就不能簡單的去掉文本中的符號了,這里通常需要建立專有名詞字典來解決。
(2)字符序列類型包括郵件地址(如jblack@mail.yahoo.com)、URL(如http://stuff.big.com/new/specials.html)、IP地址(如142.32.48.231)和包裹追蹤號碼(1Z9999W99845399981)等等。一種做法是不對包括貨幣量、數字、URL等在內的詞條進行索引,這是因為如果對這些詞條進行索引則會顯著擴大索引的詞匯量。當然,這樣做會對用戶的搜索產生一些限制。比如,人們可能會在程序缺陷(bug)庫中搜索錯誤發生的行號,但是經過上述處理之后的系統顯然不能返回正確結果。如果這類數據需要詞條化,那么利用正則是一個不錯的辦法。
(3)即使根據空格進行拆分有時也會將概念上本應該看成單個詞條的對象分開,比如一些名稱(San Francisco,Los Angeles)、外來短語(au fait)或那些書寫時可分可合的復合詞(white space vs whitespace)。其他的例子還包括電話號碼[(800)234-2333]、日期(Mar11,1983)等。如果在空格處拆分這些對象可能會導致很差的檢索結果,比如,輸入York University(約克大學)時會返回包含New York University(紐約大學)的文檔。連字符和空格甚至會互相影響。這種情況就和中文文本中分詞類似了。
(4)對于一些主要的東亞語言(如漢語、日語、韓語和泰語等)來說,由于詞之間并不存在空格,所以問題更加嚴重。分詞的方法包括基于詞典的最大匹配法(采用啟發式規則來進行未定義詞識別)和基于機器學習序列模型的方法(如隱馬爾可夫模型或條件隨機場模型)等,后者需要在手工切分好的語料上進行訓練(分詞作為NLP領域一個非常重要的研究內容,我們后面會專門獨立一章來介紹分詞常用算法ヾ(?°?°?)ノ゙)。由于存在多種切分可能,上述分詞方法都有可能導致錯誤的切分結果,因此,永遠不能保證只能得到一個完全一致的唯一切分結果。另一個解決方法則摒棄了基于詞的索引策略而采用短字符序列的方法(如字符的k-gram方法)。這種方法并不關心詞項是否會跨越詞的邊界。該方法之所以能夠引起人們的興趣主要有以下3個原因:第一,一個漢字更像是一個音節而不是字符,它往往具有語義信息;第二,大部分詞都很短(最常見的漢語詞長度是2個字);第三,由于缺乏公認的分詞標準,詞的邊界有時也很難確定。
1.2 去停用詞
某些情況下,一些常見詞在文檔和用戶需求進行匹配時價值并不大,需要徹底從詞匯表中去除。這些詞稱為停用詞(stop word)。一個常用的生成停用詞表的方法就是將詞項按照文檔集頻率(collection frequency,每個詞項在文檔集中出現的頻率)從高到低排列,然后手工選擇那些語義內容與文檔主題關系不大的高頻詞作為停用詞。停用詞表中的每個詞將在索引過程中被忽略。
?英文常用停用詞表
不對停用詞建立索引一般情況下不會對系統造成太大的影響,比如搜索時采用the或by進行查詢似乎沒有什么意義。但是,對于短語查詢來說情況并非如此,比如短語查詢President of the United States中包含兩個停用詞,但是它比查詢President AND“United States”更精確。如果忽略掉to,那么flights to London(因為這里的to并不是以介詞的身份出現)的意義將會丟失。搜索Vannevar Bush的那篇經典文章As we may think時,如果將前3個單詞都看作停用詞,那么搜索將會很困難,因為系統只返回包含think的文章。更為嚴重的是,一些特定的查詢類型會受到更大的影響。比如一些歌名或者著名的詩歌片段可能全部由常用的停用詞組成(如To be or not to be,Let It Be,I don’t want to be等)。
1.3 詞條歸一化
將文檔和查詢轉換成一個個的詞條之后,最簡單的情況就是查詢中的詞條正好和文檔中的詞條相一致。然而在很多情況下,即使詞條之間并不完全一致,但實際上人們希望它們之間能夠進行匹配。比如查詢USA時我們希望能夠返回包含U.S.A.的文檔。
詞條歸一化(token normalization)就是將看起來不完全一致的多個詞條歸納成一個等價類,以便在它們之間進行匹配的過程。
最常規的做法有以下兩種:
(1)隱式地建立等價類,每類可以用其中的某個元素來命名。比如,在文檔和查詢中,都把詞條anti-discriminatory和antidiscriminatory映射成詞項antidiscriminatory,這樣對兩個詞中的任一個進行搜索,都會返回包含其中任一詞的文檔。這種處理方法的優點在于:一方面,等價類的建立過程是隱式的,不需要事先計算出等價類的全部元素,在映射規則下輸出相同結果的詞項一起構成等價類集合;另一方面,僅僅構建“去除字符”這種映射規則也比較容易。
(2)顯示建立等價類,維護多個非歸一化詞條之間的關聯關系。該方法可以進一步擴展成同義詞詞表的手工構建,比如將car和automobile歸成同義詞。這些詞項之間的關系可以通過兩種方式來實現。第一種常用的方式是采用非歸一化的詞條進行索引,并為某個查詢詞項維護一張由多個詞組成的查詢擴展詞表。當輸入一個查詢詞項時,則根據擴展詞表進行擴展并將擴展后得到的多個詞所對應的倒排記錄表合在一塊(如下圖一)。另一種方式是在索引構建時就對詞進行擴展(如下圖二)。比如,對于包含automobile的文檔,我們同時也用car來索引(同樣,包含car的文檔也用automobile來索引)。
圖一
圖二
另一方面,由于兩個關聯詞的擴展詞表之間可以存在交集但不必完全相同,所以上述兩種方式相對于隱式建立等價類的方法來說更具靈活性。這也意味著從不同關聯詞出發可以進行不對稱的擴展。下圖出了一個例子。該例子中,如果用戶輸入windows,那么我們希望返回包含Windows操作系統的文檔。但是如果用戶輸入window,雖然此時可以和小寫的windows相匹配,但是不太可能會和Windows操作系統中的Windows相匹配。
隱式建立等價類或查詢擴展的使用幅度仍然是個開放的問題。適度使用絕對沒錯,但是過度使用很容易會在無意間造成非預期的擴展結果。例如,通過刪除U.S.A.中的句點可以把它轉化成USA,由于在首字母省略用法中存在這種轉換模式,所以上面的做法乍看上去非常合理。但是,如果輸入查詢C.A.T.,返回的很多包含cat的文檔卻肯定不是我們想要的結果。
接下來我們將給出一些在實際當中會遇到的詞條歸一化問題及其對策:
(1)重音及變音符號問題
英語中變音符號的使用越來越少見,盡管如此,人們很可能希望cliche和cliché或者naive和na?ve能匹配。這可以通過在詞條歸一化時去掉變音符號來實現。而在許多其他語言中,變音符號屬于文字系統的常規部分,不同的變音符號表示不同的發音。有時候,不同單詞之間的區別只是重音不同。比如,西班牙語中,pe?a的意思是“懸崖”,而pena的意思卻是“悲哀”。然而,關鍵并不是規范或者語言學問題,而是用戶如何構造查詢來查找包含這些詞的文檔。
(2)大小寫轉換問題
大小寫轉換(case-folding)問題的一個一般處理策略是將所有的字母都轉換成小寫。這種做法通常的效果不錯,比如這樣可以允許句首的Automobile和查詢automobile匹配。對于Web搜索引擎來說,這種做法也很有好處,因為大多數用戶輸入ferrari時實際想找的是Ferrari(法拉利)車。
(3)英語中的其他問題
英語中還存在一些獨特的歸一化做法。比如,用戶希望將ne’er和never、英式英語的拼寫方式colour和美式英語的拼寫方式color等同起來。日期、時間和其他類似的對象往往以多種形式出現,這給歸一化造成了額外的負擔。人們可能希望將3/12/91和Mar.12,1991統一起來。但是,要正確處理這個例子將會十分復雜,因為在美國,3/12/91指的1991年3月12日(Mar.12,1991),而在歐洲,卻指的是1991年12月3日(3Dec.1991)
1.4 詞干還原和詞性歸并
出于語法上的要求,文檔中常常會使用詞的不同形態,比如organize、organizes和organizing。另外,語言中也存在大量意義相近的同源詞,比如democracy、democratic和democratization。在很多情況下,如果輸入其中一個詞能返回包含其同源詞的文檔,那么這樣的搜索似乎非常有用。
詞干還原和詞形歸并的目的都是為了減少屈折變化的形式,并且有時會將派生詞轉化為基本形式。
詞干還原:通常指的是一個很粗略的去除單詞兩端詞綴的啟發式過程,并且希望大部分時間它都能達到這個正確目的,這個過程也常常包括去除派生詞綴。
詞形歸并:通常指利用詞匯表和詞形分析來去除屈折詞綴,從而返回詞的原形或詞典中的詞的過程,返回的結果稱為詞元。
這兩個過程的區別還在于:詞干還原在一般情況下會將多個派生相關詞合并在一起,而詞形歸并通常只將同一詞元的不同屈折形式進行合并。詞干還原或詞形歸并往往通過在索引過程中增加插件程序的方式來實現,這類插件程序有很多,其中既有商業軟件也有開源軟件。
?
基于跳表的快速合并算法
上一章我們講解了倒排記錄表的基本合并算法:同時在兩個表中遍歷,并且最后算法的時間復雜度為記錄表大小的線性函數。假定兩個表的大小分別是m和n,那么合并過程有O(m+n)次操作。很自然的一個問題就是我們能否做得更好?也就是說,能否在亞線性時間內完成合并過程?下面我們將看到,如果索引變化不太頻繁的話那么答案是肯定的。
如果待合并的兩個倒排表數據量很大, 但是交集很少時, 會是什么情況呢?
[1, 2, 3, 4, 5, ... 10001, 10005] [1, 10001, 10008]如果對這兩個做合并操作, 最后的交集結果只有 ?[1, 10001] 2個元素, 但是卻要做10001次移動和比較操作, 所以肯定有什么辦法來優化這一點. 可能你已經想到了, 我們做了這么多無用比較, 是因為我們每次指針向前移動的步子太小了點, 如果我們在每次比較后向前多移動一點, 可以忽略很多無用的操作. 這就是跳表的思想.
跳表(skip list)—— 在構建索引的同時在倒排記錄表上建立跳表(如下圖所示)。跳表指針能夠提供捷徑來跳過那些不可能出現在檢索結果中的記錄項。構建跳表的兩個主要問題是:在什么位置設置跳表指針?如何利用跳表指針進行倒排記錄表的快速合并?
我們以上圖為例來先考慮快速合并的問題。假定我們在兩個表中遍歷一直到發現共同的記錄8為止,將8放入結果表中之后我們繼續移動兩個表的指針。假定第一個表的指針移到16處,而第二個表的指針移到41處,兩者中較小項為16。這時候我們并不繼續移動上面的表指針,而是檢查跳表指針的目標項,此時為28,仍然比41要小,因此此時可以直接把上表的表指針移到28處,這樣就跳過了19和23兩項。基于跳表的倒排記錄表合并算法有很多變形,它們的主要不同可能在于跳表檢查的時機不一樣。
我們再考察另一個問題,即在什么位置上放置跳表指針?這里存在一個指針個數和比較次數之間的折中問題。跳表指針越多意味著跳躍的步長越短,那么在合并過程中跳躍的可能性也更大,但同時這也意味著需要更多的指針比較次數和更多的存儲空間。跳表指針越少意味著更少的指針比較次數,但同時也意味著更長的跳躍步長,也就是說意味著更少的跳躍機會。放置跳表指針位置的一個簡單的啟發式策略是,在每個㏒?P處均勻放置跳表指針,其中P是倒排記錄表的長度。這個策略在實際中效果不錯,但是仍然有提高的余地,因為它并沒有考慮查詢詞項的任何分布細節。
# 基于跳表的倒排記錄表快速合并算法 a = range(10008) b = [1, 10001, 10008]i = j = 0 result = [] step = 100 count = 0 while i < len(a) and j < len(b):if a[i] == b[j]:result.append(a[i])i = i + 1j = j + 1count = count + 1elif a[i] < b[j]:while (i + step < len(a)) and a[i + step] <= b[j]:i = i + stepcount = count + 1else:i = i + 1count = count + 1else:while (j + step < len(b)) and b[j + step] <= a[i]:j = j + stepcount = count + 1else:j = j + 1count = count + 1 print(result) # [1, 10001] print(count) # 207上面代碼中故意構造了一個很大的集合 [0 ... 10007], 然后用變量count作為計數器來分析兩個算法分別執行的操作次數, 可以看到采用跳表算法時(我們模擬了step=100)的計算次數是207, 而用之前的方式計算次數是10008, 可見性能提升了很多倍.
這里有幾點說明下:
1. 這里為了簡單說明跳表的思路, 全部用了數組表示倒排表, 其實真實的數據結構應該是鏈表結構(linked list). 這才符合磁盤存儲結構.?
2. 跳表的原始結構算法比這個復雜, 而且根據場景的不同, 跳表有不同的實現. 這里因為不是利用跳表的快速查詢功能, 所以沒有多級指針索引概念, 詳細跳表實現查考:?Skip Lists: A Probabilistic Alternative to Balanced Trees
含位置信息的倒排記錄表
先來看一個問題,當用戶將“Stanford University”這個查詢中的兩個詞看成一個整體的時候,用戶是為了查詢和Stanford University這所高校相關的信息。但是如果是基于布爾查詢(詳見第一章)的話,將會被拆解成Stanford AND University進行查詢,從而一篇含有句子The inventor Stanford Ovshinsky never went to university的文檔會推送給用戶,這并不是我們想要的。那么如何解決這個問題呢?這里引入二元詞索引。
3.1 二元詞索引
處理短語查詢的一個辦法就是將文檔中每個接續詞對看成一個短語。例如,文本 Friends,Romans, Countrymen 會產生如下的二元接續詞對
friends?romans romans countrymen這種方法將每個接續詞對看成詞項,這樣馬上就能處理兩個詞構成的短語查詢,更長的查詢可以分成多個短查詢來處理。比如,按照上面的方法可以將查詢 stanford university palo alto
分成如下的布爾查詢:
“stanford university” AND “university palo” AND “palo alto”
可以期望該查詢在實際中效果會不錯,但是偶爾也會有錯誤的返回例子。對于該布爾查詢返回的文檔,我們并不知道其是否真正包含最原始的四詞短語。在所有可能的查詢中,用名詞和名詞短語來表述用戶所查詢的概念具有相當特殊的地位。但是相關的名詞往往被各種虛詞分開,比如短語the abolition of slavery或者renegotiation of the constitution。這種情況下,可以采用如下方法來建立二元詞索引:首先對文本進行詞條化然后進行詞性標注,這樣就可以把每個詞項歸成名詞(N,也包括專有名詞)、虛詞(X,冠詞和介詞)和其他詞。然后將形式為NX*N非詞項序列看成一個擴展的二元詞。利用上述算法,可以將查詢cost overruns on a power plant分析成“cost overruns” AND “overruns power” AND “power plant”,實際上忽略中間的那個二元詞所形成的查詢的效果會更好。如果使用更精確的詞性模式來定義擴展二元詞可能會取得更好的結果。
二元詞索引的概念可以擴展到更長的詞序列(三元、四元...),如果索引中包含變長的詞序列,通常就稱為短語索引(phrase index)。實際上,利用二元詞索引來處理單個詞的查詢不太方便(必須要掃描整個詞匯表來發現包含該查詢詞的二元詞),因此同時還需要有基于單個詞的索引。盡管總有可能得到錯誤的匹配結果,但是在長度為3或者更長的索引短語上發生匹配錯誤的可能性實際上卻很小。然而在另一方面,存儲更長的短語很可能會大大增加詞匯表的大小。窮盡所有長度超過2的短語并維護其索引絕對是一件令人生畏的事情,即使只窮盡所有的二元詞也會大大增加詞匯表的大小。
3.2 位置信息索引
很顯然,基于上面談到的原因,二元詞索引并非標準的解決方案。實際中更常用的一種方式是采用所謂的位置信息索引(positional index,簡稱位置索引)。在這種索引中,對每個詞項,以如下方式存儲倒排記錄
單詞be的文檔頻率是178239,在文檔1中出現2次,位置分別是17、25。
為處理短語查詢,仍然需要訪問各個詞項的倒排記錄表。像以往一樣,這里可以采用最小文檔頻率優先的策略,從而可以限制后續合并的候選詞項的數目。在合并操作中,同樣可以采用前面提到的各種技術來實現,但是這里不只是簡單地判斷兩個詞項是否出現在同一文檔中,而且還需要檢查它們出現的位置關系和查詢短語的一致性。這就需要計算出詞之間的偏移距離。
舉個栗子,假如用戶輸入"boy friend"進行搜索, 如果只要出現了"boy" 或者 "friend"的文檔都搜索出來, 那么下面三篇文檔都滿足要求:
the boy and the girl are good friends
you are my boy friend
the boy has many friends.
現在用戶應該只想搜出文檔 2 出來. 基于"位置信息索引"方式, 我們可以做到這一點.
這種搜索方法類似于k詞近鄰搜索 —— a /k b
這里,/k 意味著“ 從左邊或右邊相距在 k 個詞之內,若k=1,則意味著a、b相鄰” 。很顯然,位置索引能夠用于鄰近搜索,而二元詞索引則不能。
有了這個索引存儲結構, 要找出不同的短語就比較容易了, 比如用戶想搜索"boy friend", 就可以轉化成 boy /1 friend 即可以完成要求。只要找出在文檔中, boy出現的位置剛好在friend前一個位置的所有文檔. 所以文檔2滿足我們的要求被搜索出來. 下面用python簡單實現下這個算法:
# p1, p2是兩個上述結構的倒排記錄表, k是兩個詞項的位置在k以內 def positional_interset(p1, p2, k):result = [] # 最終的搜索結果, 以(文檔id, 詞項1的位置, 詞項2的位置)形式存儲while p1 is not None and p2 is not None: # 當p1, 和 p2 都沒有達到最尾部時if p1.docId == p2.docId: # 如果兩個詞項出現在同一個文檔中l = [] # 臨時變量, 用來存儲計算過程中滿足位置距離的位置對信息pp1 = p1.positionpp2 = p2.positionwhile pp1 is not None: # 先固定pp1的位置, 循環移動pp2的位置進行檢查while pp2 is not None:if abs(pp1.pos - pp2.pos) <= k: # 如果pp1和pp2的距離小于k, 則滿足要求l.append(pp2.pos) # 添加到臨時變量pp2 = pp2.next # pp2向后移一個位置elif pp2.pos > pp1.pos: # 如果pp2當前的位置相對pp1已經超過給定的范圍(構不成短語要求), 則停止移動pp2, 后續后把pp1再往前移動一個位置breakwhile not l and abs(l[0] - pp1.pos) > k: # 當每次移動一次pp1時, l里面會存儲上一次計算所得的pp2的一些位置, 這里要過濾那些相對于當前pp1最新位置, 那些不再滿足要求的pp2的位置del l[0]for p in l:result.append[(p1.docId, pp1.pos, p)] # 把最終的結果加入到結果集中pp1 = pp1.next # pp1向前移動一個位置, 重復上次邏輯計算p1 = p1.nextp2 = p2.nextelif p1.docId < p2.docId:p1 = p1.nextelse:p2 = p2.next?毋庸置疑,采用位置索引會加深倒排記錄表合并操作的漸進復雜性,這是因為需要檢查的項的個數不再受限于文檔數目而是文檔集中出現的所有的詞條的個數 T。也就是說,布爾查詢的復雜度為Θ (T)而不是Θ (N)。然而,由于用戶往往期望能夠進行短語搜索和鄰近搜索,所以實際中的大部分應用并沒有其他選擇而不得不采用這種做法。
3.3 混合索引機制
二元詞索引和位置索引這兩種策略可以進行有效的合并。假如用戶通常只查詢特定的短語,如Michael Jackson,那么基于位置索引的倒排記錄表合并方式效率很低。一個混合策略是:對某些查詢使用短語索引或只使用二元詞索引,而對其他短語查詢則采用位置索引。短語索引所收錄的那些較好的查詢可以根據用戶最近的訪問行為日志統計得到,也就是說,它們往往是那些高頻常見的查詢。當然,這并不是唯一的準則。處理開銷最大的短語查詢往往是這樣一些短語,它們中的每個詞都非常常見,但是組合起來卻相對很少見。
Williams等人(2004)評估了一個更復雜的混合索引機制,其中除了包含上面兩種形式的索引外,還在它們之間引入了一個部分后續詞索引(next word index),即對每個詞項,有個后續詞索引記錄了它在文檔中的下一個詞項。論文的結論是,雖然比僅僅使用位置索引增加了26%的空間,但是面對典型的Web短語混合查詢,其完成時間大概是只使用位置索引的1/4。
本章節主要對詞項的形成和倒排索引的兩個升級版算法做了一個粗略的介紹。雖然這是搜索引擎中最基礎的東西,但值得細細挖掘的地方還有很多,畢竟每一個小點的改善都可以極大的提高用戶體驗,搜索引擎學習之路道阻且長呀~加油(`?ω?′)
可
能
喜
歡
跨平臺NLP/ML文章索引
倒排索引初體驗
讓搜索推薦更聰明的篇章標簽自動化生產
神經網絡調參指南
DFS、BFS與A*搜索算法
求關注 求投喂 拉你進高端群哦~
參考文獻
《信息檢索導論 修訂版》
倒排索引優化 - 跳表 ?—— ?博客園 海鳥
總結
以上是生活随笔為你收集整理的搜索引擎核心技术与算法 —— 词项词典与倒排索引优化的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 史上最全的分词算法与工具介绍
- 下一篇: 不是所有问题都适合用神经网络去搞!