Running a search query in Elasticsearch lets an operator confirm that indexed documents answer a specific question before dashboards, alerts, or applications depend on the result. A focused search starts with the target index, alias, or data stream and the field mappings that decide whether a field is analyzed text, an exact value, sortable data, or a date.
The Search API accepts a quick Lucene query-string search through the q parameter and structured JSON request bodies through the Query DSL. A URI q search is useful for ad hoc checks, while a JSON body is better when the query needs full-text matching, exact filters, sorting, source-field control, or hit-count behavior in one request.
An unsecured local endpoint keeps the query shape easy to read. Secured clusters should use the cluster HTTPS URL and the authentication method operators already use for that environment, such as basic authentication, an API key, and a trusted CA certificate. Fixed sample timestamps keep the date range behavior repeatable whenever the commands are rerun.
$ curl --silent --show-error --fail "http://localhost:9200/_cat/indices/app-events-search-2026.06?v&h=health,status,index,docs.count,store.size" health status index docs.count store.size green open app-events-search-2026.06 4 7kb
If the data lives behind an alias or data stream, query that logical name instead of a hidden backing index so rollovers do not break the search target.
$ curl --silent --show-error --fail "http://localhost:9200/app-events-search-2026.06/_mapping?pretty&filter_path=*.mappings.properties"
{
"app-events-search-2026.06" : {
"mappings" : {
"properties" : {
"level" : {
"type" : "keyword"
},
"message" : {
"type" : "text",
"fields" : {
"keyword" : {
"type" : "keyword",
"ignore_above" : 256
}
}
},
"service" : {
"type" : "keyword"
},
"timestamp" : {
"type" : "date"
}
}
}
}
}
Use match queries for analyzed text fields such as message. Use term filters, sorting, and exact comparisons on fields such as level, service, timestamp, or message.keyword.
$ curl --silent --show-error --fail --get "http://localhost:9200/app-events-search-2026.06/_search" --data-urlencode "pretty=true" --data-urlencode "filter_path=took,hits.total,hits.hits._id,hits.hits._source" --data-urlencode "q=message:timeout" --data-urlencode "size=2" --data-urlencode "sort=timestamp:desc"
{
"took" : 49,
"hits" : {
"total" : {
"value" : 3,
"relation" : "eq"
},
"hits" : [
{
"_id" : "evt-1004",
"_source" : {
"timestamp" : "2026-06-18T09:23:44Z",
"level" : "ERROR",
"service" : "billing",
"message" : "database timeout while writing session"
}
},
{
"_id" : "evt-1002",
"_source" : {
"timestamp" : "2026-06-18T09:17:21Z",
"level" : "WARN",
"service" : "checkout",
"message" : "upstream timeout threshold reached"
}
}
]
}
}
Use --data-urlencode so characters in the Lucene query string are escaped before curl sends the request.
If a request sends both q and a JSON query body, Elasticsearch uses the q parameter and ignores the body query.
$ curl --silent --show-error --fail --header "Content-Type: application/json" --request POST "http://localhost:9200/app-events-search-2026.06/_search?pretty&filter_path=took,hits.total,hits.hits._id,hits.hits._source" --data '{
"size": 2,
"_source": ["timestamp", "level", "service", "message"],
"sort": [
{ "timestamp": { "order": "desc" } }
],
"query": {
"bool": {
"must": [
{ "match": { "message": "timeout" } }
],
"filter": [
{ "term": { "level": "ERROR" } },
{ "range": { "timestamp": { "gte": "2026-06-18T09:00:00Z", "lt": "2026-06-18T10:00:00Z" } } }
]
}
}
}'
{
"took" : 14,
"hits" : {
"total" : {
"value" : 2,
"relation" : "eq"
},
"hits" : [
{
"_id" : "evt-1004",
"_source" : {
"timestamp" : "2026-06-18T09:23:44Z",
"level" : "ERROR",
"service" : "billing",
"message" : "database timeout while writing session"
}
},
{
"_id" : "evt-1001",
"_source" : {
"timestamp" : "2026-06-18T09:15:00Z",
"level" : "ERROR",
"service" : "checkout",
"message" : "connection timeout while opening upstream socket"
}
}
]
}
}
The match clause searches analyzed text tokens, while the term and range filters narrow the result set without changing score.
Tool: JSON Formatter
$ curl --silent --show-error --fail --header "Content-Type: application/json" --request POST "http://localhost:9200/app-events-search-2026.06/_search?pretty&filter_path=hits.total,hits.hits._id,hits.hits._source" --data '{
"from": 1,
"size": 1,
"_source": ["timestamp", "level", "message"],
"sort": [
{ "timestamp": { "order": "desc" } }
],
"query": {
"match": { "message": "timeout" }
}
}'
{
"hits" : {
"total" : {
"value" : 3,
"relation" : "eq"
},
"hits" : [
{
"_id" : "evt-1002",
"_source" : {
"timestamp" : "2026-06-18T09:17:21Z",
"level" : "WARN",
"message" : "upstream timeout threshold reached"
}
}
]
}
}
Use from and size only for shallow paging. By default, Elasticsearch blocks paging beyond 10,000 hits through index.max_result_window; use search_after with the same query and sort values for deeper paging.
$ curl --silent --show-error --fail --header "Content-Type: application/json" --request POST "http://localhost:9200/app-events-search-2026.06/_search?pretty&filter_path=took,hits.total" --data '{
"track_total_hits": true,
"query": {
"match": {
"message": "timeout"
}
}
}'
{
"took" : 5,
"hits" : {
"total" : {
"value" : 3,
"relation" : "eq"
}
}
}
track_total_hits set to true returns the exact count at extra query cost. A relation value of eq means the count is exact; gte means Elasticsearch returned a lower bound.