从 Elasticsearch runtime fields 到 ES|QL:将传统工具适配到当前技术

作者:来自 Elastic Jeffrey Rengifo

学习如何将五种常见的 Elasticsearch runtime field 模式迁移到其 ES|QL 等价实现,包含并排代码对比以及在何种情况下适合使用每种方法的指导。

动手体验 Elasticsearch:深入了解 Elasticsearch Labs 仓库中的示例 notebooks,开始免费的云试用,或现在就在本地机器上试用 Elastic。

Elasticsearch runtime fields 解决了在查询时计算值而无需重新索引(reindexing)的问题。但它们伴随着 Painless 脚本的复杂性以及随文档数量增长而增加的性能成本。Elasticsearch Query Language(ES|QL)提供了更强大的替代方案,具有专用执行引擎、管道处理,并且无需脚本。在本文中,你将学习如何将五种常见的 runtime field 模式映射到其 ES|QL等价实现,从而使你的查询现代化,并理解在何种情况下使用每种方法更合适。

前置条件

  • Elasticsearch 8.15+(用于支持 :: 类型转换操作符;核心 ES|QL 功能从 8.11 开始可用)

Runtime fields 与 ES|QL 对比

Runtime fields 于 Elasticsearch 7.11 引入,用于在查询时定义字段。你无需重新索引数据,而是可以编写 Painless 脚本来动态计算值:

bash 复制代码
`

1.  PUT my-index/_mapping
2.  {
3.    "runtime": {
4.      "full_address": {
5.        "type": "keyword",
6.        "script": {
7.          "source": "emit(doc['address'].value + ':' + doc['port'].value)"
8.        }
9.      }
10.    }
11.  }

`AI写代码![](https://csdnimg.cn/release/blogv2/dist/pc/img/runCode/icon-arrowwhite.png)

这可以工作,但也带来一些权衡:

  • Painless 脚本开销:每个 runtime field 都需要脚本知识,而且语法类似 Java,而不是查询式语法。
  • 性能成本:runtime fields 在查询时对每个文档进行计算。Elasticsearch 将其归类为 "昂贵查询",可能会被集群设置拒绝。
  • 隔离计算:每个 runtime field 独立计算。在同一查询中,无法将多个转换串联或使用一个字段的输出作为另一个字段的输入。

ES|QL 改变了这一局面。它拥有自己的执行引擎(不转换为 Query DSL),在节点间并发运行查询,并提供完整的字段计算工具集:EVALGROKDISSECT、类型转换以及管道链式处理。

让我们看看每种 runtime field 模式如何映射到 ES|QL。

设置示例数据

本文中的所有代码片段都可以在 Kibana Dev Tools 控制台中执行。

为了跟随操作,创建一个包含数据的示例索引,用于演示这五种模式。这模拟了一个服务器日志场景,包含混合字段类型、原始消息以及一些有意设置的数据质量问题:

bash 复制代码
`

1.  PUT server-logs
2.  {
3.    "mappings": {
4.      "properties": {
5.        "host": { "type": "keyword" },
6.        "port": { "type": "keyword" },
7.        "raw_message": { "type": "text" },
8.        "response_time": { "type": "keyword" },
9.        "status_code": { "type": "keyword" },
10.        "region": { "type": "keyword" }
11.      }
12.    }
13.  }

`AI写代码![](https://csdnimg.cn/release/blogv2/dist/pc/img/runCode/icon-arrowwhite.png)

现在索引一些示例文档:

bash 复制代码
`

1.  POST _bulk
2.  { "index": { "_index": "server-logs" } }
3.  { "host": "web-01", "port": "8080", "raw_message": "2024-01-15 INFO user=alice action=login duration=230ms", "response_time": "145", "status_code": "200", "region": "us-east" }
4.  { "index": { "_index": "server-logs" } }
5.  { "host": "web-02", "port": "443", "raw_message": "2024-01-15 ERROR user=bob action=upload duration=1200ms", "response_time": "not_available", "status_code": "500", "region": "eu-west" }
6.  { "index": { "_index": "server-logs" } }
7.  { "host": "api-01", "port": "3000", "raw_message": "2024-01-15 WARN user=charlie action=query duration=890ms", "response_time": "890", "status_code": "200", "region": "us-east" }
8.  { "index": { "_index": "server-logs" } }
9.  { "host": "api-02", "port": "3000", "raw_message": "2024-01-16 INFO user=diana action=export duration=3400ms", "response_time": "3400", "status_code": "200", "region": "ap-south" }
10.  { "index": { "_index": "server-logs" } }
11.  { "host": "web-01", "port": "8080", "raw_message": "2024-01-16 ERROR user=eve action=login duration=50ms", "response_time": "50", "status_code": "401", "region": "US-EAST" }

`AI写代码![](https://csdnimg.cn/release/blogv2/dist/pc/img/runCode/icon-arrowwhite.png)

注意 ,response_time 被存储为 keyword(这是现实中常见的错误),而最后一个文档使用了 "US-EAST" 而不是 "us-east"(这是一个我们稍后会修复的数据质量问题)。

模式 1:字段拼接

一个常见的 runtime field 使用场景是将两个字段组合成一个。例如,创建一个 host:port 标识符。

runtime field 方法

你可以在查询时内联定义它。查询时的方法避免修改 mapping,但仍然需要 Painless 脚本,并且只作用于单个搜索请求:

bash 复制代码
`

1.  GET server-logs/_search
2.  {
3.    "runtime_mappings": {
4.      "endpoint": {
5.        "type": "keyword",
6.        "script": {
7.          "source": "emit(doc['host'].value + ':' + doc['port'].value)"
8.        }
9.      }
10.    },
11.    "fields": ["endpoint"],
12.    "_source": false
13.  }

`AI写代码![](https://csdnimg.cn/release/blogv2/dist/pc/img/runCode/icon-arrowwhite.png)

ES|QL 方法

你可以使用 _query API endpoint 运行 ES|QL 查询:

python 复制代码
`

1.  POST _query
2.  {
3.    "query": """
4.      FROM server-logs
5.      | EVAL endpoint = CONCAT(host, ":", port)
6.      | KEEP host, port, endpoint
7.      | LIMIT 1
8.    """
9.  }

`AI写代码

CONCAT 接受两个或多个参数,并始终返回 keyword。

注意:为简洁起见,本文其余的 ES|QL 示例仅展示查询本身。在 Kibana Dev Tools 中运行时,请将它们包裹在 POST _query { "query": "..." } 中。

何时使用

如果你需要该字段在所有查询中持久存在,并在 Kibana 仪表板中可用,请使用 mapping 级别的 runtime field。如果你只在 Query DSL 的单次搜索请求中需要它,请使用查询时的 runtime field。如果你用于临时分析或探索性工作,ES|QL 更简单。

模式 2:从非结构化文本中提取数据

从原始日志消息中提取结构化数据是另一个经典的 runtime field 模式。

Runtime field 方法

Painless 使用 Java 的 regex Matcher 类:

bash 复制代码
`

1.  GET server-logs/_search
2.  {
3.    "runtime_mappings": {
4.      "log_user": {
5.        "type": "keyword",
6.        "script": {
7.          "source": "def matcher = /user=(\\w+)/.matcher(params._source['raw_message']); if (matcher.find()) { emit(matcher.group(1)); }"
8.        }
9.      }
10.    },
11.    "fields": ["log_user"],
12.    "_source": false
13.  }

`AI写代码![](https://csdnimg.cn/release/blogv2/dist/pc/img/runCode/icon-arrowwhite.png)

这种方法冗长。你需要了解 Painless 的正则语法、处理 Matcher 对象,并正确调用 emit()。

ES|QL 方法:GROK

ES|QL 提供了两个专门用于文本提取的命令。GROK 使用基于正则的模式:

css 复制代码
`

1.  FROM server-logs
2.  | GROK raw_message "%{WORD:timestamp_date} %{WORD:log_level} user=%{WORD:user} action=%{WORD:action} duration=%{WORD:duration}"
3.  | KEEP user, log_level, action, duration

`AI写代码

GROK 使用 %{SYNTAX:SEMANTIC} 模式格式。它可以在单条可读命令中提取多个字段。

ES|QL 方法:DISSECT

对于具有一致分隔符的结构化数据,DISSECT 更快,因为它不使用正则表达式:

yaml 复制代码
`

1.  FROM server-logs
2.  | DISSECT raw_message "%{timestamp_date} %{log_level} user=%{user} action=%{action} duration=%{duration}"
3.  | KEEP user, log_level, action, duration

`AI写代码

语法与 GROK 几乎相同,但 DISSECT 通过分隔符拆分数据,而不是匹配正则模式。这使得它在处理遵循一致格式的数据时更快。

何时使用 GROK 与 DISSECT

当你的数据结构可预测(相同分隔符、相同字段顺序)时使用 DISSECT。当你需要正则灵活性时使用 GROK,例如字段可能是可选的或格式不一致时。

模式 3:动态类型转换

当字段被映射为 keyword 但包含数字数据(这是一个意外常见的情况)时,runtime field 可以在查询时进行类型转换。

Runtime field 方法

python 复制代码
`

1.  GET server-logs/_search
2.  {
3.    "runtime_mappings": {
4.      "response_time_long": {
5.        "type": "long",
6.        "script": {
7.          "source": """
8.            def val = doc['response_time'].value;
9.            if (val != 'not_available') {
10.              emit(Long.parseLong(val));
11.            }
12.          """
13.        }
14.      }
15.    },
16.    "fields": ["response_time_long"],
17.    "_source": false
18.  }

`AI写代码![](https://csdnimg.cn/release/blogv2/dist/pc/img/runCode/icon-arrowwhite.png)

你需要手动处理解析异常。如果 Long.parseLong 在遇到意外值时失败,脚本会抛出错误。

ES|QL 方法

ES|QL 提供了显式的转换函数以及简写的类型转换操作符:

ini 复制代码
`

1.  FROM server-logs
2.  | EVAL response_ms = TO_LONG(response_time)
3.  | KEEP host, response_time, response_ms

`AI写代码

或者使用 :: cast 操作符(自 8.15 起可用):

markdown 复制代码
`

1.  FROM server-logs
2.  | EVAL response_ms = resonse_time::long
3.  | KEEP host, response_time, response_msp

`AI写代码

两者产生相同结果。与 Painless 的关键区别在于:转换失败会返回 null,而不是抛出异常。包含 "not_available" 的文档的 response_ms 字段将返回 null,ES|QL 会发出警告。

常用转换函数包括:

Function Converts to
`TO_LONG()` Long integer
`TO_INTEGER()` Integer
`TO_DOUBLE()` Double
`TO_DATETIME()` Date
`TO_BOOLEAN()` Boolean
`TO_IP()` IP address
`TO_VERSION()` Version

:: 操作符适用于所有这些类型(例如,field::double、field::datetime)。

何时使用

ES|QL 的优雅 null 处理使其在处理脏数据时更安全。使用 Painless 的 runtime fields 可以对错误处理进行细粒度控制,但需要更多代码。对于类型转换,ES|QL 几乎总是更好的选择。

模式 4:

Runtime fields 支持 mapping 中的 "dynamic": "runtime",通过将所有新字段创建为 runtime fields 而不是索引字段,从而防止 mapping 爆炸:

markdown 复制代码
`

1.  {
2.    "mappings": {
3.      "dynamic": "runtime",
4.      "properties": {
5.        "timestamp": { "type": "date" }
6.      }
7.    }
8.  }

`AI写代码

发送到该索引的任何新字段都会自动成为 runtime field。当你摄取具有不可预测字段名的半结构化数据时,这非常有用。

ES|QL 的适用场景

ES|QL 提供查询时的灵活性,但仍然需要字段在 mapping 中可见。这时 runtime fields 与 ES|QL 是互补的,而不是互相竞争。

如果字段存在于 _source 中但未映射,ES|QL 无法直接访问它。当前的解决方法是定义一个 runtime field,使未映射字段可见:

bash 复制代码
`

1.  PUT dynamic-logs/_mapping
2.  {
3.    "runtime": {
4.      "custom_field": {
5.        "type": "keyword",
6.        "script": {
7.          "source": "emit(params._source['custom_field'])"
8.        }
9.      }
10.    }
11.  }

`AI写代码![](https://csdnimg.cn/release/blogv2/dist/pc/img/runCode/icon-arrowwhite.png)

定义后,ES|QL 就可以查询它:

sql 复制代码
`

1.  FROM dynamic-logs
2.  | WHERE custom_field == "some_value"
3.  | KEEP timestamp, custom_field

`AI写代码

这是 runtime fields 仍然不可或缺的一个场景。它们充当桥梁,使未映射的数据对 ES|QL 可访问。

模式 5:字段覆盖用于错误修正

Runtime fields 可以通过定义与现有字段同名的 runtime field 来覆盖(shadow)索引字段。这对于无需重新索引即可修正数据非常有用。

Runtime field 方法

还记得我们的数据质量问题吗,region 字段的大小写不一致("US-EAST" 与 "us-east")?

bash 复制代码
`

1.  GET server-logs/_search
2.  {
3.    "runtime_mappings": {
4.      "region": {
5.        "type": "keyword",
6.        "script": {
7.          "source": "emit(params._source['region'].toLowerCase())"
8.        }
9.      }
10.    },
11.    "fields": ["region"],
12.    "_source": false
13.  }

`AI写代码![](https://csdnimg.cn/release/blogv2/dist/pc/img/runCode/icon-arrowwhite.png)

这会覆盖索引的 region 字段,应用于所有查询。每次搜索、聚合和 Kibana 可视化都将看到小写版本。

ini 复制代码
`

1.  FROM server-logs
2.  | EVAL region = TO_LOWER(region)
3.  | KEEP host, port, region

`AI写代码

当你在现有列名上使用 EVAL 时,ES|QL 会删除原始列并用计算值替换它。这与字段覆盖完全等效,但作用范围仅限于当前查询。

你还可以在管道中链式执行多个修正:

ini 复制代码
`

1.  FROM server-logs
2.  | EVAL region = TO_LOWER(region)
3.  | EVAL region = CASE(region == "us-east", "US East", region == "eu-west", "EU West", region == "ap-south", "AP South", region)
4.  | KEEP host, region

`AI写代码

何时使用

如果修正应适用于所有查询和 Kibana 仪表板,请使用 runtime field 覆盖。如果你只需针对特定分析修正数据,ES|QL 更灵活,因为你可以在不同查询中应用不同的转换,而无需修改 mapping。

ES|QL 管道的优势:超越 runtime fields

这就是 ES|QL 在根本上超越 runtime fields 的地方。runtime fields 是孤立的:每个字段独立计算,无法在同一查询中将一个 runtime field 的输出作为另一个的输入。

ES|QL 管道可以链式转换。以下是一个结合多个模式的单条查询:

css 复制代码
`

1.  FROM server-logs
2.  | GROK raw_message "%{WORD:log_date} %{WORD:log_level} user=%{WORD:user} action=%{WORD:action} duration=%{INT:duration_raw}ms"
3.  | EVAL duration_ms = duration_raw::long
4.  | EVAL region = TO_LOWER(region)
5.  | WHERE log_level == "ERROR" AND duration_ms > 100
6.  | STATS avg_duration = AVG(duration_ms), error_count = COUNT(*) BY region

`AI写代码

这条单独的查询:

  • 从原始文本中提取字段(GROK)。
  • 将 duration 转换为数字(EVAL + cast)。
  • 规范化 region 大小写(EVAL + TO_LOWER)。
  • 筛选高 duration 的错误(WHERE)。
  • 按 region 聚合(STATS)。

要用 runtime fields 达到同样效果,你至少需要定义三个独立的 runtime fields(用于提取、转换和规范化),然后再写一个包含过滤聚合的 Query DSL 查询。而 ES|QL 版本是一条可读性强的管道。

你甚至可以在聚合中直接使用表达式:

ini 复制代码
`

1.  FROM server-logs
2.  | EVAL response_ms = response_time::long
3.  | STATS
4.      avg_response = AVG(response_ms),
5.      p95_response = PERCENTILE(response_ms, 95),
6.      slow_count = COUNT(CASE(response_ms > 1000, 1, null))
7.    BY host

`AI写代码

结论

我们涵盖的内容:

  • ES|QL 提供了完整工具集(EVAL、GROK、DISSECT、:: 类型转换),可以在无需 Painless 脚本的情况下替代大多数 runtime field 模式。
  • ES|QL 中的类型转换失败会返回 null 而不是抛出异常,这对于真实数据更安全。
  • 管道处理(将 GROK 链入 EVAL,再链入 WHERE,再链入 STATS)超出了 runtime fields 独立使用时的能力。
  • Runtime fields 对于持久计算字段、跨所有查询的字段覆盖以及作为 ES|QL 未映射数据的桥梁仍然有价值。

一个重要的注意事项:runtime fields 和 ES|QL 都是在查询时计算值,这意味着每次查询都会付出成本。如果你发现自己重复应用相同的转换(类型修正、字段提取、数据规范化),可以考虑使用 ingest pipelines 在索引时修正数据。Ingest pipeline 允许在文档存储前进行解析、丰富和转换,使查询可以直接使用干净、类型正确的字段。runtime fields 和 ES|QL 非常适合探索性和临时分析,但对于生产工作负载,从一开始就索引正确的数据几乎总是更好的选择。

关键结论:runtime fields 并未被弃用,也不会消失。但对于大多数查询时计算模式,ES|QL 提供了更简单、更强大、更高性能的方法。当转换在前期已知时,ingest pipeline 是最有效的选项。

下一步

原文:www.elastic.co/search-labs...

相关推荐
Meepo_haha2 小时前
ES在SpringBoot集成使用
spring boot·elasticsearch·jenkins
尽兴-2 小时前
Elasticsearch 中文分词与自定义 Analyzer 实战:IK、同义词、词库治理
elasticsearch·中文分词·jenkins·分词器·字符过滤器·切词器·词项过滤器
sxhcwgcy3 小时前
Elasticsearch(ES)基础查询语法的使用
python·elasticsearch·django
顾北123 小时前
Elasticsearch DSL 从入门到实战:全文检索 + 地理查询 + SpringBoot 整合全攻略
后端·elasticsearch
海兰4 小时前
使用 TypeScript 创建 Elasticsearch MCP 服务器
服务器·elasticsearch·typescript·mcp
疯狂成瘾者12 小时前
上传到 GitHub 的步骤总结
大数据·elasticsearch·github
Elasticsearch18 小时前
Elasticsearch:如何在 workflow 里调用一个 agent
elasticsearch
Elasticsearch1 天前
Elasticsearch:如何在 Elastic AI Builder 里使用 DSL 来查询 Elasticsearch
elasticsearch
尽兴-1 天前
Spring Boot 整合 Elasticsearch 8.x 实战总结(含三种实现方式 + 完整示例)
spring boot·elasticsearch·jenkins