Elasticsearch Esql
ElasticOfficialCreated by elastic
- Skill
- Tool integration
- Data and AI
- Apache-2.0
- No issues found
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
Download the skill as a ZIP:
https://mp-staging.autogineer.com/agent-skills/elastic/elasticsearch-esql/downloadUnzip 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
_sourceto be enabled on queried indices. Indices with_sourcedisabled (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+), andINLINE STATS(9.2+) were added in later versions. On pre-8.18 clusters, useENRICHas a fallback forLOOKUP JOIN(see generation tips).INLINE STATSand counter-fieldRATE()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.numbertracks the stack line under active development (next minor from main), so clients that only semver-compare may treat Serverless as “latest.” Do not useversion.numberto gate features: ifbuild_flavoris"serverless", assume all GA and preview ES|QL features are available.
build_flavor: "default"— Stack (self-managed or Cloud-hosted). Useversion.numberfor feature availability.Snapshot builds have
version.numberlike9.4.0-SNAPSHOT. Strip the-SNAPSHOTsuffix 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
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). Thebuild_flavorfield 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.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 withGET /{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-*, orapplication_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
COALESCEunless 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 istime_series, useTS <data-stream>(notFROM),TBUCKET(interval)(notDATE_TRUNC), and wrap counter fields withSUM(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 commandsMETRICS_INFOandTS_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 viaPOST /_query. TreatMETRICS_INFOas authoritative formetric_type(counter/gauge/histogram) andfield_type(histogram,tdigest,exponential_histogramfor distribution metrics). Both must followTSand must precedeSTATS/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_nameChoose the right ES|QL feature for the task. Version gate (mandatory on Stack): before writing any query, compare
version.numberto 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)orTSfor TSDB"PromQL", "Prometheus query/dashboard/alert",
sum by (instance) (...), label matchers like{cluster="prod"}→PROMQLsource command (GA in 9.5+/Serverless, preview in 9.4); see PROMQL Command. PreferTSfor 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" →
STATSwith aggregation functions"approximate," "estimate," "rough numbers," "fast/cheap stats on huge data" →
SET approximation=true;before aSTATSquery (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), thenLIMIT nif a row count is asked; there is noRANDOM()function"latest/first value per group," "most recent status per host" →
STATS LAST(field, @timestamp) BY group(orFIRST), notSORT+LIMIT 1allow list, deny list, "exclude known IPs/users" from another index →
WHERE field [NOT] IN (FROM idx | KEEP col)(preview 9.5); useLOOKUP JOINonly 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 withTO_IP()beforeCIDR_MATCH, IP comparisons,IN (FROM …), orLOOKUP JOINon anipfield; for the last twoEVAL ip = TO_IP(src)first (the left side must be a column)"country/city of an IP," "geolocate IPs," ASN →
| IP_LOCATION geo = ip_fieldpipe command (preview 9.5)parse URLs (domain, path, query) →
| URI_PARTS parts = url_fieldpipe command (preview 9.4), notDISSECT/GROK"in my local time," "per day in New York" →
SET time_zone = "America/New_York";(9.4+), not manual offsetsflattenedfield sub-keys (e.g. OTelresource.attributes) →FIELD_EXTRACT(field, "host.name")(preview 9.5), notfield.host.name; it returnskeyword, so cast (TO_INTEGER(...)) before numeric comparisonsembed text/images at query time for vector search →
EMBEDDING(value, inference_id)(preview 9.5) orTEXT_EMBEDDING(text, inference_id)insideKNNquery 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 feature Fallback on Stack without the feature DEDUP(Serverless preview only)LIMIT 1 BY cols(9.4+;SORTfirst picks the kept row) orSTATS BY colsWHERE x NOT IN (FROM idx | KEEP c)LOOKUP JOIN idx ON x == c | WHERE c IS NULL(IN:IS NOT NULL);ONexpressions need 9.2+, elseRENAME x AS cfirstIP_LOCATIONNo query-time GeoIP: ENRICHwith an IP-range enrich policy, or the ingestgeoipprocessorFIELD_EXTRACT(or an unmapped key)FROM idx METADATA _source | EVAL v = JSON_EXTRACT(_source, "$.labels.key")— notlabels.key,labels["key"], orJSON_EXTRACT(labels, …);COALESCEwith"$.labels[\"k.sub\"]"for pre-dotted keysRead 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
PROMQLvsTSdecision 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)
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
KEEPthat directly answer the question. Do not add extra filter conditions beyond what the user specified (e.g., don't addOR level == "ERROR"when the user just said "errors").Start with
FROM index-pattern(orTS index-patternfor time series indices)Add
WHEREfor filtering (useTRANGEfor time ranges on 9.3+)Use
EVALfor computed fieldsUse
STATS ... BYfor aggregationsFor time series metrics:
TSwithSUM(RATE(...))for counters,AVG(...)for gauges, standard aggregations (SUM,AVG,PERCENTILE, … — not*_OVER_TIME) for histogram metrics, andTBUCKET(interval)for time bucketing — see the TS section in Generation Tips and Histogram MetricsFor detecting spikes, dips, or anomalies, use
CHANGE_POINTafter time-bucketed aggregationAdd
SORTandLIMITas neededRe-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 noDEDUP— rewrite with the fallback.
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 nCommon Patterns
Filter and limit:
FROM logs-*
| WHERE @timestamp > NOW() - 24 hours AND level == "error"
| SORT @timestamp DESC
| LIMIT 100Aggregate 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 DESCTop N with count:
FROM web-logs
| STATS count = COUNT(*) BY response.status_code
| SORT count DESC
| LIMIT 10Text 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 20String 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 domainLog 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 20Change 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 NULLTime 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.nameTime 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 clusterData 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_teamMultivalue 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 DESCChange 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, pvalueApproximate 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 5Full Reference
For complete ES|QL syntax including all commands, functions, and operators, read:
ES|QL Search Reference - Full-text search: MATCH, QSTR, KQL, MATCH_PHRASE, scoring, semantic search
ES|QL Search Strategy - Relevance search strategy for content indices: retrieve → fuse → rerank
ES|QL Version History - Feature availability by Elasticsearch version
Query Patterns - Natural language to ES|QL translation
Generation Tips - Best practices for query generation
Time Series Queries - TS command, time series aggregation functions, TBUCKET
PROMQL Command - PromQL source command for TSDS indices (GA in 9.5+/Serverless, preview in 9.4)
Query Approximation - Approximate STATS via sampling/extrapolation (GA in 9.5+/Serverless, preview in 9.4)
DSL to ES|QL Migration - Convert Query DSL to ES|QL
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(orand,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. InsideFUSE, backquote a column namedscore,keyorgroupafterSCORE 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()notSTDDEV(),MEDIAN_ABSOLUTE_DEVIATION()notMAD(). UseCONCAT()for strings, not+. UseCASE(cond, val, ...)notCASE WHEN...THEN...END.Wrong date part →
DATE_EXTRACTuses ES|QL part names:"hour_of_day"not"hour","day_of_month"not"day","month_of_year"not"month". UseDATE_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 ASCGuidelines
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
WHEREbeforeSTATSso 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 asLOOKUP JOINorINLINE 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/indices | elastic es cat indices --index '<pattern>' |
GET /{index}/_mapping | elastic es indices get-mapping --index '<index>' |
GET /{index}/_settings/index.mode | elastic es indices get-settings --index '<index>' --name index.mode |
GET /_resolve/index/{name} | elastic es indices resolve-index --name '<name>' |
POST /_query | elastic es esql query --format tsv --query "<esql>" |
Files
- ATTRIBUTION.mdAdded by AutoGineer977 B
- LICENSELicence file added by AutoGineer11.1 KiB
- SKILL.md28.3 KiB
- references/dsl-to-esql-migration.md38.5 KiB
- references/esql-reference.md100.8 KiB
- references/esql-search-strategy.md10.6 KiB
- references/esql-search.md26.3 KiB
- references/esql-version-history.md46.4 KiB
- references/generation-tips.md31.9 KiB
- references/promql-command.md26.0 KiB
- references/query-approximation.md11.2 KiB
- references/query-patterns.md14.7 KiB
- references/time-series-queries.md31.3 KiB
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.numberfromGET /(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: checkbuild_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-SNAPSHOTsuffix 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
| Version | Release | Status | Key Additions |
|---|---|---|---|
| 8.11 | Nov 2023 | Tech Preview | Initial ES|QL release |
| 8.12 | Jan 2024 | Tech Preview | Spatial types, PROFILE |
| 8.13 | Mar 2024 | Tech Preview | Async queries, cross-cluster ENRICH |
| 8.14 | May 2024 | GA | Spatial functions, regex optimization |
| 8.15 | Aug 2024 | GA | Type casting (::), Arrow output |
| 8.16 | Oct 2024 | GA | Per-aggregation WHERE, new math/string functions |
| 8.17 | Dec 2024 | GA | MATCH, QSTR full-text functions |
| 8.18 | Feb 2025 | GA | LOOKUP JOIN (preview), scoring, KQL |
| 8.19 | Apr 2025 | GA | MATCH_PHRASE, FORK, CHANGE_POINT (preview) |
| 9.0 | Feb 2025 | GA | Released with 8.18 features |
| 9.1 | Jun 2025 | GA | Full-text functions GA, FORK (preview) |
| 9.2 | Oct 2025 | GA | Multi-field joins, TS, INLINE STATS (preview), CHANGE_POINT GA, FUSE (preview), RERANK (preview) |
| 9.3 | Jan 2026 | GA | INLINE STATS GA, SET directive (preview), Lucene-pushable JOIN predicates |
| 9.4 | May 2026 | GA | TS/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.5 | Aug 2026 | GA | PROMQL 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
| Command | Introduced | GA | Notes |
|---|---|---|---|
FROM | 8.11 | 8.14 | Source command |
WHERE | 8.11 | 8.14 | Filtering |
EVAL | 8.11 | 8.14 | Computed columns |
STATS ... BY | 8.11 | 8.14 | Aggregations with grouping |
SORT | 8.11 | 8.14 | Ordering results |
LIMIT | 8.11 | 8.14 | Result set size |
KEEP | 8.11 | 8.14 | Column selection |
DROP | 8.11 | 8.14 | Column removal |
RENAME | 8.11 | 8.14 | Column renaming |
DISSECT | 8.11 | 8.14 | Pattern extraction |
GROK | 8.11 | 8.14 | Log parsing |
ENRICH | 8.11 | 8.14 | Data enrichment |
MV_EXPAND | 8.11 | 9.4 | Multi-value expansion (GA) |
SHOW | 8.11 | 8.14 | Metadata display |
ROW | 8.11 | 8.14 | Literal row creation |
LOOKUP JOIN | 8.18/9.0 | 8.19/9.1 | SQL-style LEFT JOIN with lookup indices |
INLINE STATS | 9.2 | 9.3 | Inline aggregations (like window functions) |
FORK | 8.19/9.1 | 9.4 | Multiple execution branches |
FUSE | 9.2 | 9.5 | Combine results from FORK branches (GA in 9.5) |
TS | 9.2 | 9.4 | Time series source command (GA in 9.4) |
PROMQL | 9.4 | 9.5 | Source command using PromQL syntax on TSDS (GA in 9.5) |
METRICS_INFO | 9.4 | 9.4 | TSDS metric catalogue (after TS) |
TS_INFO | 9.4 | 9.4 | Per-(metric, time series) metadata (after TS) |
TS_COLLAPSE | 9.5 | Preview | Collapse PROMQL per-step output into one row per time series |
RERANK | 9.2 | 9.4 | Re-score results with inference (GA in 9.4) |
COMPLETION | 8.19/9.1 | 9.3 | LLM text generation |
SAMPLE | 8.19/9.1 | 9.5 | Random sampling (GA in 9.5) |
CHANGE_POINT | 8.19/9.1 | 9.2 | Spike/dip detection (Platinum license); BY grouping GA in 9.5 |
MMR | 9.4 | Preview | Maximal Marginal Relevance diversification |
URI_PARTS | 9.4 | Preview | Parse URI into structured columns (pipe command) |
USER_AGENT | 9.4 | Preview | Parse user agent into structured columns (pipe command) |
REGISTERED_DOMAIN | 9.4 | Preview | REGISTERED_DOMAIN: extract from hostname (pipe command) |
IP_LOCATION | 9.5 | Preview | Query-time GeoIP enrichment (pipe command); WITH {database_file, properties, first_only} |
| Subqueries in FROM | 9.4 | 9.5 | UNION ALL subqueries; ROW/TS sources added in 9.5 |
LIMIT BY | 9.4 | Preview | Grouped top-N |
DEDUP | Serverless | Preview | Serverless preview; not available on Stack 9.5 — use LIMIT 1 BY cols or STATS BY cols |
| Views | 9.4 | Preview | Virtual indices; 9.5: cross-project, now available on Serverless |
Full-Text Search Functions
| Function | Introduced | GA | Notes |
|---|---|---|---|
MATCH(field, query) | 8.17 | 8.19/9.1 | Basic full-text matching |
QSTR(query_string) | 8.17 | 8.19/9.1 | Query string syntax (Lucene) |
KQL(kql_string) | 8.18/9.0 | 8.19/9.1 | Kibana Query Language |
MATCH_PHRASE(field, phrase) | 8.19/9.1 | 8.19/9.1 | Exact phrase matching |
Match operator (:) | 8.17 | 8.19/9.1 | Shorthand for MATCH |
MATCH on expressions (9.5+ preview):
MATCHand the:operator can also match non-indexed expressions (e.g.EVALcolumns; not afterSTATSor with options on 9.5 Stack, both allowed on Serverless) — row-by-row scan instead of inverted index. Wrapkeywordexpressions inTO_TEXTfor analyzed matching.
Vector and Advanced Search Functions
| Function / option | Introduced | Status | Notes |
|---|---|---|---|
KNN | 9.2 (preview) | GA (9.4) | Dense vector / semantic_text k-nearest neighbours |
TEXT_EMBEDDING | 9.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) |
EMBEDDING | 9.5 (preview) | Preview | Multimodal input via embedding endpoint |
TOP_SNIPPETS | 9.3 (preview) | Preview | 9.5 adds highlight, pre_tag, post_tag, encoder, order, analyzer |
DECAY | 9.3 (preview) | Preview | Distance-based scoring |
Inference timeout option | 9.5 | GA | COMPLETION/RERANK WITH {"timeout": ...}; TEXT_EMBEDDING option |
Scoring support:
METADATA _scoreavailable from 8.18/9.0Must use
SORT _score DESCto rank by relevance
Spatial Functions
| Function | Introduced | Notes |
|---|---|---|
GEO_POINT type | 8.12 | Basic spatial type support |
CARTESIAN_POINT type | 8.12 | Cartesian coordinate support |
ST_INTERSECTS | 8.14 | Geometry intersection test |
ST_CONTAINS | 8.14 | Containment test |
ST_DISJOINT | 8.14 | Disjoint test |
ST_WITHIN | 8.14 | Within test |
ST_X, ST_Y | 8.14 | Coordinate extraction |
ST_DISTANCE | 8.15 | Distance calculation |
ST_EXTENT_AGG | 8.18/9.0 | Bounding box aggregation |
ST_ENVELOPE | 8.18/9.0 | Bounding box for geometry |
ST_DIMENSION | 9.4 | Geometry dimension (0/1/2) |
ST_GEOMETRYTYPE | 9.4 | Geometry type as string |
ST_ISEMPTY | 9.4 | Test if geometry is empty |
ST_BUFFER | 9.4 | Buffer around geometry |
ST_SIMPLIFYPRESERVETOPOLOGY | 9.4 | Simplify geometry preserving topology |
ST_UNION | 9.5 | Preview — union of two geometries |
ST_INTERSECTION | 9.5 | Preview — shared area |
ST_DIFFERENCE | 9.5 | Preview — first minus second |
ST_SYMDIFFERENCE | 9.5 | Preview — area in exactly one |
Date/Time Functions
| Function | Introduced | Notes |
|---|---|---|
NOW() | 8.11 | Current timestamp |
DATE_TRUNC | 8.11 | Truncate to interval |
DATE_EXTRACT | 8.11 | Extract date parts |
DATE_FORMAT | 8.11 | Format dates; honors SET time_zone from 9.4 |
DATE_PARSE | 8.11 | Parse date strings; honors SET time_zone from 9.4 |
DATE_DIFF | 8.13 | Difference between dates |
date_nanos type | 8.17 (preview) | Nanosecond precision timestamps |
TRANGE | 9.3 (preview) | Time range filter on @timestamp |
String Functions
| Function | Introduced | Notes |
|---|---|---|
LEFT, RIGHT | 8.11 | Substring extraction |
SUBSTRING | 8.11 | Position-based extraction |
CONCAT | 8.11 | String concatenation |
TRIM, LTRIM, RTRIM | 8.11 | Whitespace removal |
TO_UPPER, TO_LOWER | 8.13 | Case conversion |
LOCATE | 8.14 | Find substring position |
SPACE | 8.16 | Generate spaces |
REVERSE | 8.16 | Reverse string |
BIT_LENGTH, BYTE_LENGTH | 8.17 | String length in bits/bytes |
STARTS_WITH, ENDS_WITH | 8.11 | Prefix/suffix matching |
CONTAINS | 9.2 | Substring containment check |
JSON_EXTRACT | 9.4 | Extract from JSON string by JSONPath (GA in 9.5) |
FIELD_EXTRACT | 9.5 | Extract sub-field from flattened object as keyword |
Multi-Value Functions
| Function | Introduced | Notes |
|---|---|---|
MV_COUNT | 8.11 | Count values |
MV_CONCAT | 8.11 | Join values |
MV_FIRST, MV_LAST | 8.13 | First/last value |
MV_MIN, MV_MAX | 8.11 | Min/max value |
MV_SUM, MV_AVG | 8.11 | Sum/average |
MV_MEDIAN | 8.11 | Median value |
MV_SORT | 8.14 | Sort multi-values |
MV_SLICE | 8.14 | Slice multi-values |
MV_PERCENTILE | 8.16 | Percentile calculation |
MV_PSERIES_WEIGHTED_SUM | 8.16 | Weighted sum |
MV_DIFFERENCE | 9.4 | Set difference of two MV fields |
MV_UNION | 9.4 | Set union of two MV fields |
MV_INTERSECTION | 9.4 | Set intersection of two MV fields |
MV_INTERSECTS | 9.4 | True if MV fields share a value |
MV_CONTAINS | 9.4 | True if first MV contains all of second |
Aggregation Functions
| Function | Introduced | Notes |
|---|---|---|
COUNT, COUNT_DISTINCT | 8.11 | Counting |
SUM, AVG | 8.11 | Basic aggregations |
MIN, MAX | 8.11 | Extended to strings/IPs in 8.16 |
MEDIAN, MEDIAN_ABSOLUTE_DEVIATION | 8.11 | Statistical |
PERCENTILE | 8.11 | Percentile calculation |
TOP | 8.15 | Top N values |
VALUES | 8.14 | Unique values (GA in 9.4) |
ST_EXTENT_AGG | 8.18/9.0 | Spatial bounding box |
WEIGHTED_AVG | 8.16 | Weighted average |
STD_DEV | 8.18/9.0 | Standard deviation |
VARIANCE | 8.18/9.0 | Variance |
FIRST(value, sort_field) | 9.4 | Value from row with earliest sort field |
LAST(value, sort_field) | 9.4 | Value from row with latest sort field |
EARLIEST(@timestamp) | 9.4 | Min @timestamp (1-arg shorthand) |
LATEST(@timestamp) | 9.4 | Max @timestamp (1-arg shorthand) |
SPARKLINE | 9.5 | Preview — per-bucket values for a sparkline |
Grouping Functions
| Function | Introduced | Notes |
|---|---|---|
BUCKET | 8.11 | Numeric/date bucketing in BY clause |
CATEGORIZE | 8.18/9.0 | Auto-categorization of text in BY clause |
TBUCKET | 9.2 | Time bucketing from @timestamp; preferred in TS (GA in 9.4) |
WITHOUT | 9.4 | Group 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.nameIP Functions
| Function | Introduced | Notes |
|---|---|---|
CIDR_MATCH | 8.11 | Check IP against CIDR ranges |
IP_PREFIX | 8.14 | Extract network prefix from IP |
TO_IP | 8.11 | Convert 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.
| Function | Introduced | Status | Notes |
|---|---|---|---|
RATE | 9.2 (preview) | GA (9.4) | Per-second rate of counter increase |
IRATE | 9.2 (preview) | GA (9.4) | Instant rate (last two data points) |
INCREASE | 9.2 (preview) | GA (9.4) | Absolute counter increase in window |
DELTA | 9.2 (preview) | GA (9.4) | Absolute change of a gauge |
IDELTA | 9.2 (preview) | GA (9.4) | Change between last two data points |
AVG_OVER_TIME | 9.2 (preview) | GA (9.4) | Average value over time |
SUM_OVER_TIME | 9.2 (preview) | GA (9.4) | Sum of values over time |
MIN_OVER_TIME | 9.2 (preview) | GA (9.4) | Minimum value over time |
MAX_OVER_TIME | 9.2 (preview) | GA (9.4) | Maximum value over time |
FIRST_OVER_TIME | 9.2 (preview) | GA (9.4) | Earliest value by @timestamp |
LAST_OVER_TIME | 9.2 (preview) | GA (9.4) | Latest value by @timestamp (implicit default for numeric/gauge) |
COUNT_OVER_TIME | 9.2 (preview) | GA (9.4) | Count of values over time |
COUNT_DISTINCT_OVER_TIME | 9.2 (preview) | GA (9.4) | Count of distinct values over time |
PRESENT_OVER_TIME | 9.2 (preview) | GA (9.4) | true if field has values in window |
ABSENT_OVER_TIME | 9.2 (preview) | GA (9.4) | true if field has no values in window |
DERIV | 9.3 (preview) | GA (9.4) | Derivative via linear regression |
PERCENTILE_OVER_TIME | 9.3 (preview) | GA (9.4) | Percentile of values over time |
STDDEV_OVER_TIME | 9.3 (preview) | GA (9.4) | Population standard deviation over time |
VARIANCE_OVER_TIME | 9.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
TBUCKETinterval in theBYclause; 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
TBUCKETinterval. 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
| Function | Introduced | Notes |
|---|---|---|
CLAMP | 9.3 (preview) | Clamp values to [min, max] range |
CLAMP_MIN | 9.3 (preview) | Set lower bound for values |
CLAMP_MAX | 9.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.
| Syntax | Introduced | Status | Notes |
|---|---|---|---|
WHERE field IN (FROM ... | KEEP ...) | 9.5 | Preview | Filter by matching a subquery result set |
WHERE field NOT IN (FROM ... | KEEP ...) | 9.5 | Preview | Exclude 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 / Type | Introduced | Status | Notes |
|---|---|---|---|
date_range type | 9.5 | Preview | Time range with start/end bounds |
TO_DATE_RANGE(string) | 9.5 | Preview | Parse "from..to" string into date_range |
TO_RANGE(from, to) | 9.5 | Preview | Construct date_range from two dates |
RANGE_INTERSECTS(a, b) | 9.5 | Preview | Check if two ranges (or range + date) overlap |
RANGE_WITHIN(a, b) | 9.5 | Preview | Check if range a is within range b |
RANGE_CONTAINS(a, b) | 9.5 | Preview | Check if range a contains range b |
range_min(field) | 9.5 | Preview | Extract lower bound of a date_range |
range_max(field) | 9.5 | Preview | Extract upper bound of a date_range |
Type Casting
| Syntax | Introduced | Notes |
|---|---|---|
TO_STRING(x) | 8.11 | Function-based casting |
TO_INTEGER(x) | 8.11 | Function-based casting |
TO_DOUBLE(x) | 8.11 | Function-based casting |
x::string | 8.15 | Operator-based casting |
x::integer | 8.15 | Operator-based casting |
TO_COUNTER(x) / x::counter | 9.5 (GA) | Treat a numeric metric as a counter in TS (e.g. RATE) |
TO_GAUGE(x) / x::gauge | 9.5 (GA) | Treat a metric as a gauge in TS |
TO_TEXT(x) | 9.5 | Preview — 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
WHEREto filter to relevant subsetUse
STATSto aggregate at query timeFor exports, use Search API with
search_afterinstead
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 DESCRemaining limitations (all versions):
No per-function timezone argument —
DATE_TRUNC(1 hour, @timestamp, "America/New_York")does not workDATE_FORMATandDATE_PARSEdo not accept timezone parameters directly; useSET time_zoneinstead
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 hourNested 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 inKEEP,WHERE, orEVALfails withUnknown column(withSET unmapped_fields = "load"the subfield isnull)Must flatten data at index time for ES|QL access
Unsupported Field Types
These field types are not supported or have limitations:
| Type | Status |
|---|---|
nested | Not supported — omitted from results (not null); referencing it fails with Unknown column |
flattened | Tech 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) |
join | Not supported |
date_range | Tech Preview (9.5) — supported via date_range type and range functions |
binary | Not supported |
completion | Not supported |
rank_feature | Not supported |
histogram | Supported 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: lookupsettingLookup 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_MATCHin join conditionsFurther 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 DESCWHERE 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 DESCNot 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
| Feature | Version | Notes |
|---|---|---|
| Basic CCS | 8.13 | Query remote clusters |
| Cross-cluster ENRICH | 8.13 | Enrich with remote data |
| Cross-cluster LOOKUP JOIN | 9.2 | Join with remote lookup indices |
skip_unavailable | 8.17 | Graceful handling of unavailable clusters |
Output Formats
| Format | Version | Notes |
|---|---|---|
| JSON | 8.11 | Default format |
| CSV | 8.11 | Tabular output |
| TSV | 8.11 | Tab-separated |
| Arrow | 8.15 | Apache Arrow IPC format |
| MD | 9.6 | Markdown pipe table (GitHub-flavored); Serverless; Stack 9.6+ |
API Endpoints
| Endpoint | Version | Notes |
|---|---|---|
POST /_query | 8.11 | Synchronous query |
POST /_query/async | 8.13 | Async query submission |
GET /_query/async/{id} | 8.13 | Get async query results |
DELETE /_query/async/{id} | 8.13 | Cancel async query |
PUT /_query/view/{name} | 9.4 | Create/update view |
GET /_query/view/{name} | 9.4 | Get view definition |
DELETE /_query/view/{name} | 9.4 | Delete view |
PUT /_query/data_source/{name} | 9.5 | Data source config (experimental, not production) |
PUT /_query/dataset/{name} | 9.5 | Dataset config (experimental, not production) |
Performance Tips by Version
8.14+
Regex patterns are optimized
Enrich supports text fields
8.15+
Use
::casting instead ofTO_*functions (cleaner syntax)Arrow format for analytics tool integration
8.17+
Use
MATCH/QSTRinstead ofLIKE/RLIKEfor text search (50-1000x faster)Full-text functions use Lucene optimizations
9.1+
Use
INLINE STATSto avoid multiple queriesFull-text functions are GA and stable
9.2+
Use
TSwithRATE,AVG_OVER_TIME, etc. for time series metrics aggregations (preview in 9.2-9.3, GA in 9.4)Use
TBUCKETfor time bucketing in TS queries (GA in 9.4)Multi-field
LOOKUP JOINfor complex correlationsFUSEfor hybrid search scoring
9.3+
Use
TRANGEinstead of manualWHERE @timestampfiltersSliding window parameter for time series functions (e.g.
RATE(field, 10m)); in 9.2-9.3 the window must be a multiple of theTBUCKETinterval, this restriction is lifted in 9.4CLAMP,CLAMP_MIN,CLAMP_MAXfor bounding metric valuesHistogram metrics in
TS(preview): queryexponential_histogram/tdigestwith standard aggregations; cast plainhistogramwith::exponential_histogramor::tdigest— see Histogram Metrics
9.4+
TS,TBUCKET, and all time series aggregation functions are GA — safe for production metrics queriesHistogram metrics in
TSare GA (exponential_histogram/tdigest; plainhistogramstill requires a cast)Use
METRICS_INFO/TS_INFOto discover TSDS schemas instead of inspecting mappings or field capabilitiesUse
WITHOUT(dim, ...)to group by all dimensions except specific ones — avoids enumerating every dimension manuallySliding window accepts arbitrary durations —
RATE(field, 7m)withTBUCKET(5 minute)now worksUse
SET time_zonewith IANA strings for timezone-aware queries instead of manualEVALoffset arithmeticUse
SET approximation = trueto approximate largeSTATSsummaries via sampling/extrapolation (preview in 9.4, GA in 9.5+ and Serverless) — returns estimates with confidence intervals; see query-approximation.mdUse
SET unmapped_fields = "load"to query fields missing from some indices without errorsUse
FIRST/LAST(orEARLIEST/LATEST) instead ofSORT+LIMIT 1for grouped first/last-value queriesUse
PROMQLwhen porting Prometheus dashboards/alerts; otherwise preferTSfor 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_fieldsis GA — production-ready for schema-on-read queries (forSET approximation, GA in 9.5, see the 9.4+ tips above)JSON_EXTRACTis GA — combine withSET unmapped_fields = "load"for a complete schema-on-read workflowSubqueries in
FROMare GA — useROWandTSas subquery source commands in addition toFROMUse
WHERE field IN (FROM lookup_index | KEEP field)(preview) to filter against reference datasets without joinsUse
IP_LOCATION(preview) for query-time GeoIP enrichment without ingest pipeline configuration (before 9.5, no query-time GeoIP: useENRICHor the ingestgeoipprocessor)Use
EMBEDDING(value, inference_id)(preview) to generate dense vector embeddings from multimodal input (text, image, audio, video, PDF) via inference endpointsFlattened fields (preview) load as a
flattenedroot column; read sub-keys only withFIELD_EXTRACT(root, "dotted.key")and cast thekeywordresult for numeric use —JSON_EXTRACTon_sourceremains the pre-9.5 workaroundUse
CHANGE_POINT value ON key BY group(GA) for per-series change detection instead of one query per groupUse
SPARKLINE(agg, @timestamp, buckets, from, to)(preview) for compact per-group trend arraysUse
x::counter/x::gauge(GA) insideTSwhen a metric's mapping lacks the righttime_series_metric
Patch-release behavior changes:
9.5.1 —
PROMQLworks ondate_nanostime series indices9.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.
Download ZIP
The ZIP holds the skill’s files, its licence files and an ATTRIBUTION.md. Download elasticsearch-esql.zip