Match vs Term Query

intermediate elasticsearch query-dsl match term

This is the most asked Elasticsearch question. Get it wrong and the interviewer immediately knows we’ve never actually used ES in production.

In simple language: match runs our search text through the same analyzer that indexed the field, then looks for the resulting tokens. term skips the analyzer entirely and searches for the exact value as-is.

That single difference explains 90% of the “why isn’t my query returning results?” bugs we see.

The flow — what actually happens

When we index a document with "title": "MacBook Pro 16-inch", the standard analyzer lowercases and tokenizes it into ["macbook", "pro", "16", "inch"]. Those tokens go into the inverted index.

Now if we search:

  • match: "MacBook" → analyzer turns it into ["macbook"] → matches the token → hit.
  • term: "MacBook" → looks for the literal string MacBook in the index → no match (because the index only has macbook).
  • term: "macbook" → matches the token → hit.
MATCH query
Input: "MacBook Pro"
↓ analyzer (lowercase + tokenize)
Tokens: ["macbook", "pro"]
↓ search inverted index
→ matches docs with either token
TERM query
Input: "MacBook Pro"
↓ no analyzer, raw bytes
Token: "MacBook Pro"
↓ search inverted index
→ no match (index has lowercase tokens)

When to use which

Think of it like this — use match for human-typed search (search bars, autocomplete) and term for structured/exact data (status flags, IDs, tags, enums).

GET /products/_search
{
  "query": {
    "match": { "title": "wireless bluetooth headphones" }
  }
}

The above is perfect for a search bar. ES will tokenize, lowercase, and find docs containing any of those words (with relevance scoring).

GET /orders/_search
{
  "query": {
    "term": { "status": "shipped" }
  }
}

This is perfect for filtering by a known enum value. We know status is always one of pending | shipped | delivered, so we don’t want fuzzy matching.

The keyword field trick

Here’s where most people get burned. By default, a string field gets mapped as both text (analyzed) AND keyword (not analyzed). To do an exact match on a name field:

GET /users/_search
{
  "query": {
    "term": { "name.keyword": "Manish Prajapati" }
  }
}

Notice the .keyword suffix. Without it, term on a text field will almost never work because the original string isn’t in the index — only its tokens are.

Scoring difference

match queries are run in query context — they compute a relevance score (BM25). term queries can run in filter context (inside bool.filter) where they’re cached and don’t compute scores. That makes term filters dramatically faster for repeated queries.

GET /products/_search
{
  "query": {
    "bool": {
      "must":   [{ "match": { "title": "laptop" } }],
      "filter": [{ "term":  { "in_stock": true } }]
    }
  }
}

Quick rules of thumb

  • Searching free text from a user? match.
  • Filtering by status, ID, boolean, tag, enum? term.
  • Got results that don’t make sense? Check if the field is text or keyword — that’s almost always the culprit.
  • Want exact-match on a string? Use term on field.keyword.