Loading

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.

Note

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_EXTRACT always returns keyword values, 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_EXTRACT resolves a key the same way regardless of how the document was written.
  • Extracting an object returns null: pointing FIELD_EXTRACT at an object instead of a leaf returns null.
  • Missing keys and JSON null return null: FIELD_EXTRACT returns null if the key does not exist, if the stored value is JSON null, or if either argument is null.

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
"""
}
		
  1. The document stores a boolean, a double, and a long.
  2. FIELD_EXTRACT returns each value as a keyword, 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"
}
		
  1. The document nests b inside a. Elasticsearch stores it as the dotted key a.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
  """
}
		
  1. This document uses a literal dotted key instead of a nested object.
  2. 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")
"""
}
		
  1. a holds an object, not a leaf value.
  2. Extracting the object returns null. Extract the leaf a.b instead.

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