函数分数查询

编辑

function_score 允许您修改查询检索到的文档的分数。例如,如果某个评分函数计算量很大,并且仅在已过滤的文档集上计算分数就足够了,则这会很有用。

要使用 function_score,用户必须定义一个查询和一个或多个函数,这些函数将为查询返回的每个文档计算新的分数。

function_score 可以像这样仅使用一个函数:

resp = client.search(
    query={
        "function_score": {
            "query": {
                "match_all": {}
            },
            "boost": "5",
            "random_score": {},
            "boost_mode": "multiply"
        }
    },
)
print(resp)
response = client.search(
  body: {
    query: {
      function_score: {
        query: {
          match_all: {}
        },
        boost: '5',
        random_score: {},
        boost_mode: 'multiply'
      }
    }
  }
)
puts response
res, err := es.Search(
	es.Search.WithBody(strings.NewReader(`{
	  "query": {
	    "function_score": {
	      "query": {
	        "match_all": {}
	      },
	      "boost": "5",
	      "random_score": {},
	      "boost_mode": "multiply"
	    }
	  }
	}`)),
	es.Search.WithPretty(),
)
fmt.Println(res, err)
const response = await client.search({
  query: {
    function_score: {
      query: {
        match_all: {},
      },
      boost: "5",
      random_score: {},
      boost_mode: "multiply",
    },
  },
});
console.log(response);
GET /_search
{
  "query": {
    "function_score": {
      "query": { "match_all": {} },
      "boost": "5",
      "random_score": {}, 
      "boost_mode": "multiply"
    }
  }
}

有关支持的函数列表,请参阅 函数分数

此外,可以组合多个函数。在这种情况下,可以选择仅在文档与给定的过滤查询匹配时才应用该函数。

resp = client.search(
    query={
        "function_score": {
            "query": {
                "match_all": {}
            },
            "boost": "5",
            "functions": [
                {
                    "filter": {
                        "match": {
                            "test": "bar"
                        }
                    },
                    "random_score": {},
                    "weight": 23
                },
                {
                    "filter": {
                        "match": {
                            "test": "cat"
                        }
                    },
                    "weight": 42
                }
            ],
            "max_boost": 42,
            "score_mode": "max",
            "boost_mode": "multiply",
            "min_score": 42
        }
    },
)
print(resp)
response = client.search(
  body: {
    query: {
      function_score: {
        query: {
          match_all: {}
        },
        boost: '5',
        functions: [
          {
            filter: {
              match: {
                test: 'bar'
              }
            },
            random_score: {},
            weight: 23
          },
          {
            filter: {
              match: {
                test: 'cat'
              }
            },
            weight: 42
          }
        ],
        max_boost: 42,
        score_mode: 'max',
        boost_mode: 'multiply',
        min_score: 42
      }
    }
  }
)
puts response
res, err := es.Search(
	es.Search.WithBody(strings.NewReader(`{
	  "query": {
	    "function_score": {
	      "query": {
	        "match_all": {}
	      },
	      "boost": "5",
	      "functions": [
	        {
	          "filter": {
	            "match": {
	              "test": "bar"
	            }
	          },
	          "random_score": {},
	          "weight": 23
	        },
	        {
	          "filter": {
	            "match": {
	              "test": "cat"
	            }
	          },
	          "weight": 42
	        }
	      ],
	      "max_boost": 42,
	      "score_mode": "max",
	      "boost_mode": "multiply",
	      "min_score": 42
	    }
	  }
	}`)),
	es.Search.WithPretty(),
)
fmt.Println(res, err)
const response = await client.search({
  query: {
    function_score: {
      query: {
        match_all: {},
      },
      boost: "5",
      functions: [
        {
          filter: {
            match: {
              test: "bar",
            },
          },
          random_score: {},
          weight: 23,
        },
        {
          filter: {
            match: {
              test: "cat",
            },
          },
          weight: 42,
        },
      ],
      max_boost: 42,
      score_mode: "max",
      boost_mode: "multiply",
      min_score: 42,
    },
  },
});
console.log(response);
GET /_search
{
  "query": {
    "function_score": {
      "query": { "match_all": {} },
      "boost": "5", 
      "functions": [
        {
          "filter": { "match": { "test": "bar" } },
          "random_score": {}, 
          "weight": 23
        },
        {
          "filter": { "match": { "test": "cat" } },
          "weight": 42
        }
      ],
      "max_boost": 42,
      "score_mode": "max",
      "boost_mode": "multiply",
      "min_score": 42
    }
  }
}

整个查询的提升。

有关支持的函数列表,请参阅 函数分数

每个函数的过滤查询产生的分数无关紧要。

如果某个函数没有给定过滤器,则等效于指定 "match_all": {}

首先,每个文档都由定义的函数进行评分。参数 score_mode 指定如何组合计算的分数。

multiply

分数相乘(默认)

sum

分数相加

avg

分数求平均值

first

应用具有匹配过滤器的第一个函数

max

使用最大分数

min

使用最小分数

由于分数可能具有不同的刻度(例如,衰减函数在 0 和 1 之间,但 field_value_factor 是任意的),并且有时需要函数对分数产生不同的影响,因此可以使用用户定义的 weight 来调整每个函数的分数。weight 可以在 functions 数组中为每个函数定义(如上例所示),并与相应函数计算的分数相乘。如果在没有任何其他函数声明的情况下给出 weight,则 weight 会充当一个简单返回 weight 的函数。

如果将 score_mode 设置为 avg,则各个分数将通过加权平均值进行组合。例如,如果两个函数返回的分数为 1 和 2,并且它们的权重分别为 3 和 4,则它们的分数将组合为 (1*3+2*4)/(3+4)而不是 (1*3+2*4)/2

可以通过设置 max_boost 参数来限制新分数不超过某个限制。max_boost 的默认值为 FLT_MAX。

新计算的分数将与查询的分数组合。参数 boost_mode 定义了如何组合:

multiply

查询分数和函数分数相乘(默认)

replace

仅使用函数分数,忽略查询分数

sum

查询分数和函数分数相加

avg

average

max

查询分数和函数分数中的最大值

min

查询分数和函数分数中的最小值

默认情况下,修改分数不会更改哪些文档匹配。要排除不满足特定分数阈值的文档,可以将 min_score 参数设置为所需的分数阈值。

要使 min_score 生效,需要对查询返回的所有文档进行评分,然后逐个过滤掉。

function_score 查询提供了多种类型的评分函数。

脚本分数

编辑

script_score 函数允许您封装另一个查询,并可选择使用从文档中的其他数值字段值导出的计算(使用脚本表达式)来自定义其评分。这是一个简单的示例:

resp = client.search(
    query={
        "function_score": {
            "query": {
                "match": {
                    "message": "elasticsearch"
                }
            },
            "script_score": {
                "script": {
                    "source": "Math.log(2 + doc['my-int'].value)"
                }
            }
        }
    },
)
print(resp)
response = client.search(
  body: {
    query: {
      function_score: {
        query: {
          match: {
            message: 'elasticsearch'
          }
        },
        script_score: {
          script: {
            source: "Math.log(2 + doc['my-int'].value)"
          }
        }
      }
    }
  }
)
puts response
const response = await client.search({
  query: {
    function_score: {
      query: {
        match: {
          message: "elasticsearch",
        },
      },
      script_score: {
        script: {
          source: "Math.log(2 + doc['my-int'].value)",
        },
      },
    },
  },
});
console.log(response);
GET /_search
{
  "query": {
    "function_score": {
      "query": {
        "match": { "message": "elasticsearch" }
      },
      "script_score": {
        "script": {
          "source": "Math.log(2 + doc['my-int'].value)"
        }
      }
    }
  }
}

在 Elasticsearch 中,所有文档分数都是正 32 位浮点数。

如果 script_score 函数产生的分数具有更高的精度,则会将其转换为最接近的 32 位浮点数。

同样,分数必须为非负数。否则,Elasticsearch 将返回错误。

除了不同的脚本字段值和表达式外,_score 脚本参数可用于检索基于封装查询的分数。

脚本编译会被缓存以加快执行速度。如果脚本具有需要考虑的参数,则最好重用同一脚本并为其提供参数:

resp = client.search(
    query={
        "function_score": {
            "query": {
                "match": {
                    "message": "elasticsearch"
                }
            },
            "script_score": {
                "script": {
                    "params": {
                        "a": 5,
                        "b": 1.2
                    },
                    "source": "params.a / Math.pow(params.b, doc['my-int'].value)"
                }
            }
        }
    },
)
print(resp)
response = client.search(
  body: {
    query: {
      function_score: {
        query: {
          match: {
            message: 'elasticsearch'
          }
        },
        script_score: {
          script: {
            params: {
              a: 5,
              b: 1.2
            },
            source: "params.a / Math.pow(params.b, doc['my-int'].value)"
          }
        }
      }
    }
  }
)
puts response
const response = await client.search({
  query: {
    function_score: {
      query: {
        match: {
          message: "elasticsearch",
        },
      },
      script_score: {
        script: {
          params: {
            a: 5,
            b: 1.2,
          },
          source: "params.a / Math.pow(params.b, doc['my-int'].value)",
        },
      },
    },
  },
});
console.log(response);
GET /_search
{
  "query": {
    "function_score": {
      "query": {
        "match": { "message": "elasticsearch" }
      },
      "script_score": {
        "script": {
          "params": {
            "a": 5,
            "b": 1.2
          },
          "source": "params.a / Math.pow(params.b, doc['my-int'].value)"
        }
      }
    }
  }
}

请注意,与 custom_score 查询不同,查询的分数会与脚本评分的结果相乘。如果您希望禁止此操作,请设置 "boost_mode": "replace"

权重

编辑

weight 分数允许您将分数乘以提供的 weight。有时可能需要这样做,因为在特定查询上设置的提升值会被规范化,而对于此评分函数则不会。数字值的类型为 float。

"weight" : number

随机

编辑

random_score 生成的分数均匀分布在 0 到 1 之间(不包括 1)。默认情况下,它使用内部 Lucene 文档 ID 作为随机性来源,这非常高效,但不幸的是,由于文档可能会被合并重新编号,因此无法重现。

如果您希望分数是可重现的,则可以提供 seedfield。然后,将根据此种子、所考虑文档的 field 的最小值和一个基于索引名称和分片 ID 计算的盐值来计算最终分数,以便具有相同值但存储在不同索引中的文档获得不同的分数。请注意,同一分片内且 field 的值相同的文档仍将获得相同的分数,因此通常最好使用对所有文档都具有唯一值的字段。一个很好的默认选择可能是使用 _seq_no 字段,其唯一的缺点是如果文档更新,分数也会更改,因为更新操作也会更新 _seq_no 字段的值。

可以在不设置字段的情况下设置种子,但这已被弃用,因为这需要在 _id 字段上加载 fielddata,这会消耗大量内存。

resp = client.search(
    query={
        "function_score": {
            "random_score": {
                "seed": 10,
                "field": "_seq_no"
            }
        }
    },
)
print(resp)
response = client.search(
  body: {
    query: {
      function_score: {
        random_score: {
          seed: 10,
          field: '_seq_no'
        }
      }
    }
  }
)
puts response
res, err := es.Search(
	es.Search.WithBody(strings.NewReader(`{
	  "query": {
	    "function_score": {
	      "random_score": {
	        "seed": 10,
	        "field": "_seq_no"
	      }
	    }
	  }
	}`)),
	es.Search.WithPretty(),
)
fmt.Println(res, err)
const response = await client.search({
  query: {
    function_score: {
      random_score: {
        seed: 10,
        field: "_seq_no",
      },
    },
  },
});
console.log(response);
GET /_search
{
  "query": {
    "function_score": {
      "random_score": {
        "seed": 10,
        "field": "_seq_no"
      }
    }
  }
}

字段值因子

编辑

field_value_factor 函数允许您使用文档中的字段来影响分数。它类似于使用 script_score 函数,但是它避免了脚本开销。如果对多值字段使用,则仅在计算中使用该字段的第一个值。

例如,假设您有一个使用数值 my-int 字段索引的文档,并且希望使用此字段影响文档的分数,一个这样做的示例如下所示:

resp = client.search(
    query={
        "function_score": {
            "field_value_factor": {
                "field": "my-int",
                "factor": 1.2,
                "modifier": "sqrt",
                "missing": 1
            }
        }
    },
)
print(resp)
response = client.search(
  body: {
    query: {
      function_score: {
        field_value_factor: {
          field: 'my-int',
          factor: 1.2,
          modifier: 'sqrt',
          missing: 1
        }
      }
    }
  }
)
puts response
const response = await client.search({
  query: {
    function_score: {
      field_value_factor: {
        field: "my-int",
        factor: 1.2,
        modifier: "sqrt",
        missing: 1,
      },
    },
  },
});
console.log(response);
GET /_search
{
  "query": {
    "function_score": {
      "field_value_factor": {
        "field": "my-int",
        "factor": 1.2,
        "modifier": "sqrt",
        "missing": 1
      }
    }
  }
}

这将转换为以下评分公式:

sqrt(1.2 * doc['my-int'].value)

field_value_factor 函数有许多选项:

field

要从文档中提取的字段。

factor

可选因子,用于与字段值相乘,默认为 1

modifier

要应用于字段值的修饰符,可以是以下之一:noneloglog1plog2plnln1pln2psquaresqrtreciprocal。默认为 none

修饰符 含义

none

不对字段值应用任何乘数

log

取字段值的常用对数。由于此函数将返回负值,并且如果对 0 到 1 之间的值使用将导致错误,因此建议改用 log1p

log1p

将 1 添加到字段值并取常用对数

log2p

将 2 添加到字段值并取常用对数

ln

取字段值的自然对数。由于此函数将返回负值,并且如果对 0 到 1 之间的值使用将导致错误,因此建议改用 ln1p

ln1p

将 1 添加到字段值并取自然对数

ln2p

将 2 添加到字段值并取自然对数

square

将字段值平方(将其自身相乘)

sqrt

取字段值的平方根

reciprocal

求倒数 字段值,与 1/x 相同,其中 x 是字段的值

missing
如果文档没有该字段,则使用该值。修饰符和因子仍然像从文档中读取一样应用于它。

field_value_score 函数产生的分数必须为非负数,否则将引发错误。logln 修饰符如果对 0 到 1 之间的值使用,则会产生负值。请务必使用范围过滤器限制字段的值以避免这种情况,或使用 log1pln1p

请记住,对 0 取 log() 或对负数开平方根是一种非法操作,将引发异常。请务必使用范围过滤器限制字段的值以避免这种情况,或使用 log1pln1p

衰减函数

编辑

衰减函数使用一个函数对文档进行评分,该函数根据文档的数值字段值与用户给定的原点之间的距离进行衰减。这类似于范围查询,但具有平滑的边缘而不是硬边界。

要在具有数值字段的查询中使用距离评分,用户必须为每个字段定义一个 origin(原点)和一个 scale(比例)。origin 用于定义计算距离的“中心点”,而 scale 用于定义衰减率。衰减函数指定为

"DECAY_FUNCTION": { 
    "FIELD_NAME": { 
          "origin": "11, 12",
          "scale": "2km",
          "offset": "0km",
          "decay": 0.33
    }
}

DECAY_FUNCTION 应该为 linear(线性), exp(指数)或 gauss(高斯)之一。

指定的字段必须是数值型、日期型或地理点型字段。

在上面的示例中,该字段是一个 geo_point(地理点),并且可以以地理格式提供原点。scaleoffset(偏移量)在这种情况下必须给出单位。如果您的字段是日期字段,则可以将 scaleoffset 设置为天、周等。示例

resp = client.search(
    query={
        "function_score": {
            "gauss": {
                "@timestamp": {
                    "origin": "2013-09-17",
                    "scale": "10d",
                    "offset": "5d",
                    "decay": 0.5
                }
            }
        }
    },
)
print(resp)
response = client.search(
  body: {
    query: {
      function_score: {
        gauss: {
          "@timestamp": {
            origin: '2013-09-17',
            scale: '10d',
            offset: '5d',
            decay: 0.5
          }
        }
      }
    }
  }
)
puts response
const response = await client.search({
  query: {
    function_score: {
      gauss: {
        "@timestamp": {
          origin: "2013-09-17",
          scale: "10d",
          offset: "5d",
          decay: 0.5,
        },
      },
    },
  },
});
console.log(response);
GET /_search
{
  "query": {
    "function_score": {
      "gauss": {
        "@timestamp": {
          "origin": "2013-09-17", 
          "scale": "10d",
          "offset": "5d",         
          "decay": 0.5            
        }
      }
    }
  }
}

原点的日期格式取决于您的映射中定义的 format(格式)。如果您不定义原点,则使用当前时间。

offsetdecay(衰减)参数是可选的。

origin(原点)

用于计算距离的原点。对于数值字段,必须作为数字给出;对于日期字段,必须作为日期给出;对于地理字段,必须作为地理点给出。地理和数值字段是必需的。对于日期字段,默认值为 now(现在)。原点支持日期数学运算(例如 now-1h)。

scale(比例)

所有类型都必需。定义计算出的分数将等于 decay 参数的距离,该距离是从原点 + 偏移量开始计算的。对于地理字段:可以定义为数字+单位(1km,12m,…)。默认单位为米。对于日期字段:可以定义为数字+单位(“1h”,“10d”,…)。默认单位为毫秒。对于数值字段:任何数字。

offset(偏移量)

如果定义了 offset,则衰减函数只会计算距离大于定义的 offset 的文档的衰减函数。默认值为 0。

decay(衰减)

decay 参数定义文档在 scale 指定的距离处如何评分。如果未定义 decay,则距离为 scale 的文档将得分为 0.5。

在第一个示例中,您的文档可能代表酒店,并包含一个地理位置字段。您想根据酒店与给定位置的距离计算衰减函数。您可能不会立即看到为高斯函数选择什么比例,但是您可以说:“在距离所需位置 2 公里的距离处,分数应降低到三分之一。” 然后将自动调整参数“scale”,以确保得分函数为距离所需位置 2 公里的酒店计算得分 0.33。

在第二个示例中,字段值在 2013-09-12 和 2013-09-22 之间的文档将获得权重 1.0,而距离该日期 15 天的文档将获得权重 0.5。

支持的衰减函数

编辑

DECAY_FUNCTION 确定衰减的形状

gauss(高斯)

正态衰减,计算为

Gaussian

其中 sigma 被计算以确保得分在距离 origin+-offsetscale 处的值为 decay

sigma calc

请参阅 正态衰减,关键字 gauss,其中包含演示 gauss 函数生成的曲线的图表。

exp(指数)

指数衰减,计算为

Exponential

再次,参数 lambda 被计算以确保得分在距离 origin+-offsetscale 处的值为 decay

lambda calc

请参阅 指数衰减,关键字 exp,其中包含演示 exp 函数生成的曲线的图表。

linear(线性)

线性衰减,计算为

Linear.

再次,参数 s 被计算以确保得分在距离 origin+-offsetscale 处的值为 decay

s calc

与正态衰减和指数衰减相比,此函数实际上会在字段值超过用户给定的比例值的两倍时将分数设置为 0。

对于单个函数,三个衰减函数及其参数可以这样可视化(此示例中的字段称为“age”)

decay 2d

多值字段

编辑

如果用于计算衰减的字段包含多个值,则默认情况下,选择最接近原点的值来确定距离。可以通过设置 multi_value_mode 来更改此设置。

min

距离是最小距离

max

距离是最大距离

avg

距离是平均距离

sum

距离是所有距离的总和

示例

    "DECAY_FUNCTION": {
        "FIELD_NAME": {
              "origin": ...,
              "scale": ...
        },
        "multi_value_mode": "avg"
    }

详细示例

编辑

假设您正在某个城镇寻找酒店。您的预算有限。此外,您希望酒店靠近市中心,因此酒店距离所需位置越远,您入住的可能性就越小。

您希望根据到市中心的距离以及价格对符合您条件的查询结果(例如,“酒店,南希,不吸烟”)进行评分。

直观上,您想将市中心定义为原点,也许您愿意从酒店步行 2 公里到市中心。
在这种情况下,位置字段的 原点 是市中心,比例 是 ~2 公里。

如果您的预算较低,您可能会更喜欢便宜的而不是贵的。对于价格字段,原点 将是 0 欧元,而 比例 取决于您愿意支付多少钱,例如 20 欧元。

在此示例中,这些字段可能称为酒店价格的“price”和酒店坐标的“location”。

在这种情况下,price 的函数将是

"gauss": { 
    "price": {
          "origin": "0",
          "scale": "20"
    }
}

此衰减函数也可以是 linearexp

对于 location

"gauss": { 
    "location": {
          "origin": "11, 12",
          "scale": "2km"
    }
}

此衰减函数也可以是 linearexp

假设您想将这两个函数乘以原始分数,则请求将如下所示

resp = client.search(
    query={
        "function_score": {
            "functions": [
                {
                    "gauss": {
                        "price": {
                            "origin": "0",
                            "scale": "20"
                        }
                    }
                },
                {
                    "gauss": {
                        "location": {
                            "origin": "11, 12",
                            "scale": "2km"
                        }
                    }
                }
            ],
            "query": {
                "match": {
                    "properties": "balcony"
                }
            },
            "score_mode": "multiply"
        }
    },
)
print(resp)
response = client.search(
  body: {
    query: {
      function_score: {
        functions: [
          {
            gauss: {
              price: {
                origin: '0',
                scale: '20'
              }
            }
          },
          {
            gauss: {
              location: {
                origin: '11, 12',
                scale: '2km'
              }
            }
          }
        ],
        query: {
          match: {
            properties: 'balcony'
          }
        },
        score_mode: 'multiply'
      }
    }
  }
)
puts response
res, err := es.Search(
	es.Search.WithBody(strings.NewReader(`{
	  "query": {
	    "function_score": {
	      "functions": [
	        {
	          "gauss": {
	            "price": {
	              "origin": "0",
	              "scale": "20"
	            }
	          }
	        },
	        {
	          "gauss": {
	            "location": {
	              "origin": "11, 12",
	              "scale": "2km"
	            }
	          }
	        }
	      ],
	      "query": {
	        "match": {
	          "properties": "balcony"
	        }
	      },
	      "score_mode": "multiply"
	    }
	  }
	}`)),
	es.Search.WithPretty(),
)
fmt.Println(res, err)
const response = await client.search({
  query: {
    function_score: {
      functions: [
        {
          gauss: {
            price: {
              origin: "0",
              scale: "20",
            },
          },
        },
        {
          gauss: {
            location: {
              origin: "11, 12",
              scale: "2km",
            },
          },
        },
      ],
      query: {
        match: {
          properties: "balcony",
        },
      },
      score_mode: "multiply",
    },
  },
});
console.log(response);
GET /_search
{
  "query": {
    "function_score": {
      "functions": [
        {
          "gauss": {
            "price": {
              "origin": "0",
              "scale": "20"
            }
          }
        },
        {
          "gauss": {
            "location": {
              "origin": "11, 12",
              "scale": "2km"
            }
          }
        }
      ],
      "query": {
        "match": {
          "properties": "balcony"
        }
      },
      "score_mode": "multiply"
    }
  }
}

接下来,我们将展示三种可能的衰减函数中每种函数的计算得分如何。

正态衰减,关键字 gauss

编辑

在上面的示例中选择 gauss 作为衰减函数时,乘数的轮廓图和表面图如下所示

cd0e18a6 e898 11e2 9b3c f0145078bd6f
ec43c928 e898 11e2 8e0d f3c4519dbd89

假设您的原始搜索结果匹配了三家酒店

  • “背包客旅馆”
  • “酒驾旅馆”
  • “贝尔维尤住宿加早餐酒店”。

“酒驾旅馆”离您定义的位置很远(将近 2 公里),而且不是很便宜(大约 13 欧元),因此它的系数较低,为 0.56。“贝尔维尤住宿加早餐酒店”和“背包客旅馆”都非常靠近定义的位置,但是“贝尔维尤住宿加早餐酒店”更便宜,因此它的乘数为 0.86,而“背包客旅馆”的值为 0.66。

指数衰减,关键字 exp

编辑

在上面的示例中选择 exp 作为衰减函数时,乘数的轮廓图和表面图如下所示

082975c0 e899 11e2 86f7 174c3a729d64
0b606884 e899 11e2 907b aefc77eefef6

线性衰减,关键字 linear

编辑

在上面的示例中选择 linear 作为衰减函数时,乘数的轮廓图和表面图如下所示

1775b0ca e899 11e2 9f4a 776b406305c6
19d8b1aa e899 11e2 91bc 6b0553e8d722

衰减函数支持的字段

编辑

仅支持数值型、日期型和地理点型字段。

如果缺少字段怎么办?

编辑

如果文档中缺少数值字段,则该函数将返回 1。