Painless 执行 API编辑

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

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

请求编辑

POST /_scripts/painless/_execute

描述编辑

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

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

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

请求正文编辑

脚本

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

script 的属性
emit

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

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

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

emit 的签名

emit 的签名取决于字段的 type

布尔值

emit(boolean)

日期

emit(long)

双精度

emit(double)

地理点

emit(double lat, double lon)

IP

emit(String)

长整型

emit(long)

关键字

emit(String)

上下文

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

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

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

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

context_setup

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

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

context_setup 的属性
文档
(必需,字符串)在内存中临时索引并可从脚本访问的文档。
索引
(必需,字符串)包含与索引文档兼容的映射的索引。您可以通过在索引前加上远程集群别名来指定远程索引。例如,remote1:my_index 表示您要在“remote1”集群上的“my_index”索引上执行 painless 脚本。如果您已 配置了与该远程集群的连接,则此请求将被转发到“remote1”集群。
参数
Map,只读)指定作为变量传递给脚本的任何命名参数。
查询

(可选,对象)

仅当 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、存储字段和文档值可供正在测试的脚本使用。

请求编辑

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
    }
  }
}

八小时,以毫秒表示

罗伯特·海因莱因的写作速度惊人

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

在这种情况下,作者是阿拉斯泰尔·雷诺兹。根据 2000-03-15 的发行日期,该脚本计算出作者在 1999 年 2 月 19 日开始写《启示空间》。在一年多的时间里写出一本 585 页的书真是令人印象深刻!

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

double_field编辑

double 类型的数值数据使用 double_field 上下文。例如,假设您有传感器数据,其中包含一个值为 5.6 的 voltage 字段。在索引了数百万个文档后,您发现型号为 QVKC92Q 的传感器报告的电压偏低了 1.7 倍。您无需重新索引数据,而是可以使用运行时字段来修复它。

您需要将此值乘以 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。以下请求将一个 @timestamp 字段(类型为 date)添加到索引映射中

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.ROOT));
    """
  },
  "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 字段,您想计算这些值之间的差值。

以下请求将一个 measures 对象添加到映射中,该对象有两个字段,类型均为 long

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 字段,您想将其拆分为多个可以单独访问的子字段。

以下请求将一个 message 字段(类型为 keyword)添加到映射中

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"
    ]
  }
}