用 Golang 寫一個搜索引擎 (0x04) --- B + 樹

本篇較長較枯燥,請保持耐心看完。git

前面兩章介紹了一下倒排索引以及倒排索引字典的兩種存儲結構,分別是跳躍表哈希表,本篇咱們介紹另外一種數據結構,他也被大量使用在信息檢索領域,我在github上實現的搜索引擎的詞典也是用的這個數據結構,它就是B+樹。github

首先,咱們看看什麼是樹,樹是程序設計中一個很是基礎的數據結構,記得大學時候的數據結構課,鏈表,棧,隊列,而後就是樹了,雖然那時候想必你們都被前序遍歷,中序遍歷,後序遍歷折騰過,不過樹確實是一種很是有用的數據結構。算法

上一篇咱們說過,表2的第一列首要解決的問題就是能快速找到對應的詞,而後找到對應詞的倒排列表,除了跳躍表和哈希表,B+樹也能知足條件,B+樹是B樹的變種,咱們B樹咱們就不看了,感興趣的你們能夠直接去google一下,咱們主要講的是B+樹,下圖就是一個3層的B+樹,我畫出來可能和你們搜出來的有點出入,可是不要緊,關鍵B+樹這種數據結構的思想你們瞭解了就行。sql

假設咱們有一組數字 34,40,67,5,37,12,45,24,那麼,把他們存成B+樹就是下圖這個樣子。數據庫

咱們很明顯看到幾個特色編程

  • 每一個節點的大小爲2
  • 非葉子層的最後一個節點的最後一個元素爲NULL
  • 最底層的葉子節點是順序排列的,這個例子是從小到大
  • 上面的內節點的每個元素都指向的下一級節點中最大的一個數相等

我儘可能的把B+樹說簡單點,網上的資料也好,查書也好,看上去都挺複雜的,首先咱們看看怎麼創建這棵樹,我儘可能用圖了,少一些文字也好理解一點,前方大量圖預警。數組

首先,咱們的數組是34,12,5,67,37,40,45,24服務器

####第一步,初始化B+樹,是這樣子的微信

這時候,啥也沒有,可是佔用了兩個節點,標識爲的,表示這個元素無心義,標記爲NULL表示無窮大cookie

####第二步,插入34這個元素,那麼圖變成這樣子

咱們看到,插入的過程是順着指針一直走到葉子節點,發現葉子節點是空的,而後把元素插入到葉子節點的頭部,而後返回上一級節點,將NULL後移,而後把第一個元素置爲他的子節點的最大值,請記住這句話:置爲他的子節點的最大值

####第三步,接着插入第二個元素12

這個步驟複雜一點

  • 從根節點開始遍歷,發現12小於根節點的某一個元素【在這裏是第1個元素】,順着指針往下走
  • 到達葉子節點,發現12小於葉子節點的某一個元素,說明能夠放在這個葉子節點中,而且葉子節點還有一個空位置,那麼直接把12按大小順序插入到這個節點中

####第四步,而後是插入5

這一步更復雜一點,產生了分裂

  • 從根節點開始遍歷,5小於34,順着指針往下走,到達葉子節點
  • 到達葉子節點,發現5小於葉子節點的某一個元素,說明能夠放在這個葉子節點中,可是,這個節點已經滿了,那麼,分裂出一個新的節點,將5放到老節點中,被擠走的元素順移到新節點中
  • 返回上一級節點,因爲第一個葉子節點的最大元素已經變成12了,因此將該節點的元素由34改爲指向的葉子節點的最大元素12
  • 因爲新生成了一個節點,將NULL這個元素指向新生成的節點

####第五步,接着咱們插入67

這一步比較簡單

  • 從根節點開始遍歷,67小於NULL,順着指針往下走,到達葉子節點
  • 到達葉子節點,發現67大於該節點的每個元素,而且葉子節點有空位,直接插入便可

####第六步,咱們插入37,插完這個後面的我就不寫了,感興趣能夠本身畫一下

這一步複雜了,這一步不只分裂了,並且分裂了兩次,而且層數增長了一層

  • 從根節點開始遍歷,37小於NULL,順着指針往下走,到達葉子節點
  • 到達葉子節點,37小於葉子節點中的67,表示能夠插入到這個節點中,可是節點滿了,咱們按照第四步的操做,分裂節點。
  • 分裂完了之後,產生了一個[34,37],一個[67,無]兩個節點,往上走的時候,發現上一層的節點插入了37之後也滿了,繼續按照第四步分裂。
  • 分裂完了之後,發現上層沒有節點了,那麼就新建一個根節點當上層節點,按照分裂的步驟給根節點賦值。

按照這六步,前5個元素就插入到B+樹中了,後面的步驟您能夠本身走一走,B+樹基本的思想就是這樣子的,可能我沒有按照教科書上的作法來講,但這並不影響你們的理解,我相信看完了之後雖然你腦子裏沒有標準的算法步驟,但應該有個大體的輪廓了,只不過須要本身再仔細想一想步驟。

####總的來講,B+樹的插入步驟無外乎如下幾個步驟

  • 每次都要從根節點開始
  • 比較大小,找到小於當前值的元素,順着指針往下走,繼續比較大小,一直到達葉子節點,那麼這個葉子節點就是你要操做的節點了。
  • 在葉子節點只有幾種操做,一是葉子節點有空位置,那麼直接插入進去,一是葉子節點滿了,那麼分裂一個節點出來。
  • 無論在葉子節點進行了那種操做,最後都要順着指針回去,若是沒有分裂,那麼上層就不會分裂,可能會更新上層節點元素的值,若是分裂了,那麼就帶着兩個分裂的節點往上走,該更新值就更新值,該分裂就分裂。
  • 若是一層一層分裂到最上層了,那麼就新增一個根節點吧

查找操做和更新操做幾乎同樣,就是更新操做的前面兩步,就不說了。

通常的更新的時候也是先查找,找到葉子節點,再更新,而後順着指針往上走繼續分裂,這個順着往上走通常狀況下首先想到的是雙向指針,可是雙向指針分裂的時候有點麻煩,須要把兩個指針都從新指新節點,我實現的時候用了一個棧,查找葉子節點的時候把通過的節點依次壓棧,到達葉子節點後,完成插入操做,往上遍歷的時候依次把棧彈出來就好了,少了一個指針。

回到上一篇說的那個表2的第一列,若是是那個表的話,用這個B+樹加上倒排鏈的話,最後的數據結構就長成這樣子了(字符串的大小我隨便寫的,中文的順序排列哥的腦子排不出來,你就把他們當作從小到大的順序吧)

好了,至此,一個倒排索引就創建好了,由兩部分組成,我實現的時候就是這麼實現的,一個結構用B+樹存儲字典,另一個就是一個順序的文件,B+樹的葉子節點存一個指向倒排文件的文件偏移量,固然,你也能夠用前面的哈希表或者跳躍表,甚至還有其餘類型的樹,好比trie樹來實現,或者你還有其餘新的高效數據結構也行。

####咱們再來講說B+樹,爲何選它?

以前我實現的時候用的是哈希表,並且大部分的搜索引擎用的都是哈希表,爲何用樹呢

  • 首先,爲了節省空間,若是用哈希表的話,假若有一個字段是主鍵,而且是不規則的(好比cookieid),那麼若是巨量的文檔的話,哈希表的桶就會很大,會很是佔用內存,而我調試的機器才8G內存的mac。
  • 其次再來看看哈希表,查詢的時間複雜度是O(1),看上去確實美好,若是單單是一個全文搜索引擎的話,因爲key都是字符串,並且基本都是中文字符串,整個中文的詞彙量才幾十萬,確實很好,可是若是字段不見得是中文分詞的東西,還有一些其餘的東西,好比各類ID,因爲是個通用的搜索,因此不會給具體字段去定義專門的哈希函數,因此可能會大片產生碰撞,那複雜度就不是O(1)了,若是是一個特定場景的搜索,要規避這個問題,能夠根據本身的業務需求來的,甚至可使用完美哈希函數,而我實現的時候主要是爲了更通用,因此用了B+樹。
  • 咱們再看看B+樹自己,若是咱們每一個節點能夠存儲100個元素,那麼一個4層的B+樹,能夠存儲1億條數據,不論是主鍵字段仍是其餘字段都夠了,而一個4層的B+樹檢索起來,須要遍歷4個節點,每一個節點用二分查找的話,是log100(2爲底),大概7次吧,4層的話,最差須要查詢28次,若是是3層的話,最差要21次,雖然和哈希比起來慢了這麼多,但1次循環大約須要4個CPU的時鐘週期吧,對於如今的服務器的計算機來講,就算21次循環+條件判斷也是微秒級別的,感受不太出來差異,況且不可能每一次都那麼點背,都要查21次吧。
  • 再有,個人索引生成的時候是按段生成的,後面會涉及到索引的多個段的合併,若是是B+樹的話,字典是順序的,你看上面那個圖,葉子節點是有指針連起來的,因此合併段的時候可使用一個多路歸併就合併完了,要是哈希的話,因爲不是順序的,合併起來須要從新哈希一遍,比較麻煩。
  • 還有,B+樹這種數據結構很是適合磁盤檢索,只須要把每一個節點的大小設定成一個磁盤頁的大小(通常是4K,至於爲何設成頁的大小,和機械硬盤的結構以及預讀取機制有關,感興趣的能夠本身查查,不過如今都是SSD了,這個的影響不是很大了),把指針改爲磁盤的頁編號,那麼不用加載進內存,直接在磁盤上就能進行檢索,特別適合巨量數據量的詞典(好比主鍵),索引數據庫的索引(好比Mysql的inneDB)基本上都是B+樹實現的,若是你們感興趣能夠單開一篇說這個。
  • 最後,B+樹因爲是順序存儲的,因此能夠進行範圍搜索(雖然我沒有用),而哈希表只能進行全等的搜索。

最後說說我實現的這棵B+樹,首先,爲了更少的佔用內存,我是用的磁盤的形式實現的,而且用了mmap的方式來加快讀寫速度,沒有用雙向指針,而用的棧來記錄查詢的路徑,速度還行吧,構造一棵10萬個隨機字符串的樹大約須要3秒,隨機查詢10萬次大約須要150毫秒,每次1.5微秒。

固然,我實現的時候比較倉促,就是按照算法硬編碼快速擼出來的,因此我這個B+樹還有很是大的優化空間,首先,個人key如今是肯定的,不能超過64字節,而且每一個節點最多100個元素,當時爲了快,肯定的key和元素個數比較好編程,若是變成動態的更加節省空間,其次,沒有特別的考慮連續key的狀況,連續key的插入會形成空間浪費一半,還有,把速度問題交給了mmap來解決,若是內存足夠,實際上啓動的時候預讀取非葉子節點到內存的話,查詢起來會更快,不過目前基本上知足需求了,你們若是對B+樹實現很感興趣,能夠看看bolt這個項目,這個是一個B+樹實現的KVDB,並且是帶事務的哦。

若是你以爲不錯,歡迎轉發給更多人看到,也歡迎關注個人公衆號,主要聊聊搜索,推薦,廣告技術,還有瞎扯。。文章會在這裏首先發出來:)掃描或者搜索微信號XJJ267或者搜索西加加語言就行

相關文章
相關標籤/搜索