ElasticSearch进阶之多域搜索

我们在使用ElasticSearch真正进行搜索的时候其实很多时候并不是一个简单的term的搜索,而是多个域的组合,比如说我们可能会搜索一本书名为《War and Peach》然后作者是”Leo Tolstoy”的书。本文就来讲解一下ElasticSearch中对常见的多域搜索有哪些实现方法。

简单实现

像上面提到的这个例子,最简单的实现就是使用match语句来匹配每一个限制条件,然后用一个bool把它们combine起来,如下所示:

我们知道bool语句中是match的内容越多,它的得分就越高,所以这个实现中假如title和author都match的情况下得分是比单一match要高的,这也符合我们的想法。

更进一步,假如这个时候,我偏向于我找的这本书是由某些人进行翻译的,比如我觉得”Constance Garnett”或者”Louise Maude”翻译的这本“War and Peace”我比较喜欢,我们如何来实现这个呢?可以如下所示:

同样是使用bool来处理,这里你可能会问为什么需要在里面再嵌套一个bool的语句,为什么不把它们四个条件放在一个平行的维度。这里就涉及到一个分数的计算细节了。我们主要希望翻译作者这个条件所占的比重只有三分之一,也就是说和书名以及作者是同等的重要。假如我们把两条翻译的作者match放到同一个层级,那么书名和作者其实就只剩1/4的比重了,而每个翻译的作者则各占1/4的比重,这显然不是我们想要的结果。

不同占比的语句

这个时候假如你说,其实我更看重书名和作者,翻译的人如果是他们最好,但是不是他们我也无所谓,也就是说这个时候书名和作者的占比要更重一点。有没有办法来实现这个呢?答案也是有的,我们可以如下所示:

这里使用了boost来设置比重,默认是1。boost越高也就意味着比重越大,一般设置成1到10之间的数。

单独的查询字符串

其实上面这种方法使用起来很有效,然而在现实中我们会遇到各种各样的情况,一个比较常见的现象就是用户就输入了一串字符,然后就希望你能给他返回他想要的结果,这个时候我们也不知道它输入的字符中哪个是对应哪个域的,比如上面那个例子用户输入了War and Peace Leo Tolstoy, 我们是把War and peace对应到title还是把Leo Tolstoy对应到title呢?还是把War and对应到title呢?这个问题粗看起来其实不是那么容易解决,当然也不是不能解决。要想解决这个问题,一般来说有以下这些方法,我们来逐一研究一下:

最佳域

假设我们现在有两本书,如下所示:

这个时候有个用户想搜索 “brown fox”这个语句,从我们人眼来判断,应该是第二本的匹配度高一点,毕竟他里面有讲到 brown fox这两个词。但假如我们运行下面这个查询:

然而这个查询得到的结果其实并不是我们所预期的,它的结果中书本1的得分要高一点。这是什么原因呢?其实也很简单,因为第一本在title和body中都存在brown这个词,也就是说在bool的两个平行查询中它都有得分,而第二本书title中并没有brown这个词,就意味有1/2的查询是没有得分的,即使它在第二个body中匹配了两个词,它的得分也没有上面两个查询都有得分来得高。

所以这个时候把两个match平等地对待其实好像不是太合理。我们解决这个问题的一个办法就是不管title还是body,谁匹配的好,我就相信谁,也就是说取两者的最高匹配值。这可以使用dis_max来实现,具体查询如下所示:

这样一来就可以得到我们想要的结果了。

然后使用这种max的处理就意味着我们在最大值相等的情况下,会丢失下一个层级的比较。比如说我们现在搜索quick pets,这两本书都不能完全匹配,第一本书只有title匹配quick,body完全没有匹配,而第二本title匹配了pet,body也匹配了quick。我们显然希望第二本书的返回分数要高一点,但是假如我们只使用上面这种dis_max的处理方法,就会得到两个完全一样的分数。那这个问题如何解决呢?如下所示:

这里使用了一个新的参数tie_breaker,它的意思就是max的那一块还是保留,但是别的方面也不忽略,我们把别的方面的分数乘以tie_breaker (这里是0.3)之后再进行加和处理。说白了就是别的层级打个折来计算。这样一来我们就可以得到我们想要的结果了。不过需要注意的是这里的tie_breaker不宜设置得过大,一般来说靠近0会比较好,比如0.1到0.3,再大就容易削弱max的作用了。

最多域

我们知道其实文本搜索里面有两个很重要的指标,一个就是返回尽可能多的符合查询要求的结果,我们称之为recall,另外一个就是不相关的结果尽量不返回。很多时候要想同时达到这两个目标就很难,但是我们有一个共识,就是把最相关的结果尽量返回在第一页上。

一个常见的提高recall的方法就是进行有可能match的查询,举个例子,比如我们搜索quick brown fox,那么包含fast fox的结果我们也需要进行匹配,而且是一个很不错的匹配返回。这里的很不错当然是相对于别的结果,比如quick cat来说,假如我们有精准的quick fox可以匹配的话,这个结果显然是要比fast fox更加匹配。那么如何做到这样呢?

首先,我们要让fast和quick来进行匹配,这个我想大家不陌生,我们只要在index的时候包含同义词就可以了,这样fast,quick都会index到同一个词。但是这样做的结果就是fast和quick的相关性就一样了,也就是说我们不能让quick fox的相关性比fast fox高了,怎么解决这个问题呢?通常的处理方法就是我们可以再次进行一次index,这次是比较精准的index,这样一来在查询的时候先进行广度匹配的查询,如果符合了再进行一次精准匹配的查询,两者结合进行排序就可以得到我们想要的结果了。

我们下面来看一个例子说明实现上面的想法,我们有下面两个文本:

我们使用standard和english两种analyzer来进行index。

这个时候假如我们搜索jumping rabbits,对于english的analyzer来说,jumping最终会变成jump,rabbits也会变成rabbit,所以两个文本的得分是一样:

但是假如我们引入standard index,如下所示(title就是english,title.std是standard):

这样一来,两个analyzer就会combine起来,很显然第一个文本的匹配度就会高很多:

当然你也可以在查询的时候通过boost来设置每个analyzer在最终score中所占的比重。

跨域

除了上面两种处理方法,其实还有一个比较常见的问题,比如说你有一个地址是这样写的:

这个时候你可能会搜索“Poland Street London”,也就是说我们会把多个域组合在一起,那这个时候我们怎么来进行查询呢?

一个比较傻瓜的方法就是把每个域都查询一下,然后用bool再组合起来,如下所示:

也许这个时候聪明的你应该会说其实我们应该用最多域来实现:

这是一个不错的想法,但是他有很一些问题:

  1. 它搜索的是某一个域最符合查询结果的,而不是跨几个域的最匹配的。
  2. 每个term在某个域的频率可能不一样,这个会影响最终的分数结果。从而最终影响结果的排序。
  3. 不能使用minimum_should_match参数,也就无法减少一些长的term引入的相关性问题。

上面这些问题其实归根结底还是因为我们要搜索多域,一个常见的解决方法就是假如我们能够把这些域都组合起来,那么这些问题就不见了,就像上面我们可以使用address来包含所有的street,city,country,poscode的信息,然后进行统一搜索。

这个解决方法听起来不错,在ElasticSearch是如何具体实现的呢?一种方法就是使用_all filed, 它会把所有的域都index到一个大的域,这个比较方便,但是显然不够灵活,我们肯定更希望能够有一些自定义的方式来决定哪些域需要被加进来,哪些不需要,而Copy_to就是一个很好的参数:

这里我们就是把first_name和last_name加入到了full_name中,这样假如你直接搜索的时候就可以直接查询full_name了。

上面这种方法很好,只不过它有 一个要求,就是必须在index的时候就做好,那么ElasticSearch有没有什么好的方法可以不用在index的时候做,而是在查询的时候做呢?答案也是有的,可以使用cross_fields来实现:

这里就是把first_name以及last_name作为了一个域来进行查询。这个查询还有一个好处就是你还可以使用boost参数来设置每个域的权重,这样一来就可以得到更符合我们期待的结果了。

总结

本文详细介绍了ElasticSearch中多域的查询方法,重点介绍了三种实现方法,分别是最佳域,最多域和跨域,希望对想了解相关内容的同学能有所帮忙。

You may also like...

1 Response

  1. October 10, 2021

    […] 我们在之前的文章《ElasticSearch进阶之多域的搜索》中介绍了什么是多域搜索。这里短语匹配在多域搜索中其实也会存在一个问题,我们来看下面这个例子,我们有下面两个名字: […]

Leave a Reply

Your email address will not be published. Required fields are marked *