Lucene中的堆(Heap)[ScorerDocQueue,TopScoreDocCollector] lucene 大数据量 快速 排序
2011-11-07 12:14 yuejianjun 阅读(714) 评论(0) 编辑 收藏 举报http://quweiprotoss.blog.163.com/blog/static/408828832011523114133876/
一个经典的问题,也就是10^N个数,远超过内存的大小,如何排序。答案虽然我自己也想到了,但别人更早想到,经典做法,把文件拆成多份,然后多线程对文件分别进行排序,然后进行多路归并,多路归并时,经典做法就是用优先队列。这也是Lucene在And操作时选择的方法,在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](http://img.ph.126.net/ztSOfsXY9XNa5NyXjbCw5w==/3280590853564415734.jpg)
你假设看不到上图中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中也有upHeap和downHeap的图,画的也很好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个得分最高的文档时,也用到了优先队列。
在IndexSearcher的search函数中:
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();
}
pqTop是pq优先队列的第一个元素,它保持的是优先队列中得分最低的score值,注意,优先队列内部是全部文档中得分最高的一些doc id和它们的socre。最后三行的写法看起来有点….。updateTop中调用了downHeap,downHeap与ScorerDocQueue中的几乎一样,只是比较函数变为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);
}
循环中的pop和populateResults中的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个数,远超过内存的大小,如何排序。答案虽然我自己也想到了,但别人更早想到,经典做法,把文件拆成多份,然后多线程对文件分别进行排序,然后进行多路归并,多路归并时,经典做法就是用优先队列。这也是Lucene在And操作时选择的方法,在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](http://img.ph.126.net/ztSOfsXY9XNa5NyXjbCw5w==/3280590853564415734.jpg)
你假设看不到上图中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中也有upHeap和downHeap的图,画的也很好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个得分最高的文档时,也用到了优先队列。
在IndexSearcher的search函数中:
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();
}
pqTop是pq优先队列的第一个元素,它保持的是优先队列中得分最低的score值,注意,优先队列内部是全部文档中得分最高的一些doc id和它们的socre。最后三行的写法看起来有点….。updateTop中调用了downHeap,downHeap与ScorerDocQueue中的几乎一样,只是比较函数变为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);
}
循环中的pop和populateResults中的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);
}