Lucene源码分析 - MinShouldMatchSumScorer

本文深入分析Lucene 6.3.0中的MinShouldMatchSumScorer,探讨tail作为小顶堆的实现、nextDoc和advance的基本逻辑及详细代码,讨论其在满足最小匹配次数条件下的迭代器管理策略。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

本文的代码以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次。基本不步骤:

  1. 将lead中所有的迭代器插入tail堆中,如果tail堆满,则添加到head优先队列,并将这个迭代器后移到下一个docId上
  2. 从head中取出第一个迭代器,将迭代器设置为lead第一个元素,其当前位置的docId就是本次需要检测是否满足条件的docId
  3. 遍历head的剩余迭代器,如果迭代器的dodId等于lead中docId,则添加到lead中
  4. 检查lead中的元素个数是否有minShouldMatch个,如果有,则直接返回lead中的docId
  5. 如果没有,还需要判断tail中元素个数 + lead中元素个数是否超过minShouldMatch个,如果超过了,则取出tail中第一个元素,将其位置后移得到一个docId,看是否与lead中的docId一致,如果一致,则加入lead中,如果不一致,则加入head中
  6. 如果没超过,那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)

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值