ES|QL and flattened fields
ES|QL can read flattened fields directly
and extract their sub-fields using the FIELD_EXTRACT function.
A flattened field maps an entire object as a single field, indexing all leaf values as keywords.
It is commonly used for objects with a large or unpredictable set of keys, for example OpenTelemetry
resource.attributes, where each service contributes its own set of attributes.
Currently ES|QL can access only the root flattened field directly and you must use FIELD_EXTRACT to
get the subfield. This will change in a future release. See elasticsearch/issues/152537.
Use FIELD_EXTRACT to pull a single sub-field out of a flattened root as a keyword column.
The function takes the flattened field and the path as its arguments:
FROM flattened_otel_logs
| WHERE @timestamp == "2020-01-01T00:02:48.461Z"
| EVAL host.name = field_extract(resource.attributes, "host.name")
| KEEP @timestamp, host.name
| @timestamp:date | host.name:keyword |
|---|---|
| 2020-01-01T00:02:48.461Z | infra-filebeat-6vjxr |
The second argument is the dotted path within the flattened field.
- Flattened fields only contain keywords:
FIELD_EXTRACTalways returnskeywordvalues, so numbers and booleans come back as their string representation. - Keys are always in the collapsed dotted form: nested objects are stored as dotted keys, and
FIELD_EXTRACTresolves a key the same way regardless of how the document was written. - Extracting an object returns null: pointing
FIELD_EXTRACTat an object instead of a leaf returnsnull. - Missing keys and JSON
nullreturnnull:FIELD_EXTRACTreturnsnullif the key does not exist, if the stored value is JSONnull, or if either argument isnull.
Numbers and booleans come back as their string representation, for example: "184896", "true".
Use casts to get other types. Create an index, index a document, and run a query that casts the extracted values:
PUT /flattened-cast-demo
{
"mappings": {
"properties": {
"attrs": { "type": "flattened" }
}
}
}
POST /flattened-cast-demo/_doc?refresh
{
"attrs": { "b": false, "d": 1.2, "l": 123 }
}
POST /_query
{
"query": """
FROM flattened-cast-demo
| EVAL b = FIELD_EXTRACT(attrs, "b")::BOOLEAN,
d = FIELD_EXTRACT(attrs, "d")::DOUBLE,
l = FIELD_EXTRACT(attrs, "l")::LONG
"""
}
- The document stores a boolean, a double, and a long.
FIELD_EXTRACTreturns each value as akeyword, so cast it to the type you want.
This query returns the following:
{
"columns": [
{"name": "attrs", "type": "flattened"},
{"name": "b", "type": "boolean"},
{"name": "d", "type": "double"},
{"name": "l", "type": "long"}
],
"values": [
[{"b": "false", "d": "1.2", "l": "123"}, false, 1.2, 123]
]
}
When you index a flattened field Elasticsearch "flattens" it. {"a": {"b": "v"}} becomes
{"a.b": "v"}. When you load the flattened with ES|QL, you get the flattened result:
PUT /flattened-keys-demo
{
"mappings": {
"properties": {
"name": { "type": "keyword" },
"attrs": { "type": "flattened" }
}
}
}
POST /flattened-keys-demo/_doc?refresh
{ "name": "nested", "attrs": { "a": { "b": "something" } } }
POST /_query
{
"query": "FROM flattened-keys-demo"
}
- The document nests
binsidea. Elasticsearch stores it as the dotted keya.b.
This query returns the following:
{
"columns": [
{"name": "attrs", "type": "flattened"},
{"name": "name", "type": "keyword"}
],
"values": [
[{"a.b": "something"}, "nested"]
]
}
The path parameter of FIELD_EXTRACT operates on exactly that normalized path.
So { "a.b": "something" } is the same as { "a": { "b": "something" } }:
POST /flattened-keys-demo/_doc?refresh
{ "name": "pre-dotted","attrs": { "a.b": "something" } }
POST /_query
{
"query": """
FROM flattened-keys-demo
| EVAL ab = FIELD_EXTRACT(attrs, "a.b")
| SORT name ASC
"""
}
- This document uses a literal dotted key instead of a nested object.
- The same path resolves both documents, since they collapse to the same key.
This query returns the following:
{
"columns": [
{"name": "attrs", "type": "flattened"},
{"name": "name", "type": "keyword"},
{"name": "ab", "type": "keyword"}
],
"values": [
[{"a.b": "something"}, "nested", "something"],
[{"a.b": "something"}, "pre-dotted","something"]
]
}
FIELD_EXTRACT can only extract leaf fields. If you point it at an object, it returns null.
Address the leaf directly. For example, use "http.request.body.size" rather than "http.request".
Create an index, index a document whose value is an object, and extract that object key:
PUT /flattened-object-demo
{
"mappings": {
"properties": {
"attrs": { "type": "flattened" }
}
}
}
POST /flattened-object-demo/_doc?refresh
{ "attrs": { "a": { "b": "something" } } }
POST /_query
{
"query": """
FROM flattened-object-demo
| EVAL a = FIELD_EXTRACT(attrs, "a")
"""
}
aholds an object, not a leaf value.- Extracting the object returns
null. Extract the leafa.binstead.
This query returns the following, with a set to null because a is an object:
{
"columns": [
{"name": "attrs", "type": "flattened"},
{"name": "a", "type": "keyword"}
],
"values": [
[{"a.b": "something"}, null]
]
}
FIELD_EXTRACT: the function reference, including null handling and JSONPath restrictions.flattenedfield type: mapping parameters, typed sub-fields withproperties, andpassthrough.- Supported field types: the full list of field types ES|QL can read.
- Multi-value functions: functions for working with multi-valued columns.