本文的代码以lucene-core 6.3.0为准,包含MinShouldMatchSumScorer的
iterator
函数和执行原理。转载请注明出处。
0 基本信息
- 对于每个迭代器而言,cost()代表这个迭代器的迭代路径长度,对于倒排表的迭代器,cost()返回的是倒排表的长度。
lead
变量是链表,存的迭代器的当前位置的docId都相等。tail
变量是堆结构, 按照迭代器的cost构建的小顶堆。head
变量是优先队列,按照迭代器当前位置的docId从小到大排序。freq
表示lead
链表中迭代器的数量。
1 源码分析
1.1 tail 是小顶堆
tail是个小顶堆,删除元素只在堆顶进行,添加元素只在堆的末尾进行,删除元素的函数popTail
:
private DisiWrapper popTail() {
final DisiWrapper result = tail[0]; // 被删除的堆顶元素
tail[0] = tail[--tailSize]; // 从堆尾取出一个元素放到堆顶,这个时候堆是不平衡的
downHeapCost(tail, tailSize); // 从堆顶往下调整堆
return result;
}
从堆顶往下调整,将cost最小的迭代器不断往上推,downHeapCost
函数如下:
private static void downHeapCost(DisiWrapper[] heap, int size) {
int i = 0;
final DisiWrapper node = heap[0]; // 记录整个堆的 root节点
int j = leftNode(i);
if (j < size) { // 判断当前的 root节点是否是叶子节点
int k = rightNode(j);
if (k < size && heap[k].cost < heap[j].cost) {
// 如果当前节点有左右子节点,那么选出子节点中cost最小的那个,记录到 j
j = k;
}
// 判断当前 root节点是否比左右子节点中cost最小的那个还要小
// 如果是,则将最小的那个节点推到 root节点
if (heap[j].cost < node.cost) {
do {
// 将cost较小的节点推到 root节点
heap[i] = heap[j];
// 将 root节点设置为较cost小的节点
i = j;
j = leftNode(i);
k = rightNode(j);
if (k < size && heap[k].cost < heap[j].cost) { // 寻找 root节点的子节点中 cost较小的那个
j = k;
}
} while (j < size && heap[j].cost < node.cost); // 循环直到 root节点比子节点中最小的那个还要小为止
heap[i] = node; // 将整个堆的 root节点设置到当前的 root节点
}
}
}
另外在堆的末尾添加元素的addTail
函数和调整堆的函数upHeapCost
的原理跟上面两个函数差不多。
1.2 nextDoc的基本逻辑
nextDoc
的目的是找到所有迭代器的下一个docId中,docId的出现次数至少minShouldMatch次。基本不步骤:
- 将lead中所有的迭代器插入tail堆中,如果tail堆满,则添加到head优先队列,并将这个迭代器后移到下一个docId上
- 从head中取出第一个迭代器,将迭代器设置为lead第一个元素,其当前位置的docId就是本次需要检测是否满足条件的docId
- 遍历head的剩余迭代器,如果迭代器的dodId等于lead中docId,则添加到lead中
- 检查lead中的元素个数是否有minShouldMatch个,如果有,则直接返回lead中的docId
- 如果没有,还需要判断tail中元素个数 + lead中元素个数是否超过minShouldMatch个,如果超过了,则取出tail中第一个元素,将其位置后移得到一个docId,看是否与lead中的docId一致,如果一致,则加入lead中,如果不一致,则加入head中
- 如果没超过,那lead中的docId是不满足条件的,那么直接把lead中的所有元素插入tail,并取出head中第一个元素设置为lead第一个元素,重复步骤4,直到找到一个满足条件的docId,或者所有迭代器移到了末尾
从这里看出来,tail存在的意义是每次检测某个docId是否满足条件的时候,总是把cost较小的迭代器拿出来,并后移检测。事实上没有tail,这个功能还是能做,那tail的存在是否真的意义,就取决于总是取cost较小的迭代器是否有意义,但是就我读到的lucene中的内容,cost并不是指迭代器后移成本的大小,而是指迭代路径的长度,所以我认为这里的tail并没有太大的意义,如果读到博客的你有什么更好的思路,麻烦联系我修改。联系方式在博客最后面。
1.3 nextDoc的详细代码
初始化MinShouldMatchSumScorer
代码:
MinShouldMatchSumScorer(Weight weight, Collection<Scorer> scorers, int minShouldMatch, float[] coord) {
super(weight);
if (minShouldMatch > scorers.size()) {
throw new IllegalArgumentException("minShouldMatch should be <= the number of scorers");
}
if (minShouldMatch < 1) {
throw new IllegalArgumentException("minShouldMatch should be >= 1");
}
this.minShouldMatch = minShouldMatch;
this.coord = coord;
this.doc = -1;
head = new DisiPriorityQueue(scorers.size() - minShouldMatch + 1);
// tail中最多是 minShouldMatch - 1 个scorers
tail = new DisiWrapper[minShouldMatch - 1];
// 所有 scorer 都写入lead链表
for (Scorer scorer : scorers) {
addLead(new DisiWrapper(scorer));
}
List<ChildScorer> children = new ArrayList<>();
for (Scorer scorer : scorers) {
children.add(new ChildScorer(scorer, "SHOULD"));
}
this.childScorers = Collections.unmodifiableCollection(children);
this.cost = cost(scorers, minShouldMatch);
}
iterator
迭代器的nextDoc
的具体代码如下:
public int nextDoc() throws IOException {
// 将 lead中所有的变量写入 tail堆,剩余的变量写入 head队列中
// 这样 head队列中的迭代器就是cost最小的迭代器,将cost最小的迭代器
// 都移动到各自的下一个 docId的位置上
for (DisiWrapper s = lead; s != null; s = s.next) {
// 将 lead 中所有的迭代器写入 tail 堆中
final DisiWrapper evicted = insertTailWithOverFlow(s);
if (evicted != null) {
// tail 中的元素个数已经满,写入 head队列,并且都移动到下一个docId的位置
if (evicted.doc == doc) {
evicted.doc = evicted.iterator.nextDoc();
} else {
evicted.doc = evicted.iterator.advance(doc + 1);
}
// 迭代器写入 head 队列中
head.add(evicted);
}
}
// 设置 lead的元素,确定当前检测的 docId,这个 docId不一定满足条件
setDocAndFreq();
// 返回满足条件的 docId
return doNext();
}
setDocAndFreq
函数计算出head
队列中,与最小docId相同的迭代器个数,并将这些迭代器写入lead链表。
private void setDocAndFreq() {
lead = head.pop();
lead.next = null;
freq = 1;
doc = lead.doc; // docId 就是 head中第一个迭代器的 docId
while (head.size() > 0 && head.top().doc == doc) {
addLead(head.pop());
}
}
doNext
函数检测如果lead中迭代器个数超过minShouldMatch个,那么直接返回当前docId,如果不超过,则将tail中cost比较大的迭代器位置后移,直到找到足够数量的docId,或者不可能有minShouldMatch个docId为止。
private int doNext() throws IOException {
while (freq < minShouldMatch) {
if (freq + tailSize >= minShouldMatch) {
// 如果当前 tail中还有足够的迭代器,让当前 docId 的数量有可能超过 minShouldMatch
// 那么久取 tail中的迭代器,并后移获取docId,如果等于当前 docId,则写入 lead链表
advanceTail();
} else {
// tail中迭代器数量加上 lead中迭代器数量不能让当前 docId数量达到条件
// 将 lead中的迭代器写入 tail
pushBackLeads();
// 重新计算下一个 docId是否满足条件
setDocAndFreq();
}
}
return doc;
}
1.4 advance的详细代码
advance
函数的基本逻辑跟nextDoc
差不多,只是初始化的时候,需要把lead中的迭代器都移动到target所在的位置上,把head中的迭代器的docId小于target的都移动到target所在的位置上,迭代器只能往后一定,如果传入的target的值比当前docId还小,那得到的结果是没有任何保证的。
public int advance(int target) throws IOException {
// 将 lead 中的迭代器都插叙tail和head
for (DisiWrapper s = lead; s != null; s = s.next) {
final DisiWrapper evicted = insertTailWithOverFlow(s);
if (evicted != null) {
// 插入head中的迭代器都要后移到target上
evicted.doc = evicted.iterator.advance(target);
head.add(evicted);
}
}
// 将 head中迭代器的 docId小于 target的迭代器后移
DisiWrapper headTop = head.top();
while (headTop.doc < target) {
// 确定 当前迭代器的 cost是否会比 tail中最小的还要小
final DisiWrapper evicted = insertTailWithOverFlow(headTop);
evicted.doc = evicted.iterator.advance(target);
headTop = head.updateTop(evicted);
}
// 设置 lead的元素,确定当前检测的 docId,这个 docId不一定满足条件
setDocAndFreq();
// 返回满足条件的 docId
return doNext();
}
以上就是MinShouldMatchSumScorer的所有源码及我的解读,如果有错误的地方,请联系我修改,谢谢!(企鹅号308384127)