script_score 查询

脚本评分:使用 script(脚本) 为返回的文档提供自定义分数。

如果评分函数使用成本很高,但只需要计算一组经过过滤的文档的分数,则script_score 查询非常有用。

请求示例

下面这个 script_score 查询为每个返回的文档分配一个分数,该分数等于 likes 字段的值除以 10

GET /_search
{
    "query" : {
        "script_score" : {
            "query" : {
                "match": { "message": "elasticsearch" }
            },
            "script" : {
                "source" : "doc['likes'].value / 10 "
            }
        }
     }
}

script_score的顶级参数

query
(必需, query 对象) 用来返回文档的 query。
script

(必需, script 对象) 用于计算query返回的文档的分数的脚本。

script_score 查询的最终相关性评分不能为负数。 为了支持某些搜索优化,Lucene 要求分数为正数或0

min_score
(可选, float) 分数低于此浮点数的文档将从搜索结果中排除。
boost
(可选, float) 由 script 产生的文档的分数乘以 boost 以产生最终的文档的分数。默认为 1.0

注意

在脚本中使用相关性评分

在脚本中,你可以访问访问_score 变量,该变量表示文档的当前相关性评分。

一些预定义的函数

你可以在script中使用任何可用的无痛函数(painless function)。 还可以使用下面几个预定义的函数来自定义评分:

建议使用这些预定义的函数,而不是编写自己的函数。 这些功能从 Elasticsearch 的内部机制来说更高效。

饱和度 (saturation)

saturation(value,k) = value/(k + value)

"script" : {
    "source" : "saturation(doc['likes'].value, 1)"
}
sigmoid

sigmoid(value, k, a) = value^a/ (k^a + value^a)

"script" : {
    "source" : "sigmoid(doc['likes'].value, 2, 1)"
}
随机分数函数

random_score 函数生成从0到1(不包括1)均匀分布的分数。

randomScore函数的语法是:randomScore(<seed>, <fieldName>)。 它有一个作为整数值的必需的参数 seed 和一个作为字符串值的可选参数 fieldName

"script" : {
    "source" : "randomScore(100, '_seq_no')"
}

如果省略了参数fieldName,Lucene 内部的文档 id 将被用作随机性的来源。 这非常有效,但却是不可再现的,因为文档可能会通过合并(merge)重新编号。

"script" : {
    "source" : "randomScore(100)"
}

请注意,在同一个分片中具有相同字段值的文档将获得相同的分数,因此通常希望对整个分片中的所有文档使用具有唯一值的字段。 一个好的默认选择可能是使用_seq_no字段,它唯一的缺点是如果文档被更新,分数将会改变,因为更新操作也会更新_seq_no字段的值。

数值字段的衰减函数

你可以在这里阅读更多关于衰减函数的内容。

  • double decayNumericLinear(double origin, double scale, double offset, double decay, double docValue)
  • double decayNumericExp(double origin, double scale, double offset, double decay, double docValue)
  • double decayNumericGauss(double origin, double scale, double offset, double decay, double docValue)
"script" : {
    "source" : "decayNumericLinear(params.origin, params.scale, params.offset, params.decay, doc['dval'].value)",
    "params": { 
        "origin": 20,
        "scale": 10,
        "decay" : 0.5,
        "offset" : 0
    }
}

使用 params 允许只编译一次脚本,即使 params 改变了。

geo字段的衰减函数
  • double decayGeoLinear(String originStr, String scaleStr, String offsetStr, double decay, GeoPoint docValue)
  • double decayGeoExp(String originStr, String scaleStr, String offsetStr, double decay, GeoPoint docValue)
  • double decayGeoGauss(String originStr, String scaleStr, String offsetStr, double decay, GeoPoint docValue)
"script" : {
    "source" : "decayGeoExp(params.origin, params.scale, params.offset, params.decay, doc['location'].value)",
    "params": {
        "origin": "40, -70.12",
        "scale": "200km",
        "offset": "0km",
        "decay" : 0.2
    }
}
date字段的衰减函数
  • double decayDateLinear(String originStr, String scaleStr, String offsetStr, double decay, JodaCompatibleZonedDateTime docValueDate)
  • double decayDateExp(String originStr, String scaleStr, String offsetStr, double decay, JodaCompatibleZonedDateTime docValueDate)
  • double decayDateGauss(String originStr, String scaleStr, String offsetStr, double decay, JodaCompatibleZonedDateTime docValueDate)
"script" : {
    "source" : "decayDateGauss(params.origin, params.scale, params.offset, params.decay, doc['date'].value)",
    "params": {
        "origin": "2008-01-01T01:00:00Z",
        "scale": "1h",
        "offset" : "0",
        "decay" : 0.5
    }
}

date的衰减函数仅限于默认格式和默认时区的日期。 也不支持使用now进行计算。

vector字段的函数

vector字段的函数 可通过script_score查询来访问。

允许执行昂贵的查询

如果 search.allow_expensive_queries 设置为 false 则脚本评分查询不会被执行。

更快的选择

script_score 查询计算每个匹配文档或命中的得分。 有更快的替代查询类型,可以有效地跳过非竞争命中:

  • 如果你想提升一些静态字段的文档,可以使用 rank_feature 查询。
  • 如果你想提升与给定日期或地理坐标点更接近的文档,请使用 distance_feature 查询。

从函数评分(function_score)查询转换

我们正在废弃 function_score 查询,建议使用 script_score 查询代替之。

可以使用 script_score 查询从 function_score 查询实现以下函数:

script_score

你在 函数评分(function_score) 查询中的 script_score 里的代码,可以直接复制到 脚本评分(script_score) 查询中,无需任何修改。

weight

weight 函数可以通过下面的脚本在脚本评分查询中实现:

"script" : {
    "source" : "params.weight * _score",
    "params": {
        "weight": 2
    }
}
random_score

随机评分函数(random score function)中描述的那样去使用 randomScore 函数。

field_value_factor

field_value_factor 函数可以通过脚本轻松实现:

"script" : {
    "source" : "Math.log10(doc['field'].value * params.factor)",
    "params" : {
        "factor" : 5
    }
}

要检查文档是否有缺失值,可以使用 doc['field'].size() == 0。 例如,如果文档没有字段 field,此脚本将使用1作为它的值:

"script" : {
    "source" : "Math.log10((doc['field'].size() == 0 ? 1 : doc['field'].value()) * params.factor)",
    "params" : {
        "factor" : 5
    }
}

下表列出了如何通过脚本实现 field_value_factor 修饰符:

修饰符 在脚本评分中的实现

none

-

log

Math.log10(doc['f'].value)

log1p

Math.log10(doc['f'].value + 1)

log2p

Math.log10(doc['f'].value + 2)

ln

Math.log(doc['f'].value)

ln1p

Math.log(doc['f'].value + 1)

ln2p

Math.log(doc['f'].value + 2)

square

Math.pow(doc['f'].value, 2)

sqrt

Math.sqrt(doc['f'].value)

reciprocal

1.0 / doc['f'].value

衰减(decay)函数

script_score 查询具有可在脚本中使用的等效的衰减函数(decay function)

vector字段的函数

在向量(vector)函数的计算过程中,所有匹配的文档都被线性扫描。 因此,预计查询时间会随着匹配文档的数量而线性增长。 因此,建议使用 query 参数来限制匹配文档的数量。

dense_vector函数

让我们创建一个带有dense_vector类型的映射的索引,然后添加并索引几个文档进去。

PUT my_index
{
  "mappings": {
    "properties": {
      "my_dense_vector": {
        "type": "dense_vector",
        "dims": 3
      },
      "status" : {
        "type" : "keyword"
      }
    }
  }
}

PUT my_index/_doc/1
{
  "my_dense_vector": [0.5, 10, 6],
  "status" : "published"
}

PUT my_index/_doc/2
{
  "my_dense_vector": [-0.5, 10, 10],
  "status" : "published"
}

POST my_index/_refresh

cosineSimilarity 函数计算给定的查询向量(query_vector) 和文档向量(vector) 之间的余弦相似度。

GET my_index/_search
{
  "query": {
    "script_score": {
      "query" : {
        "bool" : {
          "filter" : {
            "term" : {
              "status" : "published" 
            }
          }
        }
      },
      "script": {
        "source": "cosineSimilarity(params.query_vector, 'my_dense_vector') + 1.0", 
        "params": {
          "query_vector": [4, 3.4, -0.2]  
        }
      }
    }
  }
}

要限制应用到脚本评分计算的文档数量,请提供一个 filter。

该脚本将余弦相似度加 1.0,以防止分数为负。

为了利用脚本优化,提供一个 查询向量(query_vector) 作为脚本参数。

如果文档的 dense_vector 字段的维数与 查询向量(query_vector) 的维数不同,将会抛出一个错误。

dotProduct 函数计算给定的查询向量(query_vector) 和文档的向量(vector) 之间的点积。

GET my_index/_search
{
  "query": {
    "script_score": {
      "query" : {
        "bool" : {
          "filter" : {
            "term" : {
              "status" : "published"
            }
          }
        }
      },
      "script": {
        "source": """
          double value = dotProduct(params.query_vector, 'my_dense_vector');
          return sigmoid(1, Math.E, -value); 
        """,
        "params": {
          "query_vector": [4, 3.4, -0.2]
        }
      }
    }
  }
}

使用标准的 sigmoid 函数可以防止分数为负。

l1norm 函数计算给定的查询向量(query_vector) 和文档向量(vector) 之间的 L1距离(曼哈顿距离)。

GET my_index/_search
{
  "query": {
    "script_score": {
      "query" : {
        "bool" : {
          "filter" : {
            "term" : {
              "status" : "published"
            }
          }
        }
      },
      "script": {
        "source": "1 / (1 + l1norm(params.queryVector, 'my_dense_vector'))", 
        "params": {
          "queryVector": [4, 3.4, -0.2]
        }
      }
    }
  }
}

与表示相似性的 cosineSimilarity 不同,下面显示的 l1norml2norm 表示距离或差异。 这意味着,向量(vector)越相似,l1norml2norm 函数产生的分数越低。 因此,因为我们需要更多相似的向量(vector)来获得更高的分数,所以我们颠倒了 l1norml2norm 的输出。 此外,为了避免文档向量(vector)与查询完全匹配时被 0 除,我们在分母中添加了 1

l2norm 函数计算给定的查询向量(query_vector) 和文档向量(vector) 之间的 L2距离(欧几里德距离)。

GET my_index/_search
{
  "query": {
    "script_score": {
      "query" : {
        "bool" : {
          "filter" : {
            "term" : {
              "status" : "published"
            }
          }
        }
      },
      "script": {
        "source": "1 / (1 + l2norm(params.queryVector, 'my_dense_vector'))",
        "params": {
          "queryVector": [4, 3.4, -0.2]
        }
      }
    }
  }
}

如果文档没有执行 向量(vector) 函数的 vector 字段的值,将会抛出一个错误。

可以通过 doc['my_vector'].size() == 0 检查文档的字段my_vector是否有值。 整个脚本可能如下所示:

"source": "doc['my_vector'].size() == 0 ? 0 : cosineSimilarity(params.queryVector, 'my_vector')"
sparse_vector函数

在 7.6 中废弃。

sparse_vector类型已废弃并将在8.0版本中移除。

我们来创建一个包含sparse_vector类型的映射的索引,添加并索引几个文档进去:

PUT my_sparse_index
{
  "mappings": {
    "properties": {
      "my_sparse_vector": {
        "type": "sparse_vector"
      },
      "status" : {
        "type" : "keyword"
      }
    }
  }
}
PUT my_sparse_index/_doc/1
{
  "my_sparse_vector": {"2": 1.5, "15" : 2, "50": -1.1, "4545": 1.1},
  "status" : "published"
}

PUT my_sparse_index/_doc/2
{
  "my_sparse_vector": {"2": 2.5, "10" : 1.3, "55": -2.3, "113": 1.6},
  "status" : "published"
}

POST my_sparse_index/_refresh

cosineSimilaritySparse 函数计算给定的查询向量(query_vector) 和文档向量(vector) 之间的余弦相似性。

GET my_sparse_index/_search
{
  "query": {
    "script_score": {
      "query" : {
        "bool" : {
          "filter" : {
            "term" : {
              "status" : "published"
            }
          }
        }
      },
      "script": {
        "source": "cosineSimilaritySparse(params.query_vector, 'my_sparse_vector') + 1.0",
        "params": {
          "query_vector": {"2": 0.5, "10" : 111.3, "50": -1.3, "113": 14.8, "4545": 156.0}
        }
      }
    }
  }
}

dotProductSparse 函数计算给定的查询向量(query_vector) 和文档向量(vector) 之间的点积。

GET my_sparse_index/_search
{
  "query": {
    "script_score": {
      "query" : {
        "bool" : {
          "filter" : {
            "term" : {
              "status" : "published"
            }
          }
        }
      },
      "script": {
        "source": """
          double value = dotProductSparse(params.query_vector, 'my_sparse_vector');
          return sigmoid(1, Math.E, -value);
        """,
         "params": {
          "query_vector": {"2": 0.5, "10" : 111.3, "50": -1.3, "113": 14.8, "4545": 156.0}
        }
      }
    }
  }
}

l1normSparse 函数计算给定的查询向量(query_vector) 和文档向量(vector) 之间的 L1 距离。

GET my_sparse_index/_search
{
  "query": {
    "script_score": {
      "query" : {
        "bool" : {
          "filter" : {
            "term" : {
              "status" : "published"
            }
          }
        }
      },
      "script": {
        "source": "1 / (1 + l1normSparse(params.queryVector, 'my_sparse_vector'))",
        "params": {
          "queryVector": {"2": 0.5, "10" : 111.3, "50": -1.3, "113": 14.8, "4545": 156.0}
        }
      }
    }
  }
}

l2normSparse 函数计算给定的查询向量(query_vector) 和文档向量(vector) 之间的 L2 距离。

GET my_sparse_index/_search
{
  "query": {
    "script_score": {
      "query" : {
        "bool" : {
          "filter" : {
            "term" : {
              "status" : "published"
            }
          }
        }
      },
      "script": {
        "source": "1 / (1 + l2normSparse(params.queryVector, 'my_sparse_vector'))",
        "params": {
          "queryVector": {"2": 0.5, "10" : 111.3, "50": -1.3, "113": 14.8, "4545": 156.0}
        }
      }
    }
  }
}
explain(解释)请求

使用explain请求可以查看如何计算分数的解释。 script_score 查询可以通过设置参数 explanation 来添加自己的解释:

GET /twitter/_explain/0
{
    "query" : {
        "script_score" : {
            "query" : {
                "match": { "message": "elasticsearch" }
            },
            "script" : {
                "source" : """
                  long likes = doc['likes'].value;
                  double normalizedLikes = likes / 10;
                  if (explanation != null) {
                    explanation.set('normalized likes = likes / 10 = ' + likes + ' / 10 = ' + normalizedLikes);
                  }
                  return normalizedLikes;
                """
            }
        }
     }
}

注意,当在普通的 _search 请求中使用时,explanation 将为 null,因此使用条件保护是最佳实践。