Generated banner for Elasticsearch Esql
Generated logo for Elasticsearch Esql

Elasticsearch Esql

ElasticOfficialCreated by elastic

  • Skill
  • Tool integration
  • Data and AI
  • Apache-2.0
  • No issues found

From the author

Execute ES|QL (Elasticsearch Query Language) queries, use when the user wants to query Elasticsearch data, analyze logs, aggregate metrics, explore data, or create charts and dashboards from ES|QL results.
— the description by elastic

AutoGineer summary

Written by AutoGineer from the skill’s own files, not by the author

This skill provides a structured approach to executing ES|QL (Elasticsearch Query Language) queries against Elasticsearch deployments. It guides users through verifying cluster connectivity, discovering index schemas, and generating piped queries for data analysis, log exploration, and metric aggregation. The skill emphasizes version-aware query generation, ensuring that features are used only when compatible with the target cluster, and provides fallback patterns for older environments.

What it does

  • Verifies cluster connectivity and detects deployment type (Stack vs. Serverless).
  • Discovers index schemas and field types to ensure accurate query generation.
  • Generates ES|QL queries using piped commands for filtering, transformation, and aggregation.
  • Provides version-specific guidance and fallback patterns for advanced ES|QL features.
  • Supports time series data analysis using specialized TS commands and TBUCKET functions.
  • Executes queries via the elastic CLI and returns results in a tabular format.

When to use it

  • When you need to perform ad-hoc analysis or log exploration on Elasticsearch data.
  • When you want to aggregate metrics or create time-series visualizations.
  • When you need to transform data using piped operations like EVAL, STATS, and SORT.
  • When you are migrating from legacy Query DSL to the piped ES|QL syntax.

Advantages

  • Ensures query accuracy by mandating schema discovery before generation.
  • Prevents compatibility errors by checking cluster version against feature availability.
  • Provides standardized fallback patterns for environments lacking newer features.
  • Promotes clean, readable query structures using the piped ES|QL paradigm.

Usage examples

“Show me the top 10 error logs from the last 24 hours.”
The agent discovers the relevant index, generates an ES|QL query using FROM, WHERE, and LIMIT, and executes it via the elastic CLI.
“Calculate the average CPU usage per hour for my metrics index.”
The agent identifies the index as a time series, uses the TS command with TBUCKET, and returns the aggregated hourly averages.

Install

  1. Download the skill as a ZIP:

    https://autogineer.com/agent-skills/elastic/elasticsearch-esql/download
  2. Unzip it into the cross-client skills folder of your project:

    mkdir -p .agents/skills && unzip elasticsearch-esql.zip -d .agents/skills

Documentation by elastic

Elasticsearch ES|QL

Execute ES|QL queries against Elasticsearch: discover the schema, choose the right ES|QL feature for the task, generate the simplest correct query, and run it.

<!-- begin-partial: preamble -->

Environment Configuration

This skill executes Elasticsearch operations through the elastic CLI. If the elastic CLI is not installed, tell the user what it is needed for. Do not guess credentials, call the HTTP API directly, or attempt other workarounds.

This skill references operations in HTTP-shorthand form (e.g., GET /, GET /_cat/indices, GET /{index}/_mapping, GET /{index}/_settings/index.mode, POST /_query). The Operations table at the end of this document maps each shorthand to the equivalent elastic CLI command — always use the CLI rather than calling the HTTP API directly.

<!-- end-partial: preamble -->

What is ES|QL?

ES|QL (Elasticsearch Query Language) is a piped query language for Elasticsearch. It is NOT the same as:

  • Elasticsearch Query DSL (JSON-based)

  • SQL

  • EQL (Event Query Language)

ES|QL uses pipes (|) to chain commands: FROM index | WHERE condition | STATS aggregation BY field | SORT field | LIMIT n

Prerequisite: ES|QL requires _source to be enabled on queried indices. Indices with _source disabled (e.g., "_source": { "enabled": false }) will cause ES|QL queries to fail.

Version Compatibility: ES|QL was introduced in 8.11 (tech preview) and became GA in 8.14. Features like LOOKUP JOIN (8.18+), MATCH (8.17+), and INLINE STATS (9.2+) were added in later versions. On pre-8.18 clusters, use ENRICH as a fallback for LOOKUP JOIN (see generation tips). INLINE STATS and counter-field RATE() have no fallback before 9.2. Check references/esql-version-history.md for feature availability by version.

Cluster Detection: Call GET / to determine the cluster type and version:

  • build_flavor: "serverless" — Elastic Cloud Serverless. version.number tracks the stack line under active development (next minor from main), so clients that only semver-compare may treat Serverless as “latest.” Do not use version.number to gate features: if build_flavor is "serverless", assume all GA and preview ES|QL features are available.

  • build_flavor: "default" — Stack (self-managed or Cloud-hosted). Use version.number for feature availability.

  • Snapshot builds have version.number like 9.4.0-SNAPSHOT. Strip the -SNAPSHOT suffix and use the major.minor for version checks. Snapshot builds include all features from that version plus potentially unreleased features from development — if a query fails with an unknown function/command, it may simply not have landed yet. Elastic employees commonly use snapshot builds for testing.

Process

  1. Verify the connection and detect the deployment type. Call GET / first. This confirms connectivity and detects whether the deployment is a Serverless project (all features available) or a versioned cluster (features depend on version). The build_flavor field is the authoritative signal — if it equals "serverless", ignore the reported version number and use all ES|QL features freely. If the call fails, stop and point the user at the CLI configuration instructions rather than guessing endpoints or credentials.

  2. Discover the schema (required — never guess index or field names). List candidate indices with GET /_cat/indices (pass a pattern to narrow), then fetch field types for the chosen index with GET /{index}/_mapping.

    Always run schema discovery before generating queries. Index names and field names vary across deployments and cannot be reliably guessed. Even common-sounding data (e.g., "logs") may live in indices named logs-test, logs-app-*, or application_logs. Field names may use ECS dotted notation (source.ip, service.name) or flat custom names — the only way to know is to check.

    Prefer simplicity: Query a single index unless the user explicitly asks for data across multiple sources. Do not combine indices with different schemas using COALESCE unless specifically requested — pick the single most relevant index for the question. When multiple indices contain similar data, prefer the one with the most complete schema for the task at hand.

    Detect time series indices. Check the index mode with GET /{index}/_settings/index.mode. If it is time_series, use TS <data-stream> (not FROM), TBUCKET(interval) (not DATE_TRUNC), and wrap counter fields with SUM(RATE(...)). Read the full TS section in Generation Tips before writing any time series query. For TSDS indices on 9.4+, prefer the in-language discovery commands METRICS_INFO and TS_INFO (both GA) over inspecting mappings — they enumerate the metric catalogue and the dimension labels of each time series directly, and are run as ES|QL queries via POST /_query. Treat METRICS_INFO as authoritative for metric_type (counter/gauge/histogram) and field_type (histogram, tdigest, exponential_histogram for distribution metrics). Both must follow TS and must precede STATS/SORT/LIMIT. See Time Series Queries:

    TS metrics-tsds | METRICS_INFO | SORT metric_name
    TS metrics-tsds | TS_INFO | KEEP metric_name, dimensions | SORT metric_name
  3. Choose the right ES|QL feature for the task. Version gate (mandatory on Stack): before writing any query, compare version.number to the version tag of every command and function you plan to use. For each feature newer than the cluster (for example a 9.5 feature on 9.4), use its fallback from the table below or tell the user it is unavailable on their version — never emit it, and never silently drop the part of the question it answers. Serverless has every released GA and preview feature. Then match the user's intent to the most appropriate ES|QL feature, preferring a single advanced query over multiple basic ones:

    • "find patterns," "categorize," "group similar messages" → CATEGORIZE(field)

    • "spike," "dip," "anomaly," "when did X change" → CHANGE_POINT value ON key

    • "trend over time," "time series" → STATS ... BY BUCKET(@timestamp, interval) or TS for TSDB

    • "PromQL", "Prometheus query/dashboard/alert", sum by (instance) (...), label matchers like {cluster="prod"} → PROMQL source command (GA in 9.5+/Serverless, preview in 9.4); see PROMQL Command. Prefer TS for native ES|QL phrasing.

    • "search," "find documents matching" → MATCH (default), QSTR (advanced boolean), KQL (Kibana migration). For content/document relevance search, follow the ES|QL Search Strategy

    • "count," "average," "breakdown" → STATS with aggregation functions

    • "approximate," "estimate," "rough numbers," "fast/cheap stats on huge data" → SET approximation=true; before a STATS query (GA in 9.5+/Serverless, preview in 9.4); see Query Approximation

    • "random sample," "random subset of rows" → | SAMPLE 0.1 (row probability; preview 8.19/9.1, GA 9.5), then LIMIT n if a row count is asked; there is no RANDOM() function

    • "latest/first value per group," "most recent status per host" → STATS LAST(field, @timestamp) BY group (or FIRST), not SORT + LIMIT 1

    • allow list, deny list, "exclude known IPs/users" from another index → WHERE field [NOT] IN (FROM idx | KEEP col) (preview 9.5); use LOOKUP JOIN only when you need the other index's columns

    • "unique combinations," "no duplicates" → STATS BY a, b; Serverless only: KEEP a, b | DEDUP (preview)

    • IPs mapped as keyword → convert with TO_IP() before CIDR_MATCH, IP comparisons, IN (FROM …), or LOOKUP JOIN on an ip field; for the last two EVAL ip = TO_IP(src) first (the left side must be a column)

    • "country/city of an IP," "geolocate IPs," ASN → | IP_LOCATION geo = ip_field pipe command (preview 9.5)

    • parse URLs (domain, path, query) → | URI_PARTS parts = url_field pipe command (preview 9.4), not DISSECT/GROK

    • "in my local time," "per day in New York" → SET time_zone = "America/New_York"; (9.4+), not manual offsets

    • flattened field sub-keys (e.g. OTel resource.attributes) → FIELD_EXTRACT(field, "host.name") (preview 9.5), not field.host.name; it returns keyword, so cast (TO_INTEGER(...)) before numeric comparisons

    • embed text/images at query time for vector search → EMBEDDING(value, inference_id) (preview 9.5) or TEXT_EMBEDDING(text, inference_id) inside KNN

    • query files in S3 (Parquet/CSV/NDJSON) without ingesting, or a named federated dataset → FROM <dataset> via Data Federation (experimental 9.5, not for production; ECH and self-managed only, not Serverless); only when asked, and warn

    9.5 or Serverless-only featureFallback on Stack without the feature
    DEDUP (Serverless preview only)LIMIT 1 BY cols (9.4+; SORT first picks the kept row) or STATS BY cols
    WHERE x NOT IN (FROM idx | KEEP c)LOOKUP JOIN idx ON x == c | WHERE c IS NULL (IN: IS NOT NULL); ON expressions need 9.2+, else RENAME x AS c first
    IP_LOCATIONNo query-time GeoIP: ENRICH with an IP-range enrich policy, or the ingest geoip processor
    FIELD_EXTRACT (or an unmapped key)FROM idx METADATA _source | EVAL v = JSON_EXTRACT(_source, "$.labels.key") — not labels.key, labels["key"], or JSON_EXTRACT(labels, …); COALESCE with "$.labels[\"k.sub\"]" for pre-dotted keys
  4. Read only the reference for the feature you need before generating the query (the bold "read before" ones are mandatory for their feature). Do not read every reference up front; open another only if the query fails:

    • Generation Tips - key patterns (TS/TBUCKET/RATE, per-agg WHERE, LOOKUP JOIN, CIDR_MATCH), common templates, and ambiguity handling

    • Time Series Queries - read before any TS query: inner/outer aggregation model, TBUCKET syntax, RATE constraints, histogram metrics

    • PROMQL Command — read before any PROMQL query: options, output schema, handling unsupported PromQL, and PROMQL vs TS decision matrix (GA in 9.5+/Serverless, preview in 9.4)

    • ES|QL Complete Reference - full syntax for all commands and functions

    • ES|QL Search Strategy — for content/document relevance search (retrieve → fuse → rerank)

    • ES|QL Search Reference — for full-text search function syntax (MATCH, QSTR, KQL, scoring)

    • Query Approximation — read before using SET approximation: output columns, sampling/confidence-level tuning, unsupported functions and patterns (GA in 9.5+/Serverless, preview in 9.4)

  5. Generate the query following ES|QL syntax. Prefer the simplest query that answers the question — do not add extra indices, fields, or transformations unless the user asks for them. Only include fields in KEEP that directly answer the question. Do not add extra filter conditions beyond what the user specified (e.g., don't add OR level == "ERROR" when the user just said "errors").

    • Start with FROM index-pattern (or TS index-pattern for time series indices)

    • Add WHERE for filtering (use TRANGE for time ranges on 9.3+)

    • Use EVAL for computed fields

    • Use STATS ... BY for aggregations

    • For time series metrics: TS with SUM(RATE(...)) for counters, AVG(...) for gauges, standard aggregations (SUM, AVG, PERCENTILE, … — not *_OVER_TIME) for histogram metrics, and TBUCKET(interval) for time bucketing — see the TS section in Generation Tips and Histogram Metrics

    • For detecting spikes, dips, or anomalies, use CHANGE_POINT after time-bucketed aggregation

    • Add SORT and LIMIT as needed

    • Re-check the version gate before answering: on Stack 9.4 or earlier the query must not contain IN (FROM, IP_LOCATION, FIELD_EXTRACT, or any other 9.5 feature, and on any Stack no DEDUP — rewrite with the fallback.

  6. Execute the query with POST /_query. Request tabular (TSV) output for clean, decoration-free results that are easy to read and post-process.

ES|QL Quick Reference

Version availability: This section omits version annotations for readability. Check ES|QL Version History for feature availability by Elasticsearch version.

Basic Structure
FROM index-pattern
| WHERE condition
| EVAL new_field = expression
| STATS aggregation BY grouping
| SORT field DESC
| LIMIT n
Common Patterns

Filter and limit:

FROM logs-*
| WHERE @timestamp > NOW() - 24 hours AND level == "error"
| SORT @timestamp DESC
| LIMIT 100

Aggregate by time: For time series (TSDS) indices, prefer TS with TRANGE and TBUCKET over FROM + DATE_TRUNC (see the time series section below).

TS metrics-*
| WHERE TRANGE(7 days)
| STATS avg_cpu = AVG(cpu.percent) BY bucket = TBUCKET(1 hour)
| SORT bucket DESC

Top N with count:

FROM web-logs
| STATS count = COUNT(*) BY response.status_code
| SORT count DESC
| LIMIT 10

Text search (8.17+): Use MATCH as the default for full-text search instead of LIKE/RLIKE — it is significantly faster and supports relevance scoring. MATCH on a text field is usually sufficient on its own — do not add redundant keyword equality filters (e.g., category == "X") alongside MATCH unless the user explicitly requests filtering. Use QSTR only when you need advanced boolean logic, wildcards, or multi-field searches in a single expression. The first argument to MATCH must be one real field name — not a string listing several fields (e.g. "title,content") and not multiple field arguments; combine fields with MATCH(a, "q") OR MATCH(b, "q"). KQL is available from 8.18/9.0+. For content/document search use cases, follow the ES|QL Search Strategy. See ES|QL Search Reference for the full function guide.

FROM documents METADATA _score
| WHERE MATCH(content, "search terms")
| SORT _score DESC
| LIMIT 20

String extraction: Use DISSECT for structured delimiter-based patterns (preferred — produces named fields) and GROK for regex-based extraction. For simple cases, SUBSTRING(s, start, len) for fixed-position extraction, SPLIT(s, delim) to split into a multivalue, LOCATE(substr, s) to find a character position. SPLIT returns a multivalue — use MV_FIRST, MV_LAST, or MV_SLICE to pick elements. INSTR and STRPOS do not exist — use LOCATE. REGEXP_EXTRACT does not exist — use GROK.

// Extract domain from email using DISSECT (preferred — produces named fields)
FROM customers
| DISSECT email "%{local}@%{domain}"
| STATS count = COUNT(*) BY domain

// Alternative: extract domain from email using SPLIT
FROM customers
| EVAL domain = MV_LAST(SPLIT(email, "@"))
| STATS count = COUNT(*) BY domain

Log categorization (Platinum license): Use CATEGORIZE to auto-cluster log messages into pattern groups. Prefer this over running multiple STATS ... BY field queries when exploring or finding patterns in unstructured text. CATEGORIZE(field) must be the first grouping key in STATS ... BY — any other grouping (a time bucket, a service name) has to come after it: BY category = CATEGORIZE(message), bucket = BUCKET(@timestamp, 1 hour), never the reverse (it fails to parse).

FROM logs-*
| WHERE @timestamp > NOW() - 24 hours
| STATS count = COUNT(*) BY category = CATEGORIZE(message)
| SORT count DESC
| LIMIT 20

Change point detection (Platinum license): Use CHANGE_POINT to detect spikes, dips, and trend shifts in a metric series. Prefer this over manual inspection of time-bucketed counts.

FROM logs-*
| STATS c = COUNT(*) BY t = BUCKET(@timestamp, 30 seconds)
| SORT t
| CHANGE_POINT c ON t
| WHERE type IS NOT NULL

Time series metrics: With TS, use TRANGE for time filtering (9.3+) or omit it entirely — do not add a redundant WHERE @timestamp > NOW() - ... alongside TBUCKET. The TBUCKET duration defines the aggregation window.

// Counter metric: SUM(RATE(...)) with TBUCKET(duration)
TS metrics-tsds
| WHERE TRANGE(1 hour)
| STATS SUM(RATE(requests)) BY TBUCKET(1 hour), host

// Gauge metric: AVG(...) — no RATE needed
TS metrics-tsds
| STATS avg_cpu = AVG(cpu) BY service.name, bucket = TBUCKET(5 minutes)
| SORT bucket

// Histogram metric: standard aggregation (merge); cast for wildcard/mixed streams
TS metrics-*
| STATS total_gc = SUM(jvm.gc.duration::exponential_histogram) BY TBUCKET(1 hour), service.name

Time series with PromQL syntax (GA in 9.5+/Serverless, preview in 9.4): Use the PROMQL source command when the user explicitly asks for PromQL, references Prometheus syntax (sum by (instance) (...), label matchers like {cluster="prod"}), or is migrating a Prometheus dashboard or alert. Otherwise prefer TS. Its output flows into the rest of the ES|QL pipeline; the time column is step, and there is no @timestamp column. Not every PromQL construct is supported yet. Read PROMQL Command before writing a PROMQL query: it mirrors the Elasticsearch docs, including best practices, single-value results, and the unsupported constructs.

// Adaptive Kibana query — date picker drives time range and step
PROMQL index=metrics-* http_rate=(sum(rate(http_requests_total)))

// (9.5+; Serverless) Current value for a metric or gauge panel — instant query at the end of the date picker range
PROMQL index=metrics-* time=?_tend load=(avg(node_load1))

// Named result, post-processed with ES|QL
PROMQL index=k8s step=1h bytes=(max by (cluster) (network.bytes_in))
| STATS max_bytes = MAX(bytes) BY cluster
| SORT cluster

Data enrichment with LOOKUP JOIN: The basic ON clause matches fields by name in both indices (LOOKUP JOIN idx ON field_name). When the join key has a different name in the source, use RENAME first to align names. 9.2+ tech preview also supports expression predicates (ON expr == expr); see ES|QL Complete Reference for details. After LOOKUP JOIN, lookup columns are available by their original field names — do not table-qualify them (e.g., write threat_level, not threat_intel.threat_level). Ordering tip: when the question asks for top-N results, SORT and LIMIT before LOOKUP JOIN to reduce enrichment cost. For general listings or full enrichment, place LOOKUP JOIN right after FROM/WHERE.

// Field name mismatch — RENAME before joining
FROM support_tickets
| RENAME product AS product_name
| LOOKUP JOIN knowledge_base ON product_name

// Aggregate, limit, THEN enrich (top-N only)
FROM orders
| STATS total_spent = SUM(total) BY customer_id
| SORT total_spent DESC
| LIMIT 3
| LOOKUP JOIN customers_lookup ON customer_id
| KEEP name, customer_id, total_spent

// Multi-field join (9.2+)
FROM application_logs
| LOOKUP JOIN service_registry ON service_name, environment
| KEEP service_name, environment, owner_team

Multivalue field filtering: Use MV_CONTAINS to check if a multivalue field contains a specific value. Use MV_COUNT to count values.

// Filter by multivalue membership
FROM employees
| WHERE MV_CONTAINS(languages, "Python")

// Find entries matching multiple values
FROM employees
| WHERE MV_CONTAINS(languages, "Java") AND MV_CONTAINS(languages, "Python")

// Count multivalue entries
FROM employees
| EVAL num_languages = MV_COUNT(languages)
| SORT num_languages DESC

Change point detection (alternate example): Use when the user asks about spikes, dips, or anomalies. Requires time-bucketed aggregation, SORT, then CHANGE_POINT.

FROM logs-*
| STATS error_count = COUNT(*) BY bucket = DATE_TRUNC(1 hour, @timestamp)
| SORT bucket
| CHANGE_POINT error_count ON bucket AS type, pvalue

Approximate STATS (GA in 9.5+/Serverless, preview in 9.4): Prepend SET approximation=true; to a STATS query to get fast estimates via sampling and extrapolation on large datasets when exact values are not required. The result adds _approximation_confidence_interval(col) and _approximation_certified(col) columns per estimated quantity — report those bounds, do not present estimates as exact. COUNT_DISTINCT, MIN, MAX, FIRST, LAST, TOP (and a few others) are not supported and fall back to exact execution; use the SAMPLE command for those. Pipelines with 2+ STATS, or using the TS/PROMQL source command, also fall back. To tune it, pass a map instead of true: SET approximation={"confidence_level":0.95}; or SET approximation={"rows":N}; (N ≥ 10,000), or both in one map — never SET approximation=true, confidence_level=0.95;. See Query Approximation.

SET approximation=true;
FROM web_traffic
| WHERE @timestamp >= NOW() - 1 week
| STATS total_hits = COUNT(*), avg_load_time = AVG(page_load_ms) BY country_code
| SORT total_hits DESC
| LIMIT 5

Full Reference

For complete ES|QL syntax including all commands, functions, and operators, read:

Error Handling

When query execution fails, read the error message from Elasticsearch and correct the query. Common issues:

  • Field doesn't exist → Always inspect the mapping (GET /{index}/_mapping) and list indices (GET /_cat/indices) before writing a query. Never guess field or index names — they vary across deployments.

  • Type mismatch → Use type conversion functions (TO_STRING, TO_INTEGER, etc.)

  • Syntax error → Review ES|QL reference for correct syntax. Always use double quotes for strings, never single quotes.

  • Reserved keywords → Never use first, last, by, in, is, on, with (or and, or, not, like, rlike, null, true, false, asc, desc, nulls) bare as a column name or dotted segment. Rename aliases (first_seen) or backquote real field names. Inside FUSE, backquote a column named score, key or group after SCORE BY/KEY BY/GROUP BY. Backquoting any identifier always works; on an unexplained parse error on a name, backquote it. See Generation Tips.

  • No results → Check time range and filter conditions

  • Wrong function name → ES|QL uses underscored names: STD_DEV() not STDDEV(), MEDIAN_ABSOLUTE_DEVIATION() not MAD(). Use CONCAT() for strings, not +. Use CASE(cond, val, ...) not CASE WHEN...THEN...END.

  • Wrong date part → DATE_EXTRACT uses ES|QL part names: "hour_of_day" not "hour", "day_of_month" not "day", "month_of_year" not "month". Use DATE_DIFF("day", start, end) for date arithmetic, not subtraction.

Examples

Each example follows the process: inspect the mapping first, then write the simplest correct query.

"Top 10 source IPs by request count in the last hour" — filter by time window, then aggregate and rank:

FROM logs-*
| WHERE @timestamp > NOW() - 1 hour
| STATS requests = COUNT(*) BY source.ip
| SORT requests DESC
| LIMIT 10

"Average response time per service, only for 5xx responses" — filter to errors before aggregating:

FROM traces-*
| WHERE http.response.status_code >= 500
| STATS avg_ms = AVG(duration_ms) BY service.name
| SORT avg_ms DESC

"Error count per day for the last week" — bucket by day with DATE_TRUNC:

FROM logs-*
| WHERE log.level == "error" AND @timestamp > NOW() - 7 days
| STATS errors = COUNT(*) BY day = DATE_TRUNC(1 day, @timestamp)
| SORT day ASC

Guidelines

  • Inspect before querying. Read the mapping (GET /{index}/_mapping) and list indices (GET /_cat/indices) before writing a query — never guess field or index names.

  • Filter early. Put WHERE before STATS so aggregation runs over the smallest row set.

  • Always bound results. End exploratory queries with LIMIT.

  • Quote correctly. Use double quotes for string literals, never single quotes.

  • Respect version gating. Confirm feature availability with GET / (build_flavor, version.number) and references/esql-version-history.md before using newer commands such as LOOKUP JOIN or INLINE STATS.

  • Correct on error, do not guess. Read the Elasticsearch error, fix the specific issue, and re-run.

Operations

HTTP API (shorthand)elastic CLI command
GET /elastic es info
GET /_cat/indiceselastic es cat indices --index '<pattern>'
GET /{index}/_mappingelastic es indices get-mapping --index '<index>'
GET /{index}/_settings/index.modeelastic es indices get-settings --index '<index>' --name index.mode
GET /_resolve/index/{name}elastic es indices resolve-index --name '<name>'
POST /_queryelastic es esql query --format tsv --query "<esql>"

Files

references/esql-version-history.md

ES|QL Version History and Feature Availability

This document tracks ES|QL language features, commands, and functions across Elasticsearch versions. Use this to determine compatibility when writing queries for specific Elasticsearch deployments.

Paired releases: Certain minor versions shipped simultaneously with nearly identical feature sets. When a feature appears in one, assume it is in both unless explicitly noted otherwise. Paired versions: 8.18 / 9.0, 8.19 / 9.1.

Serverless: Elastic Cloud Serverless reports a forward-moving version.number from GET / (aligned with the next minor from main), so clients that only semver-compare often behave as if the cluster is “latest.” Do not rely on that for feature gating: check build_flavor — if it is "serverless", all GA and preview features are available and you should skip version-based gates. For snapshot builds (e.g., 9.4.0-SNAPSHOT), strip the -SNAPSHOT suffix and use the major.minor for version checks.

Table of Contents

  • Version Timeline Overview

  • Feature Availability by Version

  • Major Limitations

  • Cross-Cluster Query Support

  • Output Formats

  • API Endpoints

  • Performance Tips by Version

  • Version Detection

  • References

Version Timeline Overview

VersionReleaseStatusKey Additions
8.11Nov 2023Tech PreviewInitial ES|QL release
8.12Jan 2024Tech PreviewSpatial types, PROFILE
8.13Mar 2024Tech PreviewAsync queries, cross-cluster ENRICH
8.14May 2024GASpatial functions, regex optimization
8.15Aug 2024GAType casting (::), Arrow output
8.16Oct 2024GAPer-aggregation WHERE, new math/string functions
8.17Dec 2024GAMATCH, QSTR full-text functions
8.18Feb 2025GALOOKUP JOIN (preview), scoring, KQL
8.19Apr 2025GAMATCH_PHRASE, FORK, CHANGE_POINT (preview)
9.0Feb 2025GAReleased with 8.18 features
9.1Jun 2025GAFull-text functions GA, FORK (preview)
9.2Oct 2025GAMulti-field joins, TS, INLINE STATS (preview), CHANGE_POINT GA, FUSE (preview), RERANK (preview)
9.3Jan 2026GAINLINE STATS GA, SET directive (preview), Lucene-pushable JOIN predicates
9.4May 2026GATS/TBUCKET GA, time series funcs GA, WITHOUT/METRICS_INFO/TS_INFO, PROMQL (preview), Views, RERANK GA, MV_EXPAND/VALUES GA, SET time_zone, SET approximation (preview)
9.5Aug 2026GAPROMQL GA, FUSE GA, SAMPLE GA, IN/NOT IN subquery (preview), IP_LOCATION (preview), TS_COLLAPSE (preview), EMBEDDING (preview), FIELD_EXTRACT (preview), Flattened fields (preview), SET approximation GA, Unmapped fields GA, JSON_EXTRACT GA, Subqueries GA, CHANGE_POINT BY GA, TO_COUNTER/TO_GAUGE GA, SPARKLINE (preview), date_range (preview), MATCH on expressions (preview), ST_UNION & spatial set ops (preview), TO_TEXT (preview), SET column_metadata (preview), Data Federation (experimental)

Feature Availability by Version

Commands
CommandIntroducedGANotes
FROM8.118.14Source command
WHERE8.118.14Filtering
EVAL8.118.14Computed columns
STATS ... BY8.118.14Aggregations with grouping
SORT8.118.14Ordering results
LIMIT8.118.14Result set size
KEEP8.118.14Column selection
DROP8.118.14Column removal
RENAME8.118.14Column renaming
DISSECT8.118.14Pattern extraction
GROK8.118.14Log parsing
ENRICH8.118.14Data enrichment
MV_EXPAND8.119.4Multi-value expansion (GA)
SHOW8.118.14Metadata display
ROW8.118.14Literal row creation
LOOKUP JOIN8.18/9.08.19/9.1SQL-style LEFT JOIN with lookup indices
INLINE STATS9.29.3Inline aggregations (like window functions)
FORK8.19/9.19.4Multiple execution branches
FUSE9.29.5Combine results from FORK branches (GA in 9.5)
TS9.29.4Time series source command (GA in 9.4)
PROMQL9.49.5Source command using PromQL syntax on TSDS (GA in 9.5)
METRICS_INFO9.49.4TSDS metric catalogue (after TS)
TS_INFO9.49.4Per-(metric, time series) metadata (after TS)
TS_COLLAPSE9.5PreviewCollapse PROMQL per-step output into one row per time series
RERANK9.29.4Re-score results with inference (GA in 9.4)
COMPLETION8.19/9.19.3LLM text generation
SAMPLE8.19/9.19.5Random sampling (GA in 9.5)
CHANGE_POINT8.19/9.19.2Spike/dip detection (Platinum license); BY grouping GA in 9.5
MMR9.4PreviewMaximal Marginal Relevance diversification
URI_PARTS9.4PreviewParse URI into structured columns (pipe command)
USER_AGENT9.4PreviewParse user agent into structured columns (pipe command)
REGISTERED_DOMAIN9.4PreviewREGISTERED_DOMAIN: extract from hostname (pipe command)
IP_LOCATION9.5PreviewQuery-time GeoIP enrichment (pipe command); WITH {database_file, properties, first_only}
Subqueries in FROM9.49.5UNION ALL subqueries; ROW/TS sources added in 9.5
LIMIT BY9.4PreviewGrouped top-N
DEDUPServerlessPreviewServerless preview; not available on Stack 9.5 — use LIMIT 1 BY cols or STATS BY cols
Views9.4PreviewVirtual indices; 9.5: cross-project, now available on Serverless
Full-Text Search Functions
FunctionIntroducedGANotes
MATCH(field, query)8.178.19/9.1Basic full-text matching
QSTR(query_string)8.178.19/9.1Query string syntax (Lucene)
KQL(kql_string)8.18/9.08.19/9.1Kibana Query Language
MATCH_PHRASE(field, phrase)8.19/9.18.19/9.1Exact phrase matching
Match operator (:)8.178.19/9.1Shorthand for MATCH

MATCH on expressions (9.5+ preview): MATCH and the : operator can also match non-indexed expressions (e.g. EVAL columns; not after STATS or with options on 9.5 Stack, both allowed on Serverless) — row-by-row scan instead of inverted index. Wrap keyword expressions in TO_TEXT for analyzed matching.

Vector and Advanced Search Functions
Function / optionIntroducedStatusNotes
KNN9.2 (preview)GA (9.4)Dense vector / semantic_text k-nearest neighbours
TEXT_EMBEDDING9.3 (preview)GA (9.4)Text → dense_vector via text_embedding endpoint
V_COSINE, V_DOT_PRODUCT, etc.9.3 (preview)GA (9.4)Vector similarity functions (V_L1_NORM, V_L2_NORM, V_HAMMING)
EMBEDDING9.5 (preview)PreviewMultimodal input via embedding endpoint
TOP_SNIPPETS9.3 (preview)Preview9.5 adds highlight, pre_tag, post_tag, encoder, order, analyzer
DECAY9.3 (preview)PreviewDistance-based scoring
Inference timeout option9.5GACOMPLETION/RERANK WITH {"timeout": ...}; TEXT_EMBEDDING option

Scoring support:

  • METADATA _score available from 8.18/9.0

  • Must use SORT _score DESC to rank by relevance

Spatial Functions
FunctionIntroducedNotes
GEO_POINT type8.12Basic spatial type support
CARTESIAN_POINT type8.12Cartesian coordinate support
ST_INTERSECTS8.14Geometry intersection test
ST_CONTAINS8.14Containment test
ST_DISJOINT8.14Disjoint test
ST_WITHIN8.14Within test
ST_X, ST_Y8.14Coordinate extraction
ST_DISTANCE8.15Distance calculation
ST_EXTENT_AGG8.18/9.0Bounding box aggregation
ST_ENVELOPE8.18/9.0Bounding box for geometry
ST_DIMENSION9.4Geometry dimension (0/1/2)
ST_GEOMETRYTYPE9.4Geometry type as string
ST_ISEMPTY9.4Test if geometry is empty
ST_BUFFER9.4Buffer around geometry
ST_SIMPLIFYPRESERVETOPOLOGY9.4Simplify geometry preserving topology
ST_UNION9.5Preview — union of two geometries
ST_INTERSECTION9.5Preview — shared area
ST_DIFFERENCE9.5Preview — first minus second
ST_SYMDIFFERENCE9.5Preview — area in exactly one
Date/Time Functions
FunctionIntroducedNotes
NOW()8.11Current timestamp
DATE_TRUNC8.11Truncate to interval
DATE_EXTRACT8.11Extract date parts
DATE_FORMAT8.11Format dates; honors SET time_zone from 9.4
DATE_PARSE8.11Parse date strings; honors SET time_zone from 9.4
DATE_DIFF8.13Difference between dates
date_nanos type8.17 (preview)Nanosecond precision timestamps
TRANGE9.3 (preview)Time range filter on @timestamp
String Functions
FunctionIntroducedNotes
LEFT, RIGHT8.11Substring extraction
SUBSTRING8.11Position-based extraction
CONCAT8.11String concatenation
TRIM, LTRIM, RTRIM8.11Whitespace removal
TO_UPPER, TO_LOWER8.13Case conversion
LOCATE8.14Find substring position
SPACE8.16Generate spaces
REVERSE8.16Reverse string
BIT_LENGTH, BYTE_LENGTH8.17String length in bits/bytes
STARTS_WITH, ENDS_WITH8.11Prefix/suffix matching
CONTAINS9.2Substring containment check
JSON_EXTRACT9.4Extract from JSON string by JSONPath (GA in 9.5)
FIELD_EXTRACT9.5Extract sub-field from flattened object as keyword
Multi-Value Functions
FunctionIntroducedNotes
MV_COUNT8.11Count values
MV_CONCAT8.11Join values
MV_FIRST, MV_LAST8.13First/last value
MV_MIN, MV_MAX8.11Min/max value
MV_SUM, MV_AVG8.11Sum/average
MV_MEDIAN8.11Median value
MV_SORT8.14Sort multi-values
MV_SLICE8.14Slice multi-values
MV_PERCENTILE8.16Percentile calculation
MV_PSERIES_WEIGHTED_SUM8.16Weighted sum
MV_DIFFERENCE9.4Set difference of two MV fields
MV_UNION9.4Set union of two MV fields
MV_INTERSECTION9.4Set intersection of two MV fields
MV_INTERSECTS9.4True if MV fields share a value
MV_CONTAINS9.4True if first MV contains all of second
Aggregation Functions
FunctionIntroducedNotes
COUNT, COUNT_DISTINCT8.11Counting
SUM, AVG8.11Basic aggregations
MIN, MAX8.11Extended to strings/IPs in 8.16
MEDIAN, MEDIAN_ABSOLUTE_DEVIATION8.11Statistical
PERCENTILE8.11Percentile calculation
TOP8.15Top N values
VALUES8.14Unique values (GA in 9.4)
ST_EXTENT_AGG8.18/9.0Spatial bounding box
WEIGHTED_AVG8.16Weighted average
STD_DEV8.18/9.0Standard deviation
VARIANCE8.18/9.0Variance
FIRST(value, sort_field)9.4Value from row with earliest sort field
LAST(value, sort_field)9.4Value from row with latest sort field
EARLIEST(@timestamp)9.4Min @timestamp (1-arg shorthand)
LATEST(@timestamp)9.4Max @timestamp (1-arg shorthand)
SPARKLINE9.5Preview — per-bucket values for a sparkline
Grouping Functions
FunctionIntroducedNotes
BUCKET8.11Numeric/date bucketing in BY clause
CATEGORIZE8.18/9.0Auto-categorization of text in BY clause
TBUCKET9.2Time bucketing from @timestamp; preferred in TS (GA in 9.4)
WITHOUT9.4Group time series by every dimension except the listed ones (GA)
Per-Aggregation WHERE

Available since 8.16. Allows filtering individual aggregations without affecting others:

| STATS total = COUNT(*), errors = COUNT(*) WHERE level == "error" BY service.name
IP Functions
FunctionIntroducedNotes
CIDR_MATCH8.11Check IP against CIDR ranges
IP_PREFIX8.14Extract network prefix from IP
TO_IP8.11Convert string to IP type
Time Series Aggregation Functions

Available under TS ... | STATS. See time-series-queries.md for full reference. All time series aggregation functions in this table — both the 9.2-introduced set and the 9.3-introduced set (DERIV, PERCENTILE_OVER_TIME, STDDEV_OVER_TIME, VARIANCE_OVER_TIME) — are GA since 9.4.

FunctionIntroducedStatusNotes
RATE9.2 (preview)GA (9.4)Per-second rate of counter increase
IRATE9.2 (preview)GA (9.4)Instant rate (last two data points)
INCREASE9.2 (preview)GA (9.4)Absolute counter increase in window
DELTA9.2 (preview)GA (9.4)Absolute change of a gauge
IDELTA9.2 (preview)GA (9.4)Change between last two data points
AVG_OVER_TIME9.2 (preview)GA (9.4)Average value over time
SUM_OVER_TIME9.2 (preview)GA (9.4)Sum of values over time
MIN_OVER_TIME9.2 (preview)GA (9.4)Minimum value over time
MAX_OVER_TIME9.2 (preview)GA (9.4)Maximum value over time
FIRST_OVER_TIME9.2 (preview)GA (9.4)Earliest value by @timestamp
LAST_OVER_TIME9.2 (preview)GA (9.4)Latest value by @timestamp (implicit default for numeric/gauge)
COUNT_OVER_TIME9.2 (preview)GA (9.4)Count of values over time
COUNT_DISTINCT_OVER_TIME9.2 (preview)GA (9.4)Count of distinct values over time
PRESENT_OVER_TIME9.2 (preview)GA (9.4)true if field has values in window
ABSENT_OVER_TIME9.2 (preview)GA (9.4)true if field has no values in window
DERIV9.3 (preview)GA (9.4)Derivative via linear regression
PERCENTILE_OVER_TIME9.3 (preview)GA (9.4)Percentile of values over time
STDDEV_OVER_TIME9.3 (preview)GA (9.4)Population standard deviation over time
VARIANCE_OVER_TIME9.3 (preview)GA (9.4)Population variance over time

Sliding window parameter (second argument):

  • 9.2-9.3 (preview) — accepted window values are limited to multiples of the TBUCKET interval in the BY clause; if no window is specified, the bucket interval is used implicitly.

  • 9.4+ (GA) — all window values are accepted, with performance optimizations when the window is a multiple of the TBUCKET interval. Mixing windows that are smaller than the time bucket for one metric with windows larger than the time bucket for another metric in the same query is not allowed.

Conditional Functions
FunctionIntroducedNotes
CLAMP9.3 (preview)Clamp values to [min, max] range
CLAMP_MIN9.3 (preview)Set lower bound for values
CLAMP_MAX9.3 (preview)Set upper bound for values
WHERE IN / NOT IN (9.5+ preview)

Subquery-based set membership filtering. The reference set is any ES|QL expression returning one column.

SyntaxIntroducedStatusNotes
WHERE field IN (FROM ... | KEEP ...)9.5PreviewFilter by matching a subquery result set
WHERE field NOT IN (FROM ... | KEEP ...)9.5PreviewExclude rows matching a subquery result set

Before 9.5, use LOOKUP JOIN idx ON field == col | WHERE col IS NULL for NOT IN (IS NOT NULL for IN).

date_range Type and Functions (9.5+ preview)
Function / TypeIntroducedStatusNotes
date_range type9.5PreviewTime range with start/end bounds
TO_DATE_RANGE(string)9.5PreviewParse "from..to" string into date_range
TO_RANGE(from, to)9.5PreviewConstruct date_range from two dates
RANGE_INTERSECTS(a, b)9.5PreviewCheck if two ranges (or range + date) overlap
RANGE_WITHIN(a, b)9.5PreviewCheck if range a is within range b
RANGE_CONTAINS(a, b)9.5PreviewCheck if range a contains range b
range_min(field)9.5PreviewExtract lower bound of a date_range
range_max(field)9.5PreviewExtract upper bound of a date_range
Type Casting
SyntaxIntroducedNotes
TO_STRING(x)8.11Function-based casting
TO_INTEGER(x)8.11Function-based casting
TO_DOUBLE(x)8.11Function-based casting
x::string8.15Operator-based casting
x::integer8.15Operator-based casting
TO_COUNTER(x) / x::counter9.5 (GA)Treat a numeric metric as a counter in TS (e.g. RATE)
TO_GAUGE(x) / x::gauge9.5 (GA)Treat a metric as a gauge in TS
TO_TEXT(x)9.5Preview — keyword → analyzed text (optional analyzer)

Major Limitations

Pagination (Not Supported)

ES|QL does not support cursor-based pagination like the Search API's search_after or scroll.

Current behavior:

  • Default: 1,000 rows returned

  • Maximum: 10,000 rows (configurable via esql.query.result_truncation_max_size)

  • No cursor or continuation token

  • GitHub tracking issue: #100000

Workarounds:

  • Use WHERE to filter to relevant subset

  • Use STATS to aggregate at query time

  • For exports, use Search API with search_after instead

Time Zone Support (GA in 9.4+; Serverless)

ES|QL supports query-wide timezone via the SET time_zone directive (GA in 9.4+; Serverless). This accepts IANA timezone strings and UTC offsets, and applies to all date/time operations including DATE_TRUNC, DATE_FORMAT, NOW(), bucketing, and display.

SET time_zone = "America/New_York";
FROM logs-*
| STATS errors = COUNT(*) BY hour = DATE_TRUNC(1 hour, @timestamp)
| SORT hour DESC

Remaining limitations (all versions):

  • No per-function timezone argument — DATE_TRUNC(1 hour, @timestamp, "America/New_York") does not work

  • DATE_FORMAT and DATE_PARSE do not accept timezone parameters directly; use SET time_zone instead

Versions before 9.4: No timezone support. All dates are processed in UTC — released 9.3 builds reject both SET time_zone and the time_zone request parameter. Workaround: use EVAL to add/subtract hours manually:

| EVAL local_time = timestamp + 1 hour
Nested Fields (Not Supported)

ES|QL cannot query nested field types. Unlike other unsupported types (which return null), nested fields are not returned at all — they are silently omitted from results.

  • Cannot use nested paths like nested_field.sub_field — referencing the nested field or a subfield in KEEP, WHERE, or EVAL fails with Unknown column (with SET unmapped_fields = "load" the subfield is null)

  • Must flatten data at index time for ES|QL access

Unsupported Field Types

These field types are not supported or have limitations:

TypeStatus
nestedNot supported — omitted from results (not null); referencing it fails with Unknown column
flattenedTech Preview (9.5) — root column only; sub-keys via FIELD_EXTRACT; JSON_EXTRACT pre-9.5; before 9.5 the column is returned as unsupported (null)
joinNot supported
date_rangeTech Preview (9.5) — supported via date_range type and range functions
binaryNot supported
completionNot supported
rank_featureNot supported
histogramSupported in TS via cast only (::exponential_histogram/::tdigest; 9.3 preview / 9.4 GA)
JOIN Limitations

LOOKUP JOIN (8.18/9.0+):

  • Only LEFT OUTER JOIN behavior

  • Lookup index must use index.mode: lookup setting

  • Lookup index limited to single shard (max 2B docs)

  • Cross-cluster joins require lookup index on all clusters

  • Only supports equality joins before 9.2

LOOKUP JOIN improvements in 9.2 (tech preview):

  • Multi-field joins supported

  • Complex join predicates with <, >, <=, >=

  • Expression-based join conditions

LOOKUP JOIN improvements in 9.3 (tech preview):

  • Lucene-pushable predicates: MATCH, QSTR, KQL, CIDR_MATCH in join conditions

  • Further performance gains for filtered joins

Subqueries

ES|QL supports subqueries in FROM (preview in 9.4, GA in 9.5) for combining results from multiple pipelines (UNION ALL semantics). These are non-correlated — each branch is independent. 9.5 adds ROW and TS as valid subquery source commands.

FROM
  (FROM web_logs | WHERE status >= 500 | KEEP @timestamp, message, service.name),
  (FROM app_logs | WHERE level == "error" | KEEP @timestamp, message, service.name)
| SORT @timestamp DESC

WHERE IN / NOT IN (9.5+ preview):

ES|QL supports WHERE field IN (subquery) and WHERE field NOT IN (subquery) for filtering rows against a reference dataset. The subquery must return a single column.

FROM logs-*
| WHERE source.ip NOT IN (FROM allowed_ips | KEEP ip)
| STATS count = COUNT(*) BY source.ip
| SORT count DESC

Not supported:

  • Correlated subqueries (branches cannot reference outer columns)

  • Nested SELECT / CTEs (Common Table Expressions)

Use INLINE STATS (9.2+) for per-row vs. aggregate comparison patterns.

Cross-Cluster Query Support

FeatureVersionNotes
Basic CCS8.13Query remote clusters
Cross-cluster ENRICH8.13Enrich with remote data
Cross-cluster LOOKUP JOIN9.2Join with remote lookup indices
skip_unavailable8.17Graceful handling of unavailable clusters

Output Formats

FormatVersionNotes
JSON8.11Default format
CSV8.11Tabular output
TSV8.11Tab-separated
Arrow8.15Apache Arrow IPC format
MD9.6Markdown pipe table (GitHub-flavored); Serverless; Stack 9.6+

API Endpoints

EndpointVersionNotes
POST /_query8.11Synchronous query
POST /_query/async8.13Async query submission
GET /_query/async/{id}8.13Get async query results
DELETE /_query/async/{id}8.13Cancel async query
PUT /_query/view/{name}9.4Create/update view
GET /_query/view/{name}9.4Get view definition
DELETE /_query/view/{name}9.4Delete view
PUT /_query/data_source/{name}9.5Data source config (experimental, not production)
PUT /_query/dataset/{name}9.5Dataset config (experimental, not production)

Performance Tips by Version

8.14+
  • Regex patterns are optimized

  • Enrich supports text fields

8.15+
  • Use :: casting instead of TO_* functions (cleaner syntax)

  • Arrow format for analytics tool integration

8.17+
  • Use MATCH/QSTR instead of LIKE/RLIKE for text search (50-1000x faster)

  • Full-text functions use Lucene optimizations

9.1+
  • Use INLINE STATS to avoid multiple queries

  • Full-text functions are GA and stable

9.2+
  • Use TS with RATE, AVG_OVER_TIME, etc. for time series metrics aggregations (preview in 9.2-9.3, GA in 9.4)

  • Use TBUCKET for time bucketing in TS queries (GA in 9.4)

  • Multi-field LOOKUP JOIN for complex correlations

  • FUSE for hybrid search scoring

9.3+
  • Use TRANGE instead of manual WHERE @timestamp filters

  • Sliding window parameter for time series functions (e.g. RATE(field, 10m)); in 9.2-9.3 the window must be a multiple of the TBUCKET interval, this restriction is lifted in 9.4

  • CLAMP, CLAMP_MIN, CLAMP_MAX for bounding metric values

  • Histogram metrics in TS (preview): query exponential_histogram / tdigest with standard aggregations; cast plain histogram with ::exponential_histogram or ::tdigest — see Histogram Metrics

9.4+
  • TS, TBUCKET, and all time series aggregation functions are GA — safe for production metrics queries

  • Histogram metrics in TS are GA (exponential_histogram / tdigest; plain histogram still requires a cast)

  • Use METRICS_INFO / TS_INFO to discover TSDS schemas instead of inspecting mappings or field capabilities

  • Use WITHOUT(dim, ...) to group by all dimensions except specific ones — avoids enumerating every dimension manually

  • Sliding window accepts arbitrary durations — RATE(field, 7m) with TBUCKET(5 minute) now works

  • Use SET time_zone with IANA strings for timezone-aware queries instead of manual EVAL offset arithmetic

  • Use SET approximation = true to approximate large STATS summaries via sampling/extrapolation (preview in 9.4, GA in 9.5+ and Serverless) — returns estimates with confidence intervals; see query-approximation.md

  • Use SET unmapped_fields = "load" to query fields missing from some indices without errors

  • Use FIRST/LAST (or EARLIEST/LATEST) instead of SORT + LIMIT 1 for grouped first/last-value queries

  • Use PROMQL when porting Prometheus dashboards/alerts; otherwise prefer TS for native ES|QL (preview in 9.4, GA in 9.5+ and Serverless; supported PromQL functions/operators vary by version — see promql-command.md)

9.5+
  • SET unmapped_fields is GA — production-ready for schema-on-read queries (for SET approximation, GA in 9.5, see the 9.4+ tips above)

  • JSON_EXTRACT is GA — combine with SET unmapped_fields = "load" for a complete schema-on-read workflow

  • Subqueries in FROM are GA — use ROW and TS as subquery source commands in addition to FROM

  • Use WHERE field IN (FROM lookup_index | KEEP field) (preview) to filter against reference datasets without joins

  • Use IP_LOCATION (preview) for query-time GeoIP enrichment without ingest pipeline configuration (before 9.5, no query-time GeoIP: use ENRICH or the ingest geoip processor)

  • Use EMBEDDING(value, inference_id) (preview) to generate dense vector embeddings from multimodal input (text, image, audio, video, PDF) via inference endpoints

  • Flattened fields (preview) load as a flattened root column; read sub-keys only with FIELD_EXTRACT(root, "dotted.key") and cast the keyword result for numeric use — JSON_EXTRACT on _source remains the pre-9.5 workaround

  • Use CHANGE_POINT value ON key BY group (GA) for per-series change detection instead of one query per group

  • Use SPARKLINE(agg, @timestamp, buckets, from, to) (preview) for compact per-group trend arrays

  • Use x::counter / x::gauge (GA) inside TS when a metric's mapping lacks the right time_series_metric

Patch-release behavior changes:

  • 9.5.1 — PROMQL works on date_nanos time series indices

  • 9.5.2 — a WHERE field IN (subquery) whose subquery folds to an empty set returns no rows

Serverless (latest)

Serverless includes all 9.5 features plus some that have not reached Stack yet, for example the md output format (Stack 9.6+), the DEDUP command (preview), and SET project_routing for cross-project search (Serverless only). Serverless also lifts some 9.5 Stack restrictions: MATCH on expressions works after STATS and with options, and MATCH_PHRASE and inline : accept FIELD_EXTRACT(…).

Version Detection

To check ES|QL availability and version:

# Check Elasticsearch version and build flavor (use build_flavor to detect Serverless)
curl -s localhost:9200 | jq '.version | {number, build_flavor}'

# Test ES|QL availability
curl -X POST localhost:9200/_query \
  -H "Content-Type: application/json" \
  -d '{"query": "ROW x = 1"}'

References

Requirements and compatibility

Works with: Claude Code, Claude apps, OpenAI Codex, GitHub Copilot, Cursor, Gemini CLI, Kiro, OpenCode, Goose, Amp, Roo Code, JetBrains Junie, Factory Droid, Devin (ex-Windsurf), Mistral Vibe, OpenHands, Letta

“Works with” means the tool reads the open Agent Skills format; a skill written for one tool may use features others lack.

The author says: Elasticsearch 8.14 or later (ES|QL GA; introduced 8.11 as tech preview), self-managed, Elastic Cloud Hosted, or Elastic Cloud Serverless; individual ES|QL features are version-gated (see references/esql-version-history.md). Requires the `elastic` CLI ≥ 0.2 with `stack es` support.

Security

No issues found by our static scan· No issues found

A static scan is not a guarantee. Skills are third-party content: review a skill before you let an agent use it.

Attribution and licence

Created by
elastic
Publisher
Elastic (Official)
Source
elastic/agent-skills · skills/elasticsearch/elasticsearch-esql · commit 09d879d
Licence
Apache License 2.0 (redistribution allowed)
  • LICENSE (added by AutoGineer from LICENSE)
  • ATTRIBUTION.md (written by AutoGineer)
Synced
Also distributed in
elastic/agent-skills

AutoGineer is not affiliated with or endorsed by elastic or Elastic.

Download ZIP

The ZIP holds the skill’s files, its licence files and an ATTRIBUTION.md. Download elasticsearch-esql.zip