当前位置: 首页 > 网络应用技术

您真的了解Lucene中的Phrasequry吗?

时间:2023-03-08 17:32:39 网络应用技术

  您只需要在这里知道这种格式。稍后描述很方便。每个参数是什么意思?

  短语Query的翻译也是一个短语查询。由于它是一个短语查询,通常包含多个单词,Lucene中的多个单词是多个术语。文件。但是,该文档仅包含仅满足第一个条件的这些指定的TEMS。短语中的重要参数斜率是确定文档是否满足短语查询的另一个基础。

  那么什么是Slop?互联网上的许多信息说,Slop评判了最大的编辑距离,实际上是不准确的。Lucene的官方文件实际上并没有直接解释。本文的目的是从源代码分析的来源解释此斜率。

  要说slop,您必须首先了解phrasequry中术语的概念。在Phrasequry中定义的术语指定了一个位置。Lucene在执行查询时会执行查询重写。如果最小的位置不是0,则所有项的所有位置都将被减去最小的位置,并且必须将第一个位置减去。

  有了位置的概念,SLOP在短语的定义中都是术语。通过移动术语,所有术语都只能达到由位置定义的相对位置的最大步长限制。在移动由位置定义的最小步骤数之后,它被描述为描述中的斜率距离。

  我们使用以下文本来计算一些短语的坡度距离来查询深度理解:

  最后看一下Lucene中的短语的定义:

  如果您理解了措辞,那么理解多重曲二氏菌会变得更加简单。

  多重句子也是一个短语查询。Multiphrasequry和Phrasequer之间的最大差异是,多重曲柄可以在同一位置设置术语集合。

  查看lucene中多酶的定义(可以比较词组的定义,只有术语列表之间的差异):

  多重Query查询的结果实际上是多个短语结果的集合。计算在Multiphrasequery中以多键酶中的术语集合的术语集合。笛卡尔积累中的每个结果都是一个短语,而多酶的结果是每个短语结果的浓度。

  让我们看看一个多重句话的示例:

  多酶([a,d]:0,[c,e]:1,slop:3)查询结果是:

  phrasequery(a:0,c:1,slop:3)或phrasequery(a:0,e:1,slop:3)或phrasequery(d:0,c:1,slop:3)或phrasequery(d:0,E:1,Slop:3)

  从Phrasequry中理解多重句子是相对简单的,但是Lucene中的短语查询匹配算法将phrasequery用作一种特殊的多重脉冲。这也符合通用工程实施的直觉,并且代码实现通常更常见。

  接下来,让我们看一下如何找到Lucene中匹配的短语文档。

  满足短语的文件必须符合两个条件。

  该文档必须包括短语查询中定义的所有术语。

  在Lucene中,此条件使用术语倒置链(包括术语的文档编号列表)。

  看到一些例子:

  只需满足条件一个,而不一定要满足短语查询,因为所有术语的斜率距离不超过斜率。尽管我们介绍了如何计算坡度距离,但是否有更直观的判断方式?继续看不起,这是本文的亮点之一。

  对于phrasequery(term1:pos1,term2:pos2,slop),我们将该位置标记为文本中的term1_pos,并且文本中的术语2的位置记录为term2_pos.ter2_pos。对于tirs1_pos和term2_pos的相对位置,我们需要讨论情况:

  如果短语查询需要满足SLOP的要求,则必须存在:

  term2_pos -term1_pos <= pos2 - pos1 + slop,

  转化为: (term2_pos - pos2) - (term1_pos - pos1) <= ?slop,我们记为 (1)。

  如果短语查询需要满足slop的要求则必须有:

  term2_pos + slop - term1_pos >= pos2 -pos1,

  两边取负号:term1_pos - term2_pos - slop <= pos1 - pos2

  再转化为:(term1_pos - pos1) - (term2_pos - pos2) <= slop,我们记为 (2)。

  结合 (1)(2) 可得:

  |(term1_pos - pos1) - (term2_pos - pos2)| <= slop,

  即:Max(term1_pos - pos1, term2_pos - pos2) - Min(term1_pos - pos1, term2_pos - pos2) <= slop

  这就是短语查询是否满足slop要求的判断条件,也是Lucene中对于slop不为0查找算法(SloppyPhraseMatcher)的核心思想:。

  如果slop为0,则满足:|(term1_pos - pos1) - (term2_pos - pos2)| <= 0,

  即:|(term1_pos - pos1) - (term2_pos - pos2)| = 0

  最终得:term1_pos - pos1 = term2_pos - pos2

  这里有个印象,Lucene中针对slop为0的情况实现了一个单独的查找算法:ExactPhraseMatcher。

  以上是两个position的情况,N个position的大家可以自行推导,这边给出结论:

  Max(term1_pos - pos1, term2_pos - pos2, ..., termN_pos - posN) - Min(term1_pos - pos1, term2_pos - pos2, ..., termN_pos - posN) <= slop

  后文描述中,我们把termN_pos - posN叫做PhrasePos。请记住这个说明!!!

  有了上面推导的结论,在查找之前,我们先计算每个position的PhrasePos列表,然后执行查找过程就是判断所有position的PhrasePos中最大值和最小值的差是否小于等于slop,如果是,那就是满足PhraseQuery的一个匹配位置。

  这句话描述比较简单,但是查找短语匹配的算法真正执行需要区分不同的情况,接下来我们用例子来说明算法的执行流程。

  假设我们要从“a b d a b c”中查找满足PhraseQuery(a:0, b:1, c:2, slop:0)的短语查询。

  首先,我们需要计算每个position的PhrasePos列表,如下图中的PhrasePos_a,PhrasePos_b和PhrasePos_c所示。

  根据我们在上节中的结论有如果slop为0,则各个position的PhrasePos必须是相等的。所以问题也就转化成了从多个PhrasePos中找交集。多个有序列表找交集我们之前有文章介绍过了,这里就不重复介绍了,不懂的欢迎前去考古。

  slop不为0的情况下,我们分为四种场景来描述算法执行过程,但是场景三和场景四都是可以转化成场景一和场景二,所以场景一和场景二一定要理解。

  场景一:每个position只有一个term,并且每个position的term都不相同假设我们要从“a a b c a b c”中查找满足PhraseQuery(a:1, b:2, slop:2)的短语查询。

  首先,我们需要计算每个position的PhrasePos列表,如下图中的PhrasePos_a和PhrasePos_b所示。当前查找的位置由绿红两个箭头所示,分别指向了两个position的PhrasePos列表的第一个位置。

  当前的PhrasePos_a < PhrasePos_b,所以第一轮查找以a开头的短语,停止条件是PhrasePos_a > PhrasePos_b。

  计算初始的匹配距离为 matchLength = 0 - (-1)= 1 < ?2,所以我们已经找到一个匹配位置,但是这个不一定是以a开始的最短匹配,我们可以继续查找a的下一个位置,直到PhrasePos_a > PhrasePos_b:

  PhrasePos_a的下一个位置是0, 0 - 0 = 0 < slop,并且更新matchLength = 0,同时记录更优的匹配位置,然后继续查找PhrasePos_a。

  PhrasePos_a的下一个位置是3,大于PhrasePos_b,所以我们找到了一个以a开始的短语匹配,对应文本中的position是[1, 2]。接下来需要开始查找以b开头的短语匹配。初始matchLength=3-0=3>slop,不满足短语查询条件,所以需要找b的下一个位置。

  PhrasePos_b的下一个位置是3,3-3=0

  所以,对于例子中我们一共找到了两个匹配位置。后续就不单独说明记录找到的位置,只讲查找流程。

  场景二:每个position只有一个term,并且不同的position的term可能相同假设我们要从“a a b c a b a c”中查找满足PhraseQuery(a:1, b:2, a:3, slop:2)的短语查询。

  同样地,我们需要先计算每个position的PhrasePos列表,如下图所示。因为短语中包含了两个a,因此我们用PhrasePos_a_0和PhrasePos_a_1分别表示position=1和position=3的a的PhrasePos列表。当前查找的位置由绿红蓝三个箭头所示,分别指向了三个position的PhrasePos列表的第一个位置。

  因为初始位置PhrasePos_a_0和PhrasePos_a_1冲突了(对应的PhrasePos是文本中的同一个位置),所以我们需要先处理冲突的情况。碰到冲突的情况,我们把PhrasePos小的继续往后查找一个位置(为什么是PhrasePos小的往后找一个位置?最后一节我们单独讨论),如下图所示,PhrasePos_a_1指向了-2。

  当前最小的是PhrasePos_a_1,第2小的是PhrasePos_a_0,所以第一轮查找以PhrasePos_a_1开始,停止条件是PhrasePos_a_1 > PhrasePos_a_0,这个是以PhrasePos_a_1为短语起点开始查找的结束条件,自行理解下。

  计算初始的匹配距离为 matchLength = PhrasePos_b - PhrasePos_a_1 = 0 - (-2)= 2 <= 2,所以初始位置满足匹配要求,但是它不一定是以PhrasePos_a_1开始的最短匹配,我们可以继续查找PhrasePos_a_1的下一个位置,直到PhrasePos_a_1 > PhrasePos_a_0:

  如下图所示,PhrasePos_a_1的下一个位置指向1,大于PhrasePos_a_0,所以我们找到了一个以PhrasePos_a_1开始的匹配。

  当前最小的是PhrasePos_a_0,第2小的是PhrasePos_b,所以第二轮查找以PhrasePos_a_0开始,停止条件是PhrasePos_a_0 > phrasepos_b。

  初始匹配距离的第二轮是matchLength = phrasepos_a_1 -phrasepos_a_0 = 1 - (--1)= 2 <= 2,所以初始位置满足匹配要求,但是它不一定是以PhrasePos_a_0开始的最短匹配,我们可以继续查找PhrasePos_a_0的下一个位置,直到PhrasePos_a_0> phrasepos_b:

  如下图所示,phrasepos_a_0的下一个位置为0,并且当前匹配距离为匹配= phrasepos_a_1 -phrasepos_a_0 = 1-0 = 1是较短的匹配。但不一定是从phrasepos_a_0的最短匹配开始。我们可以继续找到phrasepos_a_0的下一个位置,直到phrasepos_a_0> phrasepos_b:

  如下图所示,phrasepos_a_0的下一个位置为3,它比phrasepos_b大,因此我们找到了与phrasepos_a_0的匹配匹配。

  如上图所示,当前的phrasepos_a_0和phrasepos_a_1冲突。我们需要首先处理冲突。在冲突的情况下,我们继续在phrasepos中找到一个位置,如下图所示,phrasepos_a_0指向3。

  最小的是phrasepos_b,第二个小是phrasepos_a_0,因此第三轮查找phrasepos_b,停止条件是phrasepos_b> phrasepos_a_a_0。

  初始匹配距离的第三轮是MatchLength = phrasepos_a_1 -phrasepos_b = 3-0 = 3> 2,因此不满足匹配要求的初始位置,我们可以继续找到phrasepos_b的下一个位置,直到phrasepos_b> phrasepos_a_a_0:phrasepos_b> phrasepos_a_a_0:

  如下图所示,phrasepos_b的下一个位置指向3,当前匹配距离匹配= phrasepos_a_1 -phrasepos_a_0 = 3-3 = 0 0 0

  场景三:每个position可能有多个term,并且每个position的term集合都不相同这种情况其实就是MultiPhraseQuery的查找。对于MultiPhraseQuery的查找,只比场景一中多做一步,就是对每个position的term集合中的每个term计算PhrasePos的并集。

  对于MultiPhraseQuery([a, b]:0, [c, d]:1, slop:2)查找的起始状态如下图所示,剩下流程和场景一相同。

  场景四:每个position可能有多个term,并且各个position的term集合可能有重叠这种情况其实也是MultiPhraseQuery的查找。情况四只比场景二中多做一步,就是对每个position的term集合中的每个term计算PhrasePos的并集。

  对于MultiPhraseQuery([a, b]:0, [a, c]:1, slop:2)查找的起始状态如下图所示,剩下流程和场景二相同。

  知道了短语匹配四种不同情况如何查找匹配的算法流程之后,我们就可以进入Lucene的源码实现了。

  在Lucene中PhraseQuery和MultiPhraseQuery的匹配查找算法的实现是同一套(其实还有PhraseWildcardQuery,这里我们就不介绍了)。查找短语匹配的顶层接口是PhraseMatcher,Lucene中区分了slop为0和非0两种情况,这两种情况的查找实现分别是ExactPhraseMatcher和SloppyPhraseMatcher。

  首先我们看下,短语查询匹配查找的顶层接口,我们只看跟短语查找有关的方法:

  ExactPhraseMatcher是处理slop为0的情况。ExactPhraseMatcher中的大部分方法都比较简单,这里就直接都列出来:

  查找匹配的核心方法,我们单独拿出来分析。在分析具体实现之前,先回顾下查找策略中的特殊情况,

  如果slop为0,则满足:term1_pos - pos1 = term2_pos - pos2,

  所以,如果已经定位到了term1的位置,则term2的位置必须是term2_pos ?= term1_pos - pos1 + pos2。

  有了这结论,我们以position最小的PostingsAndPosition为查找起点,每次定位到一个位置之后,根据上述结论查找其他的PostingsAndPosition。

  代码的逻辑实现和之前我们介绍倒排链交集的实现非常类似:

  短语查询中slop不为0的情况使用SloppyPhraseMatcher来查找匹配。

  在SloppyPhraseMatcher的实现中,最复杂的就是处理不同position的term集合有重叠的情况。

  从前面的算法描述我们知道,在查找匹配之前需要先算每个position的PhrasePos列表。Lucene的实现并没有一次性把PhrasePos列表计算好,而是用一个工具类可以获取position的下一个PhrasePos,我们先看下这个工具类:PhrasePositions。

  PhrasePositions短语查询中一个position对应了一个PhrasePositions,PhrasePositions最重要的是它可以返回position的下一个PhrasePos。

  SloppyPhraseMatcher的成员变量

  SloppyPhraseMatcher的初始化SloppyPhraseMatcher的初始化在reset方法中,整体流程如下所示:

  第一次初始化需要执行每个position的term集合是否有重叠的处理,这部分逻辑比较复杂:

  获取重叠的term集合,并为每个重叠的term编号term的编号是按照重叠term出现的次序,只保证唯一,没有其他要求:

  对PhrasePositions进行分组,包含相同term的PhrasePositions分为一组对包含相同term的PhrasePositions分组是为了处理PhrasePos的冲突的情况。

  首先获取全部包含重叠term的PhrasePositions集合:

  接着分为两种情况处理分组:

  整体的分组逻辑:

  对同组的PhrasePositions按offset排序按offset排序的目的是对每个新文档执行reset的时候,如果存在重叠的term则更高效。

  因为碰到冲突总是PhrasePos小的往后找一个位置,而对于同term的重叠冲突,offset大的那个PhrasePos肯定比较小,因此初始状态如果冲突的话,冲突同组的PhrasePositions从offset小的开始处理会保证后面解决offset大冲突不会影响已经处理过的。

  如果上面的描述觉得不是很清楚,我这边给个例子:对于文本"a d c a b c",MultiPhraseQuery([a, b]:0, [a, c, d]:1, [c, d]:2, [a, d]:3, slop:3)。各个position的PhrasePos列表如下图所示,你可以分别按offset从大到小和从小到大分别走一遍advanceRepeatGroups方法的逻辑就能深切感受到为什么需要排序。

  处理因为重叠term导致PhrasePos冲突的情况接着分为两种情况处理冲突:

  冲突的判断方式,如果PhrasePositions的指向文本中相同的position就是冲突了:

  PhrasePositions首先比较的是PhrasePos,然后是offset:

  查找下一个匹配在看下面代码之前,一定要理解算法中的场景二。

  处理冲突的情况:

  每个匹配位置对相关性的贡献

  先来看SloppyPhraseMatcher的官方文档注释,我们拆开来看:

  SloppyPhraseMatcher的功能Find all slop-valid position-combinations (matches) encountered while traversing/hopping the PhrasePositions.

  通过遍历所有position的PhrasePosition(就是我们前面说的PhrasePos)查找所有满足slop距离要求的匹配位置。

  每个匹配贡献的得分The sloppy frequency contribution of a match depends on the distance:

  Example:

  for query "a b"~2, a document "x a b a y" can be matched twice: once for "a b" (distance=0), and once for "b a" (distance=2).

  越短的slop距离贡献的得分越高。

  冲突的解决方式Possibly not all valid combinations are encountered, because for efficiency we always propagate the least PhrasePosition. This allows to base on PriorityQueue and move forward faster.

  纯属是为了性能考虑,所以冲突的时候都是PhrasePos小的需要往后找一个位置,这样可以快向前推进。

  算法的缺点As result, for example, document "a b c b a" would score differently for queries "a b c"~4 and "c b a"~4, although they really are equivalent.

  因为对于 "a b c"~4,SloppyPhraseMatcher从"a b c b a"找到的是["a b c", "b c b a", "c b a"]。而"c b a"~4找到的是["a b c", "b c b a"]。

  Similarly, for doc "a b c b a f g", query "c b"~2 would get same score as "g f"~2, although "c b"~2 could be matched twice.

  因为对于"c b"~2 ,SloppyPhraseMatcher从"a b c b a f g"找到的是"c b",前面的"b c"会被忽略。

  展望We may want to fix this in the future (currently not, for performance reasons).

  一句话,目前并没有修改的打算。

  单term的短语查询end为冲突时,短语最后一个term所在的位置。

  冲突term的下一个位置小于等于end对于从"a a b c a b a c"中查找PhraseQuery(a:0, b:1, a:2, slop:2)。

  如上图所示的冲突状态:

  归纳: 如果冲突的term在文本的下一个位置在当前短语最后一个term位置之前,则把冲突中PhrasePos小的往后找一个位置,肯定是减少匹配长度的,更可能满足查询要求。

  证明:

  因为在冲突的时候,如果

  current(PhrasePos_a_0) ?> 电流(phrasepos_a_1)和下一个(phrasepos_a_0) <= end 且 ?next(PhrasePos_a_1) <= end,

  则必有next(PhrasePos_a_0) ?> next(PhrasePos_a_1),

  所以end - current(PhrasePos_a_0) ?> end -current(phrasepos_a_1),

  因此,您需要在phrasepos_a_1中找到一个位置。

  从“ A A B C A B A C”中找到Phrasequry(a:0,b:1,a:2,slop:2)的冲突术语的下一个位置大于最终。

  上面显示的冲突状态:

  归纳:如果冲突期限是当前短语当前短语的最后位置,则冲突中的小词句将向后找到一个位置,这肯定会降低匹配的长度,并且更有可能满足查询要求。

  证明:

  因为冲突时

  如果当前(phrasepos_a_0)>当前(phrasepos_a_1)和next(phrasepos_a_0)> end和next(phrasepos_a_1)> end,end,

  然后必须有下一个(phrasepos_a_0)>下一个(phrasepos_a_1),并且

  因此,下一个(phrasepos_a_1)-current(phrasepos_a_0)<next(phrasepos_a_0)-current(phrasepos_a_1),

  因此,您需要在phrasepos_a_1中找到一个位置。

  如果是multi -term.term.look,则在下面的示例:

  多重素([[a,b]:0,[a,c]:1,slop:10)实际上,有一个匹配的位置“ A B”,但找不到它。

  这种情况只能将MultiphraseQuery转换为多个词组,而无需修改源代码,然后通过Booleanquery的《应该言语》来处理它。当然,这将有绩效损失,这取决于业务选择。

  感谢您在这里见到官员。如果有任何遗漏,欢迎讨论。