Elasticsearch Cheatsheet - Query DSL & REST API Commands
For operators and search engineers who must keep a cluster green and answer "why is this index slow or unassigned". The hard part is interpreting health, what drives shard relocation, and ILM policy trade-offs. By the end you can check cluster status, find and diagnose red/unassigned shards, manage index aliases and lifecycle, issue term/range/multi-search queries, and trace slow queries and disk pressure.
Typical Use Case
For engineers in front of a cluster asking why an index is red, whether shards are unassigned, why a query is slow, or why the heap fills. Everything uses curl rather than a client SDK, deliberately, so you can see exactly what the server received. Covers cluster state and the _cat endpoints, allocation explain, ILM, document CRUD including _bulk and its ndjson format, the query DSL, aggregations and the slow log. When a write is rejected, read _cat/shards first.
Cluster Status 6
curl localhost:9200/_cluster/health?prettycurl localhost:9200/_cat/nodes?vcurl localhost:9200/_cat/health?vcurl localhost:9200/_cluster/stats?prettycurl localhost:9200/_cat/allocation?vcurl localhost:9200/_cat/master?vShard & Index 6
curl localhost:9200/_cat/indices?vcurl localhost:9200/_cat/shards?v&h=index,shard,prirep,state,node,unassigned.reasoncurl localhost:9200/_cluster/allocation/explain?prettycurl -XDELETE localhost:9200/<index>curl localhost:9200/_ilm/explain/<index>?prettycurl -XPUT localhost:9200/<index>/_settings -H "Content-Type:application/json" -d '{"number_of_replicas":1}'Document CRUD 6
curl -XPOST localhost:9200/<index>/_doc -d '{"name":"tom"}' -H "Content-Type:application/json"curl -XPUT localhost:9200/<index>/_doc/1 -d '{"name":"tom"}' -H "Content-Type:application/json"curl -XPOST localhost:9200/<index>/_update/1 -d '{"doc":{"age":20}}' -H "Content-Type:application/json"curl -XDELETE localhost:9200/<index>/_doc/1curl -XPOST localhost:9200/<index>/_bulk -d '{"index":{"_id":"1"}}
{"name":"a"}
' -H "Content-Type:application/x-ndjson"curl localhost:9200/<index>/_doc/1?prettySearch 6
curl localhost:9200/<index>/_search?q=name:tom&prettycurl localhost:9200/<index>/_search -d '{"query":{"match":{"name":"tom"}}}' -H "Content-Type:application/json"curl localhost:9200/<index>/_search -d '{"query":{"term":{"status":"active"}}}' -H "Content-Type:application/json"curl localhost:9200/<index>/_search -d '{"query":{"range":{"age":{"gte":18,"lte":30}}}}' -H "Content-Type:application/json"curl localhost:9200/<index>/_search -d '{"query":{"bool":{"must":[{"match":{"name":"tom"}}],"filter":[{"term":{"status":"active"}}]}}}' -H "Content-Type:application/json"curl localhost:9200/<index>/_search -d '{"query":{"match_all":{}},"sort":[{"created_at":"desc"}],"from":0,"size":10}' -H "Content-Type:application/json"Aggregation 5
curl localhost:9200/<index>/_search -d '{"size":0,"aggs":{"by_status":{"terms":{"field":"status"}}}}' -H "Content-Type:application/json"curl localhost:9200/<index>/_search -d '{"size":0,"aggs":{"avg_age":{"avg":{"field":"age"}}}}' -H "Content-Type:application/json"curl localhost:9200/<index>/_search -d '{"size":0,"aggs":{"sales":{"date_histogram":{"field":"created_at","calendar_interval":"1d"}}}}' -H "Content-Type:application/json"curl localhost:9200/<index>/_search -d '{"size":0,"aggs":{"price_ranges":{"range":{"field":"price","ranges":[{"to":100},{"from":100,"to":500},{"from":500}]}}}}' -H "Content-Type:application/json"curl localhost:9200/<index>/_search -d '{"size":0,"aggs":{"users":{"cardinality":{"field":"user_id"}}}}' -H "Content-Type:application/json"Query & Slowlog 5
curl localhost:9200/<index>/_count?prettycurl localhost:9200/_nodes/stats/thread_pool?prettycurl localhost:9200/_nodes/stats/breaker?prettytail -f /var/log/elasticsearch/*_index_search_slowlog.jsoncurl localhost:9200/<index>/_settings?prettyDisk & Ops 6
curl localhost:9200/_cluster/settings?prettycurl -XPUT localhost:9200/_cluster/settings -H "Content-Type:application/json" -d '{"transient":{"cluster.routing.allocation.disk.watermark.low":"85%"}}'curl -XPOST localhost:9200/_forcemerge?max_num_segments=1df -h /var/lib/elasticsearchcurl -XPOST localhost:9200/<index>/_closecurl -XPOST localhost:9200/<index>/_refreshParameter matrix
| Parameter | Effect | Example |
|---|---|---|
?pretty | Formats the response for reading. Add it to almost anything while debugging, since raw JSON otherwise collapses onto one line. | curl localhost:9200/_cluster/health?pretty |
_cat/nodes?v | Lists nodes with their roles, heap and load, and it is the entry point for node-level problems. | curl localhost:9200/_cat/nodes?v |
_cat/health?v | A compact view of cluster status, node counts and shard counts, easier to read than the JSON form. | curl localhost:9200/_cat/health?v |
_cat/allocation?v | Shows shard distribution and disk usage per node, which makes data skew visible. | curl localhost:9200/_cat/allocation?v |
_cat/master?v | Identifies the current master; check it first when master election looks confused. | curl localhost:9200/_cat/master?v |
_cat/indices?v | Lists every index with document count, store size and health, which answers "which index is growing". | curl localhost:9200/_cat/indices?v |
_cat/shards?v&h=index,shard,prirep,state,node,unassigned.reason | Selects columns with -h and includes the unassigned reason, the standard command for shard anomalies. | curl localhost:9200/_cat/shards?v&h=index,shard,prirep,state,node,unassigned.reason |
_cluster/allocation/explain | Explains why a given shard was not assigned, naming concrete reasons such as disk watermarks or node counts. | curl localhost:9200/_cluster/allocation/explain?pretty |
-XPUT <index>/_settings -d '{"number_of_replicas":1}' | Changes the replica count at runtime. A single-node test cluster needs this set to zero before the index turns green. | curl -XPUT localhost:9200/<index>/_settings -H "Content-Type:application/json" -d '{"number_of_replicas":1}' |
_ilm/explain/<index> | Shows which step of an index lifecycle policy an index is stuck on, the starting point when a managed index cannot be deleted or written. | curl localhost:9200/_ilm/explain/<index>?pretty |
-XPOST <index>/_doc | Indexes a document with a generated id; success returns 201 along with the new _id. | curl -XPOST localhost:9200/<index>/_doc -d '{"name":"tom"}' -H "Content-Type:application/json" |
-XPUT <index>/_doc/1 | Writes with a caller-supplied id. Repeating it replaces the whole document rather than merging fields. | curl -XPUT localhost:9200/<index>/_doc/1 -d '{"name":"tom"}' -H "Content-Type:application/json" |
-XPOST <index>/_update/1 -d '{"doc":{...}}' | A partial update, with the changed fields wrapped in a doc object. That wrapper is the key difference from PUT. | curl -XPOST localhost:9200/<index>/_update/1 -d '{"doc":{"age":20}}' -H "Content-Type:application/json" |
_bulk with application/x-ndjson | Bulk indexing. Every line must end with a newline including the last one, and a format slip fails the entire batch. | curl -XPOST localhost:9200/<index>/_bulk -d '{"index":{"_id":"1"}}\n{"name":"a"}\n' -H "Content-Type:application/x-ndjson" |
_search?q=name:tom | The Lucene query syntax, the fastest way to probe whether a field exists and how it is analysed. | curl localhost:9200/<index>/_search?q=name:tom&pretty |
{"query":{"match":{"name":"tom"}}} | A match query analyses the input and sorts by relevance score, which makes it the default choice for full-text search. | curl localhost:9200/<index>/_search -d '{"query":{"match":{"name":"tom"}}}' -H "Content-Type:application/json" |
{"query":{"term":{"status":"active"}}} | A term query is not analysed and has to match a complete term in the inverted index; picking the wrong one here is a frequent cause of "the data is there but nothing comes back". | curl localhost:9200/<index>/_search -d '{"query":{"term":{"status":"active"}}}' -H "Content-Type:application/json" |
{"query":{"range":{"age":{"gte":18,"lte":30}}}} | A range query, used for numeric and date filtering. | curl localhost:9200/<index>/_search -d '{"query":{"range":{"age":{"gte":18,"lte":30}}}}' -H "Content-Type:application/json" |
{"query":{"bool":{"must":[...],"filter":[...]}}} | Clauses under must contribute to scoring while filter clauses do not and can be cached, so put every non-relevance condition in filter. | curl localhost:9200/<index>/_search -d '{"query":{"bool":{"must":[{"match":{"name":"tom"}}],"filter":[{"term":{"status":"active"}}]}}}' -H "Content-Type:application/json" |
"size":0 + aggs | Setting size to zero computes aggregations without returning documents, the standard shape for an aggregate-only search. | curl localhost:9200/<index>/_search -d '{"size":0,"aggs":{"by_status":{"terms":{"field":"status"}}}}' -H "Content-Type:application/json" |
date_histogram + calendar_interval | Buckets by calendar interval (1d, 1w, 1M), which handles time zones and month boundaries more naturally than a fixed interval. | curl localhost:9200/<index>/_search -d '{"size":0,"aggs":{"sales":{"date_histogram":{"field":"created_at","calendar_interval":"1d"}}}}' -H "Content-Type:application/json" |
cardinality | Approximate distinct count, far cheaper in time and memory than an exact one at the price of accuracy. | curl localhost:9200/<index>/_search -d '{"size":0,"aggs":{"users":{"cardinality":{"field":"user_id"}}}}' -H "Content-Type:application/json" |
_nodes/stats/breaker | Reports circuit breaker trips; frequent trips on the parent breaker mean memory pressure has reached the limit. | curl localhost:9200/_nodes/stats/breaker?pretty |
_nodes/stats/thread_pool | Shows queue depth and rejection counts for the search and write thread pools, which is the evidence that a query overwhelmed the cluster. | curl localhost:9200/_nodes/stats/thread_pool?pretty |
_forcemerge?max_num_segments=1 | Force-merges segments to reduce their count and speed up queries. It is IO-heavy, so throttle it in production. | curl -XPOST localhost:9200/_forcemerge?max_num_segments=1 |
_close / _refresh | _close halts reads and writes on an index to save resources, while _refresh makes the newest writes searchable at the cost of a new segment. | curl -XPOST localhost:9200/<index>/_close |
Transient cluster settings with disk.watermark | Adjusts disk watermarks at runtime. A transient setting is lost on restart; only persistent survives. | curl -XPUT localhost:9200/_cluster/settings -H "Content-Type:application/json" -d '{"transient":{"cluster.routing.allocation.disk.watermark.low":"85%"}}' |
Common pitfalls
SymptomThe document was definitely indexed, yet searching for the same keyword returns nothing.
CauseA term query was pointed at a text field that is analysed. term does not analyse, so it can only match a whole term.
FixUse match for full-text fields and reserve term for non-analysed ones such as keyword fields, status values and identifiers.
SymptomA _bulk request returns 400 and nothing at all was written.
CauseThe ndjson is malformed: the action line and the document line each need their own line, every line has to end with a newline, and so does the final line.
FixBuild the request body from a file rather than concatenating a string for -d, make sure the file ends with a newline, and validate on a handful of items before scaling up.
SymptomThe index stays red and rebuilding the index does not fix it.
CauseA primary shard is unassigned; red means data is unavailable, whereas yellow only means replicas are missing.
FixRun curl on _cluster/allocation/explain to get the concrete reason, most often a disk watermark or too few nodes, deal with that and then recheck _cat/shards.
SymptomOn a single-node test cluster the index never leaves yellow.
CauseThe replica count is 1 while there is only one node, so the replica has nowhere to go.
FixSet number_of_replicas to 0, which is normal for a single-node environment rather than a fault.
SymptomA cluster setting change is gone again after a restart.
CauseIt was written into the transient block, which only lives as long as the node process.
FixWrite anything that must survive into the persistent block, and confirm the disk watermark setting was accepted by the cluster.
SymptomQueries slow down and the thread pool shows a large rejected count.
CauseDeep paging from a large offset or a wide aggregation fills the search thread pool and requests get rejected; the rejection count is visible with curl localhost:9200/_nodes/stats/thread_pool?pretty
FixSwitch deep paging to search_after or a PIT, narrow the aggregation scope, and rate-limit the query endpoint.
SymptomA freshly indexed document cannot be found, then turns up a moment later.
CauseNear-real-time indexing: writes typically become visible about a second later.
FixCall _refresh when immediate visibility is required, keeping in mind it creates a new segment and should not run on every write in production.
SymptomAfter a force merge the cluster IO spikes and queries get jittery instead of faster.
CauseA merge reads large amounts of segment data and rebuilds index structures, which is inherently IO-heavy.
FixRun it in a quiet window with a limited batch size, and for frequently written indexes rely on rollover by size rather than repeated merges.
Troubleshooting
1Start with overall cluster health
curl localhost:9200/_cluster/health?prettyRed means a primary shard is unavailable, yellow means replicas are missing.
2Inspect shard state and unassigned reasons
curl localhost:9200/_cat/shards?v&h=index,shard,prirep,state,node,unassigned.reasonThe unassigned.reason column points straight at the cause.
3Get the concrete reason a shard was not assigned
curl localhost:9200/_cluster/allocation/explain?prettyTypical output points at disk watermarks, shard awareness or insufficient nodes.
4Find which index keeps growing
curl localhost:9200/_cat/indices?vCompare the store size and docs.count across several runs.
5Check memory pressure and the circuit breakers
curl localhost:9200/_nodes/stats/breaker?prettyA parent breaker count that keeps climbing means memory has topped out.
6Read the slow query log
tail -f /var/log/elasticsearch/*_index_search_slowlog.jsonIt only has records if the search slowlog was enabled beforehand.
Command Examples
Troubleshooting yellow: shards first, then allocation explain
curl localhost:9200/_cat/health?v
curl localhost:9200/_cat/shards?v&h=index,shard,prirep,state,node,unassigned.reason
curl localhost:9200/_cluster/allocation/explain?prettyA single-node cluster has replicas=1 by default, so shards can never be placed twice and yellow is the expected state, not a fault. When a multi-node cluster is still yellow, unassigned.reason in _cat/shards names the cause (disk watermark, not enough nodes, awareness), and _cluster/allocation/explain prints the decider decision in full.
Output
health status index pri rep docs.count store.size yellow open orders 3 1 1204 8.1mb
Search right after writing: refresh manually
curl -XPOST localhost:9200/<index>/_doc -d '{"name":"tom"}' -H "Content-Type:application/json"
curl -XPOST localhost:9200/<index>/_refresh
curl localhost:9200/<index>/_search?q=name:tom&prettyWrites land in memory and only become searchable at refresh, which commits a near-real-time segment; the default interval is one second. "Not found right after insert" is normal behaviour, not a bug. Any test that writes then asserts must call _refresh or it will fail intermittently, and the production indexing path should not set refresh_interval very small just for real-time visibility, which badly slows indexing.
Output
{"_index":"orders","result":"created","_id":"Yb3nXk0B..."}bool query: must scores, filter is cached
curl -XPOST localhost:9200/<index>/_search -d '{"query":{"bool":{"must":[{"match":{"name":"tom"}}],"filter":[{"term":{"status":"active"}}]}}}' -H "Content-Type:application/json"An identical clause in filter does not contribute to relevance scoring and its result set is cacheable: put pass/fail conditions such as status in filter, and fields that drive ranking and BM25 (name, title) in must. The throughput difference on a search endpoint is substantial.
Batch writes with _bulk instead of looping single calls
curl -XPOST localhost:9200/<index>/_bulk -d '{"index":{"_id":"1"}}
{"name":"a"}
' -H "Content-Type:application/x-ndjson"_bulk speaks x-ndjson: every two lines form one action (metadata line, then document line), and the payload must end with a newline or the last entry is silently dropped. Looping single _doc calls pays one HTTP round trip per document, typically one to two orders of magnitude less throughput, which decides between minutes and hours on a million-document import.
size:0 aggregations: statistics only, no hits
curl -XPOST localhost:9200/<index>/_search -d '{"size":0,"aggs":{"by_status":{"terms":{"field":"status"}},"avg_age":{"avg":{"field":"age"}}}}' -H "Content-Type:application/json"The default size is 10, so even when you only care about aggregations ES assembles and serializes ten documents; size:0 skips that entirely, which visibly speeds up reporting queries and shrinks responses. The field in a terms aggregation must be keyword — aggregating a text field fails with fielddata disabled.
Common Pitfalls
- curl -XDELETE localhost:9200/<index> removes the index and every document in it, with no recycle bin. Confirm the exact name with curl localhost:9200/_cat/indices?v first — production and staging indices often differ only by suffix — and make sure nothing is still writing to it.
- Do not run an unfiltered full _search on a production index "just to look at the data". A query with no filter allocates threads on every touched shard, can drive the cluster to red, and can trip the circuit breaker. Always bound diagnostics queries with explicit filters and a size limit.
- Raising number_of_replicas with -XPUT immediately triggers replica allocation and doubles the disk footprint; setting it to 0 removes the high-availability copy. Both belong in an ILM policy: a manual -XPUT is silently overwritten when the policy fires, which is why such a setting appears "impossible to change".
- Deep pagination with from + size hits a hard limit (max_result_window defaults to 10000), and cost grows linearly with from. Use search_after for deep pages, or scroll / PIT. Raising max_result_window drastically increases node memory pressure and can trip the breaker.
- term targets keyword fields, match targets analyzed text fields. Mixing them up produces no error, just no results: a term against a text field compares the un-analyzed whole term and always returns empty. Check curl localhost:9200/<index>/_mapping?pretty to confirm the field type before blaming the data.
- Lowering cluster.routing.allocation.disk.watermark via -XPUT lets the cluster keep accepting writes after the disk is actually full, which crashes nodes and corrupts indices. A transient setting is lost on restart (it needs persistent to survive), so always change it back after the incident.
Tips
- Cluster red? First find unassigned shards with _cat/shards, then check reasons via allocation/explain — don't force allocation.
- Slow queries: first tell whether filters skip cache or aggregation cardinality is too high; use search_after instead of from+size for deep pagination.
- Disk over flood watermark rejects writes — clear cold indices or raise the watermark before adding nodes.
- Use match for full-text on text fields and term for exact on keyword fields; mixing them returns no results.
- Bulk writes via _bulk are an order of magnitude faster than per-doc _doc; set Content-Type to application/x-ndjson.
FAQ
My cluster went red or yellow — what order should I investigate in?
Three steps. First locate the unassigned shard: curl localhost:9200/_cat/shards?v&h=index,shard,prirep,state,node,unassigned.reason shows exactly which index and replica failed. Second, get the reason: curl localhost:9200/_cluster/allocation/explain?pretty returns the detailed explanation. Third, check the big picture: curl localhost:9200/_cat/allocation?v for per-node disk watermarks and df -h /var/lib/elasticsearch for real usage. If it is only a replica-count problem, curl -XPUT localhost:9200/<index>/_settings with a body of {"number_of_replicas":1} adjusts it dynamically — resist force-allocating before that.
term returns nothing — will switching to match fix it?
Do not switch blindly; check the field mapping first. match analyses the query text and is what you want on text fields for full-text search, while term skips analysis and matches one exact token, which suits keyword fields. Using term against a text field (or match against a keyword) yields no results. Also verify that status-style fields have a keyword subfield. When combining conditions, prefer putting them in bool's filter rather than must, since filter is cached and noticeably faster. curl localhost:9200/<index>/_settings?pretty is the place to confirm index settings.
I just wrote a document and cannot search for it — is the cluster broken?
No, there is a short refresh interval during which recent writes are not yet searchable. Force visibility with curl -XPOST localhost:9200/<index>/_refresh. For bulk ingestion, switch to the _bulk endpoint instead of per-document _doc calls — it is an order of magnitude faster, requires Content-Type: application/x-ndjson, and expects newline-separated action and document lines. Refresh costs write throughput, so never put it on every production write; to confirm existence use the realtime get at curl localhost:9200/<index>/_doc/1.
I only want statistics, not documents — how do I structure an aggregation?
Set "size":0 so the response carries only buckets. The common shapes: terms to group by field as in {"aggs":{"by_status":{"terms":{"field":"status"}}}}, avg for the mean with sum/max/min alongside it, cardinality for distinct counts using the approximate HyperLogLog algorithm (small precision trade-off, far lower memory), date_histogram to bucket over time with calendar_interval values such as 1d, 1M or 1y, and range to slice numeric values using a ranges array of from/to intervals.
Disk is nearly full — is adding nodes really the only option?
First confirm that a watermark is blocking writes: curl localhost:9200/_cluster/settings?pretty shows settings such as cluster.routing.allocation.disk.watermark.low, and df -h /var/lib/elasticsearch shows real usage — once you cross the flood watermark writes get rejected. Then you have three levers: temporarily raise the threshold with curl -XPUT localhost:9200/_cluster/settings setting transient cluster.routing.allocation.disk.watermark.low (transient, so use with care); reclaim space by deleting cold indices with -XDELETE, or merging segments on read-only indices via curl -XPOST localhost:9200/_forcemerge?max_num_segments=1 to cut their on-disk footprint; or curl -XPOST localhost:9200/<index>/_close to shut an idle index and release resources until you open it again.
Official References
Each command links to its official documentation below, so you can verify the latest usage and read deeper.
Maintained by LaoHand
Publicly updated on Jul 21, 2026, continuously proofread against official docs.
Contact Us
Wrong command or description? Send us corrections, business inquiries or product feedback by email.
Contact Us