简要介绍
有时候需要一些复杂逻辑时,就需要用到ES提供的脚本,可以在字段、自定义分数、排序等场景下使用。
ES默认的脚本叫做painless
。
在支持脚本的ES API中,基本都循序以下的语法格式
"script": {
"lang": "...",
"source" | "id": "...",
"params": {
"name": "zhangsan",
"age": 20
}
}
-
lang:脚本语言,默认为
painless
-
source: 脚本本身
-
id: 脚本id, 可以引用一段脚本
-
params: 脚本参数,支持传递字符串、数字参数,类型是一个Map。若脚步中需要参数,尽量不要硬编码到脚本中,因为脚本会进行编译,通过参数传递给脚本仅仅只需要编译一次,而参数不一样硬编码到脚本中,脚本内容会不一样,导致需要编译多次。
特别的,source在控制台书写时支持多行字符串语法,需要使用"""开头,与Java的多行字符串语法类似。
"source": """
// 脚本
"""
painless简要介绍
它的语法基本和Java类似,可以使用大多数Java提供的标准类库,区别是不需要导包。
Shared API为所有场景下都可以使用的API,其它API需要看上下文场景使用。Shared API已经提供了Java中最常用的几个包的标准类库了
- java.lang
- java.math
- java.text
- java.time
- java.util
等等,可参见Shared API。
比如在排序场景中使用,则找Sort Context
。
会标明哪些API可以使用,以及语法示例。
内置的doc变量
在大多数上下文场景中,都会提供一个Map
类型的内置doc
变量,它包含了当前文档的所有字段。
painless
为访问Map的值提供了简便语法,无序调用get方法,形如map['key']。
获取字段值
// 单个值的类型
def value = doc['fieldname'].value;
// 多个值的类型(数组), 使用doc['fieldname'].value只会返回第一个值
def value = doc['fieldname'].get(index);
判断该字段是否有值
// size方法返回值的个数,单值类型存在值则为1, 注意值为null是判定为没有值的
if (doc['fieldname'].size() > 0) {
// doSomething
}
注:
- 如果没有该字段,使用doc['fieldname'].value会抛异常
- 不是所有字段类型都可以使用doc变量来获取,这个需要看对应上下文中的文档描述
ES字段类型对应的Java类型,如下面表格所示。
ES字段类型 | 简单类型 | 包装类型 |
---|---|---|
boolean | boolean | Boolean |
keyword | String | String |
wildcard | String | String |
long | long | Long |
integer | long | Long |
short | long | Long |
byte | long | Long |
double | double | Double |
scaled_float | double | Double |
half_float | double | Double |
unsigned_long | long | Long |
date | ZonedDateTime | ZonedDateTime |
详情可参考此链接
排序案例
比如实现这么一个逻辑,索引中存在一个日期字段,排序规则如下
若大于等于当前日期,距离当前日期越近的排在前面,即这部分日期升序。
若小于等于当前日期,距离当前日期越近的也排在前面,但是优先级要次于上面大于等于当前日期的数据。
日期字段不存在的排最后面。
总结就是:未到期的升序排,已过期的降序排,未到期的优先于已过期的,值不存在的优先级最低。
索引
PUT /es_script_index
{
"mappings": {
"dynamic": "strict",
"properties": {
"id": {
"type": "long"
},
"name": {
"type": "text"
},
"expireDate": {
"type": "date",
"format": "yyyy-MM-dd"
}
}
}
}
数据
POST /es_script_index/_bulk
{"index": {"_id": 1}}
{"id": 1, "expireDate": "2024-10-20"}
{"index": {"_id": 2}}
{"id": 2, "expireDate": "2024-10-22"}
{"index": {"_id": 3}}
{"id": 3, "expireDate": "2024-10-18"}
{"index": {"_id": 4}}
{"id": 4, "expireDate": "2024-10-19"}
{"index": {"_id": 5}}
{"id": 5, "expireDate": "2024-10-21"}
{"index": {"_id": 6}}
{"id": 6, "expireDate": null}
{"index": {"_id": 7}}
{"id": 7, "expireDate": "2024-10-17"}
{"index": {"_id": 8}}
{"id": 8}
排序DSL
GET /es_script_index/_search
{
"from": 0,
"size": 20,
"query": {
"match_all": {}
},
"sort": [
{
"_script": {
"type": "number",
"order": "desc",
"script": {
"source": """
if (doc['expireDate'].size() > 0) {
// doc['expireDate'].value为ZonedDateTime
LocalDate expireDate = doc['expireDate'].value.toLocalDate();
LocalDate currentDate = LocalDate.parse(params['currentDateStr']);
// 已过期的返回之间的天数
if (expireDate.isBefore(currentDate)) {
return ChronoUnit.DAYS.between(currentDate, expireDate);
}
// 未过期的,先计算天数,然后再用一个很大的值减掉,这样越近的数值就会越大
return Integer.MAX_VALUE - ChronoUnit.DAYS.between(currentDate, expireDate);
}
// 不存在返回最小值, 优先级最低
return Integer.MIN_VALUE;
""",
"params": {
"currentDateStr": "2024-10-20"
}
}
}
},
{
"id": "desc"
}
]
}
解释:
type:值为number或者string,对应脚本的返回值类型为double或者String
order:asc/desc
字段案例
索引结构和数据同上
GET /es_script_index/_search
{
"query": {
"match_all": {}
},
"fields": ["*"],
"script_fields": {
"diffDays": {
"script": {
"source": """
if (doc['expireDate'].size() > 0) {
// doc['expireDate'].value为ZonedDateTime
LocalDate expireDate = doc['expireDate'].value.toLocalDate();
LocalDate currentDate = LocalDate.parse(params['currentDateStr']);
// 已过期的返回之间的天数
if (expireDate.isBefore(currentDate)) {
return ChronoUnit.DAYS.between(currentDate, expireDate);
}
// 未过期的,先计算天数,然后再用Long的最大值减掉,这样越近的数值就会越大
return Integer.MAX_VALUE - ChronoUnit.DAYS.between(currentDate, expireDate);
}
// 不存在返回最小值, 优先级最低
return Integer.MIN_VALUE;
""",
"params": {
"currentDateStr": "2024-10-20"
}
}
}
}
}
注意:动态构造的script_fields
无法使用于排序中。