终于把序号写到了第十篇(其实已是第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或者搜索中文西加加语言就行