cheatsheets/elasticsearch

Elasticsearch Cheat Sheet

Elasticsearch is a search and analytics engine you talk to over HTTP with JSON. This sheet covers the Query DSL, aggregations, KQL and the REST APIs for indexes, documents and cluster health, with one orders index used in every example.

Updated Oct 12, 2026Tested against Elasticsearch 9.5 and 8.19

Also works with OpenSearch.

Cluster and Cat APIs

Requests on this page use Kibana Dev Tools console form. The Running Queries with curl section shows how to send the same request from a shell.

EndpointWhat it does
Cluster name and version ()
Status green, yellow or red, plus shard counts
Health of one index
One-line health summary with column headers
List all indexes with docs count and size
Only indexes that are not fully replicated
Document count for one index
List aliases and the indexes behind them
Nodes with roles, heap and CPU
Every shard and why it is unassigned
Disk used per node, shards per node
Explain the first unassigned shard
Retry shards that failed allocation too often

A single-node cluster reports yellow because replica shards have no second node to live on.

Example: confirm the unassigned replicas on , then set its replica count to zero.

console
GET _cat/shards/orders?v&h=index,shard,prirep,state,unassigned.reason

PUT /orders/_settings
{
  "index": { "number_of_replicas": 0 }
}

In 1bench: the Elasticsearch GUI has an Overview tab with cluster status, each node's heap, CPU and RAM, and unassigned shards, so you are not stitching together five calls.

Index Management

EndpointWhat it does
Create an index, with optional settings and mappings
Show field types
Mapping of a single field
Add new fields (existing field types cannot change)
Show shards, replicas, refresh interval
Change dynamic settings such as
Make recent writes searchable now
/ Take an index offline and back
Copy documents into a new index, e.g. to change a mapping
Copy an index as is (block writes first)
Point an alias at a new index in one atomic step
Settings and mappings applied to new matching indexes
Lifecycle policy: rollover, shrink, delete by age
Which lifecycle phase each index is in
DestructiveDelete the index and every document in it

There is no rename API. Reindex or clone into the new name, then switch an alias. is the composable template API; the legacy endpoint is deprecated.

In this mapping, is text for search with a keyword sub-field for sorting and aggregations, and is nested so each line item matches on its own.

Example: create the index used on the rest of this page.

console
PUT /orders
{
  "settings": { "number_of_shards": 1, "number_of_replicas": 1 },
  "mappings": {
    "properties": {
      "order_id":   { "type": "keyword" },
      "customer":   { "type": "text", "fields": { "keyword": { "type": "keyword" } } },
      "status":     { "type": "keyword" },
      "total":      { "type": "scaled_float", "scaling_factor": 100 },
      "created_at": { "type": "date" },
      "shipped_at": { "type": "date" },
      "items": {
        "type": "nested",
        "properties": {
          "sku": { "type": "keyword" },
          "qty": { "type": "integer" }
        }
      }
    }
  }
}

To change a field type later, create with the new mapping, copy the data, and move an alias that your app reads from.

console
POST /_reindex
{
  "source": { "index": "orders" },
  "dest":   { "index": "orders_v2" }
}

POST /_aliases
{
  "actions": [
    { "remove": { "index": "orders",    "alias": "orders_current" } },
    { "add":    { "index": "orders_v2", "alias": "orders_current" } }
  ]
}

Documents

EndpointWhat it does
Index a document with your own ID (replaces if it exists)
Index a document with a generated ID
Index only if the ID is new, else 409
Get a document by ID, with metadata
Get only the document body
Get several documents by ID in one call
Partial update with , or a
with Update, or create if missing
DestructiveDelete one document by ID
Many index, create, update and delete actions in one request
Update every document matching a query
DestructiveDelete every document matching a query
Count documents, optionally with a

Each bulk line must be a single line of JSON, and the body must end with a newline.

Example: bulk-load two orders, mark one shipped, then cancel pending orders older than 30 days.

console
POST /_bulk
{ "index": { "_index": "orders", "_id": "1001" } }
{ "order_id": "1001", "customer": "Acme Corp", "status": "paid", "total": 249.90, "created_at": "2026-10-01T09:12:00Z", "items": [ { "sku": "SKU-42", "qty": 2 } ] }
{ "index": { "_index": "orders", "_id": "1002" } }
{ "order_id": "1002", "customer": "Globex", "status": "pending", "total": 89.00, "created_at": "2026-08-14T16:40:00Z", "items": [ { "sku": "SKU-7", "qty": 1 } ] }

POST /orders/_update/1001
{
  "doc": { "status": "shipped", "shipped_at": "2026-10-03T11:00:00Z" }
}

POST /orders/_update_by_query?conflicts=proceed
{
  "query": {
    "bool": {
      "filter": [
        { "term":  { "status": "pending" } },
        { "range": { "created_at": { "lt": "now-30d/d" } } }
      ]
    }
  },
  "script": {
    "source": "ctx._source.status = params.status",
    "params": { "status": "cancelled" }
  }
}

In 1bench: the Elasticsearch GUI lets you edit documents in a table, stage several changes and save them in one request with version checks, instead of writing an call per document.

Query DSL

Every search goes to with a object. Full-text queries analyze your input; term-level queries match the stored value exactly.

TaskQuery
All documents
Full-text search on a text field
Words in this exact order
One search across several fields, boosts one
Exact value (SQL )
Any of several values (SQL )
Numbers or dates between bounds (, , , )
Field has a value (SQL )
Field is null or missing (SQL )
Not equal (SQL )
Pattern match (SQL ) on a keyword field
Starts with
Regular expression
Typo tolerance
Look up by document IDs
Lucene syntax in one string
Match inside a nested array object, see the example below
Compare two fields in the same document
Vector similarity on a fieldtop-level option, or the query

Elasticsearch 9.0 removed the old , , and range parameters and the endpoint. Use / and inside .

Each query object above goes in the field of a search request. This one uses date math for the last seven full days in Berlin time:

console
GET /orders/_search
{
  "query": {
    "range": {
      "created_at": { "gte": "now-7d/d", "lt": "now/d", "time_zone": "Europe/Berlin" }
    }
  }
}

A plain on and would match across different items; keeps them together.

Example: orders that contain at least two units of SKU-42 in the same line item.

console
GET /orders/_search
{
  "query": {
    "nested": {
      "path": "items",
      "query": {
        "bool": {
          "filter": [
            { "term":  { "items.sku": "SKU-42" } },
            { "range": { "items.qty": { "gte": 2 } } }
          ]
        }
      }
    }
  }
}

In 1bench: the Elasticsearch GUI has a visual query builder with field pickers and type-aware filters, and the DSL it generates opens in a REST console you can edit.

Bool Queries

ClauseQuery
Must match, adds to score
Must match, no scoring, cacheable
Should match, raises score
Must not match, no scoring
Require N of the should clauses
Weight one clause higher

Put exact conditions (status, dates, IDs) in and keep for text you want ranked. With no or , at least one clause has to match.

console
GET /orders/_search
{
  "query": {
    "bool": {
      "should": [
        { "term": { "status": "paid" } },
        { "term": { "status": "shipped" } },
        { "range": { "total": { "gte": 200 } } }
      ],
      "minimum_should_match": 2
    }
  }
}

Example: Acme orders over 100 from this year, excluding cancelled ones, with shipped orders ranked first.

console
GET /orders/_search
{
  "query": {
    "bool": {
      "must": [
        { "match": { "customer": "acme" } }
      ],
      "filter": [
        { "range": { "total": { "gt": 100 } } },
        { "range": { "created_at": { "gte": "now/y" } } }
      ],
      "must_not": [
        { "term": { "status": "cancelled" } }
      ],
      "should": [
        { "term": { "status": { "value": "shipped", "boost": 2 } } }
      ]
    }
  }
}

Aggregations

Set when you only want the buckets, not the hits.

TaskAggregation
Group by field, count per value (SQL )
Count distinct values (approximate)
Sum, average, min, max, , ,
All basic stats at once
Buckets per day, week or month with
Buckets per numeric step with
Custom numeric ranges
Latest document per group inside
Page through every bucket with
Sort buckets by a metric on
One document per value in the hits (search option, not an aggregation)

returns the top 10 buckets by default. Raise , or use to page through all of them.

console
GET /orders/_search
{
  "size": 0,
  "aggs": {
    "by_customer": {
      "terms": { "field": "customer.keyword", "size": 5 },
      "aggs": {
        "latest_order": {
          "top_hits": {
            "size": 1,
            "sort": [ { "created_at": "desc" } ],
            "_source": ["order_id", "total", "status"]
          }
        }
      }
    }
  }
}

Example: monthly revenue for the past year, with the top three customers by spend inside each month.

console
GET /orders/_search
{
  "size": 0,
  "query": { "range": { "created_at": { "gte": "now-1y/M" } } },
  "aggs": {
    "by_month": {
      "date_histogram": { "field": "created_at", "calendar_interval": "month" },
      "aggs": {
        "revenue": { "sum": { "field": "total" } },
        "top_customers": {
          "terms": {
            "field": "customer.keyword",
            "size": 3,
            "order": { "spend": "desc" }
          },
          "aggs": { "spend": { "sum": { "field": "total" } } }
        }
      }
    }
  }
}

Sorting and Pagination

TaskQuery
Sort by a field (SQL )
Sort by relevance, then date
Sort on a text fieldsort on its sub-field
Page with offset (SQL / ) and
Exact total hit count past 10,000
Return only some fields
Return no source, only listed fields
Trim the response itself
Highlight matches in text
Deep pagination past 10,000 with a point in time
Open and close a point in time,

cannot go past 10,000 (). Past that, use .

console
GET /orders/_search
{
  "from": 20,
  "size": 10,
  "_source": ["order_id", "customer", "total"],
  "sort": [ { "created_at": "desc" } ]
}

Open a point in time, search without an index in the path, then pass the last hit's values as for the next page. Each response returns the current to use next.

Example: walk through every order newest first with a point in time and .

console
POST /orders/_pit?keep_alive=1m

GET /_search
{
  "size": 1000,
  "pit": { "id": "<pit_id from the previous response>", "keep_alive": "1m" },
  "sort": [ { "created_at": "desc" } ],
  "search_after": [1790845920000, 4294967298]
}

DELETE /_pit
{
  "id": "<pit_id>"
}

KQL

KQL (Kibana Query Language) is what you type in the Kibana search bar. It only filters: no aggregations, no sorting. Text fields are analyzed; keyword, numeric and date values must match exactly.

TaskKQL
Field equals value
Exact phrase
Any of several values
Range
Date range with date math
Not
Field exists
Field missing
Wildcard
Nested field
Grouping

, and are not case-sensitive. KQL has no fuzzy or regex search; switch the search bar to Lucene syntax for or .

Example: paid orders over 100 from the last 30 days that have not shipped yet.

text
status: paid and total > 100 and created_at >= now-30d and not shipped_at: *

Running Queries with curl

Kibana Dev Tools takes and a JSON body. From a shell, the same request becomes a curl call to port 9200. Since 8.0, security is on by default: HTTPS with a generated CA and the user.

TaskRequest
Dev Tools request followed by the JSON body
curl with username and password
curl with an API key
Send a JSON body
Send a bulk file
Readable output
Quick Lucene search in the URL
ES|QL query with

Example: fetch five paid orders in Dev Tools, then send the same request from a shell with curl.

console
GET /orders/_search
{
  "query": { "term": { "status": "paid" } },
  "size": 5
}
bash
curl -s -u "elastic:$ELASTIC_PASSWORD" --cacert http_ca.crt \
  -H 'Content-Type: application/json' \
  -X GET 'https://localhost:9200/orders/_search?pretty' \
  -d '{
    "query": { "term": { "status": "paid" } },
    "size": 5
  }'

ES|QL answers a related question, top customers by paid revenue, and returns rows and columns instead of hits:

console
POST /_query?format=txt
{
  "query": "FROM orders | WHERE status == \"paid\" | STATS revenue = SUM(total) BY customer.keyword | SORT revenue DESC | LIMIT 10"
}

OpenSearch Compatibility

OpenSearch forked from Elasticsearch 7.10.2. Most of this page carries over; these are the parts that do not.

ElasticsearchOpenSearch
Query DSL, bool, aggregations, , , Same syntax
KQL in KibanaDQL in OpenSearch Dashboards, near-identical syntax
ES|QL ()Not available; PPL and SQL instead
ILM ()ISM ()
and and the k-NN plugin's query
Official Elasticsearch clientsRefuse to connect; use the OpenSearch clients

Gotchas

  • on a text field usually finds nothing. Text is stored as lowercase tokens, so misses. Use on , or on .
  • stops at 10,000, and stops counting at 10,000 with . Use for deep pages and or for exact totals.
  • You cannot change a field's type once it is mapped. Add a new field or reindex into a new index behind an alias.
  • New writes show up in search only after a refresh (every second by default). sees them right away; may not.
  • and cannot be undone without a snapshot. Wildcard deletes such as fail by default because is true, so do not turn it off to save typing.
  • bucket counts and are approximate on multi-shard indexes. Check before reporting exact numbers.

More on Elasticsearch

Work with Elasticsearch without memorizing it

1bench is a desktop GUI for Elasticsearch. Connect, browse your data and run queries in one window, with the query DSL still there when you want it.

Open Elasticsearch in 1bench

Elasticsearch FAQ

What is the difference between text and keyword in Elasticsearch?
A text field is analyzed into lowercase tokens for full-text search with match. A keyword field stores the exact value for term filters, sorting and aggregations. Dynamic mapping gives every string both, as field and field.keyword, and the keyword copy skips values longer than 256 characters (ignore_above). For case-insensitive exact matching, give the keyword field a normalizer with the lowercase filter.
How do I find documents where a field is null or missing?
Wrap an exists query in bool must_not: { "bool": { "must_not": { "exists": { "field": "shipped_at" } } } }. Elasticsearch does not index null or an empty array, so both count as missing, while an empty string counts as present. If you need to search for null explicitly, set null_value in the mapping so nulls are indexed as a placeholder value.
How does should work in an Elasticsearch bool query?
With no must or filter clause, at least one should clause has to match. Once a must or filter clause is present, should clauses become optional and only raise the score. Set minimum_should_match (a number like 2, or a percentage like "75%") to require matches either way. Inside filter context nothing is scored, so should only decides which documents match.
Should I use search_after or scroll in Elasticsearch?
Use search_after with a point in time (PIT). Elastic no longer recommends scroll for deep pagination. A PIT keeps a consistent view of the index while you page, and search_after has no 10,000 hit limit. Scroll still works for one-off exports, but each scroll context holds resources on the cluster until it expires or you clear it.
Is the Elasticsearch Query DSL the same in OpenSearch?
Mostly. OpenSearch forked from Elasticsearch 7.10.2, so match, term, range, bool, aggregations, _bulk, _reindex and the _cat APIs carry over unchanged. Features Elastic added later do not: ES|QL, the 8.x security defaults and the newer kNN syntax. Index lifecycle (ISM instead of ILM), point in time and security APIs use different endpoints in OpenSearch.