ES脚本使用

简要介绍

有时候需要一些复杂逻辑时,就需要用到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提供的标准类库,区别是不需要导包。

脚本官方文档

painless官方文档

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无法使用于排序中。

posted on 2024-10-20 14:12  wastonl  阅读(25)  评论(0编辑  收藏  举报