Elasticsearch 基础教程

Elasticsearch 高级教程

Elasticsearch 插件

Elasticsearch 笔记

Elasticsearch 脚本


Elasticsearch 的脚本模块主要是对 ES 的字段进行再处理操作。例如,可以用来重新评估查询的自定义得分,可以对索引中的某个字段再次加工处理;在解决复杂业务问题(如,自定义评分、自定义文本相关度、自定义过滤、自定义聚合分析等)时,脚本是 Elasticsearch 强悍的利器之一;其中 Elasticsearch 脚本最典型的使用场景是作为搜索粗排模块使用。

脚本使用

脚本语言

Elasticsearch 脚本模块可以支持多种脚本语言,可以通过语言插件来支持不同的语言脚本。默认的脚本语言可以通过 script.default_lang 进行配置修改,任何使用脚本的地方,都可以通过设置 lang 参数来指定脚本的语言。

脚本语言插件
语言 沙盒 必要插件 用途
painless 内置  
groovy 内置  
expression 内置 快速自定义评分与排序
mustache 内置 模板
javascript lang-javascript  
python lang-python  
java   自己写 专业 API

脚本语言在设计时,是考虑到安全沙盒的。然而,非沙盒语言可能是一个安全问题。

脚本使用

无论 Elasticsearch API 支持何种脚本,语法都遵循相同的模式:

"script": {
  "lang":   "...",  
  "source" | "id": "...", 
  "params": { ... } 
}
  • 脚本编写的语言,默认为 painless。
  • 脚本本身可以指定为内联脚本的 source 或存储脚本的 id。
  • 应传递给脚本的任何命名参数。

在 Elasticsearch 中使用脚本有三种方式:

  1. 直接在请求体中使用脚本,又叫内联(inline)脚本
  2. 脚本可以使用 _scripts 端点存储在集群状态中,并通过脚本 id 来使用
  3. 把脚本存储在本地磁盘中,默认位置为 config/scripts,通过脚本 id 来使用

脚本操作

下面介绍 Elasticsearch 的脚本 CRUD(增删改查)操作。

脚本的创建

如上所述,脚本的添加也主要有三种形式,对应上述三种使用方式。

第一种使用方式就是直接在请求体中使用脚本,这种方式是动态编译的,当然 Elasticsearch 针对这种形式的脚本也有相应的缓存策略

Elasticsearch 第一次看到一个新脚本,它会编译它并将编译后的版本存储在缓存中,编译可能是一个繁重的过程。

如果需要将变量传递给脚本,则应将它们作为命名参数传递给脚本本身而不是硬编码值,例如,如果你希望能够将字段值乘以不同的乘数,请不要将乘数硬编码到脚本中:

"source": "doc['my_field'] * 2"

相反,将其作为命名参数传递:

"source": "doc['my_field'] * multiplier",
"params": {
  "multiplier": 2
}

如上所述第一个版本每次乘数改变时都必须重新编译,第二个版本只编译一次。第二个版本的写法就是模型和数据做了分离

如果你在很短的时间内编译了太多新的脚本(缓存中不存在的),Elasticsearch 将使用 circuit_breaking_exception 错误拒绝新的动态脚本。

默认情况下,每分钟将编译最多 15 个内联脚本,你可以通过参数 script.max_compilations_rate 动态更改此设置。

第二种添加方式是将脚本使用 _scripts 端点存储在集群状态中,起到将脚本进行存储并持久化,存储的脚本指定全局唯一的 id 作为标识

如下示例,首先,在集群状态下,创建名为 calculate-score 的脚本:

POST _scripts/calculate-score
{
  "script": {
    "lang": "painless",
    "source": "Math.log(_score * 2) + params.my_modifier"
  }
}

其中,calculate-score 为指定的脚本 id,后续使用该脚本时,通过该 id 进行引用使用。

第三种添加方式是先把脚本存储在文件中,默认脚本存放位置在 config/scripts 下,建立对应的脚本文件

以 painless 脚本语言为例,创建文件名 filescriptdemo.groovy,其中 filescriptdemo 为脚本名称,也是脚本唯一的标识,文件扩展名为脚本使用的脚本语言。

文件内容示例如下,表示对最终得分进行 log 相关的变换:

return Math.log(_score * 2);

文件形式的脚本需要涉及到脚本自动重载的情况,即周期性扫描 config/scripts 目录的修改。新的和修改的脚本会被重载,删除的脚本会从预加载脚本缓存中移除。重载频率可以使用 resource.reload.interval 设置指定,默认值为 60s。设置 script.auto_reload_enabledfalse 可以完全禁用脚本重载。

脚本的删除

第一种使用方式的脚本,默认情况下,所有脚本都被缓存,因此只有在更新发生时才需要重新编译它们,默认情况下,脚本没有基于时间的过期,但你可以使用 script.cache.expire 设置更改此行为,你可以使 script.cache.max_size 设置配置此缓存的大小,默认情况下,缓存大小为 100。

存储脚本的大小限制为 65,535 字节,这可以通过设置 script.max_size_in_bytes 设置来增加软限制来更改,但如果脚本非常大,则应考虑原生脚本引擎。

GET /_cat/nodes?v&h=id,port,v,script.*

id   port v     script.compilations script.cache_evictions
MyUs 9300 5.6.4               38407                  38307
T50o 9300 5.6.4               38940                  38840
vK2H 9300 5.6.4               39552                  39452
lWj_ 9300 5.6.4              114195                 114095
x7oS 9300 5.6.4              119973                 119873
BAKD 9300 5.6.4               38715                  38615

如上所述是真实场景使用的情况,通过 _cat 命令查看的编译的脚本情况,发现总编译数(script.compilations)和缓存剔除的脚本数(script.cache_evictions)之差为 100,即脚本缓存的默认设置 script.cache.max_size 为 100。

第二种基于存储索引的脚本方式,删除操作非常简单,指定脚本 id,进行 DELETE 请求即可。

示例如下:

DELETE _scripts/script_id_demo

如果删除成功,会返回如下所示:

{
  "acknowledged": true
}

若指定脚本不存在,会返回如下所示:

{
  "error": {
    "root_cause": [
      {
        "type": "resource_not_found_exception",
        "reason": "stored script [script_id_demo] does not exist and cannot be deleted"
      }
    ],
    "type": "resource_not_found_exception",
    "reason": "stored script [script_id_demo] does not exist and cannot be deleted"
  },
  "status": 404
}

第三种将脚本存储在文件中的使用方式的删除就非常直接,就是直接删除相应目录文件即可。

需要注意的是,存储在文件中的脚本,删除时,不会立即生效,因为和使用时一样,都是需要脚本重载(如上提到的)或重启后才生效,因为它们都是预加载在缓存中的。

脚本的修改

Elasticsearch 的脚本在实际使用场景中很少用修改操作,第一种使用方式不存在修改之说;第二种使用方式 POST 改写指定脚本 id 即可覆盖;第三种将脚本文件修改即可,但是等到脚本重载或重启 ES 方可生效。

脚本的查询

第一种方式的脚本创建由开发者控制之外,删除查询操作都由 ES 控制。

第二种方式的脚本查询方式如下:

GET _scripts/script_id_demo

第三种方式的脚本查询无须多言,到 ES 相对应目录(默认 config/scripts)查看即可。

脚本的表达式

文档的字段访问

大多数脚本围绕指定文档字段数据来使用。doc['field_name'] 可以用来访问文档内指定字段数据。注意,只能是简单值字段(不能返回 JSON 对象),并且只有不分词字段或单索引词字段是有意义的。

脚本提取数据
表达式 描述
doc['field_name'].value 字段的本地值
doc['field_name'].values 字段的本地数组值。如果字段没有值,返回一个空数组
doc['field_name'].empty 布尔值,表示文档中的字段是否有值
doc['field_name'].multiValued 布尔值,表示文档中有多个值的字段
doc['field_name'].lat 地理点类型的纬度
doc['field_name'].lon 地理点类型的经度
doc['field_name'].lats 地理点类型的纬度(复数)
doc['field_name'].lons 地理点类型的经度(复数)
doc['field_name'].distance(lat,lon) 地理点字段和提供的经纬度之间的平面距离(米)
doc['field_name'].distanceWithDefault(lat,lon,default) 地理点字段和提供的经纬度和默认值的平面距离(米)
doc['field_name'].distanceInMiles(lat,lon) 地理点字段和提供的经纬度之间的平面距离(英里)
doc['field_name'].distanceInMilesWithDefault(lat,lon,default) 地理点字段和提供的经纬度和默认值的平面距离(英里)
doc['field_name'].distanceInKm(lat,lon) 地理点字段和提供的经纬度之间的平面距离(千米)
doc['field_name'].distanceInKmWithDefault(lat,lon,default) 地理点字段和提供的经纬度和默认值的平面距离(千米)
doc['field_name'].arcDistance(lat,lon) 地理点字段和提供的经纬度之间的天穹距离(米)
doc['field_name'].arcDistanceWithDefault(lat,lon,default) 地理点字段和提供的经纬度和默认值的天穹距离(米)
doc['field_name'].arcDistanceInMiles(lat,lon) 地理点字段和提供的经纬度之间的天穹距离(英里)
doc['field_name'].arcDistanceInMilesWithDefault(lat,lon,default) 地理点字段和提供的经纬度和默认值的天穹距离(英里)
doc['field_name'].arcDistanceInKm(lat,lon) 地理点字段和提供的经纬度之间的天穹距离(千米)
doc['field_name'].arcDistanceInKmWithDefault(lat,lon,default) 地理点字段和提供的经纬度和默认值的天穹距离(千米)
doc['field_name'].factorDistance(lat,lon) 地理点字段从提供的经纬度之间的距离因子
doc['field_name'].factorDistance(lat,lon,default) 地理点字段从提供的经纬度和默认值的距离因子
doc['field_name'].geohashDistance(geohash) 地理点字段从提供的地理散列的天穹距离(米)
doc['field_name'].geohashDistanceInKm(geohash) 地理点字段从提供的地理散列的天穹距离(千米)
doc['field_name'].geohashDistanceInMiles(geohash) 地理点字段从提供的地理散列的天穹距离(英里)

es(Elasticsearch)painless 脚本获取文档(document)的字段值的场景描述。

在脚本中访问文档的得分

当使用脚本计算文档得分(例如,通过 function_score 查询),可以在 painless 脚本中使用 _score 变量访问得分。

源字段

当执行脚本的时候,可以访问源字段。每个文档都会加载源字段、分析并且提供给脚本进行评估。使用方式,形如 _source.obj1.obj2.field3