倒排链上的查询剪枝技术学习总结

背景

   最近对海量高维数据检索产生比较浓厚的兴趣,学习相关技术,其中对检索倒排索引这方面又学到了新东西,在这整理巩固一下。在海量数据查询上,倒排索引用得最多,倒排索引其实就是正排方式的逆转排列,海量数据中一个内容的倒排链通常也会很长,减少不必要的链表遍历是提升检索效率关键。本文大部分内容都转自别人的博客博客,这里大致列出其中核心的步骤,算是一个总结。


skiplist备忘

   如今大部分工具使用的倒排链已经不是简单的链表了。一个常用,比如lucene中用的,叫skiplist,是一种高效的链表结构,在查询、添加、删除的时间复杂度上做到O(logN)。数据结构如下图:


查询的过程很简单,从顶层开始,往后查询遇到节点的next()比待查的大或者到NIL了,节点不变下移一层继续向后查询,如此反复,直到到了底层还没查到。skiplist的资料也比较多,这里就不赘述了。


链表集合操作

   直接引用转述这篇博文:http://www.cnblogs.com/forfuture1978/archive/2010/04/04/1704258.html  。作者很细致地把过程都列出来了,真是方便了大家啊,建议顺着读一边。

    链表集合求交 

      lucene中用的是ConjunctionScorer ,大致过程是每条倒排链不断的推进到小于等于当前最大节点的位置。当然实现细节还是很丰富的,作者很细心的把过程都列出来了,建议顺着读一边。这里摘抄部分:

首先把倒排链按第一个next排序:

    

查看0~7的倒排链的第一个和最后一个是否相同,不同就开始找;取最后一个倒排的第一个元素8作为终点, 第一个链表开始找8


第0个链表 跳过1到了10,那么8也不用找了都去找10就行了


第1根链表找到了11,那么10也不用找了,找11,之后都这么做

......

之后遇到11,本次交集操作找到一个11,

  后续的计算也是同理,当然整个代码实现会比较复杂和讨巧。基本思路就是每条倒排链能根据当前文档迅速跳过不符合的docid,由于倒排链可以用skiplist查询,因此即使很长的倒排链,如果交集的数量很少,整个求解过程可以很快跳过不需要比较的节点。

     链表求并集: 

     集合求并明显是一个不落的取出来,必然得不能随便略过。lucene中用的是DisjunctionSumScorer,既然链表已经是有序了,那么每次弹出最小的docid,就能保证查询的过程不会漏。如果有minShouldMatch (mm) 的要求,即最少命中几个,只要累加的和小于其直接丢弃即可,先看直观的做法:

原始的倒排链:scorer就是一个倒排链

 保持score在(当前doc)上有序:

    开始,计算2

2计算了一次,nrMatcher(2) = 1,scorer0推进到3, 保持scorer有序后,改变位置:

,同理算scorer_1, scorer_3 的 

2最后统计完是3次,开始下一轮计算

,后续也是一样,省略很多步之后是计算doc7的某个时候:

总结这种计算方式:每次链表后移一步,不断保持scorer的当前doc有序, 这种方式每次倒排链只能移动一步,而且就必须调整倒排链之间的顺序因此消耗不小。在minShouldMatch(mm)的条件下能否利用上这个约束实现一些剪枝?

 minShouldMatch上的优化

     lucene中已经实现了把交集和并集计算结合起来加速这种minShouldMatch的OR查询了。博客提到讲倒排链分为两部分,mm-1个scorer放进一个mmstack中,剩下的放进heap,首先从heap中按原生方法取出当前已经统计好的docid,这个docid之后到mmstack中每个求交。我当时读下来时候还云里雾里,heap中的计算方式和原来一样,当然剩下的量少了,但是求交也是每个倒排都做,求交操作恐怕不比原来简单往后移一个doc简单,计算一分为二但计算量似乎没少。经过一些思考,还有简单看了一下MinShouldMatchScorer的实现,终于大致明白其中的技巧:求交的部分能像集合求交介绍那种快速省略不满足要求的doc。MinShouldMatchScorer的实现略微复杂,这里直接按照思路分析一下:

假设要求nrMatch至少4,那么图(2)中, 从第4个scorer看(scorer2),2就完全不可能满足要求。

,具体的办法是先把scorer0,1,3 放进mmstack,scorer_2和score_4放heap, 首先heap中的3统计出来,2次,(scorer_2变为5,7,8;scorer_4变为7)之后mmstack中的0,1,3倒排链分别要统计3,那么他们的2都直接跳过了,scorer0有一个3,那3的次数就是3,当然不符合要求,之后整体就剩下这样:

,  这次要数的是7(第4个槽),scorer0,2,3 放进mmstack,scorer_4和score_1放heap,heap中是统计次数是1,很明显 scorer0,2,3 都有,而5也都直接跳过了,最后7的次数是4,符合要求。

    为啥mmstack是mm-1个,而不是mm个?如果是mm个,你从mm+1的槽开始统计,恰好mmstack中存在mm个一样的排在第一个,你就会漏统计。例如图(2)中如果是算mm=3, 你从第4个槽开始需要统计的是3,那么你会漏掉出现了3次的doc2。

可以看到这种办法相比原始一个个统计的办法,核心是这种算法能保证每次有mm-1个倒排链可以进行快速跳跃,这样只要进行几次所有的倒排链会被迅速被剪枝。


maxscorer求法

    在高维OR查询中根据评分取TopK,是个很普通的需求,全查询出来再部分排序是一种办法,那么有没有办法减少查询的数量?下面介绍个MaxScorer就是提升OR查询效率的,基本思想是考察每个词的贡献上限来估计文档的相关性上限,从而建立一个阈值,对倒排链进行减枝,从而得到提速的效果 (是不是就是WAND算法?)。具体一点,

对于一个OR查询,每个Term都有一个倒排链Inverted List,每个list有长有短,每个list中的每个doc都包含了该term,doc都有和当前term的一个相似性打分(可以看做是term对doc的比重)。选最大打分的出来,作为该list的Upper Bound。根据这个ub将倒排链升序排序,每个term存一个累积阈值c* =自己ub + 上一个term累积阈值c',当topK中最小的阈值大于某个term的阈值时候,我们就可以断定整条倒排链都不需要检查了,直接跳过。

     直接套用博客的内容,

假设queryString=“The OR conferenceOR  berlinOR buzzwords”,

简单按照term对应的docFreq(包含该Term的文档数)作为term对doc的打分从大到小排序。图中方块的大小代表该文档和term的相似性打分的大小程度,这个值在索引时候就能够确定,并且可以得到每个term对应的倒排列表中的最大打分值(红框)。基本倒排如下:


    

     在搜索阶段,得到倒排列表之后,得到最大得分,然后最大得分累加起来。

C*(the) = S*(the),

C*(conference)= S*(the) + S*(conference),

C*(berlin)= S*(the) + S*(conference) + S*(berlin),

C*(buzzwords)= S*(the) + S*(conference) + S*(berlin)+ S*(buzzwords)

下面两个图分别是the和buzzwords的c*累积



   假设要计算 Top-2 ,可以得到文档4和2,记录最小的阈值threshold,过程如下:

   

       每次倒排移动,都要拿threshold和当前c*进行比较,如果c* < threshold,整条倒排都不用比了。如果还没有,那就一直得遍历倒排链。期间需要不断更新top2的候选集,不断踢掉top2中最小的,并更新threshold。比如到了下图的时候:



     top2列表一直有变化,到文档9和4的时候 threshold超过了c*(the),c*和threshold的比较用蓝框圈出调整C*为下一个即C*(conference),那么这时跳跃表跳到docID=16处再开始比较,:

 

    

    倒排的比较到了16,进行16位置的计算之后,top2变成了16和9,threshold也更新了。继续,开始计算18:


  

   计算到doc 18的时候,又发现c*(conference) < threshold了,除了更新top2堆之外,还可以认定conference的倒排链后面就不再需要遍历了。直接跳到29去比较。


    比较完,top2结果就出来了。

    可以看出中间过程的秘诀在于 c*与topk最小的threshold的比较,为何threshold一大于c*就能直接剪枝整个倒排链?因为每个term的c* 都是包含自己打分贡献的最大可能,既然已经是最大值了,如果还小于threshold,那么包括自己term在内的所有累加过的terms 都不可能超过这个threshold,换句话说一个文档只包含这么几个terms 那累加后的权重必然也小于c*, 因此没必要继续检查了;如果文档还包含其他更大的term,则还有累加以后超过threshold的可能,后面也不会漏掉。例如上图中文档4, 19, 27 最多只包含到conference,它们累加完the和conference的打分肯定不如conference的 c*大,因为它们只会覆盖the 和 conference的倒排链,只要threshold已经超过conference的c*, 那么这几篇肯定没必要比了。

   我感觉maxScorer似乎不必按docFreq进行排序,似乎可以根据打分公式来设计,如果真的这样那么这个算法就不是那么容易实现了,(lucene中不知道是不是DisjunctionMaxScorer,源码看起来和算法不太一样,又或者是lucene对topk的计算已经抽象得很深了?)。我猜想MaxScorer还可以和minShouldMatch结合起来,当然那就更复杂了。

    剪枝是搜索算法中的精髓,无论数据挖掘还是AI中,都有组合爆炸的搜索空间需要有效剪枝的问题,我相信学习剪枝更能增加算法在实际中的有效运用。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值