終於把序號寫到了第十篇(其實已是第13篇了),前面寫了幾個外篇,我看上篇機器學習的那篇看的人不少,後面會再找一兩個點再寫寫,後面可能會算法部分和架構部分穿插着寫了,想到哪裏就寫哪裏了,今天咱們繼續咱們的搜索引擎架構部分,主要來講說數據的檢索。c++
對以前文章感興趣的話,能夠點擊下面的連接,或者直接在SF上看整個專欄--吳說
搜索架構
用Golang寫一個搜索引擎(0x00)從零開始
用Golang寫一個搜索引擎(0x01)基本概念
用Golang寫一個搜索引擎(0x02)倒排索引技術
用Golang寫一個搜索引擎(0x03)跳躍表,哈希表
用Golang寫一個搜索引擎(0x04)B+樹
用Golang寫一個搜索引擎(0x06)索引構建
用Golang寫一個搜索引擎(0x07)正排索引
用Golang寫一個搜索引擎(0x08)索引的段
用Golang寫一個搜索引擎(0x09)數據增刪改
搜索算法
用Golang寫一個搜索引擎(0x05)文本相關性排序
用Golang寫一個搜索引擎(0xFF)搜索排序
搜索引擎(0xFE)--- 用機器學習再談排序算法
以前咱們說完了數據的增,刪,改,還剩下一個查沒有寫,今天來寫寫查。segmentfault
搜索引擎最核心的東西就是查了,在查上面,也有不少好玩的數據結構和算法,咱們一個一個來講說。數組
以前說了搜索引擎的核心底層數據結構包括兩個:倒排索引和正排索引,倒排索引主要用來檢索,正排索引主要用來過濾,咱們就以一個搜索請求來講說搜索引擎的檢索。微信
好比咱們有最近3個月新浪微博的數據在搜索引擎中,數據有發佈者暱稱,微博內容,微博發佈時間,要搜索的關鍵詞是長沙雅禮中學,並且只要看最近10天的數據,那麼這麼一個典型的搜索場景是怎麼來進行的呢?咱們先假設咱們數據是這樣的4條數據數據結構
"nickname" : "長沙天氣預報" "content":"今每天氣真好" "datetime":"2016-05-06"
"nickname" : "路人甲" "content":"長沙天氣真好,我如今在雅禮中學" "datetime":"2016-01-21"
"nikename" : "雅禮中學官微" "content":"長沙天氣真好" "datetime":"2016-05–05"架構
首先關鍵詞來了之後要進行的第一步是query分析,query分析至關複雜,也直接影響到搜索效果,有不少算法上的東西,這個須要單獨說,呵呵。機器學習
固然,最簡單的query分析就是切詞了,咱們這裏不講算法,因此query分析直接就是切詞了,切詞完了之後變成了長沙/雅禮/中學,後面的10天這個約束條件就變成了range(2016-05-01,2016-05-10),好了,query分析就完了。數據結構和算法
query分析完了之後,就開始進行倒排檢索了,倒排檢索就是按照切詞的結果,一個term一個term的從倒排文件中拉取倒排鏈,簡單來講是這樣的學習
//從B+樹中獲取關鍵字倒排鏈的文件偏移offset ok, offset := this.btree.Search(this.fieldName, keystr) if !ok { return nil, false } //文件偏移的第一個位置保存的倒排鏈的長度 lens := this.idxMmap.ReadInt64(int64(offset)) //經過offset和倒排鏈的長度獲取整個倒排鏈 res := this.idxMmap.ReadDocIdsArry(uint64(offset+8), uint64(lens)) return res, true
這樣,經過三次調用就獲得3個倒排鏈了,而後開始檢索的第二步。
上面的數據中,咱們要檢索長沙/雅禮/中學,咱們發現,三條數據都知足這個條件,不同的是第一條和第二條是單個字段就知足,第三條是跨字段了,須要兩個字段合起來才知足匹配條件,因此,咱們的檢索過程並非簡單的求倒排鏈的交集,而是一個求並求交的組合過程。
先單個的term在多個字段求並集,而後再多個term之間求交集,這樣能知足找到全部符合條件的結果。
多路求並也叫K路並歸,好比長沙這個term,在檢索nickname的時候,找到docid爲[1],檢索content的時候,找到docid爲[2,3],兩個docid鏈求並集,獲得[1,2,3],就是咱們最後要求的結果。
這裏只是一個兩路的求並集,比較簡單,若是是更多的字段檢索,那麼就會有超過兩個的多路求並,通常咱們使用勝者樹或者敗者樹來進行K路求並的操做。
所謂勝者樹,是一種外部歸併排序常用的數據結構,咱們這裏用他來作多路的求並集操做,由於咱們的倒排鏈是以mmap的形式進行存儲的,其實也是一種外部存儲。
簡單的說,勝者樹就是以K路的數據做爲葉子節點,創建一棵徹底二叉樹,而後葉子節點兩兩比較,勝利者進入上一層節點中,指導最後獲得一個最後的勝利者輸出,輸出之後將對應的數據更新,光這麼說太抽象了,咱們看個圖就明白了,好比咱們目前有4路求並集,4路的數據分別是[0,2,3],[1,2],[0,3],[3,4],那麼整個勝者樹的過程就是下面的這個圖,從左到右表示勝者樹的求並過程。
上面那個圖中
首先,有4個數組,那麼創建一個有四個葉子節點的平衡徹底二叉樹,每一個葉子節點存儲一個倒排鏈,把每一個倒排鏈的第一個元素拿出來準備進行比較
兩兩比較,較小的那個進入上一層節點,而後上一層節點繼續兩兩比較,較小的那個進入上一層節點,直到最後到達根節點,圖中的紅色部分
把最後戰鬥到根節點的元素輸出到最終結果中(若是最終結果中最後一個元素和這個根節點元素同樣,丟棄這個元素)
將最後獲勝的元素的倒排鏈指針後移一位,將該鏈的後一個元素拿出來進行比較
這樣的話,在時間複雜度爲O(nlogk)的狀況下,咱們就把並集求好了。
求並集的方法也有不少,咱們這裏只舉出了比較常見的,勝者樹這種結構比較好理解,但在實現的時候,通常採用勝者樹的變體敗者樹,敗者數的原理和勝者樹同樣,有區別的地方是,兩兩比較的時候敗者樹保存的是敗者的信息,而勝者接着往上層進行戰鬥,直到根節點位置,最後輸出的仍是勝者。
敗者樹的過程我也畫了個圖,在下面
敗者樹的優點在於新進來的元素,只須要跟他的父節點進行比較就好了(只須要一次指針操做就能夠找到父節點),勝者樹的話還須要找臨近節點比較(須要兩次指針操做才能找到臨近節點),而後更新父節點信息,因此敗者樹效率更優一點。
上一步中拿到了各個term的倒排鏈之後,爲了保證每一個document中都含有全部詞,就要對他們求交集了,由於倒排鏈是按照docid從小到大排列的,因此求交集其實是對多個有序數組求交集。
若是是兩個倒排鏈的話比較容易,拿兩個指針分別指向兩個倒排鏈的首部,而後比較大小,哪一個小,那麼把這個指針後移一位,若是兩個相等,那麼把這個docid取出來,直到某一條倒排鏈遍歷完,時間複雜度O(n+m),最好的狀況是O(min(n,m)),代碼也很簡單,核心的就這麼幾行:
ia,ib:=0,0 for ia < lena && ib < lenb { if a[ia].Docid == b[ib].Docid { c[lenc] = a[ia] lenc++ ia++ ib++ continue } if a[ia].Docid < b[ib].Docid { ia++ } else { ib++ } } return c
這是兩個倒排鏈求交集,若是是多個倒排鏈求交集的話,能夠兩兩求交,最後就完成了。
這樣效率比較低,這裏咱們再介紹一種方法
由於求交集最後的結果長度確定不會超過最短的倒排鏈的長度,因此咱們首先找到長度最短的倒排鏈,做爲標稱數組。
從標稱數組中依次取出每一個元素,查看元素是否是在其餘的每個倒排鏈中
若是這個元素在其餘全部倒排鏈中都存在,那麼就把這個元素放入最終結果集中。
上面的第二步又能夠從如下幾個方面優化一下
若是發現某個元素不在某個倒排鏈中,那麼他確定不在最終結果中,直接跳出,不用繼續比較了。
若是某個倒排鏈被查找完了,那麼也能夠跳出了。
查找元素是否在某個倒排鏈中,可使用下面幾個方式來優化
一是可使用二分查找來找,這樣複雜度能夠下降一個數量級。
由於咱們的docid是連續的,因此能夠設定一個哨兵指針,而後從哨兵指針日後遍歷,遍歷到倒排鏈中的元素大於當前元素就中止,而後把哨兵移動到這個元素位置就行,這樣的話只是給每一個倒排鏈表加了個哨兵,複雜度上卻下降了一個數量級了。
若是用二分查找的話,假如每一個倒排鏈長度爲N,一共有K個倒排鏈,那麼總體最壞狀況下複雜度是O(N*K*logN),若是用哨兵的方法的話,通常比這個還要快一點。
下面這個圖是優化後的多路求交,綠色的是標稱數組中的元素,紅色的是哨兵,黃色的是命中的元素。
到這一步,經過倒排文件能找到的元素就都找到了,像上面那個例子,咱們找到了2和3兩條記錄,而後就要用第二個條件最近10天的數據來進行過濾操做了。
過濾操做比較簡單,須要遍歷一邊結果集,而後用正排文件的值判斷是否知足條件,這麼遍歷一下,就只剩下記錄3了,這也是咱們最後獲得的結果。
在這裏再說一下,以前咱們說數據的增刪改的時候說了,刪除數據或者修改數據的時候實際上只是在bitmap上作了一個標記,表示這個數據刪除了,因此最後檢索的時候須要把已經刪除的數據去掉,這一步也是在這裏作,在遍歷整個結果集作正排過濾的時候能夠一塊兒判斷這個文檔是否被刪除了,只有知足過濾條件而且沒有被刪除的文檔才能留下來。
因爲正排文件的保存是按照docid來作數組下表保存的,因此查找的複雜度是O(1),時間基本上就是遍歷一邊結果集加上條件判斷的時間。
上面這些都作完了之後,就獲得了最終的完整結果集,這時候在對結果集進行統計彙總之類的操做就簡單了,這個能夠根據業務的要求,作任何形式的統計彙總需求了,文本相關性的打分通常在求交集和求並集的時候就作完了,以前說的精細化的排序也在這一步來進行。
好了,上面就是一次完整的檢索過程當中所涉及到的各類東西,關於query分析部分須要單獨說,這一篇就不展開了。但願你們看得開心。
歡迎關注個人公衆號,文章會在這裏首先發出來:)掃描或者搜索微信號XJJ267或者搜索中文西加加語言就行