代码改变世界

Lucene中的堆(Heap)[ScorerDocQueue,TopScoreDocCollector] lucene 大数据量 快速 排序

2011-11-07 12:14  yuejianjun  阅读(722)  评论(0编辑  收藏  举报

 http://quweiprotoss.blog.163.com/blog/static/408828832011523114133876/

      一个经典的问题,也就是10^N个数,远超过内存的大小,如何排序。答案虽然我自己也想到了,但别人更早想到,经典做法,把文件拆成多份,然后多线程对文件分别进行排序,然后进行多路归并,多路归并时,经典做法就是用优先队列。这也是LuceneAnd操作时选择的方法,在DisjunctionSumScorer中有ScorerDocQueue scorerDocQueue,它就是一个优先队列。

         ScorerDocQueue的成员有:

/* 保存堆中的元素 */

private final HeapedScorerDoc[] heap;

/* 堆最多元素数量 */

private final int maxSize;

/* 堆中当前元素数量 */

private int size;

/* heap[1]值相同,它是为了提高速度 */

private HeapedScorerDoc topHSD;

         HeapedScoreDoc类的成员只有两个,一个是Scorer本身,另一个是Scorer的第一个doc id也就是最小的一个doc id

private void initScorerDocQueue() throws IOException {

    scorerDocQueue = new ScorerDocQueue(nrScorers);

    for (Scorer se : subScorers) {

       if (se.nextDoc() != NO_MORE_DOCS) {

           scorerDocQueue.insert(se);

       }

    }

}

         DisjunctionSumScorer类中initScorerDocQueue函数将subScorers放入优先队列中。

public boolean insert(Scorer scorer) {

    if (size < maxSize) {

       put(scorer);

       return true;

    else {

       int docNr = scorer.docID();

       if ((size > 0) && (!(docNr < topHSD.doc))) { // heap[1] is top()

           heap[1] = new HeapedScorerDoc(scorer, docNr);

           downHeap();

           return true;

       else {

           return false;

       }

    }

}

         我没看出来什么时候会发生size>=maxSize。在size<maxSize情况下,就是《算法导论》中文版81页的MAX_HEAP_INSERT函数的大概逻辑。如果是size>=maxSize,并且如果size>0且它的第一个文档id大于原来根结点的文档id,就会替换它。再做downHeap

Lucene中的堆(Heap)[ScorerDocQueue,TopScoreDocCollector] - quweiprotoss - Koala++s blog

 

         你假设看不到上图中1那个叶子结点,并从(b)开始看,就差不多是这个函数的意思了。

private final void upHeap() {

    int i = size;

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

    HeapedScorerDoc node = heap[i];

    /* 得到它的父结点 */

    int j = i >>> 1;

    /* 如果没有到根结点,且子结点doc id 大于父结点doc id */

    while ((j > 0) && (node.doc < heap[j].doc)) {

       /* 把父结点换下来 */

       heap[i] = heap[j];

       i = j;

       /* 继续向上找 */

       j = j >>> 1;

    }

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

    heap[i] = node;

    /* 不管根结点有没有变,都重置一下 */

    topHSD = heap[1];

}

         wiki中也有upHeapdownHeap的图,画的也很好http://en.wikipedia.org/wiki/Binary_heap

private final void downHeap() {

    int i = 1;

    /* 保存根结点 */

    HeapedScorerDoc node = heap[i];

    /* 左子结点 */

    int j = i << 1;

    /* 右子结点 */

    int k = j + 1;

    /* j设置为两个子结点小doc id较小的一个 */

    if ((k <= size) && (heap[k].doc < heap[j].doc)) {

       j = k;

    }

    /* 如果大于当前结点 */

    while ((j <= size) && (heap[j].doc < node.doc)) {

       /* 把子结点换上去 */

       heap[i] = heap[j];

       i = j;

       /* 左子结点 */

       j = i << 1;

       /* 右子结点 */

       k = j + 1;

       /* j设置为两个子结点小doc id较小的一个 */

       if (k <= size && (heap[k].doc < heap[j].doc)) {

           j = k;

       }

    }

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

    heap[i] = node;

    /* 不管根结点有没有变,都重置一下 */

    topHSD = heap[1];

}

         downHeap与堆排序中循环内部的过程相似。在上面的链接中也是有图的。

public final boolean topNextAndAdjustElsePop() throws IOException {

    return checkAdjustElsePop(topHSD.scorer.nextDoc() !=

       DocIdSetIterator.NO_MORE_DOCS);

}

 

private boolean checkAdjustElsePop(boolean cond) {

    if (cond) { // see also adjustTop

       topHSD.doc = topHSD.scorer.docID();

    else { // see also popNoResult

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

       heap[size] = null;

       size--;

    }

    downHeap();

    return cond;

}

         scorer读取下一个doc后,它的doc id就会增加,或是没有更多doc了。那么就要重新调整堆结构,如果还有doc,就将当前的doc id保存,调用downHeap,否则就将最后一个元素放到第一个位置,这就更像堆排循环中的逻辑了。

         上一篇是写在AND操作时用到了优先队列,Lucene在得到top K个得分最高的文档时,也用到了优先队列。

         IndexSearchersearch函数中:

for (int i = 0; i < subReaders.length; i++) {

    collector.setNextReader(subReaders[i], docStarts[i]);

    Scorer scorer = weight.scorer(subReaders[i],

           !collector.acceptsDocsOutOfOrder(), true);

    if (scorer != null) {

       scorer.score(collector);

    }

}

         现在只关注最后一个函数:

public void score(Collector collector) throws IOException {

    collector.setScorer(this);

    int doc;

    while ((doc = nextDoc()) != NO_MORE_DOCS) {

       collector.collect(doc);

    }

}

         重要的函数是collector.collect

TopScoreDocCollector collector = TopScoreDocCollector.create(nDocs,

       !weight.scoresDocsOutOfOrder());

         TopScoreDocCollector.create函数会根据第二个参数,即是否排序,产生排序的InOrderScoreDocCollector或是不排序的OutofOrderTopScoreDocCollector。它们两者之间的区别不大,也就是是不是按doc id排序。这里看无序的collect实现:

public void collect(int doc) throws IOException {

    float score = scorer.score();

 

    /* 命中数加1 */

    totalHits++;

    /* 加上doc的起始值 */

    doc += docBase;

    if (score < pqTop.score

           || (score == pqTop.score && doc > pqTop.doc)) {

       return;

    }

    /* 设置第一个元素的值 */

    pqTop.doc = doc;

    pqTop.score = score;

    /* 调整堆结构 */

    pqTop = pq.updateTop();

}

         pqToppq优先队列的第一个元素,它保持的是优先队列中得分最低的score值,注意,优先队列内部是全部文档中得分最高的一些doc id和它们的socre。最后三行的写法看起来有点….updateTop中调用了downHeapdownHeapScorerDocQueue中的几乎一样,只是比较函数变为lessThan

protected final boolean lessThan(ScoreDoc hitA, ScoreDoc hitB) {

    if (hitA.score == hitB.score)

       return hitA.doc > hitB.doc;

    else

       return hitA.score < hitB.score;

}

         如果分数一样,比较doc id

public final TopDocs topDocs(int start, int howMany) {

    // 真正要取得多少个结果

    howMany = Math.min(size - start, howMany);

    ScoreDoc[] results = new ScoreDoc[howMany];

 

    /*

     * pq's pop()返回的是队列中的最小元素,所以需要丢弃开始的一些元素,直到

     * 达到所需的范围。注意这个循环通常不会被执行,因为通常的使用方式应该是

     * 调用方查询最后howMany个结果。这个函数主要是为了功能的完整性

     */

    for (int i = pq.size() - start - howMany; i > 0; i--) {

       pq.pop();

    }

 

    /* pq中得到请求的结果 */

    populateResults(results, howMany);

 

    return newTopDocs(results, start);

}

         循环中的poppopulateResults中的pop函数如下:

public final T pop() {

    if (size > 0) {

       T 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;

}

         取得根结点,然后调整堆结构。

protected TopDocs newTopDocs(ScoreDoc[] results, int start) {

    /* 我们计算maxScore是为了在TopDocs中设置它,如果start==0,表示最大的元素

     * 已经在results中了,那就直接将它的值用作为maxScore。如果start!=0,则将

     * 所有元素弹出直到取得最大的元素,并将它的值作为maxScore */

    float maxScore = Float.NaN;

    if (start == 0) {

       maxScore = results[0].score;

    else {

       for (int i = pq.size(); i > 1; i--) {

           pq.pop();

       }

       maxScore = pq.pop().score;

    }

 

    /* 最大值是归范化用的 */

    return new TopDocs(totalHits, results, maxScore);

}

 

 

 

      一个经典的问题,也就是10^N个数,远超过内存的大小,如何排序。答案虽然我自己也想到了,但别人更早想到,经典做法,把文件拆成多份,然后多线程对文件分别进行排序,然后进行多路归并,多路归并时,经典做法就是用优先队列。这也是LuceneAnd操作时选择的方法,在DisjunctionSumScorer中有ScorerDocQueue scorerDocQueue,它就是一个优先队列。

         ScorerDocQueue的成员有:

/* 保存堆中的元素 */

private final HeapedScorerDoc[] heap;

/* 堆最多元素数量 */

private final int maxSize;

/* 堆中当前元素数量 */

private int size;

/* heap[1]值相同,它是为了提高速度 */

private HeapedScorerDoc topHSD;

         HeapedScoreDoc类的成员只有两个,一个是Scorer本身,另一个是Scorer的第一个doc id也就是最小的一个doc id

private void initScorerDocQueue() throws IOException {

    scorerDocQueue = new ScorerDocQueue(nrScorers);

    for (Scorer se : subScorers) {

       if (se.nextDoc() != NO_MORE_DOCS) {

           scorerDocQueue.insert(se);

       }

    }

}

         DisjunctionSumScorer类中initScorerDocQueue函数将subScorers放入优先队列中。

public boolean insert(Scorer scorer) {

    if (size < maxSize) {

       put(scorer);

       return true;

    else {

       int docNr = scorer.docID();

       if ((size > 0) && (!(docNr < topHSD.doc))) { // heap[1] is top()

           heap[1] = new HeapedScorerDoc(scorer, docNr);

           downHeap();

           return true;

       else {

           return false;

       }

    }

}

         我没看出来什么时候会发生size>=maxSize。在size<maxSize情况下,就是《算法导论》中文版81页的MAX_HEAP_INSERT函数的大概逻辑。如果是size>=maxSize,并且如果size>0且它的第一个文档id大于原来根结点的文档id,就会替换它。再做downHeap

Lucene中的堆(Heap)[ScorerDocQueue,TopScoreDocCollector] - quweiprotoss - Koala++s blog

 

         你假设看不到上图中1那个叶子结点,并从(b)开始看,就差不多是这个函数的意思了。

private final void upHeap() {

    int i = size;

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

    HeapedScorerDoc node = heap[i];

    /* 得到它的父结点 */

    int j = i >>> 1;

    /* 如果没有到根结点,且子结点doc id 大于父结点doc id */

    while ((j > 0) && (node.doc < heap[j].doc)) {

       /* 把父结点换下来 */

       heap[i] = heap[j];

       i = j;

       /* 继续向上找 */

       j = j >>> 1;

    }

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

    heap[i] = node;

    /* 不管根结点有没有变,都重置一下 */

    topHSD = heap[1];

}

         wiki中也有upHeapdownHeap的图,画的也很好http://en.wikipedia.org/wiki/Binary_heap

private final void downHeap() {

    int i = 1;

    /* 保存根结点 */

    HeapedScorerDoc node = heap[i];

    /* 左子结点 */

    int j = i << 1;

    /* 右子结点 */

    int k = j + 1;

    /* j设置为两个子结点小doc id较小的一个 */

    if ((k <= size) && (heap[k].doc < heap[j].doc)) {

       j = k;

    }

    /* 如果大于当前结点 */

    while ((j <= size) && (heap[j].doc < node.doc)) {

       /* 把子结点换上去 */

       heap[i] = heap[j];

       i = j;

       /* 左子结点 */

       j = i << 1;

       /* 右子结点 */

       k = j + 1;

       /* j设置为两个子结点小doc id较小的一个 */

       if (k <= size && (heap[k].doc < heap[j].doc)) {

           j = k;

       }

    }

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

    heap[i] = node;

    /* 不管根结点有没有变,都重置一下 */

    topHSD = heap[1];

}

         downHeap与堆排序中循环内部的过程相似。在上面的链接中也是有图的。

public final boolean topNextAndAdjustElsePop() throws IOException {

    return checkAdjustElsePop(topHSD.scorer.nextDoc() !=

       DocIdSetIterator.NO_MORE_DOCS);

}

 

private boolean checkAdjustElsePop(boolean cond) {

    if (cond) { // see also adjustTop

       topHSD.doc = topHSD.scorer.docID();

    else { // see also popNoResult

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

       heap[size] = null;

       size--;

    }

    downHeap();

    return cond;

}

         scorer读取下一个doc后,它的doc id就会增加,或是没有更多doc了。那么就要重新调整堆结构,如果还有doc,就将当前的doc id保存,调用downHeap,否则就将最后一个元素放到第一个位置,这就更像堆排循环中的逻辑了。

         上一篇是写在AND操作时用到了优先队列,Lucene在得到top K个得分最高的文档时,也用到了优先队列。

         IndexSearchersearch函数中:

for (int i = 0; i < subReaders.length; i++) {

    collector.setNextReader(subReaders[i], docStarts[i]);

    Scorer scorer = weight.scorer(subReaders[i],

           !collector.acceptsDocsOutOfOrder(), true);

    if (scorer != null) {

       scorer.score(collector);

    }

}

         现在只关注最后一个函数:

public void score(Collector collector) throws IOException {

    collector.setScorer(this);

    int doc;

    while ((doc = nextDoc()) != NO_MORE_DOCS) {

       collector.collect(doc);

    }

}

         重要的函数是collector.collect

TopScoreDocCollector collector = TopScoreDocCollector.create(nDocs,

       !weight.scoresDocsOutOfOrder());

         TopScoreDocCollector.create函数会根据第二个参数,即是否排序,产生排序的InOrderScoreDocCollector或是不排序的OutofOrderTopScoreDocCollector。它们两者之间的区别不大,也就是是不是按doc id排序。这里看无序的collect实现:

public void collect(int doc) throws IOException {

    float score = scorer.score();

 

    /* 命中数加1 */

    totalHits++;

    /* 加上doc的起始值 */

    doc += docBase;

    if (score < pqTop.score

           || (score == pqTop.score && doc > pqTop.doc)) {

       return;

    }

    /* 设置第一个元素的值 */

    pqTop.doc = doc;

    pqTop.score = score;

    /* 调整堆结构 */

    pqTop = pq.updateTop();

}

         pqToppq优先队列的第一个元素,它保持的是优先队列中得分最低的score值,注意,优先队列内部是全部文档中得分最高的一些doc id和它们的socre。最后三行的写法看起来有点….updateTop中调用了downHeapdownHeapScorerDocQueue中的几乎一样,只是比较函数变为lessThan

protected final boolean lessThan(ScoreDoc hitA, ScoreDoc hitB) {

    if (hitA.score == hitB.score)

       return hitA.doc > hitB.doc;

    else

       return hitA.score < hitB.score;

}

         如果分数一样,比较doc id

public final TopDocs topDocs(int start, int howMany) {

    // 真正要取得多少个结果

    howMany = Math.min(size - start, howMany);

    ScoreDoc[] results = new ScoreDoc[howMany];

 

    /*

     * pq's pop()返回的是队列中的最小元素,所以需要丢弃开始的一些元素,直到

     达到所需的范围。注意这个循环通常不会被执行,因为通常的使用方式应该是

     调用方查询最后howMany个结果。这个函数主要是为了功能的完整性

     */

    for (int i = pq.size() - start - howMany; i > 0; i--) {

       pq.pop();

    }

 

    /* pq中得到请求的结果 */

    populateResults(results, howMany);

 

    return newTopDocs(results, start);

}

         循环中的poppopulateResults中的pop函数如下:

public final T pop() {

    if (size > 0) {

       T 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;

}

         取得根结点,然后调整堆结构。

protected TopDocs newTopDocs(ScoreDoc[] results, int start) {

    /* 我们计算maxScore是为了在TopDocs中设置它,如果start==0,表示最大的元素

     已经在results中了,那就直接将它的值用作为maxScore。如果start!=0,则将

     所有元素弹出直到取得最大的元素,并将它的值作为maxScore */

    float maxScore = Float.NaN;

    if (start == 0) {

       maxScore = results[0].score;

    else {

       for (int i = pq.size(); i > 1; i--) {

           pq.pop();

       }

       maxScore = pq.pop().score;

    }

 

    /* 最大值是归范化用的 */

    return new TopDocs(totalHits, results, maxScore);

}