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.
| Endpoint | What 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.
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
| Endpoint | What 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.
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.
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
| Endpoint | What 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.
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.
| Task | Query |
|---|---|
| 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 field | top-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:
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.
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
| Clause | Query |
|---|---|
| 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.
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.
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.
| Task | Aggregation |
|---|---|
| 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.
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.
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
| Task | Query |
|---|---|
| Sort by a field (SQL ) | |
| Sort by relevance, then date | |
| Sort on a text field | sort 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 .
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 .
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.
| Task | KQL |
|---|---|
| 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.
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.
| Task | Request |
|---|---|
| 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.
GET /orders/_search
{
"query": { "term": { "status": "paid" } },
"size": 5
}
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:
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.
| Elasticsearch | OpenSearch |
|---|---|
| Query DSL, bool, aggregations, , , | Same syntax |
| KQL in Kibana | DQL 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 clients | Refuse 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.