概述
在全文检索中,检索结果与查询条件的相关性是一个极为重要的问题,优秀的全文检索引擎应该将那些与查询条件相关性高的文档排在最前面。想象一下,如果满足查询条件的文档成千上万,让用户在这些文档中再找出自己最满意的那一条,这无异于再做一次人工检索。用户一般很少会有耐心在检索结果中翻到第3页,所以处理好检索结果的相关性对于一个检索引擎来说至关重要。Google公司就是因为发明了Page Rank算法,巧妙地解决了网页检索结果的相关性问题,才在众多搜索公司中迅速崛起。
相关性问题有两方面问题要解决,一是如何评价单个查询条件的相关性,二是如何将多个查询条件的相关性组合起来。而相关性组合问题主要出现在组合查询中,所以本章在介绍相关性评分的同时也会介绍组合查询。
6.1相关性评分
全文检索与数据库查询的一个显著区别,就是它并不一定会根据查询条件做完全精确的匹配。除了上一章介绍的模糊查询以外,全文检索还会根据查询条件给文档的相关性打分并排序,将那些与查询条件相关性高的文档排在最前面。相关性(Relevance)或相似性(Similarity)是指两个事物间相互关联的程度,在检索领域特指检索请求与检索结果之间的相关程度。在Elasticsearch返回的每一条结果中都会包含一个_score字段,这个字段的值就是当前文档匹配检索请求的相关性评分。本书称_score字段记录的相关性分值为相关度,即相关性的程度。
解决相关性问题的核心是计算相关度的算法和模型,相关度算法和模型是全文检索引擎最重要的技术之一。相关度算法和相关度模型并非完全相同的概念,相关度模型可以认为是具有相同理论基础的算法集合。所以在实际应用时都是指定到具体的相关度算法,而相关度模型则是从理论层面对相关度算法的归类。
6.2组合查询与相关度组合
组合查询是DSL中与叶子查询相对应的另一种查询类型,组合查询可以将通过某种逻辑将叶子查询组合起来,实现对多个字段与多个查询条件的任意组合。组合查询组合的子查询不仅可以是基于词项或基于全文的叶子查询,也可以是另一个组合查询。
单纯从组合查询的使用上来看,组合查询并不复杂,复杂的是组合多个子查询相关度的逻辑,这也是它们的核心区别之一。
除了组合查询存在相关度组合问题以外,叶子查询中的query_string和multi_match查询由于在执行多字段检索时会转换为组合查询,所以也存在相关度组合问题,本小节也会一并介绍。
6.2.1bool组合查询
bool组合查询将一组布尔类型子句组合起来,形成一个大的布尔条件。通过SQL语言查询数据时,如果一条数据不满足where子句的查询条件,这条记录将不会作为结果返回。但Elasticsearch的bool组合查询则不同,在它的子句中,一些子句的确会决定文档是否会作为结果返回,而另一些子句则不决定文档是否可以作为结果,但会影响到结果的相关度。
bool组合查询可用的布尔类型子句包括must、filter、should和must_not四种,它们接收参数值的类型为数组,而数组中的元素即是以JSON对象表示的叶子查询。这4种子句的具体含义见表6-1。
可见,filter和must_not单纯只用于过滤文档,而它们对文档相关度没有任何影响。换句话说,这两种子句对查询结果的排序没有作用。在这四种子句中,should子句的情况有些复杂。首先它的执行结果影响相关度,但在过滤结果上则取决于上下文。当should子句与must子句或filter子句同时出现在子句中时,should子句将不会过滤结果。也就是说,在这种情况下,即使should子句不满足,结果也会返回。例如:
在示例6-5中,只有message字段包含firefox词项的日志文档才会被返回,而geo的src字段和dest字段是否为CN只影响相关度。但是如果在查询条件中将must子句删除,那么should子句就至少要满足有一条。should子句需要满足的个数由query的minimum_should_match参数决定,默认情况下它的值为1。这个参数在第5章5.2.1节中有过详细介绍,具体请参见该节表5-2。
布尔查询在计算相关性得分时,采取了匹配越多分值越高的策略。由于filter和must_not不参与分值运算,所以它会将must和should子句的相关性分值相加后返回给用户。
6.2.2 dis_max组合查询
dis_max查询(Disjunction Max Query)也是一种组合查询,只是它在计算相关性度时与bool查询不同。dis_max查询在计算相关性分值时,会在子查询中取最大相关性分值为最终相关性分值结果,而忽略其他子查询的相关性得分。dis_max查询通过queries参数接收对象的数组,数组元素可以是前面讲解的叶子查询。例如:
在多数情况下,完全不考虑其他字段的的相关度可能并不合适,所以可以使用tie_breaker参数设置其他字段参与相关度运算的系数。这个系数会在运算最终相关度时乘以其他字段的相关度,再加上最大得分就得到最终的相关度了。所以一般来说,tie_breaker应该小于1,默认值为0。例如在示例6-6的返回结果中,即使文档message和geo字段都满足查询条件它也不一定会排在最前面。可按示例6-7那样添加tie_breaker参数并设置为0.7:
在添加了tie_breaker参数后,相关度非最高值字段在参与最终相关度结果时的权重就降低为0.7。但它们对结果排序会产生影响,完全满足条件的文档将排在结果最前面。
6.2.3 constant_score查询
constant_score查询返回结果中文档的相关度为固定值,这个固定值由boost参数设置,默认值为1.0。constant_score查询只有两个参数filter和boost,前者与bool组合查询中的filter完全相同,仅用于过滤结果而不影响分值。
由于示例6-8中通过boost参数设置了相关度,所以满足查询条件文档的_score值将都是1.3。match_all查询也可以当成是一种特殊类型的constant_score查询,它会返回索引中所有文档,而每个文档的相关度都是1.0。
6.2.4 boosting查询
boosting查询通过positive子句设置满足条件的文档,这类似于bool查询中的must子句,只有满足positive条件的文档才会被返回。boosting查询通过negative子句设置需要排除文档的条件,这类似于bool查询中的must_not子句。但与bool查询不同的是,boosting查询不会将满足negative条件的文档从返回结果中排除,而只是会拉低它们的相关性分值。
在示例6-9中,参数negative_boost设置了一个系数,当满足negative条件时相关度会乘以这个系数作为最终分值,所以这个值应该小于1而大于等于0。例如示例6-9中的请求,如果geo.src为CN的文档相关度为1.6,那么geo.dest也是CN的文档相关度就需要再乘以0.2,所以最终相关度为0.32。
6.2.5 function_score查询
function_score查询提供了一组计算查询结果相关度的不同函数,通过为查询条件定义不同打分函数实现文档检索相关性的自定义打分机制。查询条件通过function_score的query参数设置,而使用的打分函数则使用functions参数设置。例如:
function_score查询在运算相关度时,首先会通过functions指定的打分函数算出每份文档的得分。如果指定了多个打分函数,它们打分的结果会根据score_mode参数定义的模式组合起来。以示例6-10为例,functions参数定义了两个打分函数random_score和weight,random_score函数会在0-1之间产生一个随机数,而weight函数则会以指定的值为相关性分值。由于score_mode参数设置的值为max,即从所有评分函数运算结果中取最大值,而weight值为2,它将永远大于random_score产生的值,所以评分函数最终给出的分值也将永远是2。score_mode包括以下几个选项multiply、sum、avg、first、max、min,通过名称很容易判断它们的含义,分别是在所有评分函数的运算结果中取它们的乘积、和、平均值、首个值、最大值和最小值。
打分函数运算的相关性评分会与query参数中查询条件的相关度组合起来,组合的方式通过boost_mode参数指定,它的默认值与score_mode一样都是multiply。boost_mode参数的可选值与score_mode也基本一致,但没有first而多了一个replace,代表使用评分函数计算结果代替查询分值。
可见function_score是一种在运算相关度上非常灵活的组合查询,这种灵活性主要体现在它提供了一组打分函数,以及组合这些打分函数的灵活方式。打分函数包括script_score、weight、random_score、field_value_factor以及一组衰减函数,如果只需要一个打分函数运算则可以直接使用打分函数名称做设置,而不用使用functions参数。在这些函数中,weight和random_score已经在示例6-10中使用过,下面再来简单介绍一下其他打分函数。
1.script_score函数
script_score函数通过script参数接收一段脚本运算相关度,脚本执行结果必须是非负的浮点数,返回负数会异常。例如:
在示例6-11中,由于只使用一个打分函数所以可以不使用functions参数,script_score函数通过script参数接收脚本。在示例6-11的脚本中,_score代表查询自身按默认相关度算法计算出来的相关度,而doc[‘AvgTicketPrice’].value则代表取当前文档的AvgTicketPrice字段值。所以这段脚本实际上是将相关度按票价由低到高的次序做了权重的提升,票价越低最终的相关度越高。这相当是找出所有从中国到美国的航班,并按票价由低到高的次序排序。script_score中使用的脚本默认也是Painless,可使用的变量见表6-2。
在这些变量中,params可通过params参数设置。以示例6-11中的请求为例,如果将平均票价的基准系数1000设置为变量,可按如下方式设置:
2.field_value_factor函数
field_value_factor函数在计算相关度时允许加入某一字段作为干扰因子,这类似于在示例6-11中通过AvgTicketPrice字段值提升或降低相关度的最终结果。只是通过field_value_factor函数时并不需要写脚本,而仅需要设置几个参数。例如示例6-11中的需求按field_value_factor函数来计算的话可以按如下方式请求:
在示例6-13中,field_value_factor打分函数通过field参数设置了干扰字段为AvgTicketPrice,而factor则是为干扰字段设置的调整因子,它会与字段值相乘后再参与接下来的运算。modifier参数就有些复杂了,它代表了干扰字段与调整因子相乘的结果如何参与相关度运算。在示例6-13中给给出的是reciprocal,代表取倒数1/x。所以如果使用Painless脚本表式示例6-13的运算,则应该写成“1/(doc[‘AvgTicketPrice’].value*0.001)”。这与示例6-11中的“_score /(doc[‘AvgTicketPrice’].value/1000)”略有区别。所以两个示例运算出来的相关度并不相同,但排序不会有变化。读者可以将示例6-11中_score改成1,则两者运算的相关性得分也会完全相同。
modifier可用运算方法除了reciprocal以外还有很多,具体见表6-3。
3.衰减函数
衰减函数是一组通过递减方式计算相关度的函数,它们会从指定的原始点开始对相关度做衰减,离原始点距离越远相关度就越低。衰减函数中的原始点是指某一字段的具体值,由于要计算其他文档与该字段值的距离,所以要求衰减函数原始点的字段类型必须是数值、日期或地理坐标中的一种。举例来说,如果在2019年3月25日前后系统运行出现异常,所以对这个日期前后的日志比较感兴趣,就可以按如下形式发送请求:
在示例6-14中使用的衰减函数为高斯函数(gauss),定义原始点使用的字段为timestamp,而具体的原始点则通过origin参数定义在了2019年3月25日。offset参数定义了在1天的范围内相关度不衰减,也就是说2019年3月24~26日相关度不衰减。scale参数和decay参数则共同决定了衰减的程度,前者定义了衰减的跨度范围,而后者则定义了衰减多少。以示例6-14中的设置为例,代表的含义是7天后的文档相关度衰减至0.3倍。
衰减函数除了高斯函数gauss以外,还有线性函数linear和指数函数exp两种。它们在使用上与高斯函数完全相同,读者可以自行将示例6-14中的gauss替换成linear或exp,并看看它们在相关度运算结果上有什么不同。如果将这几种衰减函数以图形画出来就会发现,它们在衰减的平滑度上有着比较明显的区别,如图6-2所示。
6.2.6相关度组合
组合查询一般由多个查询条件组成,所以在计算相关度时都要考虑以何种方式组合相关度。而多数的叶子查询都只针对一个字段设置查询条件,所以只有相关度权重提升问题而没有相关度组合问题。但叶子查询中有两个特例,它们是query_string查询和multi_match查询。由于这两个查询都可以针对多个字段设置查询条件,所以它们在计算相关度时也需要考虑组合多个相关度的问题,并且它们在组合相关度时有着相似的逻辑。
query_string和multi_match查询都具有一个type参数,用于指定针对多字段检索时时的执行逻辑及相关度组合方法。type参数有5个可选值,即best_fields、most_fields、cross_fields、phrase和phrase_prefix。例如:
1.best_fields、phrase与phrase_prefix类型
best_fields类型在执行时会将与字段匹配的文档都检索出来,但在计算相关度时会取得分最高的作为整个查询的相关度。例如在示例6-15中,第一个查询通过“OriginCountry^2”的形式将OriginCountry字段的相关度权重提升到2,所以这个字段相关度会高于DestCountry字段。在best_fields类型下执行检索时,DestCountry字段对最终相关度就不会再有影响。通过查看返回结果也可以看到,OriginCountry字段为CN的文档相关度都相同,即使DestCountry字段也是CN,文档的相关度也不会提升。
best_fields适用于用户希望匹配条件全部出现在一个字段中的情况,比如在文章标题和文章内容中同时检索elasticsearch和logstash时,如果在文章标题或是文章内容中同时出现了两个词项,该文章在相关度就会高于其他文章。
不知道读者是否还记得第6.2.2节介绍的dis_max查询,在相关度计算上dis_max查询是不是与best_fields类型很像?事实上,best_fields类型的查询在执行时会转化为dis_max查询,例如示例6-15在执行时会转化为
dis_max有一个参数tie_breaker,可以设置非最高值相关度参与最终相关度运算的系数,在multi_match中使用best_fields类型时也可以使用这个参数。
phrase与phrase_prefix类型在执行逻辑上与best_fields完全相同,只是在转换为dis_max时queries查询中的子查询会使用phrase或phrase_prefix而不是match。
2.most_fields类型
most_fields类型在计算相关度时会将所有相关度累加起来,然后再除以相关度的个数以得到它们的平均值作为最终的相关度。还是以示例6-15第一个检索为例,如果将type替换为most_fields,它会将OriginCountry和DestCountry两个字段匹配CN时计算出的相关度累加,然后再用累加和除以2作为最终的相关度。所以只有当两个字段都匹配了CN,最终的相关度才会更高。这在效果上相当于将出发地和目的地都是中国的文档排在了最前面,所以适用于希望检索出多个字段中同时都包含相同词项的检索。
在实现上,most_fields类型的查询会被转化为bool查询的should子句,示例6-15中的第一个检索在most_fields类型时会被转化为
3.cross_fields类型
如果查询条件中设置了多个词项,best_fields类型和most_fields类型都支持通过operator参数设置词项之间的逻辑关系,即and和or。但它们在设置operator时是针对字段级别的而不是针对词项级别的,来看一个例子:
示例6-18设置的查询条件为firefox和success两个词项,而匹配字段也是两个message和tags。当operator设置为and时,在best_fields类型下这意味着两个字段中需要至少有一个同时包含firefox和success两个词项,而这样的日志文档并不存在。而在cross_fields类型下则会将两个词项拆分出来,然后再一个字段分配一个词项。所以在效果上它并不要求字段同时包含两个词项,而要求词项分散在两个字段中。读者可以将示例6-18中的best_fields替换为cross_fields来体验它们的区别。
以上介绍的三大类型虽然都是以multi_match查询为例,但它们在使用query_string查询时也是有效的,本书在这里就不再展开举例了。
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/100104.html