ElasticSearch进阶之邻近匹配
我们在之前的文章中介绍过标准的文本搜索,它会进行各个关键词的搜索,然后根据TF/IDF来计算相关性,最后再返回结果。这样的搜索能够根据我们搜索的关键词是否在相关的文本中存在来进行匹配,但是这还不够,它忽略了关键词之间的关系。本文就来介绍如何加入关键词之间关系的处理。
引言
我们来看一个例子,现在有下面两个文本:
- Dog beats the fox.
- Fox beats the Dog.
现在我们来搜索fox beats dog,那么这两个文本其实是都可以匹配的,而且两个文本的相关性都是一样的,但是这两个文本的意思其实是相反的,我想我们在搜索的时候应该还是有倾向性希望能够区分他们两个的,比如说搜索fox beats dog的时候希望第二个文本的相关性分数要高一点,搜索dog beats fox的时候希望第一个文本的相关性分数要高一点。下面我们来看几种处理这个问题的方法。
短语匹配 (Phrase Matching)
短语匹配的思想很简单,就是当我搜索的时候除了每个词都需要匹配,还要求他们之间的位置也是一样的。比如说我们有一个文本”quick brown fox”,在搜索quick fox的时候,就不会相匹配,因为文本中的fox没有紧接在quick的后面。那要做到这一点,是如何实现的呢?
首先在analyzed的时候就不仅需要列出每一个term,还需要列出相应term在文本中的位置信息,比如上面的文本在analyzed之后的结果如下:

我们可以清楚地看到每个term都有一个position的变量来保存它在原文本的位置信息。这个position的信息也是保存在Inverted Index中的,这样在短语匹配的时候就可以使用这些信息来进行判断了。
当我们使用短语匹配来查询quick fox的时候,只有同时符合下面的情况才认为是匹配的:
- Quick fox两个词必须都在文本中出现。
- Fox的位置必须是quick的位置加1.
Slop的使用
很显然上面这种限制太严格了,它要求fox必须在quick后面一个位置。其实我们主观上应该不会要求这么严格,比如说当我们查询quick fox的时候,我们还是希望“quick brown fox”能够被返回,毕竟在这个文本中quick fox都存在,而且相对位置也是一样的,唯一有问题的就是fox是在quick后面第二个,我们可以使用slop参数来进行处理:

这里的slop就是说term之间的最大多远的情况我们还是认为他们是匹配的。所谓的多远其实就是你需要移动几次才能让这个查询和文本一样。如下所示:

这里我们需要把fox向后移动一个位置就可以精准匹配,所以slop为1就足够了。
需要注意的有了slop这个参数之后,其实term之间的顺序也是可以变的。比如说我们来看fox quick需要几步能匹配文本:

这里我们可以看到它首先把quick移到了位置1,然后把fox移到了位置2,也就是说我们进行一次term的交换其实是需要两个slop的。这样一来当slop设置为3的时候,我们的文本也是可以匹配fox quick的短语搜索。
多域搜索
我们在之前的文章《ElasticSearch进阶之多域的搜索》中介绍了什么是多域搜索。这里短语匹配在多域搜索中其实也会存在一个问题,我们来看下面这个例子,我们有下面两个名字:

然后当我们搜索“Abraham Lincoln”的时候,它竟然也会返回,这是为什么呢?
因为我们在analyzed的时候,产生的结果是Abraham位置2,Lincoln位置3,所以在查询的时候即使我们使用短语匹配,还是会认为Lincoln就是在Abraham之后一个位置,这显然不是我们想要的结果。这个问题说白了根源还是上面两个名字和”John Abraham Lincoln Smith”这个长的短语之间没有区别。为了解决这个问题我们可以设置position_offset_gap的值,如下所示:

Position_offset_gap就是告诉ElasticSearch,对数组里面的不同元素创建position的时候,每个之间加上这个间隔的值。所以这样设置之后我们会得到这个结果:
- John – 位置1.
- Abraham – 位置2.
- Lincoln – 位置103.
- Smith – 位置104.
这样一来就自然而然解决了刚刚的问题。
邻近查询
这个时候也许你会问我们该如何设置slop的值比较合理呢?当你问出这个问题的时候,其实内心真实的想法是希望能够匹配到位置符合我们预期的文本,又不会丢失一些位置不是那么靠近的文本。这时候就轮到邻近查询出场了,它不会排除那些位置不符合的文本,而是把越靠近的文本赋予越高的分数,所以当我们按照分数的高低进行排序的时候,就自然而然能得到我们想要的结果。要想得到这个效果很简单就是把slop的值设置比较高,比如50,100之类的。

这样一来我们可以看到下面两个文本的结果:

第一个文本中quick和dog之间很靠近,所以它的分数就很高,而第二个文本quick和dog之间有很大的距离,所以分数就相对来说比较低。
参考邻近查询
虽然设置slop到一个比较大的值可以解决位置之间的问题,但是我们来回顾一下短语匹配的两个条件,一个是位置关系,另外一个是所有的term都必须在文本中出现,事实上很多时候,第二个必须所有term都出现的条件是有些苛刻的,比如我们希望只要有部分匹配就可以返回,但是精准匹配返回的分数要高一些,这个时候我们就可以把短语匹配作为一个条件要来提升最后分数的计算,如下所示:

这样一来,短语匹配作为一个should的条件,其实就是可以用来调整最后结果的相关性,而不会排除那些没有完全匹配的结果。
性能分析
短语匹配有个最大的问题就是性能问题, 也是可以理解的,比如说slop等于50的时候,我们需要进行位置和计算的量其实很大,尤其是文本数量很多的情况下,比如几百万的文本,那这个计算其实是很影响性能的。那么如何来提升这里的性能呢?ElasticSearch中引入了以下几个方法:
为结果重新打分
这个思想很简单,我们使用正常的搜索去进行匹配,但是在返回之前加入短语匹配来重新打分。这个重新打分是发生在每个shard的结果返回之前。这样一来其实需要重新计算数据量就很小,因为你只要处理每个shard的top k的结果就可以了,如下所示,使用rescore来实现:

这里的window_size就是每个shard中top k中的k的值。
找到关联词语
另外一种常见的改进性能的方法就是把句子中的一些短语先关联起来,比如说我们引言中提到的dog beats the fox,除了进行单独的term处理,它会还会被处理成下面这样:
[“dog beats”, “beats the”, “the fox”]
我们称这种单词对为shingles。当然你也可以组成三个或者更多的单词对。
这样一来当用户搜索dog beats fox的时候,第一个文本的匹配度就自然提高了。当然对dog fox这样的搜索还是没有提升,但是所幸的是现实中大家可能还是会输入一些和文本顺序更加符合的查询条件。这也算是依赖现实而不是理论优化的一种了。
总结
至此本文就详细介绍了ElasticSearch中的邻近匹配相关的概念,希望大家能有所收获。
Recent Comments