10. 深入调校ES的多字段查询

best_fields, most_fields与cross_fields

在Elasticsearch(通常简称为ES)查询中,”most_fields”, “best_fields”, and “cross_fields” 是用于多字段(multi-field)查询时的不同策略。这些策略决定了如何对针对多个字段的搜索进行评分和处理。下面对这三种策略进行解释:

best_fields:

这种策略通常用于处理那些最佳匹配字段的情况。当查询多个字段时,它会选择匹配度最高的字段,并使用该字段的评分。这种策略适用于查询的各个部分都在一个字段中的情况。例如,如果你在标题和描述字段中搜索一个词,那么系统会为每个字段生成一个评分,并取最高的那个评分。

如下,dis_max表示用queries中评分最高的作为结果的score。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
{
"query": {
"dis_max": {
"queries": [
{
"match": {
"title": "Brown fox"
}
},
{
"match": {
"body": "Brown fox"
}
}
],
"tie_breaker": 0.3
}
}
}

其中的tie_breaker参数是dis_max查询的一个选项,它允许你在dis_max查询的基础上引入一些bool查询的特性。具体来说,它改变了得分(score)的计算方式:

  1. 最佳匹配子句的得分:首先,它取最佳匹配子句(即得分最高的子句)的得分(_score)。

  2. 其他匹配子句的得分调整:然后,它将其他每个匹配子句的得分乘以tie_breaker值。tie_breaker是一个介于0到1之间的数字。这意味着这些子句的得分会根据tie_breaker的值进行降权。

  3. 得分合并和归一化:最后,它将所有这些得分加在一起,并进行归一化处理。这样,即使是得分较低的子句也会对最终得分有所贡献,但得分最高的子句仍然具有最大的影响力。

总结来说,使用tie_breaker参数的dis_max查询是一种折衷方案,它保留了dis_max查询“最好的子句最重要”的特点,同时又让其他匹配的子句也能对最终得分有所贡献,尽管其影响较小。这使得查询结果既能反映最佳匹配的重要性,又能考虑到文档与多个查询子句的匹配程度。

此外,还有一个multi_match,是es的一个语法糖,也会将语句转化为dis_max,样例如下。

支持通配符:

1
2
3
4
5
6
{
"multi_match": {
"query": "Quick brown fox",
"fields": "*_title"
}
}

支持^表示权重

1
2
3
4
5
6
7
8
9
{
"multi_match": {
"query": "Quick brown fox",
"fields": [
"*_title",
"chapter_title^2"
]
}
}

most_fields:

这种策略会在所有匹配的字段中累加评分。这适用于查询的各部分分布在不同字段中的情况。使用most_fields策略时,如果一个查询词在多个字段中出现,那么这些字段的评分会被合并,从而增加文档的总体相关性评分。

具体实现的话,上面的multi_field默认的模式是best_fields,我们在调用时可以显示指定修改为most_fields,同样我们可以指定字段的权重。

1
2
3
4
5
6
7
{
"multi_match": {
"query": "jumping rabbits",
"type": "most_fields",
"fields": [ "title^10", "title.std" ]
}
}

cross_fields:

这种策略对于处理那些查询词在多个相关字段中分布的情况特别有效。它会把多个字段当作一个大字段来处理,这对于处理像全名这样的数据特别有效。例如,如果有一个名字字段和一个姓字段,cross_fields策略会在两个字段中查找匹配项,并将它们当作一个字段来处理。这对于处理分散在不同字段但属于同一概念的数据非常有效。

在处理实体搜索时,”most_fields”方法是一种常见的搜索策略,旨在通过匹配尽可能多的字段来提高搜索结果的相关性。然而,这种方法存在一些不那么明显的问题,这些问题可能会影响搜索结果的质量和准确性。

字段匹配优先,而非词汇匹配

“most_fields”方法的核心思想是在多个字段中查找匹配任何给定词汇的情况,目标是找到匹配字段最多的文档。这种方法的一个主要问题是,它更注重字段的数量而不是跨所有字段找到最多匹配词汇的重要性。这意味着,如果一个文档在多个不太重要的字段中包含了搜索词汇,它可能会被误认为比另一个在关键字段中有更多匹配但匹配字段总数较少的文档更相关。

举例:Poland的名称可以在道路,城市,国家中都出现意义不同,那么如果Poland在两个字段中出现的得分反而比Poland Street同时出现在一个字段要高,这是不合理的

1
2
3
4
5
6
7
8
9
10
11
{
"query": {
"multi_match": {
"query": "Poland Street W1V",
"type": "most_fields",
"fields": [
"street","city","country","postcode"
]
}
}
}

缺乏灵活的结果过滤

“most_fields”方法不支持使用”operator”或”minimum_should_match”这样的参数来减少不那么相关结果的长尾效应。这限制了搜索引擎在处理大量搜索结果时过滤和优先考虑最相关结果的能力。因此,用户可能会得到很多不那么相关的搜索结果,从而降低了搜索的效率和用户满意度。

举例如下,如果operator使用了and,那么等于是四个字段都需要包含query的部分内容,这明显是不合理的,但是不用and又会有很多长尾的干扰信息

1
2
3
4
5
6
7
8
9
10
11
12
{
"query": {
"multi_match": {
"query": "Poland Street W1V",
"type": "most_fields",
"operator": "and",
"fields": [
"street","city","country","postcode"
]
}
}
}

不同字段的词频干扰

每个字段中的词频(即一个词在该字段中出现的频率)是不同的,这可能会相互干扰,导致结果排序不佳。例如,一个词在标题字段中可能很少出现,但在内容字段中频繁出现。使用”most_fields”方法时,这种词频差异可能会导致不准确的权重分配和结果排序,从而影响搜索结果的相关性和准确性。

举例如下,由于TF/IDF的特性,Peter和Smith单独都是TF很高的,这种搜索会导致Smith Williams反而很靠前,因为Smith作为first name是非常少见的。

1
2
3
4
5
6
7
8
9
{
"query": {
"multi_match": {
"query": "Peter Smith",
"type": "most_fields",
"fields": ["*_name"]
}
}
}

那么具体如何实现cross_fields,es提供了两种方式。

预处理:Custom _all Fields

如下,建立索引时如果提前考虑后,可以将特定的一批字段都copy到一个新的字段中,这样搜索是使用full_name即可

1
2
3
4
"first_name": {
"type": "string",
"copy_to": "full_name"
}

在线查询:

multi_match指定type为cross_fields即可

1
2
3
4
5
6
7
8
9
10
{
"query": {
"multi_match": {
"query": "peter smith",
"type": "cross_fields",
"operator": "and",
"fields": ["first_name","last_name"]
}
}
}

它对应的实际查询逻辑是将fileds在线合并后再查询

1
2
+blended("peter", fields: [first_name, last_name]) 
+blended("smith", fields: [first_name, last_name])

最后,在选择使用哪种策略时,需要考虑查询的性质和你的数据结构。例如,如果你的数据是高度规范化的,并且查询通常集中在单个字段,那么best_fields可能是最好的选择。如果你的数据分布在多个相关字段,并且你希望利用这些字段之间的联系,那么cross_fields可能更适合。而如果你想要在多个字段中查找相同的术语,并且希望这些匹配合并起来增强相关性,那么most_fields可能是更好的选择。

Bool查询评分机制案例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
GET /_search 
{
"query": {
"bool": {
"should": [
{
"match": {
"title": "War and Peace"
}
},
{
"match": {
"author": "Leo Tolstoy"
}
},
{
"bool": {
"should": [
{
"match": {
"translator": "Constance Garnett"
}
},
{
"match": {
"translator": "Louise Maude"
}
}
]
}
}]}}}
  1. Bool查询的基本机制:

    • 在Elasticsearch中,bool查询允许你组合多个查询子句(如must, should, must_not, filter),并根据这些子句的匹配情况来确定文档是否符合查询条件以及它们的得分(评分)。
  2. 为什么把translator子句放在单独的bool查询中:

    • 上中提到了四个match查询,这些查询都被作为should子句处理。通常,should子句表示”可以匹配但不是必需的”。如果将translator子句与标题和作者子句放在同一层级,这意味着所有这些子句将对最终评分产生相同的影响。
  3. 得分计算方式:

    • 在bool查询中,每个匹配的should子句的得分会被加起来,然后乘以匹配子句的数量,再除以总的子句数量。这意味着,如果多个子句在同一层级,每个子句对最终得分的贡献是平等的。
  4. 影响评分的问题:

    • 在上面提到的查询中,把translator子句放在一个单独的bool查询内,这样整个translator bool查询在总得分中的权重是三分之一(因为有三个大的子句:标题、作者和translator bool查询)。如果translator子句被放在与标题和作者相同的层级,那么它们将会均分权重,使得标题和作者的每个子句只占总得分的四分之一。
  5. 结论:

    • 之所以将translator子句放在单独的bool查询中,是为了控制它们对最终评分的影响。这样做可以确保标题和作者子句在评分中占有更大的权重,而不是与translator子句平等分配权重。这在某种程度上反映了不同子句对于查询结果重要性的不同评估。