信息检索笔记(10)-Lucene文档评分机制

Lucene文档评分机制

再论文档评分中提到可以不对所有文档的评分结果排序而直接选出Top K篇文档

计算出文档的得分以后,最后一步就是选出得分最高的K 篇文档呈现给用户。尽管可以先对上述所有得分进行排序然后再挑选出前K 个结果,但是一个更好的方法是通过某种堆结构只返回头K 篇文档。假定余弦相似度(或某种相似度计算方法)得分非零的文档数目是J,那么建立这样的堆结构需要2J 次比较,对于排名前K 的每篇文档从上述堆结构中返回时都需要进行logJ 次比较。

 

Wiki资料:

Binary heap: http://en.wikipedia.org/wiki/Binary_heap

Heap sort: http://en.wikipedia.org/wiki/Heapsort

堆排序图例

Heapsort-example

 

考虑实际的需求肯定不是对所有结果按照分数排序(布尔查询的结果可能有成千上万),而是找出总结果中的前20条、40条等。这里的20条、40条就要求有序了。如果堆的大小不限的话,就可以生成一个跟结果个数同样大的最大堆,然后不断的insert,adjust,最后pop前20条、40条,也许还要更多。但这是完全没有必要的,不仅空间上挥霍可能会比较大,而且对大数量数据每次adjust会相当漫长,效率极其低下。

 

所以,要采用一种“受限堆”(受限指堆的大小有一定的限制)方法。一种自然的想法是采用上面的最大堆方式,只是堆大小固定,这样在堆未满之前,新来的文档不断的insert,adjust即可。但在堆已经满时,对于新来的文档,要和堆中得分最小的文档进行比较,新文档得分小的话可以直接丢弃,比堆中得分最小的文档大时才插入堆。但对于经典Binary heap最大堆的实现来说,堆中最小的文档可能在四个叶子节点的任意一个位置上,因此如果采用这样的“受限最大堆”方法的话,对新文档的插入判断效率会比较低下。

 

进而我们可以采用最小堆的方法,新文档不断的insert,adjust,由于堆的size可能远远小于结果个数,因而很快堆就满了。这时,就需要把后续的ScoreDoc的得分跟堆顶文档比较。如果比堆顶文档小(因为咱们实际上要的是大得分文档)就直接抛弃,反之如果比堆顶文档大,则将堆顶文档替代掉,并做adjust。最后在输出top K ScoreDoc的时候,先吐出来的时候倒一下序就可以了。

 

Lucene top K ScoreDoc需要用到优先级队列PriorityQueue,他是基于上面理论的“受限最小堆”实现的。

注:java JDK也有个PriorityQueue,它采用的是不固定大小的最大堆实现的,这也是Lucene没使用它,而自己实现PriorityQueue的原因。

 

PriorityQueue代码实现分析


PriorityQueue成员

       private int size ;                  //已存文档个数,也代表指针位置

       private int maxSize ;               //堆大小

       protected Object[] heap;          // 存储堆中文档
 

文档的插入

       public Object insertWithOverflow(Object element) {

             //堆未满的时候

             if (size < maxSize) {

                  put(element);

                   return null;

            }

             //新文档跟堆顶值比较,如果比堆顶值大,则替换 堆顶文档,并调整堆

             else if (size > 0 && !lessThan(element, heap[1])) {

                  Object ret = heap[1];

                   heap[1] = element;

                  adjustTop();

                   return ret;

            } else {

                   return element;

            }

      }
 

当加入新文档时,此时堆未满,则从下往上调整

       private final void upHeap() {

             int i = size ;

             //保存最后一个结点,即刚才添加进去的叶子文档结点

            Object node = heap[i]; // save bottom node

             //得到它的父结点

             int j = i >>> 1;

             //有父节点,并且要插入的文档得分比父节点的得分要小,则要交换位置

             while (j > 0 && lessThan(node, heap[j])) {

                   heap[i] = heap [j]; // shift parents down

                  i = j;

                   //迭代父节点的父节点,以此类推

                  j = j >>> 1;

            }

             //要插入的文档放入合适位置

             heap[i] = node; // install saved node

      }

 

当堆满的时候,从上往下调整

       private final void downHeap() {

             int i = 1;

             // 第一个元素,也就是刚插入的文档

            Object node = heap[i]; // save top node

             //左子结点

             int j = i << 1; // find smaller child

             //右子结点

             int k = j + 1;

             //将j设置为两个子结点中的得分较小的一个

             if (k <= size && lessThan(heap[k], heap[j])) {

                  j = k;

            }

             while (j <= size && lessThan(heap[j], node)) {

                   //迭代进行中

                   heap[i] = heap [j]; // shift up child

                  i = j;      

                   //左子结点

                  j = i << 1;

                   //右子结点

                  k = j + 1;

                   //将j设置为两个子结点中的得分较小的一个

                   if (k <= size && lessThan(heap[k], heap[j])) {

                        j = k;

                  }

            }

             //将结点放入合适的位置

             heap[i] = node; // install saved node

      }

 

最后最小堆pop出文档

       public final Object pop() {

             if (size > 0) {

                  Object result = heap[1]; // save first value

                   heap[1] = heap [size ]; // move last to first

                   heap[size ] = null; // permit GC of objects

                   size--;

                  downHeap(); // adjust heap

                   return result;

            } else

                   return null ;

      }
 

相同得分文档的处理

堆排序是不稳定的,即同样“大小”的结果,排完序之后,本来排在前面的结果不一定还在“大小”与之相等的原来排在其后面的元素的前面了。

 

考虑对结果的分页处理,用户每一次翻页操作都是一次全新的搜索请求,请求参数与第一次完全雷同,只是请求结果的offset产生了变更。而服务器端对每次要求经过同样的逻辑搜索之后,依据“offset”截取用户想要的那段结果返回。因为翻页操作导致每次的offset不同,进而导致构建的最小堆的maxSize不同,这样之前如果相同得分文档的情况处置的比较随便的话,就会发生了不稳固的情况。即在某size的情况下,某文档排在20位,另一size的情形下,可能排在了21位,前后位次还老是很濒临,且呈现在前后页的边界处,进而导致用户翻页后某篇在前页出现的文档在后页中又再次出现。

 

综上对PriorityQueue中的abstract函数lessThan的实现要特别注意(注意相同得分文档的处理)。

Lucene中HitQueue的实现(得分相同的话,再比较文档doc id)

	protected final boolean lessThan(Object a, Object b) {

		ScoreDoc hitA = (ScoreDoc) a;

		ScoreDoc hitB = (ScoreDoc) b;

		if (hitA.score == hitB.score)

			return hitA.doc > hitB.doc;

		else

			return hitA.score < hitB.score;

	}

你可能感兴趣的:(Lucene)