Painless 执行 API

编辑

此功能处于技术预览阶段,可能会在未来的版本中更改或删除。Elastic 将努力解决任何问题,但技术预览版的功能不受官方 GA 功能的支持 SLA 约束。

Painless 执行 API 运行脚本并返回结果。

请求

编辑

POST /_scripts/painless/_execute

描述

编辑

使用此 API 构建和测试脚本,例如在为运行时字段定义脚本时。此 API 需要非常少的依赖项,如果您没有在集群上写入文档的权限,则特别有用。

该 API 使用多个上下文,这些上下文控制脚本的执行方式、运行时可用的变量以及返回类型。

每个上下文都需要一个脚本,但其他参数取决于您用于该脚本的上下文。

请求体

编辑
script

(必需,对象)要执行的 Painless 脚本。

script 的属性
emit

(必需)接受脚本估算的值。脚本可以多次调用 emit 方法以发出多个值。

emit 方法仅适用于运行时字段上下文中使用的脚本。

emit 方法不能接受 null 值。如果引用的字段没有任何值,请不要调用此方法。

emit 的签名

emit 的签名取决于字段的 type

boolean

emit(boolean)

date

emit(long)

double

emit(double)

geo_point

emit(double lat, double lon)

ip

emit(String)

long

emit(long)

keyword

emit(String)

context

(可选,字符串)脚本应在其中运行的上下文。如果未指定上下文,则默认为 painless_test

context 的属性
painless_test
如果未指定其他上下文,则为默认上下文。请参阅测试上下文
filter
将脚本视为在 script 查询中运行。请参阅过滤器上下文
score
将脚本视为在 function_score 查询中的 script_score 函数中运行。请参阅评分上下文
字段上下文

以下选项特定于字段上下文。

不保证字段上下文中的结果排序。

context_setup

(必需,对象)context 的其他参数。

painless_test 之外的所有上下文都需要此参数,如果未为 context 提供值,则为默认值。

context_setup 的属性
document
(必需,字符串)临时在内存中索引且可从脚本访问的文档。
index
(必需,字符串)包含与索引文档兼容的映射的索引。您可以通过在索引前加上远程集群别名来指定远程索引。例如,remote1:my_index 表示您想针对“remote1”集群上的“my_index”索引执行 painless 脚本。如果您已配置了连接到该远程集群,则此请求将转发到“remote1”集群。

此端点的索引表达式中不接受通配符。表达式 *:myindex 将返回错误“No such remote cluster”,表达式 logs*remote1:logs* 将返回错误“index not found”。

params
Map,只读)指定作为变量传递到脚本中的任何命名参数。
query

(可选,对象)

仅当将 score 指定为脚本 context 时,此参数才适用。

使用此参数指定用于计算分数的查询。除了确定文档是否匹配之外,查询子句还会在 _score 元数据字段中计算相关性分数。

测试上下文

编辑

painless_test 上下文运行没有其他参数的脚本。唯一可用的变量是 params,它可用于访问用户定义的值。脚本的结果始终转换为字符串。

由于默认上下文是 painless_test,因此您无需指定 contextcontext_setup

请求

编辑
POST /_scripts/painless/_execute
{
  "script": {
    "source": "params.count / params.total",
    "params": {
      "count": 100.0,
      "total": 1000.0
    }
  }
}

响应

编辑
{
  "result": "0.1"
}

过滤器上下文

编辑

filter 上下文将脚本视为在 script 查询中运行。为了进行测试,必须提供一个文档,以便它将在内存中临时索引,并且可以从脚本访问。更准确地说,被测试脚本可以使用此文档的 _source、存储字段和 doc 值。

请求

编辑
PUT /my-index-000001
{
  "mappings": {
    "properties": {
      "field": {
        "type": "keyword"
      }
    }
  }
}
POST /_scripts/painless/_execute
{
  "script": {
    "source": "doc['field'].value.length() <= params.max_length",
    "params": {
      "max_length": 4
    }
  },
  "context": "filter",
  "context_setup": {
    "index": "my-index-000001",
    "document": {
      "field": "four"
    }
  }
}

响应

编辑
{
  "result": true
}

评分上下文

编辑

score 上下文将脚本视为在 function_score 查询中的 script_score 函数中运行。

请求

编辑
PUT /my-index-000001
{
  "mappings": {
    "properties": {
      "field": {
        "type": "keyword"
      },
      "rank": {
        "type": "long"
      }
    }
  }
}
POST /_scripts/painless/_execute
{
  "script": {
    "source": "doc['rank'].value / params.max_rank",
    "params": {
      "max_rank": 5.0
    }
  },
  "context": "score",
  "context_setup": {
    "index": "my-index-000001",
    "document": {
      "rank": 4
    }
  }
}

响应

编辑
{
  "result": 0.8
}

字段上下文

编辑

字段上下文将脚本视为在搜索查询的runtime_mappings 部分中运行。您可以使用字段上下文测试不同字段类型的脚本,然后将这些脚本包含在支持它们的任何位置,例如运行时字段

根据您要返回的数据类型选择字段上下文。

boolean_field

编辑

当您想从脚本估算中返回 truefalse 值时,请使用 boolean_field 字段上下文。布尔字段接受 truefalse 值,但也接受解释为 true 或 false 的字符串。

假设您有有史以来前 100 部科幻小说的相关数据。您想编写返回布尔响应的脚本,例如书籍是否超过特定的页数,或者书籍是否在特定年份之后出版。

假设您的数据结构如下

PUT /my-index-000001
{
  "mappings": {
    "properties": {
      "name": {
        "type": "keyword"
      },
      "author": {
        "type": "keyword"
      },
      "release_date": {
        "type": "date"
      },
      "page_count": {
        "type": "double"
      }
    }
  }
}

然后,您可以在 boolean_field 上下文中编写一个脚本,指示书籍是否在 1972 年之前出版

POST /_scripts/painless/_execute
{
  "script": {
    "source": """
      emit(doc['release_date'].value.year < 1972);
    """
  },
  "context": "boolean_field",
  "context_setup": {
    "index": "my-index-000001",
    "document": {
      "name": "Dune",
      "author": "Frank Herbert",
      "release_date": "1965-06-01",
      "page_count": 604
    }
  }
}

因为《沙丘》出版于 1965 年,所以结果返回为 true

{
  "result" : [
    true
  ]
}

同样,您可以编写一个脚本来确定作者的名字是否超过了特定数量的字符。以下脚本在 author 字段上运行,以确定作者的名字是否包含至少一个字符,但少于五个字符

POST /_scripts/painless/_execute
{
  "script": {
    "source": """
      int space = doc['author'].value.indexOf(' ');
      emit(space > 0 && space < 5);
    """
  },
  "context": "boolean_field",
  "context_setup": {
    "index": "my-index-000001",
    "document": {
      "name": "Dune",
      "author": "Frank Herbert",
      "release_date": "1965-06-01",
      "page_count": 604
    }
  }
}

因为 Frank 有五个字符,脚本估值返回 false

{
  "result" : [
    false
  ]
}

date_time

编辑

有几种选项可用于在 Painless 中使用日期时间。在这个例子中,您将根据特定作者的发布日期和写作速度来估计该作者开始写书的时间。该示例做出了一些假设,但展示了如何编写一个处理日期并包含其他信息的脚本。

将以下字段添加到您的索引映射以开始

PUT /my-index-000001
{
  "mappings": {
    "properties": {
      "name": {
        "type": "keyword"
      },
      "author": {
        "type": "keyword"
      },
      "release_date": {
        "type": "date"
      },
      "page_count": {
        "type": "long"
      }
    }
  }
}

以下脚本做了一个令人难以置信的假设,即在写书时,作者只是写每一页,不做研究或修改。此外,该脚本假设写一页的平均时间是八小时。

该脚本检索 author,并做出另一个惊人的假设,根据作者的感知写作速度(又一个大胆的假设)来划分或乘以 pageTime 值。

该脚本从 pageTime 乘以 page_count 的计算结果中减去发布日期值(以毫秒为单位),以确定作者大约(基于众多假设)何时开始写这本书。

POST /_scripts/painless/_execute
{
  "script": {
    "source": """
      String author = doc['author'].value;
      long pageTime = 28800000;  
      if (author == 'Robert A. Heinlein') {
        pageTime /= 2;           
      } else if (author == 'Alastair Reynolds') {
        pageTime *= 2;           
      }
      emit(doc['release_date'].value.toInstant().toEpochMilli() - pageTime * doc['page_count'].value);
    """
  },
  "context": "date_field",
  "context_setup": {
    "index": "my-index-000001",
    "document": {
      "name": "Revelation Space",
      "author": "Alastair Reynolds",
      "release_date": "2000-03-15",
      "page_count": 585
    }
  }
}

八小时,以毫秒表示

罗伯特·A·海因莱因的写作速度非常快

阿拉斯泰尔·雷诺兹写太空歌剧的速度要慢得多

在本例中,作者是阿拉斯泰尔·雷诺兹。根据 2000-03-15 的发布日期,脚本计算出作者在 1999 年 2 月 19 日开始撰写 Revelation Space。在一年多一点的时间里写一本 585 页的书真是令人印象深刻!

{
  "result" : [
    "1999-02-19T00:00:00.000Z"
  ]
}

double_field

编辑

double 类型的数值数据使用 double_field 上下文。例如,假设您有包含 voltage 字段的传感器数据,该字段的值类似于 5.6。在索引数百万个文档之后,您发现型号为 QVKC92Q 的传感器将其电压低报了 1.7 倍。与其重新索引您的数据,不如使用运行时字段来修复它。

您需要乘以这个值,但仅适用于与特定型号匹配的传感器。

将以下字段添加到您的索引映射中。voltage 字段是 measures 对象的子字段。

PUT my-index-000001
{
  "mappings": {
    "properties": {
      "@timestamp": {
        "type": "date"
      },
      "model_number": {
        "type": "keyword"
      },
      "measures": {
        "properties": {
          "voltage": {
            "type": "double"
          }
        }
      }
    }
  }
}

以下脚本匹配任何 model_number 等于 QVKC92Q 的文档,然后将 voltage 值乘以 1.7。当您想选择特定文档并仅对与指定条件匹配的值进行操作时,此脚本非常有用。

POST /_scripts/painless/_execute
{
  "script": {
    "source": """
      if (doc['model_number'].value.equals('QVKC92Q'))
      {emit(1.7 * params._source['measures']['voltage']);}
      else{emit(params._source['measures']['voltage']);}
    """
  },
  "context": "double_field",
  "context_setup": {
    "index": "my-index-000001",
    "document": {
      "@timestamp": 1516470094000,
      "model_number": "QVKC92Q",
      "measures": {
        "voltage": 5.6
      }
    }
  }
}

结果包括计算出的电压,该电压是通过将原始值 5.6 乘以 1.7 确定的。

{
  "result" : [
    9.52
  ]
}

geo_point_field

编辑

地理点字段接受纬度-经度对。您可以通过多种方式定义地理点字段,并在文档中包含脚本的纬度和经度值。

如果您已经知道一个地理点,那么在索引映射中清楚地说明 latlon 的位置会更简单。

PUT /my-index-000001/
{
  "mappings": {
    "properties": {
      "lat": {
        "type": "double"
      },
      "lon": {
        "type": "double"
      }
    }
  }
}

然后,您可以使用 geo_point_field 运行时字段上下文来编写一个脚本,该脚本检索 latlon 值。

POST /_scripts/painless/_execute
{
  "script": {
    "source": """
      emit(doc['lat'].value, doc['lon'].value);
    """
  },
  "context": "geo_point_field",
  "context_setup": {
    "index": "my-index-000001",
    "document": {
      "lat": 41.12,
      "lon": -71.34
    }
  }
}

由于您正在处理地理点字段类型,因此响应包含格式化为 coordinates 的结果。

{
  "result" : [
    {
      "coordinates" : [
        -71.34,
        41.12
      ],
      "type" : "Point"
    }
  ]
}

地理点字段的 emit 函数接受两个参数,顺序为 latlon 之前,但输出 GeoJSON 格式将 coordinates 排序为 [ lon, lat ]

ip_field

编辑

ip_field 上下文对于包含 ip 类型的 IP 地址的数据非常有用。例如,假设您有一个来自 Apache 日志的 message 字段。此字段包含 IP 地址,但也包含您不需要的其他数据。

您可以将 message 字段作为 wildcard 添加到您的索引映射中,以接受您想放入该字段的几乎任何数据。

PUT /my-index-000001/
{
  "mappings": {
    "properties": {
      "message": {
        "type": "wildcard"
      }
    }
  }
}

然后,您可以使用 grok 模式定义一个运行时脚本,该模式从 message 字段中提取结构化字段。

该脚本匹配 %{COMMONAPACHELOG} 日志模式,该模式了解 Apache 日志的结构。如果模式匹配,脚本会发出与 IP 地址匹配的值。如果模式不匹配(clientip != null),则脚本仅返回字段值,而不会崩溃。

POST /_scripts/painless/_execute
{
  "script": {
    "source": """
      String clientip=grok('%{COMMONAPACHELOG}').extract(doc["message"].value)?.clientip;
      if (clientip != null) emit(clientip);
    """
  },
  "context": "ip_field",
  "context_setup": {
    "index": "my-index-000001",
    "document": {
      "message": "40.135.0.0 - - [30/Apr/2020:14:30:17 -0500] \"GET /images/hm_bg.jpg HTTP/1.0\" 200 24736"
    }
  }
}

响应仅包含 IP 地址,忽略 message 字段中的所有其他数据。

{
  "result" : [
    "40.135.0.0"
  ]
}

keyword_field

编辑

关键字字段通常用于排序、聚合和术语级别查询。

假设您有一个时间戳。您想根据该值计算星期几并返回它,例如 Thursday。以下请求将 date 类型的 @timestamp 字段添加到索引映射

PUT /my-index-000001
{
  "mappings": {
    "properties": {
      "@timestamp": {
        "type": "date"
      }
    }
  }
}

要根据您的时间戳返回等效的星期几,您可以在 keyword_field 运行时字段上下文中创建一个脚本

POST /_scripts/painless/_execute
{
  "script": {
    "source": """
      emit(doc['@timestamp'].value.dayOfWeekEnum.getDisplayName(TextStyle.FULL, Locale.ENGLISH));
    """
  },
  "context": "keyword_field",
  "context_setup": {
    "index": "my-index-000001",
    "document": {
      "@timestamp": "2020-04-30T14:31:43-05:00"
    }
  }
}

该脚本对 @timestamp 字段提供的值进行操作,以计算并返回星期几

{
  "result" : [
    "Thursday"
  ]
}

long_field

编辑

假设您有包含 measures 对象的传感器数据。此对象包含 startend 字段,您想计算这些值之间的差值。

以下请求将包含两个类型均为 long 的字段的 measures 对象添加到映射

PUT /my-index-000001/
{
  "mappings": {
    "properties": {
      "measures": {
        "properties": {
          "start": {
            "type": "long"
          },
          "end": {
           "type": "long"
          }
        }
      }
    }
  }
}

然后,您可以定义一个脚本,该脚本将值分配给 startend 字段并对其进行操作。以下脚本从 measures 对象中提取 end 字段的值,并将其从 start 字段中减去

POST /_scripts/painless/_execute
{
  "script": {
    "source": """
      emit(doc['measures.end'].value - doc['measures.start'].value);
    """
  },
  "context": "long_field",
  "context_setup": {
    "index": "my-index-000001",
    "document": {
      "measures": {
        "voltage": "4.0",
        "start": "400",
        "end": "8625309"
      }
    }
  }
}

响应包括脚本估算的计算值

{
  "result" : [
    8624909
  ]
}

composite_field

编辑

假设您有原始 message 字段的日志数据,您想将其拆分为多个可以单独访问的子字段。

以下请求将 keyword 类型的 message 字段添加到映射

PUT /my-index-000001/
{
  "mappings": {
    "properties": {
      "message": {
        "type" : "keyword"
      }
    }
  }
}

然后,您可以定义一个脚本,该脚本使用 grok 函数将此类消息字段拆分为子字段

POST /_scripts/painless/_execute
{
  "script": {
    "source": "emit(grok(\"%{COMMONAPACHELOG}\").extract(doc[\"message\"].value));"
  },
  "context": "composite_field",
  "context_setup": {
    "index": "my-index-000001",
    "document": {
      "timestamp":"2020-04-30T14:31:27-05:00",
      "message":"252.0.0.0 - - [30/Apr/2020:14:31:27 -0500] \"GET /images/hm_bg.jpg HTTP/1.0\" 200 24736"
    }
  }
}

响应包括脚本发出的值

{
  "result" : {
    "composite_field.timestamp" : [
      "30/Apr/2020:14:31:27 -0500"
    ],
    "composite_field.auth" : [
      "-"
    ],
    "composite_field.response" : [
      "200"
    ],
    "composite_field.ident" : [
      "-"
    ],
    "composite_field.httpversion" : [
      "1.0"
    ],
    "composite_field.verb" : [
      "GET"
    ],
    "composite_field.bytes" : [
      "24736"
    ],
    "composite_field.clientip" : [
      "252.0.0.0"
    ],
    "composite_field.request" : [
      "/images/hm_bg.jpg"
    ]
  }
}