ELK Stack — Match and Term Queries
Introduction
Elasticsearch divides its search queries into two families: full-text queries that analyze the input and term-level queries that match exact values. Choosing the right query type for each field is critical — using the wrong one is the most common mistake beginners make.
This page covers every major query type in both families with complete examples.
Full-Text Queries
Full-text queries pass the search input through the same analyzer that was used at index time. This means the input gets tokenized, lowercased, and potentially stemmed before matching.
match Query
The most commonly used query. It analyzes the input and finds documents containing any of the resulting tokens.
curl -s -X GET "localhost:9200/web-logs/_search?pretty" -H 'Content-Type: application/json' -d '{
"query": {
"match": {
"message": "user error"
}
}
}'
{
"took": 3,
"hits": {
"total": { "value": 4, "relation": "eq" },
"max_score": 1.7917595,
"hits": [
{
"_score": 1.7917595,
"_source": { "message": "User not found error occurred", "status_code": 404 }
},
{
"_score": 1.5404451,
"_source": { "message": "Internal server error during delete operation", "status_code": 500 }
},
{
"_score": 0.6931471,
"_source": { "message": "Successfully fetched user list", "status_code": 200 }
},
{
"_score": 0.5753642,
"_source": { "message": "New user created successfully", "status_code": 201 }
}
]
}
}
By default, match uses OR logic — documents matching either “user” or “error” are returned. To require both terms:
curl -s -X GET "localhost:9200/web-logs/_search?pretty" -H 'Content-Type: application/json' -d '{
"query": {
"match": {
"message": {
"query": "user error",
"operator": "and"
}
}
}
}'
{
"took": 2,
"hits": {
"total": { "value": 1, "relation": "eq" },
"max_score": 1.7917595,
"hits": [
{
"_score": 1.7917595,
"_source": { "message": "User not found error occurred", "status_code": 404 }
}
]
}
}
match_phrase Query
Requires all terms to appear in the exact order specified. Useful for searching log messages or error strings.
curl -s -X GET "localhost:9200/web-logs/_search?pretty" -H 'Content-Type: application/json' -d '{
"query": {
"match_phrase": {
"message": "server error"
}
}
}'
{
"took": 2,
"hits": {
"total": { "value": 1, "relation": "eq" },
"max_score": 1.9616585,
"hits": [
{
"_score": 1.9616585,
"_source": { "message": "Internal server error during delete operation", "status_code": 500 }
}
]
}
}
You can allow some distance between words using slop:
curl -s -X GET "localhost:9200/web-logs/_search?pretty" -H 'Content-Type: application/json' -d '{
"query": {
"match_phrase": {
"message": {
"query": "user created",
"slop": 1
}
}
}
}'
{
"took": 1,
"hits": {
"total": { "value": 0, "relation": "eq" },
"hits": []
}
}
With slop: 1, Elasticsearch allows one word between “user” and “created” — but “New user created successfully” has zero words between them, so you’d need slop: 0 or simply the default.
multi_match Query
Searches across multiple fields at once. Ideal when the search term could appear in different fields.
curl -s -X GET "localhost:9200/web-logs/_search?pretty" -H 'Content-Type: application/json' -d '{
"query": {
"multi_match": {
"query": "user",
"fields": ["message", "url"]
}
}
}'
{
"took": 3,
"hits": {
"total": { "value": 4, "relation": "eq" },
"max_score": 0.6931471,
"hits": [
{ "_score": 0.6931471, "_source": { "url": "/api/users", "message": "Successfully fetched user list" } },
{ "_score": 0.6931471, "_source": { "url": "/api/users", "message": "New user created successfully" } },
{ "_score": 0.6931471, "_source": { "url": "/api/users/42", "message": "User not found error occurred" } },
{ "_score": 0.5753642, "_source": { "url": "/api/orders", "message": "Order history fetched for user" } }
]
}
}
You can boost specific fields with the caret (^) syntax:
curl -s -X GET "localhost:9200/web-logs/_search?pretty" -H 'Content-Type: application/json' -d '{
"query": {
"multi_match": {
"query": "user",
"fields": ["message^2", "url"],
"type": "best_fields"
}
}
}'
Types of multi_match:
| Type | Behavior |
|---|---|
best_fields |
Score from the single best-matching field (default) |
most_fields |
Combine scores from all matching fields |
cross_fields |
Treat fields as one combined field |
phrase |
Run match_phrase on each field |
phrase_prefix |
Run match_phrase_prefix on each field |
Term-Level Queries
Term-level queries find documents that contain exact values in the inverted index. The search input is not analyzed — no tokenization, no lowercasing.
term Query
Finds documents containing an exact term in a field:
curl -s -X GET "localhost:9200/web-logs/_search?pretty" -H 'Content-Type: application/json' -d '{
"query": {
"term": {
"method": "GET"
}
}
}'
{
"took": 1,
"hits": {
"total": { "value": 4, "relation": "eq" },
"max_score": 0.35667494,
"hits": [
{ "_score": 0.35667494, "_source": { "method": "GET", "url": "/api/users" } },
{ "_score": 0.35667494, "_source": { "method": "GET", "url": "/api/products" } },
{ "_score": 0.35667494, "_source": { "method": "GET", "url": "/api/users/42" } },
{ "_score": 0.35667494, "_source": { "method": "GET", "url": "/api/orders" } }
]
}
}
terms Query
Match any value from a list — like SQL’s IN clause:
curl -s -X GET "localhost:9200/web-logs/_search?pretty" -H 'Content-Type: application/json' -d '{
"query": {
"terms": {
"status_code": [404, 500]
}
}
}'
{
"took": 2,
"hits": {
"total": { "value": 2, "relation": "eq" },
"hits": [
{ "_source": { "status_code": 404, "message": "User not found error occurred" } },
{ "_source": { "status_code": 500, "message": "Internal server error during delete operation" } }
]
}
}
range Query
Finds documents where a field value falls within a range. Supports gt, gte, lt, lte.
curl -s -X GET "localhost:9200/web-logs/_search?pretty" -H 'Content-Type: application/json' -d '{
"query": {
"range": {
"response_time": {
"gte": 200,
"lte": 1000
}
}
}
}'
{
"took": 1,
"hits": {
"total": { "value": 2, "relation": "eq" },
"hits": [
{ "_source": { "response_time": 350.2, "url": "/api/users", "method": "POST" } },
{ "_source": { "response_time": 210.7, "url": "/api/orders", "method": "GET" } }
]
}
}
For date ranges, you can use date math:
curl -s -X GET "localhost:9200/web-logs/_search?pretty" -H 'Content-Type: application/json' -d '{
"query": {
"range": {
"@timestamp": {
"gte": "2026-04-01T08:00:00Z",
"lt": "2026-04-01T08:30:00Z"
}
}
}
}'
{
"took": 2,
"hits": {
"total": { "value": 5, "relation": "eq" },
"hits": [ "... documents from 08:00 to 08:30 ..." ]
}
}
exists Query
Finds documents where the specified field has any non-null value:
curl -s -X GET "localhost:9200/web-logs/_search?pretty" -H 'Content-Type: application/json' -d '{
"query": {
"exists": {
"field": "client_ip"
}
}
}'
{
"took": 1,
"hits": {
"total": { "value": 6, "relation": "eq" },
"hits": [ "... all 6 documents have client_ip ..." ]
}
}
wildcard Query
Pattern matching using * (zero or more characters) and ? (single character):
curl -s -X GET "localhost:9200/web-logs/_search?pretty" -H 'Content-Type: application/json' -d '{
"query": {
"wildcard": {
"url.raw": "/api/users*"
}
}
}'
{
"took": 3,
"hits": {
"total": { "value": 3, "relation": "eq" },
"hits": [
{ "_source": { "url": "/api/users" } },
{ "_source": { "url": "/api/users" } },
{ "_source": { "url": "/api/users/42" } }
]
}
}
Warning: Wildcard queries can be slow, especially with leading wildcards like
*users. Avoid in production on large datasets.
regexp Query
Matches using a regular expression:
curl -s -X GET "localhost:9200/web-logs/_search?pretty" -H 'Content-Type: application/json' -d '{
"query": {
"regexp": {
"url.raw": "/api/users/[0-9]+"
}
}
}'
{
"took": 4,
"hits": {
"total": { "value": 2, "relation": "eq" },
"hits": [
{ "_source": { "url": "/api/users/42" } },
{ "_source": { "url": "/api/users/7" } }
]
}
}
prefix Query
Matches documents where the field value starts with a given prefix:
curl -s -X GET "localhost:9200/web-logs/_search?pretty" -H 'Content-Type: application/json' -d '{
"query": {
"prefix": {
"url.raw": "/api/user"
}
}
}'
{
"took": 1,
"hits": {
"total": { "value": 3, "relation": "eq" },
"hits": [
{ "_source": { "url": "/api/users" } },
{ "_source": { "url": "/api/users" } },
{ "_source": { "url": "/api/users/42" } }
]
}
}
Common Mistakes
Mistake 1: Using term on Analyzed Fields
A text field is analyzed at index time — “Successfully fetched” becomes tokens ["successfully", "fetched"]. A term query searches for the exact value, which won’t match:
# WRONG — This will return ZERO results
curl -s -X GET "localhost:9200/web-logs/_search?pretty" -H 'Content-Type: application/json' -d '{
"query": {
"term": {
"message": "Successfully fetched user list"
}
}
}'
{
"took": 1,
"hits": {
"total": { "value": 0, "relation": "eq" },
"hits": []
}
}
Fix: Use match for text fields, or query the .keyword sub-field if one exists.
Mistake 2: Using match on Keyword Fields
While match technically works on keyword fields, it’s misleading. The analyzer has nothing to do because keyword fields store the exact value:
# Works, but semantically wrong — use term instead
curl -s -X GET "localhost:9200/web-logs/_search?pretty" -H 'Content-Type: application/json' -d '{
"query": {
"match": {
"method": "GET"
}
}
}'
Fix: Use term for keyword fields. It’s more efficient and semantically correct.
Mistake 3: Case Sensitivity with term
term queries are case-sensitive against the stored tokens. For a text field, tokens are lowercased by the standard analyzer:
# WRONG — "GET" won't match if field is text type (stored as "get")
# RIGHT — "GET" works on keyword fields (stored as-is)
Rule of thumb:
textfields → Usematchkeywordfields → Useterm- Multi-field (
field.raw) → Usetermon the keyword sub-field
Quick Reference
| Query | Family | Use Case | Analyzes Input? |
|---|---|---|---|
match |
Full-text | Search in text fields | Yes |
match_phrase |
Full-text | Exact phrase search | Yes |
multi_match |
Full-text | Search across multiple fields | Yes |
term |
Term-level | Exact value in keyword/numeric fields | No |
terms |
Term-level | Match any value from a list | No |
range |
Term-level | Numeric/date range | No |
exists |
Term-level | Check if field has a value | No |
wildcard |
Term-level | Pattern matching with * and ? |
No |
regexp |
Term-level | Regex pattern matching | No |
prefix |
Term-level | Starts-with matching | No |
Next: Bool Queries — Combine multiple conditions with must, should, must_not, and filter.