elasticsearch-esql
Original:🇺🇸 English
Translated
1 scriptsChecked / no sensitive code detected
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.
2installs
Sourceelastic/agent-skills
Added on
NPX Install
npx skill4agent add elastic/agent-skills elasticsearch-esqlTags
Translated version includes tags in frontmatterSKILL.md Content
View Translation Comparison →Elasticsearch ES|QL
Execute ES|QL queries against Elasticsearch.
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 nPrerequisite: ES|QL requiresto be enabled on queried indices. Indices with_sourcedisabled (e.g.,_source) will cause ES|QL queries to fail."_source": { "enabled": false }Version Compatibility: ES|QL was introduced in 8.11 (tech preview) and became GA in 8.14. Features like(8.18+),LOOKUP JOIN(8.17+), andMATCH(9.2+) were added in later versions. On pre-8.18 clusters, useINLINE STATSas a fallback forENRICH(see generation tips).LOOKUP JOINand counter-fieldINLINE STATShave no fallback before 9.2. Check references/esql-version-history.md for feature availability by version.RATE()Cluster Detection: Use theresponse to determine the cluster type and version:GET /
— Elastic Cloud Serverless. Reports versionbuild_flavor: "serverless"but supports all latest ES|QL features. Ignore the version number and assume all GA and preview features are available.8.11.0 — Self-managed or Elastic Cloud Hosted. Usebuild_flavor: "default"for feature availability.version.number- Snapshot builds have
likeversion.number. Strip the9.4.0-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.-SNAPSHOT
Environment Configuration
Elasticsearch connection is configured via environment variables. Run to verify the
connection. If the test fails, suggest these setup options to the user, then stop. Do not try to explore further until a
successful connection test.
node scripts/esql.js testOption 1: Elastic Cloud (recommended for production)
bash
export ELASTICSEARCH_CLOUD_ID="deployment-name:base64encodedcloudid"
export ELASTICSEARCH_API_KEY="base64encodedapikey"Option 2: Direct URL with API Key
bash
export ELASTICSEARCH_URL="https://elasticsearch:9200"
export ELASTICSEARCH_API_KEY="base64encodedapikey"Option 3: Basic Authentication
bash
export ELASTICSEARCH_URL="https://elasticsearch:9200"
export ELASTICSEARCH_USERNAME="elastic"
export ELASTICSEARCH_PASSWORD="changeme"Option 4: Local Development with start-local
For local development and testing, use start-local to quickly spin up
Elasticsearch and Kibana using Docker or Podman:
bash
curl -fsSL https://elastic.co/start-local | shAfter installation completes, Elasticsearch runs at and Kibana at . The
script generates a random password for the user and an API key, both stored in the file inside the
created folder.
http://localhost:9200http://localhost:5601elastic.envelastic-start-localTo configure the environment variables for this skill, source the file and export the connection settings:
.envbash
source elastic-start-local/.env
export ELASTICSEARCH_URL="$ES_LOCAL_URL"
export ELASTICSEARCH_API_KEY="$ES_LOCAL_API_KEY"Then run to verify the connection.
node scripts/esql.js testOptional: Skip TLS verification (development only)
bash
export ELASTICSEARCH_INSECURE="true"Usage
Get Index Information (for schema discovery)
bash
node scripts/esql.js indices # List all indices
node scripts/esql.js indices "logs-*" # List matching indices
node scripts/esql.js schema "logs-2024.01.01" # Get field mappings for an indexExecute Raw ES|QL
bash
node scripts/esql.js raw "FROM logs-* | STATS count = COUNT(*) BY host.name | SORT count DESC | LIMIT 5"Execute with TSV Output
bash
node scripts/esql.js raw "FROM logs-* | STATS count = COUNT(*) BY component | SORT count DESC" --tsvTSV Output Options:
- or
--tsv: Output as tab-separated values (clean, no decorations)-t - : Omit the header row
--no-header
Test Connection
bash
node scripts/esql.js testGuidelines
-
Detect deployment type: Always runfirst. This detects whether the deployment is a Serverless project (all features available) or a versioned cluster (features depend on version). The
node scripts/esql.js testfield frombuild_flavoris the authoritative signal — if it equalsGET /, ignore the reported version number and use all ES|QL features freely."serverless" -
Discover schema (required — never guess index or field names):bash
node scripts/esql.js indices "pattern*" node scripts/esql.js schema "index-name"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, orlogs-app-*. Field names may use ECS dotted notation (application_logs,source.ip) or flat custom names — the only way to know is to check.service.nameThecommand reports the index mode. If it showsschema, the output includes the data stream name and copy-pasteable TS syntax — useIndex mode: time_series(notTS <data-stream>),FROM(notTBUCKET(interval)), and wrap counter fields withDATE_TRUNC. Read the full TS section in Generation Tips before writing any time series query. You can also check the index mode directly via the Elasticsearch index settings API:SUM(RATE(...))bashcurl -s "$ELASTICSEARCH_URL/<index-name>/_settings/index.mode" -H "Authorization: ApiKey $ELASTICSEARCH_API_KEY" -
Choose the right ES|QL feature for the task: Before writing queries, match the user's intent to the most appropriate ES|QL feature. Prefer 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" → or
STATS ... BY BUCKET(@timestamp, interval)for TSDBTS - "search," "find documents matching" → ,
MATCH,QSTRKQL - "count," "average," "breakdown" → with aggregation functions
STATS
- "find patterns," "categorize," "group similar messages" →
-
Read the references before generating queries:
- 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
- ES|QL Complete Reference - full syntax for all commands and functions
- ES|QL Search Reference — for full-text search queries (MATCH, QSTR, KQL, scoring)
-
Generate the query following ES|QL syntax:
- Start with (or
FROM index-patternfor time series indices)TS index-pattern - Add for filtering (use
WHEREfor time ranges on 9.3+)TRANGE - Use for computed fields
EVAL - Use for aggregations
STATS ... BY - For time series metrics: with
TSfor counters,SUM(RATE(...))for gauges, andAVG(...)for time bucketing — see the TS section in Generation Tips for the three critical syntax rulesTBUCKET(interval) - For detecting spikes, dips, or anomalies, use after time-bucketed aggregation
CHANGE_POINT - Add and
SORTas neededLIMIT
- Start with
-
Execute with TSV flag:bash
node scripts/esql.js raw "FROM index | STATS count = COUNT(*) BY field" --tsv
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
esql
FROM index-pattern
| WHERE condition
| EVAL new_field = expression
| STATS aggregation BY grouping
| SORT field DESC
| LIMIT nCommon Patterns
Filter and limit:
esql
FROM logs-*
| WHERE @timestamp > NOW() - 24 hours AND level == "error"
| SORT @timestamp DESC
| LIMIT 100Aggregate by time:
esql
FROM metrics-*
| WHERE @timestamp > NOW() - 7 days
| STATS avg_cpu = AVG(cpu.percent) BY bucket = DATE_TRUNC(1 hour, @timestamp)
| SORT bucket DESCTop N with count:
esql
FROM web-logs
| STATS count = COUNT(*) BY response.status_code
| SORT count DESC
| LIMIT 10Text search: Use , , or for full-text search instead of / — they are significantly
faster and support relevance scoring. See ES|QL Search Reference for the full guide.
MATCHQSTRKQLLIKERLIKEesql
FROM documents METADATA _score
| WHERE MATCH(content, "search terms")
| SORT _score DESC
| LIMIT 20Log categorization (Platinum license): Use to auto-cluster log messages into pattern groups. Prefer
this over running multiple queries when exploring or finding patterns in unstructured text.
CATEGORIZESTATS ... BY fieldesql
FROM logs-*
| WHERE @timestamp > NOW() - 24 hours
| STATS count = COUNT(*) BY category = CATEGORIZE(message)
| SORT count DESC
| LIMIT 20Change point detection (Platinum license): Use to detect spikes, dips, and trend shifts in a metric
series. Prefer this over manual inspection of time-bucketed counts.
CHANGE_POINTesql
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:
esql
// 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 bucketData enrichment with LOOKUP JOIN: Use when the join key has a different name in the source vs the lookup
index. Use multiple fields in when the lookup table has a composite key.
RENAMEONesql
// Field name mismatch — RENAME before joining
FROM support_tickets
| RENAME product AS product_name
| LOOKUP JOIN knowledge_base ON product_name
// 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 to check if a multivalue field contains a specific value. Use
to count values.
MV_CONTAINSMV_COUNTesql
// 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, , then .
SORTCHANGE_POINTesql
FROM logs-*
| STATS error_count = COUNT(*) BY bucket = DATE_TRUNC(1 hour, @timestamp)
| SORT bucket
| CHANGE_POINT error_count ON bucket AS type, pvalueFull Reference
For complete ES|QL syntax including all commands, functions, and operators, read:
- ES|QL Complete Reference
- ES|QL Search Reference - Full-text search: MATCH, QSTR, KQL, MATCH_PHRASE, scoring, semantic search
- 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
- DSL to ES|QL Migration - Convert Query DSL to ES|QL
Error Handling
When query execution fails, the script returns:
- The generated ES|QL query
- The error message from Elasticsearch
- Suggestions for common issues
Common issues:
- Field doesn't exist → Check schema with
node scripts/esql.js schema "index" - Type mismatch → Use type conversion functions (TO_STRING, TO_INTEGER, etc.)
- Syntax error → Review ES|QL reference for correct syntax
- No results → Check time range and filter conditions
Examples
bash
# Schema discovery
node scripts/esql.js test
node scripts/esql.js indices "logs-*"
node scripts/esql.js schema "logs-2024.01.01"
# Execute queries
node scripts/esql.js raw "FROM logs-* | STATS count = COUNT(*) BY host.name | LIMIT 10"
node scripts/esql.js raw "FROM metrics-* | STATS avg = AVG(cpu.percent) BY hour = DATE_TRUNC(1 hour, @timestamp)" --tsv