用Golang写一个搜索引擎 (0x04) --- B+树

前面两章介绍了一下倒排索引以及倒排索引字典的两种存储结构,分别是跳跃表哈希表,本篇我们介绍另一种数据结构,他也被大量使用在信息检索领域,我在github上实现的搜索引擎的词典也是用的这个数据结构,它就是B+树。

首先,我们看看什么是树,树是程序设计中一个非常基础的数据结构,记得大学时候的数据结构课,链表,栈,队列,然后就是树了,虽然那时候想必大家都被前序遍历,中序遍历,后序遍历折腾过,不过树确实是一种非常有用的数据结构。

上一篇我们说过,表2的第一列首要解决的问题就是能快速找到对应的词,然后找到对应词的倒排列表,除了跳跃表和哈希表,B+树也能满足条件,B+树是B树的变种,我们B树我们就不看了,感兴趣的大家可以直接去google一下,我们主要讲的是B+树,下图就是一个3层的B+树,我画出来可能和大家搜出来的有点出入,但是没关系,关键B+树这种数据结构的思想大家了解了就行。

假设我们有一组数字 34,40,67,5,37,12,45,24,那么,把他们存成B+树就是下图这个样子。

Btree1.png

我们很明显看到几个特点

  • 每个节点的大小为2
  • 非叶子层的最后一个节点的最后一个元素为NULL
  • 最底层的叶子节点是顺序排列的,这个例子是从小到大
  • 上面的内节点的每一个元素都指向的下一级节点中最大的一个数相等

我尽量的把B+树说简单点,网上的资料也好,查书也好,看上去都挺复杂的,首先我们看看怎么建立这棵树,我尽量用图了,少一些文字也好理解一点,前方大量图预警。

首先,我们的数组是34,12,5,67,37,40,45,24

第一步,初始化B+树,是这样子的

bt0.png

这时候,啥也没有,但是占用了两个节点,标识为的,表示这个元素无意义,标记为NULL表示无穷大

第二步,插入34这个元素,那么图变成这样子

bt1.png

我们看到,插入的过程是顺着指针一直走到叶子节点,发现叶子节点是空的,然后把元素插入到叶子节点的头部,然后返回上一级节点,将NULL后移,然后把第一个元素置为他的子节点的最大值,请记住这句话:置为他的子节点的最大值

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

bt2.png

这个步骤复杂一点

  • 从根节点开始遍历,发现12小于根节点的某一个元素【在这里是第1个元素】,顺着指针往下走
  • 到达叶子节点,发现12小于叶子节点的某一个元素,说明可以放在这个叶子节点中,并且叶子节点还有一个空位置,那么直接把12按大小顺序插入到这个节点中

第四步,然后是插入5

bt3.png

这一步更复杂一点,产生了分裂

  • 从根节点开始遍历,5小于34,顺着指针往下走,到达叶子节点
  • 到达叶子节点,发现5小于叶子节点的某一个元素,说明可以放在这个叶子节点中,但是,这个节点已经满了,那么,分裂出一个新的节点,将5放到老节点中,被挤走的元素顺移到新节点中
  • 返回上一级节点,由于第一个叶子节点的最大元素已经变成12了,所以将该节点的元素由34改成指向的叶子节点的最大元素12
  • 由于新生成了一个节点,将NULL这个元素指向新生成的节点

第五步,接着我们插入67

bt4.png

这一步比较简单

  • 从根节点开始遍历,67小于NULL,顺着指针往下走,到达叶子节点
  • 到达叶子节点,发现67大于该节点的每一个元素,并且叶子节点有空位,直接插入即可

最后,我们插入37,插完这个后面的我就不写了,感兴趣可以自己画一下

bt5.png

第六步,这一步复杂了,这一步不仅分裂了,而且分裂了两次,并且层数增加了一层

  • 从根节点开始遍历,37小于NULL,顺着指针往下走,到达叶子节点
  • 到达叶子节点,37小于叶子节点中的67,表示可以插入到这个节点中,但是节点满了,我们按照第四步的操作,分裂节点。
  • 分裂完了以后,产生了一个[34,37],一个[67,无]两个节点,往上走的时候,发现上一层的节点插入了37以后也满了,继续按照第四步分裂。
  • 分裂完了以后,发现上层没有节点了,那么就新建一个根节点当上层节点,按照分裂的步骤给根节点赋值。

按照这六步,前5个元素就插入到B+树中了,后面的步骤您可以自己走一走,B+树基本的思想就是这样子的,可能我没有按照教科书上的做法来说,但这并不影响大家的理解,我相信看完了以后虽然你脑子里没有标准的算法步骤,但应该有个大致的轮廓了,只不过需要自己再仔细想想步骤。

总的来说,B+树的插入步骤无外乎以下几个步骤

  • 每次都要从根节点开始
  • 比较大小,找到小于当前值的元素,顺着指针往下走,继续比较大小,一直到达叶子节点,那么这个叶子节点就是你要操作的节点了。
  • 在叶子节点只有几种操作,一是叶子节点有空位置,那么直接插入进去,一是叶子节点满了,那么分裂一个节点出来。
  • 不管在叶子节点进行了那种操作,最后都要顺着指针回去,如果没有分裂,那么上层就不会分裂,可能会更新上层节点元素的值,如果分裂了,那么就带着两个分裂的节点往上走,该更新值就更新值,该分裂就分裂。
  • 如果一层一层分裂到最上层了,那么就新增一个根节点吧

查找操作和更新操作几乎一样,就是更新操作的前面两步,就不说了。

一般的更新的时候也是先查找,找到叶子节点,再更新,然后顺着指针往上走继续分裂,这个顺着往上走一般情况下首先想到的是双向指针,但是双向指针分裂的时候有点麻烦,需要把两个指针都重新指新节点,我实现的时候用了一个栈,查找叶子节点的时候把经过的节点依次压栈,到达叶子节点后,完成插入操作,往上遍历的时候依次把栈弹出来就行了,少了一个指针。

回到上一篇说的那个表2的第一列,如果是那个表的话,用这个B+树加上倒排链的话,最后的数据结构就长成这样子了(字符串的大小我随便写的,中文的顺序排列哥的脑子排不出来,你就把他们看成从小到大的顺序吧)

bt6.png

好了,至此,一个倒排索引就建立好了,由两部分组成,我实现的时候就是这么实现的,一个结构用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这个项目,点击原文链接可以看到 https://github.com/bolt/bolt ,这个是一个B+树实现的KVDB,而且是带事务的哦。

OK,这一篇就写到这了,周末要出去玩了,下周继续。。。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 157,198评论 4 359
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,663评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 106,985评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,673评论 0 202
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 51,994评论 3 285
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,399评论 1 211
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,717评论 2 310
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,407评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,112评论 1 239
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,371评论 2 241
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,891评论 1 256
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,255评论 2 250
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,881评论 3 233
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,010评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,764评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,412评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,299评论 2 260

推荐阅读更多精彩内容

  • B树的定义 一棵m阶的B树满足下列条件: 树中每个结点至多有m个孩子。 除根结点和叶子结点外,其它每个结点至少有m...
    文档随手记阅读 13,005评论 0 25
  • B树 1.前言: 动态查找树主要有:二叉查找树(Binary Search Tree),平衡二叉查找树(Balan...
    铁甲依然在_978f阅读 1,377评论 0 4
  • 前面已经说了倒排索引的基本原理了,原理非常简单,也很好理解,关键是如何设计第二个倒排表,倒排表的第二列也很好设计,...
    吴YH坚阅读 970评论 0 1
  • 原文链接 B树 1.前言: 动态查找树主要有:二叉查找树(Binary Search Tree),平衡二叉查找树(...
    非典型程序员阅读 1,095评论 0 3
  • 树的概述 树是一种非常常用的数据结构,树与前面介绍的线性表,栈,队列等线性结构不同,树是一种非线性结构 1.树的定...
    Jack921阅读 4,373评论 1 31