ELK Stack — Match and Term Queries

7 minute read

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:

  • text fields → Use match
  • keyword fields → Use term
  • Multi-field (field.raw) → Use term on 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.