Fork me on GitHub

Lucene 源码系列—— Collector 收集各个 Segment 命中的 docs

在搜索阶段,每当 Lucene 找到一个满足查询条件的文档(Document),便会将该文档的文档号(docId)交给 Collector,并在 Collector 中对收集的文档号集合进行排序(sorting)、过滤(filtering)或者用户自定义的操作。

  本篇文章将根据图 1 中的类图(Class diagram),介绍 Lucene 常用的几个收集器(Collector):

图 1:

1.png

第 1 小节 Collector 处理文档

  下图中描述的是 Collector 处理文档的流程:

图 2:

2.png

获得 LeafCollector

图 3:

3.png

  当索引目录中存在多个段时,我们需要从每个段中分别找出满足查询条件的文档,LeafReaderContext 即用来描述某一个段的信息,并且通过它能获得一个 LeafCollector 对象,在本篇文章中我们只要知道 LeafReaderContext 有这个功能(functionality)即可,在后面介绍 IndexReader 的文章中会展开。

  在搜索阶段,通过 Collector 类的方法来获得 LeafCollector 对象,下面是 Collector 类的代码,由于 Collector 类是一个接口类,并且只有两个接口方法,故列出并介绍:

public interface Collector {

    LeafCollector getLeafCollector(LeafReaderContext context) throws IOException;
  
    boolean needsScores();
  
}

接口方法 getLeafCollector

  通过该方法获得一个 LeafCollector 对象,Lucene 每处理完一个段,就会调用该方法获得下一个段对应的 LeafCollector 对象。

  LeafCollector 对象有什么作用

  • 首先看下 LeafCollector 类的结构:
public interface LeafCollector {

    void setScorer(Scorer scorer) throws IOException;
 
    /** 参数doc即文档号docId*/
    void collect(int doc) throws IOException;
}
  • setScorer 方法:调用此方法通过 Scorer 对象获得一篇文档的打分,对文档集合进行排序时,可以作为排序条件之一,当然 Scorer 对象包含不仅仅是文档的打分值,在后面介绍查询的文章中会展开
  • collect 方法:在这个方法中实现了对所有满足查询条件的文档进行排序(sorting)、过滤(filtering)或者用户自定义的操作的具体逻辑。在下文中,根据图 1 中不同的收集器(Collector)会详细介绍 collect 方法的不同实现

接口方法 needsScores

  设置该方法用来告知 Lucene 在搜索阶段,当找到一篇文档时,是否对其进行打分。如果用户期望的查询结果不依赖打分,那么可以设置为 false 来提高查询性能。

处理一篇文档

图 4:

4.png

  当 Lucene 找到一篇满足查询条件的文档,会调用 LeafCollector 的 setScorer(Scorer score)方法来执行 获得文档打分 的流程,随后在 获得文档号docId 流程后获得一个 docId,最后调用 LeafCollector 的 collect(int doc)方法(参数 doc 即文档号 docId)来实执行 处理该文档 的流程,在该流程中,实现对文档进行排序(sorting)、过滤(filtering)或者用户自定义的操作。

TimeLimitingCollector

  在介绍完 Collector 处理文档的流程后,我们依次介绍图 1 中的收集器。

  TimeLimitingCollector 封装了其他的 Collector,用来限制 Collector 处理文档的时间,即设定了一次查询允许的最长时间 timeLimit。如果查询的时间超过 timeLimit,那么会抛出超时异常 TimeExceededException

  在哪些流程点会判断查询超时

  • 调用 Collector.getLeafCollector(LeafReaderContext context)方法时会执行超时判断,即图 3 中的 是否还有 LeafReaderContext 的流程点
  • 调用 LeafCollector.collect(int doc)方法时会执行超时判断,即图 4 中的 处理该文档 的流程点

  如何实现超时机制

  • 通过后台线程、解析值 resolution、计数器 counter 实现、timeLimit
    • 计数器 counter:AtomicLong 类型,用来描述查询已花费的时间
    • 解析值 resolution:long 类型的数值,触发查询超时的精度值
    • 后台线程:Thread.setDaemon(true)的线程
    • timeLimit:上文已经介绍
  • 后台线程先执行 counter 的累加操作,即调用 counter.addAndGet(resolution)的方法,随后调用 Thread.sleep(resolution)的方法,如此反复。收集文档号的线程在 判断查询超时的流程点处通过 counter.get()的值判断是否大于 timeLimit

  使用这种超时机制有什么注意点

  • 由于后台线程先执行 counter 的累加操作,随后睡眠,故收集文档号的线程超时的时间范围为 timeLimit - resolution 至 timeLimit + resolution 的区间,单位是 milliseconds,故查询超时的触发时间不是精确的
  • 由上一条可以知道,resolution 的值设置的越小,查询超时的触发时间精度越高,但是性能越差(例如线程更频繁的睡眠唤醒等切换上下文行为)
  • 由于使用了睡眠机制,在运行过程中实时将 resolution 的值被调整为比当前 resolution 较小的值时(比如由 20milliseconds 调整为 5milliseconds),可能会存在调整延迟的问题(线程正好开始睡眠 20milliseconds)
  • resolution 的值至少为 5milliseconds,因为要保证能正确的调用执行 Object.wait(long)方法

贪婪(greedy)模式:

  在开启贪婪模式的情况下(默认不开启),如果在 LeafCollector.collect( )中判断出查询超时,那么还是会收集当前的文档号并随后抛出超时异常,注意的是如果在 Collector.getLeafCollector( )中判断出查询超时,那么直接抛出超时异常。

FilterCollector

  FilterCollector 类是一个抽象类,它用来封装其他的 Collector 来提供额外的功能。

PositiveScoresOnlyCollector

  PositiveScoresOnlyCollector 首先过滤出文档的打分值大于 0 的文档号,然后将文档号交给封装的 Collector,由于 collect 方法比较简单,故列出:

public void collect(int doc) throws IOException {
    if (scorer.score() > 0) {
        in.collect(doc);
    }
}

  其中 in 是 PositiveScoresOnlyCollector 封装的 Collector,scorer 即一个 Scorer 对象。

CachingCollector

  CachingCollector 可以缓存 Collector 收集的一次搜索的结果,使得其他的 Collector 可以复用该 Collector 的数据。

  CachingCollector 缓存了哪些数据

  • List:在前面我们提到,LeafReaderContext 描述的是一个段内的信息,当索引目录中存在多个段,那么我们需要用 List 来缓存所有的 LeafReaderContext
  • List<int[ ]> docs:一个段中可能有多个满足查询条件的文档,所以使用 int[ ]来缓存那些文档的文档号,当索引目录中存在多个段时,需要用 List 来缓存每一个段中的所有文档号集合
  • List<float[ ]> scores:一个段中所有满足查询条件的文档的打分值使用 float[ ]缓存,当索引目录中存在多个段时,需要用 List 来缓存每一个段中的所有文档的打分值集合

  图 1 中 NoScoreCachingCollector、ScoreCachingCollector 两者的区别在于是否缓存文档的打分值。

CachingCollector 缓存流程图

  图 2 是 Collector 处理文档的过程,每个流程点在前面已作介绍,不赘述:

图 2:

2.png

  CachingCollector 缓存流程图跟图 2 类似,故用红框标记出不同处:

图 3:

3.png

CachingCollector 复用流程图

图 4:

4.png

  另外,CachingCollector 可以设置允许缓存文档个数最大值。

  在缓存阶段,当缓存的个数超过阈值,那么清空此前缓存的所有数据,另变量 cache 的值为 false,即这次的缓存操作置为失败,故在复用 CachingCollector 时需先检查 cache 的值是否为 true。

第 2 小节 TopDocsCollector

  TopDocsCollector 类在收集完文档后,会返回一个的 TopDocs 对象,TopDoc 对象是什么不重要,在这篇文章中我们只需要知道收集后的文档信息按照某种排序规则有序的存放在 TopDoc 对象中,该对象为搜索结果的返回值。根据不同的**排序(sorting)**规则,TopDocsCollector 派生出图 1 中的三种子类:

  • DiversifiedTopDocsCollector
  • TopScoreDocCollector
  • TopFieldCollector

  其中,根据一定的**过滤(filtering)**规则,TopScoreDocCollector、TopFieldCollector 还分别派生出两个子类:

  • TopScoreDocCollector
    • SimpleTopScoreDocCollector
    • PagingTopScoreDocCollector
  • TopFieldCollector
    • SimpleFieldCollector
    • PagingFieldCollector

  上文中我们给出了 TopDocsCollector 的 7 个子类,结合图 2 中的流程,他们之间的流程差异仅在于 处理该文档 这个流程点,即 collect(int doc)方法的不同的实现

  故在下文中,只介绍每个 Collector 的 collect(int doc)方法的具体实现

TopScoreDocCollector

  TopScoreDocCollector 类的排序规则为 “先打分,后文档号”:

  • 先打分:即先通过文档的打分进行排序,打分值越高,排名越靠前
  • 后文档号:由于文档号是唯一的,所以当打分值相等时,可以再通过文档的文档号进行排序,文档号越小,排名越靠前。

  根据过滤规则,我们接着介绍 TopScoreDocCollector 的两个子类:

  • SimpleTopScoreDocCollector:无过滤规则
  • PagingTopScoreDocCollector:有过滤规则,具体内容在下文展开

SimpleTopScoreDocCollector

  SimpleTopScoreDocCollector 的 collect(int doc)流程图:

图 5:

5.png

score 是否大于堆顶元素的 score?

图 6:

6.png

  使用优先级队列 PriorityQueue 来存放满足搜索条件的文档信息(文档信息至少包含了文档打分 score 以及文档号 docId),分数最低的文档信息位于堆顶,堆的大小默认为段中的文档总数(用户也可以指定堆的大小,即用户期望的返回结果 TopN 的 N 值)。

  为什么判断条件是 score 等于堆顶元素的 score 的情况下也不满足

  • 因为 collect(int doc)方法接受到的文档号总是按照从小到大的顺序,当 score 等于堆顶元素的 score 时,当前文档号肯定大于堆顶元素的文档号,根据上文中 TopScoreDocCollector 的排序规则,故不满足

调整堆

图 7:

7.png

  替换堆顶元素 后,我们需要调整堆重新找到分数最低的文档信息,调整的规则同样按照“先分数,后文档号”。

PagingTopScoreDocCollector

  PagingTopScoreDocCollector 是带有过滤规则的 Collector,用来实现分页功能。

  在 SimpleTopScoreDocCollector 中如果满足搜索条件的文档个数有 M 个,其中 N 为用户期望返回的个数(即 TopN),为了便于理解,我们这里假设 M > 2N,那么第一次搜索后,返回的搜索结果,即 N 篇文档,任意一篇的打分值 score 都是大于等于 剩余的(M - N)篇文档中的任意一篇,如果使用了 PagingTopScoreDocCollector,我们可以就从 (M - N)篇文档中继续找出 N 篇文档,即执行第二次搜索。该 PagingTopScoreDocCollector 可以使得通过多次调用 IndexSearcher.searchAfter(ScoreDoc after, Query query, int TopN)的方法来实现分页功能,其中 ScoreDoc 对象 after 即过滤规则。下面给出 ScoreDoc 类的部分变量:

public class ScoreDoc {
    public float score;
    public int doc;
    ... ... 
}

  score 为上文 N 篇文档中分数最低的打分值,doc 为对应的文档号,在下文中会介绍如何使用 ScoreDoc 作为过滤规则来实现分页功能。

  PagingTopScoreDocCollector 的 collect(int doc)流程图:

图 8:

8.png

  除了红色的流程点,其他流程点跟 SimpleTopScoreDocCollector 是一样的,不赘述。

是否已经被收集了?

图 9:

9.png

  是否已经被收集了 描述的是该文档号是否已经在前面的搜索中被收集了,判断的条件如下,如果为 true,说明该文档已经被收集了:

    score > after.score || (score == after.score && doc <= after.doc)
  • score:该值描述的当前文档的打分值
  • after:该值即上文中的 ScoreDoc 对象

  由于 after.score 是前面所有分页搜索的结果中分数最低的文档,所以如果当前文档的打分值大于 after.score,必定该篇文档已经在前面某次分页搜索中被收集过了。

  如果 score > after.score 不为 true,还要考虑 score == after.score 的情况,在图 6 中我们知道如果两篇文档的打分值一样,那么文档号较大的不会被收集,所以在如果当前的文档号小于等于 after.doc,必定该篇文档已经在前面某次分页搜索中被收集过了。

  从上面的介绍可以看出,如果一个段中有 M 篇文档满足搜索条件,在使用分页搜索的情况,每一次 Collector 都需要处理这 M 篇文档,只是在每一次的分页搜索时选出 N 篇文档。

第 3 小结 TopDocsCollector

TopFieldCollector

  在前面我们介绍了 TopScoreDocCollector 收集器以及它的两个子类 SimpleTopScoreDocCollector、PagingTopScoreDocCollector,它们的排序规则是"先打分,后文档号",TopFieldCollector 的排序规则是“先域比较(FieldComparator),后文档号”。

  • 先域比较(FieldComparator):根据文档(Document)中的排序域(SortField)的域值进行排序。
  • 后文档号:由于文档号是唯一的,所以当无法通过域比较获得顺序关系时,可以再通过文档的文档号进行排序,文档号越小,排名越靠前(competitive)

  我们先通过例子来介绍如何使用 TopFieldCollector 排序的例子,随后介绍排序的原理。

  本人业务中常用的排序域有 SortedNumericSortField、SortedSetSortField,其他的排序域可以看 SortField 类以及子类,在搜索阶段如果使用了域排序,那么 Lucene 默认使用 TopFieldCollector 来实现排序。

例子

SortedNumericSortField

图 2:

2.png

图 3:

3.png

  • SortedNumericSortField:根据文档(document)中 NumericDocValuesField 域的域值进行排序,如果文档中没有这个域,那么域值视为 0
    • 图 2 为索引阶段的内容,我们根据域名为“sortByNumber”的 NumericDocValuesField 域的域值进行排序,其中文档 0、文档 4 没有该域,故它的域值被默认为 0,它们按照文档号排序
    • 图 3 为搜索阶段的内容,使用 SortedNumericSortField 对结果进行排序,所以按照从小到大排序(图 3 中参数 reverse 为 true 的话,那么结果按照从大到小排序),那么排序结果为:
                           文档1 --> 文档0 --> 文档4 --> 文档3 --> 文档2

SortedSetSortField

图 4:
4.png

图 5:

5.png

  • SortedSetSortField:根据文档(document)中 NumericDocValuesField 域的域值进行排序,如果文档中没有这个域,那么域值视为 null,被视为"最小"
    • 图 3 为索引阶段的内容,允许设置相同域名的 SortedSetSortField 有多个域值,这么做的好处在于,在搜索阶段,我们可以选择其中一个域值来进行排序,提高了排序的灵活性
    • 图 4 为搜索阶段的内容,使用域名为"sortByString"的 SortedSetSortField 域的域值进行排序,其中文档 0、文档 4 没有该域,故它的域值被视为 null,它们之间按照文档号排序

  如何在搜索阶段选择排序域值:

  • 通过 SortedSetSelector.Tyte 来选择哪一个域值,SortedSetSelector 提供了下面的参数
    • MIN:选择域值最小的进行排序,例如上图中文档 1、文档 2、文档 3 会分别使用域值"a"、"c"、“b”作为排序条件,图 5 中即按照这个规则排序,由于参数 reverse 为 false,所以排序结果从小到大排序,其中文档 0、文档 4 的排序域值为 null:
                           文档0 --> 文档4 --> 文档1 --> 文档3 --> 文档2
  • MAX:选择域值最大的进行排序,例如上图中文档 1、文档 2、文档 3 会分别使用域值"y"、"z"、“x”作为排序条件,由于参数 reverse 为 false,所以排序结果从小到大排序,其中文档 0、文档 4 的排序域值为 null:
                           文档0 --> 文档4 --> 文档3 --> 文档1 --> 文档2
  • MIDDLE_MIN:选择中间域值,如果域值个数为偶数个,那么中间的域值就有两个,则取较小值,例如上图中文档 1、文档 2、文档 3 会分别使用域值"f"、"e"、“d”作为排序条件,,由于参数 reverse 为 false,所以排序结果从小到大排序,其中文档 0、文档 4 的排序域值为 null:
                           文档0 --> 文档4 --> 文档3 --> 文档2 --> 文档1
  • MIDDLE_MAX:选择中间域值,如果域值个数为偶数个,那么中间的域值就有两个,则取较大值,例如上图中文档 1、文档 2、文档 3 会分别使用域值"h"、"i"、“j”作为排序条件,,由于参数 reverse 为 false,所以排序结果从小到大排序,其中文档 0、文档 4 的排序域值为 null:
                           文档0 --> 文档4 --> 文档1 --> 文档2 --> 文档3

  SortedNumericSortField 也可以在索引阶段设置多个具有相同域名的不同域值,其用法跟 SortedSetSortField 一致,不赘述。

  接下来我们根据过滤(filtering)规则,我们接着介绍 TopFieldCollector 的两个子类:

  • SimpleFieldCollector:无过滤规则
  • PagingFieldCollector:有过滤规则,具体内容在下文展开

SimpleFieldCollector

  SimpleFieldCollector 的 collect(int doc)方法的流程图:

图 6:

6.png

预备知识

IndexWriterConfig.IndexSort(Sort sort)方法

  在初始化 IndexWriter 对象时,我们需要提供一个 IndexWriterConfig 对象作为构造 IndexWriter 对象的参数,IndexWriterConfig 提供了一个 setIndexSort(Sort sort)的方法,该方法用来在索引期间按照参数 Sort 对象提供的排序规则对一个段内的文档进行排序,如果该排序规则跟搜索期间提供的排序规则(例如图 3 的排序规则)是一样的,那么很明显 Collector 收到的那些满足搜索条件的文档集合已经是有序的(因为 Collecter 依次收到的文档号是从小到大有序的,而这个顺序描述了文档之间的顺序关系,下文会详细介绍)。

  以下是一段进阶知识,需要看过文档的增删改以及文档提交之 flush 系列文章才能理解,看不懂可以跳过

  • 我们以图 2 作为例子,在单线程下(为了便于理解),如果不设置索引期间的排序或者该排序跟搜索期间的排序规则不一致,文档 0~文档 4 对应的文档号分别是:0、1、2、3,Lucene 会按照处理文档的顺序,分配一个从 0 开始递增的段内文档号,即文档的增删改(下)(part 2)中的 numDocsInRAM ,这是文档在一个段内的真实文档号,如果在索引期间设置了排序规则如下所示:

  索引期间,图 7

7.png

  搜索期间,替换下图 3 的内容,使得图 2 中的例子中 搜索期间跟查询期间的有一样的排序规则,图 8

8.png

  • 重要)在图 7、图 8 的代码条件下,传给 Collector 的文档号依旧分别是 0、1、2、3,但是这些文档号并不分别对应文档 0~文档 4 了,根据排序规则,传给 Collector 的文档号 docId 跟文档编号的映射关系:

图 9:

9.png

  由图 9 可以知道,Collector 还是依次收到 0 ~ 4 的文档号,但是对应的文档号已经发生了变化,因为这些文档在索引期间已经根据域名为"sortByNumber"的 SortedNumerricSortField 域的域值排好序了。

  (极其重要)尽管在索引期间已经对段内的文档进行了排序,实际上文档 0~文档 4 在段内的真实文档号依旧是:0、1、2、3,只是通过图 9 中的数组实现了映射关系,故给出下图:

图 10:

10.png

  图 10 中通过数组实现的映射关系即 Sorter.DocMap 对象 sortMap,在 flush 阶段,生成 sortMap(见文档提交之 flush(三))。

小节

  TopFieldCollector 相比较 TopScoreDocCollector,尽管他们都是 TopDocsCollector 的子类,由于存在索引期间 的排序机制,使得 TopFieldCollector 的 collect(int doc)的流程更加复杂,当然带来了更好的查询性能,至于如何能提高查询性能,由于篇幅原因,会在下面介绍图 6 的 collect(int doc)的流程中展开介绍。

第 4 小结 TopDocsCollector

TopFieldCollector

  根据过滤(filtering)规则,TopFieldCollector 派生出的两个子类:

  • SimpleFieldCollector:无过滤规则
  • PagingFieldCollector:有过滤规则,具体内容在下文展开

SimpleFieldCollector

  SimpleFieldCollector 的 collect(int doc)方法的流程图:

图 2:

2.png

  在介绍每个流程之前,先介绍下 TopFieldCollector 中的几个变量:

  • trackMaxScore:该值是 TopFieldCollector 类的构造参数,用来描述是否需要记录所有满足查询条件的文档集合中最高的文档打分值,用 maxScore 用来记录该最大值
  • trackDocScores:该值是 TopFieldCollector 类的构造参数,用来描述是否需要记录所有满足查询条件的文档的文档打分值
  • totalHits:该值描述了 Collector 处理的满足搜索条件的文档数量,每当进入图 2 的流程,该值就递增一次。

  如果业务中不需要用到文档的打分值或者 maxScore,强烈建议另这两个参数为 false,因为找出 maxScore 或者文档的打分值需要遍历所有满足查询条件的文档,无法提前结束 Collector 工作(canEarlyTerminate),在满足查询提交的文档数量较大的情况下,提前结束 Collector 的收集工作能显著提高查询性能。canEarlyTerminate 会在下文中介绍

记录文档打分值最高的文档

图 3:

3.png

  如果参数 trackMaxScore 为 true,那么 Collector 每处理一篇文档,就要记录该文档的打分值 score,如果 score 大于当前 maxScore 的值,则更新 maxScore 的值。

添加文档信息

图 4:

4.png

  使用优先级队列 PriorityQueue 来存放满足搜索条件的文档信息(文档信息至少包含了文档打分 score 以及文档号 docId),分数最低的文档信息位于堆顶,堆的大小默认为段中的文档总数(用户也可以指定堆的大小,即用户期望的返回结果 TopN 的 N 值)。

  如果堆没有满,那么将文档号交给 FieldComparator,FieldComparator 的概念在 FieldComparator 的文章中介绍了,不赘述,它用来描述文档间的排序关系(从代码层面讲,通过 FieldComparator 实现了优先级队列 PriorityQueue 的 lessThan()方法),接着添加文档信息到堆中。

设置 bottom 值

图 5:

5.png

  在 添加文档信息到堆中 流程后,如果此时堆正好满了,那么我们需要设置 bottom 的值,即从我们已经处理的文档中找出最差的(the weakest,sorted last),使得当处理下一篇文档时,只需要跟这个最差的文档进行比较即可。

仅统计满足查询条件的文档个数

图 6:

6.png

  在堆满的情况的下,并且 collectedAllCompetitiveHits 为 true,直接可以退出,尽管直接退出了,还是统计了 totalHits 的值,所以从 collectedAllCompetitiveHits 的命名方式也可以看出来只是统计了 totalHits。

  满足下面条件的情况下,collectedAllCompetitiveHits 会为 true

    canEarlyStopComparing == true && canEarlyTerminate == false
  • canEarlyStopComparing:该值描述了是否可以提前结束域比较,在 Collector(三)我们提到,当索引期间通过 IndexWriterConfig.setIndexSort(Sort sort)设置的排序规则与搜索期间提供的排序规则一致时,Collector 收到的文档集合已经是有序的,在堆已满的情况下,后面处理的文档号就没有比较的必要性了,那么 canEarlyStopComparing 的值会被 true,每次获取一个段的信息时设置 canEarlyStopComparing,即调用 getLeafCollector(LeafReaderContext context) 时候设置(见 Collector(一))。
  • canEarlyTerminate:该值描述了是否可以提前结束 Collector 的收集工作,canEarlyTerminate 设置为 true 需要满足下面的条件:
trackTotalHits == false && trackMaxScore == false && canEarlyStopComparing

  如果满足上面的条件,Lucene 会通过抛出异常的方式结束 Collector,该异常会被 IndexSearcher 捕获。这样的好处在于能提高查询性能。比如说某一次查询,我们需要返回 Top5,但是满足搜索条件的文档数量有 10000W 条,那么在 Collector 中当处理了 5 篇文档后(文档在段中是有序的),就可以直接返回结果了。

  如果条件不满足,即 canEarlyTerminate 的值为 false,那么尽管我们已经收集了 Top5 的数据(查询结果不会再变化),但是要继续遍历处理剩余的 9995 篇文档,因为我们需要记录 totalHits(如果 trackTotalHits 为 true)或者需要获得打分值最大的文档(如果 trackMaxScore 为 true),所以此时 collectedAllCompetitiveHits 为 true,继续处理下一篇文档

更新最差的(the weakest,sorted last)文档

图 7:

7.png

  通过与域比较器(FieldComparator)的 bottom 值比较,如果比该值更好(competitive),那么先替换 bottom,然后重新算出新的 bottom,随后还要替换堆顶元素,然后调整堆,算出新的堆顶元素,最后退出继续处理下一篇文档。

无法提前结束域比较

图 8:

8.png

  由于通过域比较后,当前文档比 bottom 还要差,那么先通过 canEarlyStopComparing 判断出能不能提前结束比较,如果 canEarlyStopComparing 为 false,则退出并处理下一篇文档。

  canEarlyStopComparing 为 false 说明段中的文档没有按照搜索期间的排序规则进行排序,所以当前已经收集的 TopN 未必是最终的搜索结果,所以退出处理下一篇文档。

设置 collectedAllCompetitiveHits 为 true

图 9:

9.png

  可以提前结束域比较,即 canEarlyStopComparing 为 true,并且不可以提前结束 Collector 的收集工作,即 canEarlyTerminate 为 false,那么同时满足这两个条件就可以设置 collectedAllCompetitiveHits 为 true 了。使得处理下一篇文档时就可以走图 6 中的流程了。

提前结束 Collector 的收集工作

图 10:

10.png

  可以提前结束 Collector 的收集工作,那么我们先估算剩余满足查询条件的文档数量,通过线性估算出实现,估算方法不展开介绍,没有实际意义。

  接着设置一个 earlyTerminated 的值为 true,用户在得到查询结果后可以通过该值来了解 Collector 提前结束收集工作这个事件。

  通过抛出 CollectionTerminatedException 异常的方式来实现,大家可以点击链接看下源码中对这个异常的解释。

PagingFieldCollector

  PagingFieldCollector 同 Collector(二)中的 PagingTopScoreDocCollector 一样,相对于 SimpleFieldCollector 实现了分页功能,分页功能的介绍见 Collector(二),不赘述,collect(int doc)的流程图是相似的,并且用红圈标记出不同处。

  PagingFieldCollector 的 collect(int doc)方法的流程图:

图 11:

11.png

是否已经被收集了

  是否已经被收集了 描述的是该文档号是否已经在前面的搜索中被收集了,判断的条件如下,如果为 true,说明该文档已经被收集了:

    topCmp > 0 || (topCmp == 0 && doc <= afterDoc
  • topCmp:该值描述的当前文档与 FileComparator 的 top 值进行比较后的值,top 值描述的是之前所有分页搜索的结果中最差的文档,在初始化 PagingFieldCollector 对象时需要用户提供该值(通过上一次的查询结果就能获得),并设置 top 的值。如果 topCmp > 0,说明当前文档比最差的文档好(competitive),必定该篇文档已经在前面某次分页搜索中被收集过了。
  • afterDoc:该值描述的是之前所有分页搜索的结果中最差的文档的文档号,在 Collector(三)中我们说到,如果域比较无法区得出排序结果,由于文档号是唯一的,所以再根据文档号进行比较,文档号大的比文档号小的差(uncompetitive),所以在 topCmp == 0 情况下,如果当前文档号小于等于 afterDoc,必定该篇文档已经在前面某次分页搜索中被收集过了。

结语

  在下一篇文档中,我们将继续介绍最后一个 Collector,即 DiversifiedTopDocsCollector,这将是 Collector 系列文章的最后一篇。


本文地址:https://www.6aiq.com/article/1586704007975
本文版权归作者和AIQ共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出