Getting started with ES|QL queries

edit

Getting started with ES|QL queries

edit

This guide shows how you can use ES|QL to query and aggregate your data.

This getting started is also available as an interactive Python notebook in the elasticsearch-labs GitHub repository.

Prerequisites

edit

To follow along with the queries in this guide, you can either set up your own deployment, or use Elastic’s public ES|QL demo environment.

First ingest some sample data. In Kibana, open the main menu and select Dev Tools. Run the following two requests:

resp = client.indices.create(
    index="sample_data",
    mappings={
        "properties": {
            "client_ip": {
                "type": "ip"
            },
            "message": {
                "type": "keyword"
            }
        }
    },
)
print(resp)

resp1 = client.bulk(
    index="sample_data",
    operations=[
        {
            "index": {}
        },
        {
            "@timestamp": "2023-10-23T12:15:03.360Z",
            "client_ip": "172.21.2.162",
            "message": "Connected to 10.1.0.3",
            "event_duration": 3450233
        },
        {
            "index": {}
        },
        {
            "@timestamp": "2023-10-23T12:27:28.948Z",
            "client_ip": "172.21.2.113",
            "message": "Connected to 10.1.0.2",
            "event_duration": 2764889
        },
        {
            "index": {}
        },
        {
            "@timestamp": "2023-10-23T13:33:34.937Z",
            "client_ip": "172.21.0.5",
            "message": "Disconnected",
            "event_duration": 1232382
        },
        {
            "index": {}
        },
        {
            "@timestamp": "2023-10-23T13:51:54.732Z",
            "client_ip": "172.21.3.15",
            "message": "Connection error",
            "event_duration": 725448
        },
        {
            "index": {}
        },
        {
            "@timestamp": "2023-10-23T13:52:55.015Z",
            "client_ip": "172.21.3.15",
            "message": "Connection error",
            "event_duration": 8268153
        },
        {
            "index": {}
        },
        {
            "@timestamp": "2023-10-23T13:53:55.832Z",
            "client_ip": "172.21.3.15",
            "message": "Connection error",
            "event_duration": 5033755
        },
        {
            "index": {}
        },
        {
            "@timestamp": "2023-10-23T13:55:01.543Z",
            "client_ip": "172.21.3.15",
            "message": "Connected to 10.1.0.1",
            "event_duration": 1756467
        }
    ],
)
print(resp1)
response = client.indices.create(
  index: 'sample_data',
  body: {
    mappings: {
      properties: {
        client_ip: {
          type: 'ip'
        },
        message: {
          type: 'keyword'
        }
      }
    }
  }
)
puts response

response = client.bulk(
  index: 'sample_data',
  body: [
    {
      index: {}
    },
    {
      "@timestamp": '2023-10-23T12:15:03.360Z',
      client_ip: '172.21.2.162',
      message: 'Connected to 10.1.0.3',
      event_duration: 3_450_233
    },
    {
      index: {}
    },
    {
      "@timestamp": '2023-10-23T12:27:28.948Z',
      client_ip: '172.21.2.113',
      message: 'Connected to 10.1.0.2',
      event_duration: 2_764_889
    },
    {
      index: {}
    },
    {
      "@timestamp": '2023-10-23T13:33:34.937Z',
      client_ip: '172.21.0.5',
      message: 'Disconnected',
      event_duration: 1_232_382
    },
    {
      index: {}
    },
    {
      "@timestamp": '2023-10-23T13:51:54.732Z',
      client_ip: '172.21.3.15',
      message: 'Connection error',
      event_duration: 725_448
    },
    {
      index: {}
    },
    {
      "@timestamp": '2023-10-23T13:52:55.015Z',
      client_ip: '172.21.3.15',
      message: 'Connection error',
      event_duration: 8_268_153
    },
    {
      index: {}
    },
    {
      "@timestamp": '2023-10-23T13:53:55.832Z',
      client_ip: '172.21.3.15',
      message: 'Connection error',
      event_duration: 5_033_755
    },
    {
      index: {}
    },
    {
      "@timestamp": '2023-10-23T13:55:01.543Z',
      client_ip: '172.21.3.15',
      message: 'Connected to 10.1.0.1',
      event_duration: 1_756_467
    }
  ]
)
puts response
const response = await client.indices.create({
  index: "sample_data",
  mappings: {
    properties: {
      client_ip: {
        type: "ip",
      },
      message: {
        type: "keyword",
      },
    },
  },
});
console.log(response);

const response1 = await client.bulk({
  index: "sample_data",
  operations: [
    {
      index: {},
    },
    {
      "@timestamp": "2023-10-23T12:15:03.360Z",
      client_ip: "172.21.2.162",
      message: "Connected to 10.1.0.3",
      event_duration: 3450233,
    },
    {
      index: {},
    },
    {
      "@timestamp": "2023-10-23T12:27:28.948Z",
      client_ip: "172.21.2.113",
      message: "Connected to 10.1.0.2",
      event_duration: 2764889,
    },
    {
      index: {},
    },
    {
      "@timestamp": "2023-10-23T13:33:34.937Z",
      client_ip: "172.21.0.5",
      message: "Disconnected",
      event_duration: 1232382,
    },
    {
      index: {},
    },
    {
      "@timestamp": "2023-10-23T13:51:54.732Z",
      client_ip: "172.21.3.15",
      message: "Connection error",
      event_duration: 725448,
    },
    {
      index: {},
    },
    {
      "@timestamp": "2023-10-23T13:52:55.015Z",
      client_ip: "172.21.3.15",
      message: "Connection error",
      event_duration: 8268153,
    },
    {
      index: {},
    },
    {
      "@timestamp": "2023-10-23T13:53:55.832Z",
      client_ip: "172.21.3.15",
      message: "Connection error",
      event_duration: 5033755,
    },
    {
      index: {},
    },
    {
      "@timestamp": "2023-10-23T13:55:01.543Z",
      client_ip: "172.21.3.15",
      message: "Connected to 10.1.0.1",
      event_duration: 1756467,
    },
  ],
});
console.log(response1);
PUT sample_data
{
  "mappings": {
    "properties": {
      "client_ip": {
        "type": "ip"
      },
      "message": {
        "type": "keyword"
      }
    }
  }
}

PUT sample_data/_bulk
{"index": {}}
{"@timestamp": "2023-10-23T12:15:03.360Z", "client_ip": "172.21.2.162", "message": "Connected to 10.1.0.3", "event_duration": 3450233}
{"index": {}}
{"@timestamp": "2023-10-23T12:27:28.948Z", "client_ip": "172.21.2.113", "message": "Connected to 10.1.0.2", "event_duration": 2764889}
{"index": {}}
{"@timestamp": "2023-10-23T13:33:34.937Z", "client_ip": "172.21.0.5", "message": "Disconnected", "event_duration": 1232382}
{"index": {}}
{"@timestamp": "2023-10-23T13:51:54.732Z", "client_ip": "172.21.3.15", "message": "Connection error", "event_duration": 725448}
{"index": {}}
{"@timestamp": "2023-10-23T13:52:55.015Z", "client_ip": "172.21.3.15", "message": "Connection error", "event_duration": 8268153}
{"index": {}}
{"@timestamp": "2023-10-23T13:53:55.832Z", "client_ip": "172.21.3.15", "message": "Connection error", "event_duration": 5033755}
{"index": {}}
{"@timestamp": "2023-10-23T13:55:01.543Z", "client_ip": "172.21.3.15", "message": "Connected to 10.1.0.1", "event_duration": 1756467}

Run an ES|QL query

edit

In Kibana, you can use Console or Discover to run ES|QL queries:

To get started with ES|QL in Console, open the main menu and select Dev Tools.

The general structure of an ES|QL query API request is:

POST /_query?format=txt
{
  "query": """

  """
}

Enter the actual ES|QL query between the two sets of triple quotes. For example:

POST /_query?format=txt
{
  "query": """
FROM sample_data
  """
}

Your first ES|QL query

edit

Each ES|QL query starts with a source command. A source command produces a table, typically with data from Elasticsearch.

A source command producing a table from Elasticsearch

The FROM source command returns a table with documents from a data stream, index, or alias. Each row in the resulting table represents a document. This query returns up to 1000 documents from the sample_data index:

FROM sample_data

Each column corresponds to a field, and can be accessed by the name of that field.

ES|QL keywords are case-insensitive. The following query is identical to the previous one:

from sample_data

Processing commands

edit

A source command can be followed by one or more processing commands, separated by a pipe character: |. Processing commands change an input table by adding, removing, or changing rows and columns. Processing commands can perform filtering, projection, aggregation, and more.

A processing command changing an input table

For example, you can use the LIMIT command to limit the number of rows that are returned, up to a maximum of 10,000 rows:

FROM sample_data
| LIMIT 3

For readability, you can put each command on a separate line. However, you don’t have to. The following query is identical to the previous one:

FROM sample_data | LIMIT 3

Sort a table

edit
A processing command sorting an input table

Another processing command is the SORT command. By default, the rows returned by FROM don’t have a defined sort order. Use the SORT command to sort rows on one or more columns:

FROM sample_data
| SORT @timestamp DESC

Query the data

edit

Use the WHERE command to query the data. For example, to find all events with a duration longer than 5ms:

FROM sample_data
| WHERE event_duration > 5000000

WHERE supports several operators. For example, you can use LIKE to run a wildcard query against the message column:

FROM sample_data
| WHERE message LIKE "Connected*"

More processing commands

edit

There are many other processing commands, like KEEP and DROP to keep or drop columns, ENRICH to enrich a table with data from indices in Elasticsearch, and DISSECT and GROK to process data. Refer to Processing commands for an overview of all processing commands.

Chain processing commands

edit

You can chain processing commands, separated by a pipe character: |. Each processing command works on the output table of the previous command. The result of a query is the table produced by the final processing command.

Processing commands can be chained

The following example first sorts the table on @timestamp, and next limits the result set to 3 rows:

FROM sample_data
| SORT @timestamp DESC
| LIMIT 3

The order of processing commands is important. First limiting the result set to 3 rows before sorting those 3 rows would most likely return a result that is different than this example, where the sorting comes before the limit.

Compute values

edit

Use the EVAL command to append columns to a table, with calculated values. For example, the following query appends a duration_ms column. The values in the column are computed by dividing event_duration by 1,000,000. In other words: event_duration converted from nanoseconds to milliseconds.

FROM sample_data
| EVAL duration_ms = event_duration/1000000.0

EVAL supports several functions. For example, to round a number to the closest number with the specified number of digits, use the ROUND function:

FROM sample_data
| EVAL duration_ms = ROUND(event_duration/1000000.0, 1)

Calculate statistics

edit

ES|QL can not only be used to query your data, you can also use it to aggregate your data. Use the STATS ... BY command to calculate statistics. For example, the median duration:

FROM sample_data
| STATS median_duration = MEDIAN(event_duration)

You can calculate multiple stats with one command:

FROM sample_data
| STATS median_duration = MEDIAN(event_duration), max_duration = MAX(event_duration)

Use BY to group calculated stats by one or more columns. For example, to calculate the median duration per client IP:

FROM sample_data
| STATS median_duration = MEDIAN(event_duration) BY client_ip

Access columns

edit

You can access columns by their name. If a name contains special characters, it needs to be quoted with backticks (`).

Assigning an explicit name to a column created by EVAL or STATS is optional. If you don’t provide a name, the new column name is equal to the function expression. For example:

FROM sample_data
| EVAL event_duration/1000000.0

In this query, EVAL adds a new column named event_duration/1000000.0. Because its name contains special characters, to access this column, quote it with backticks:

FROM sample_data
| EVAL event_duration/1000000.0
| STATS MEDIAN(`event_duration/1000000.0`)

Create a histogram

edit

To track statistics over time, ES|QL enables you to create histograms using the BUCKET function. BUCKET creates human-friendly bucket sizes and returns a value for each row that corresponds to the resulting bucket the row falls into.

Combine BUCKET with STATS ... BY to create a histogram. For example, to count the number of events per hour:

FROM sample_data
| STATS c = COUNT(*) BY bucket = BUCKET(@timestamp, 24, "2023-10-23T00:00:00Z", "2023-10-23T23:59:59Z")

Or the median duration per hour:

FROM sample_data
| KEEP @timestamp, event_duration
| STATS median_duration = MEDIAN(event_duration) BY bucket = BUCKET(@timestamp, 24, "2023-10-23T00:00:00Z", "2023-10-23T23:59:59Z")

Enrich data

edit

ES|QL enables you to enrich a table with data from indices in Elasticsearch, using the ENRICH command.

esql enrich

Before you can use ENRICH, you first need to create and execute an enrich policy.

The following requests create and execute a policy called clientip_policy. The policy links an IP address to an environment ("Development", "QA", or "Production"):

resp = client.indices.create(
    index="clientips",
    mappings={
        "properties": {
            "client_ip": {
                "type": "keyword"
            },
            "env": {
                "type": "keyword"
            }
        }
    },
)
print(resp)

resp1 = client.bulk(
    index="clientips",
    operations=[
        {
            "index": {}
        },
        {
            "client_ip": "172.21.0.5",
            "env": "Development"
        },
        {
            "index": {}
        },
        {
            "client_ip": "172.21.2.113",
            "env": "QA"
        },
        {
            "index": {}
        },
        {
            "client_ip": "172.21.2.162",
            "env": "QA"
        },
        {
            "index": {}
        },
        {
            "client_ip": "172.21.3.15",
            "env": "Production"
        },
        {
            "index": {}
        },
        {
            "client_ip": "172.21.3.16",
            "env": "Production"
        }
    ],
)
print(resp1)

resp2 = client.enrich.put_policy(
    name="clientip_policy",
    match={
        "indices": "clientips",
        "match_field": "client_ip",
        "enrich_fields": [
            "env"
        ]
    },
)
print(resp2)

resp3 = client.enrich.execute_policy(
    name="clientip_policy",
    wait_for_completion=False,
)
print(resp3)
response = client.indices.create(
  index: 'clientips',
  body: {
    mappings: {
      properties: {
        client_ip: {
          type: 'keyword'
        },
        env: {
          type: 'keyword'
        }
      }
    }
  }
)
puts response

response = client.bulk(
  index: 'clientips',
  body: [
    {
      index: {}
    },
    {
      client_ip: '172.21.0.5',
      env: 'Development'
    },
    {
      index: {}
    },
    {
      client_ip: '172.21.2.113',
      env: 'QA'
    },
    {
      index: {}
    },
    {
      client_ip: '172.21.2.162',
      env: 'QA'
    },
    {
      index: {}
    },
    {
      client_ip: '172.21.3.15',
      env: 'Production'
    },
    {
      index: {}
    },
    {
      client_ip: '172.21.3.16',
      env: 'Production'
    }
  ]
)
puts response

response = client.enrich.put_policy(
  name: 'clientip_policy',
  body: {
    match: {
      indices: 'clientips',
      match_field: 'client_ip',
      enrich_fields: [
        'env'
      ]
    }
  }
)
puts response

response = client.enrich.execute_policy(
  name: 'clientip_policy',
  wait_for_completion: false
)
puts response
const response = await client.indices.create({
  index: "clientips",
  mappings: {
    properties: {
      client_ip: {
        type: "keyword",
      },
      env: {
        type: "keyword",
      },
    },
  },
});
console.log(response);

const response1 = await client.bulk({
  index: "clientips",
  operations: [
    {
      index: {},
    },
    {
      client_ip: "172.21.0.5",
      env: "Development",
    },
    {
      index: {},
    },
    {
      client_ip: "172.21.2.113",
      env: "QA",
    },
    {
      index: {},
    },
    {
      client_ip: "172.21.2.162",
      env: "QA",
    },
    {
      index: {},
    },
    {
      client_ip: "172.21.3.15",
      env: "Production",
    },
    {
      index: {},
    },
    {
      client_ip: "172.21.3.16",
      env: "Production",
    },
  ],
});
console.log(response1);

const response2 = await client.enrich.putPolicy({
  name: "clientip_policy",
  match: {
    indices: "clientips",
    match_field: "client_ip",
    enrich_fields: ["env"],
  },
});
console.log(response2);

const response3 = await client.enrich.executePolicy({
  name: "clientip_policy",
  wait_for_completion: "false",
});
console.log(response3);
PUT clientips
{
  "mappings": {
    "properties": {
      "client_ip": {
        "type": "keyword"
      },
      "env": {
        "type": "keyword"
      }
    }
  }
}

PUT clientips/_bulk
{ "index" : {}}
{ "client_ip": "172.21.0.5", "env": "Development" }
{ "index" : {}}
{ "client_ip": "172.21.2.113", "env": "QA" }
{ "index" : {}}
{ "client_ip": "172.21.2.162", "env": "QA" }
{ "index" : {}}
{ "client_ip": "172.21.3.15", "env": "Production" }
{ "index" : {}}
{ "client_ip": "172.21.3.16", "env": "Production" }

PUT /_enrich/policy/clientip_policy
{
  "match": {
    "indices": "clientips",
    "match_field": "client_ip",
    "enrich_fields": ["env"]
  }
}

PUT /_enrich/policy/clientip_policy/_execute?wait_for_completion=false

After creating and executing a policy, you can use it with the ENRICH command:

FROM sample_data
| KEEP @timestamp, client_ip, event_duration
| EVAL client_ip = TO_STRING(client_ip)
| ENRICH clientip_policy ON client_ip WITH env

You can use the new env column that’s added by the ENRICH command in subsequent commands. For example, to calculate the median duration per environment:

FROM sample_data
| KEEP @timestamp, client_ip, event_duration
| EVAL client_ip = TO_STRING(client_ip)
| ENRICH clientip_policy ON client_ip WITH env
| STATS median_duration = MEDIAN(event_duration) BY env

For more about data enrichment with ES|QL, refer to Data enrichment.

Process data

edit

Your data may contain unstructured strings that you want to structure to make it easier to analyze the data. For example, the sample data contains log messages like:

"Connected to 10.1.0.3"

By extracting the IP address from these messages, you can determine which IP has accepted the most client connections.

To structure unstructured strings at query time, you can use the ES|QL DISSECT and GROK commands. DISSECT works by breaking up a string using a delimiter-based pattern. GROK works similarly, but uses regular expressions. This makes GROK more powerful, but generally also slower.

In this case, no regular expressions are needed, as the message is straightforward: "Connected to ", followed by the server IP. To match this string, you can use the following DISSECT command:

FROM sample_data
| DISSECT message "Connected to %{server_ip}"

This adds a server_ip column to those rows that have a message that matches this pattern. For other rows, the value of server_ip is null.

You can use the new server_ip column that’s added by the DISSECT command in subsequent commands. For example, to determine how many connections each server has accepted:

FROM sample_data
| WHERE STARTS_WITH(message, "Connected to")
| DISSECT message "Connected to %{server_ip}"
| STATS COUNT(*) BY server_ip

For more about data processing with ES|QL, refer to Data processing with DISSECT and GROK.

Learn more

edit

To learn more about ES|QL, refer to ES|QL reference and Using ES|QL.