Skip to content
ExponaBLOG
ELASTICSEARCH & SEARCH ENGINEERING · DAY 014 / 100

Query vs filter context: a practical Elasticsearch guide

Scoring costs time.

Expona Admin6 min read

Query vs filter context: a practical Elasticsearch guide

Every query you send to Elasticsearch makes a choice: should we calculate how relevant this result is, or should we just check whether it matches at all? That choice sits at the heart of query vs filter context, and it directly impacts both your relevance scores and your cluster's CPU time. Understanding when to use each one is the difference between a search system that scales and one that burns through resources on every request.

Query context calculates relevance

Query context is what most developers think of when they imagine search. You ask Elasticsearch a question, and it answers with ranked results. The ranking comes from a relevance score—typically based on BM25—that reflects how well each document matches your criteria.

When you run a query in query context, Elasticsearch computes this score for every matching document. That computation involves:

  • Term frequency analysis across the document
  • Inverse document frequency calculations across the index
  • Field-level boosting and scoring functions
  • Aggregation of scores across multiple clauses

This is powerful for full-text search, but it's expensive. A simple example:

json
{
  "query": {
    "match": {
      "product_name": "wireless headphones"
    }
  }
}

Here, Elasticsearch will score every product whose name matches either "wireless" or "headphones". A product containing both terms will score higher than one with just "wireless". Products are sorted by that score, and you get ranked results. That scoring happens on every single document that passes the initial filter.

In a real e-commerce scenario, imagine searching across 50,000 products. Query context means 50,000 score calculations, each one non-trivial. If you run that query 1,000 times per second during peak hours, you're asking your cluster to perform 50 million relevance calculations every second. That's the cost of query context: accuracy and ranking power in exchange for CPU time.

Filter context only checks yes or no

Filter context is binary. A document either matches the filter condition or it doesn't. No scoring, no ranking calculation, no intermediate math. Elasticsearch simply checks: does this value exist? Is this date in range? Is this status equal to "active"? Yes or no.

This is ideal for exact conditions where relevance doesn't apply. A few examples:

  • Is this product in stock?
  • Is this user account active?
  • Does this order belong to the EU region?
  • Is this event timestamp after 2024-01-01?

Filter context runs dramatically faster because it skips the scoring machinery entirely. Here's a real query that mixes both contexts:

json
{
  "query": {
    "bool": {
      "must": [
        {
          "match": {
            "product_name": "headphones"
          }
        }
      ],
      "filter": [
        {
          "term": {
            "status": "active"
          }
        },
        {
          "range": {
            "price": {
              "gte": 50,
              "lte": 200
            }
          }
        }
      ]
    }
  }
}

In this query, the must clause applies query context to the product name—we calculate relevance scores so users see the best matches first. The filter clauses use filter context to eliminate inactive products and products outside the price range. Filter context reduces the candidate set before scoring even begins.

Consider a ride-hailing platform searching for available drivers. The query context scores drivers by proximity and rating. The filter context checks: has this driver accepted rides in the last 24 hours? Is the driver's vehicle insurance current? Is the driver not already on a ride? These are yes-or-no questions. Filter context answers them faster and makes no sense to score.

Filters are cached

One of the biggest performance wins with filter context comes from Elasticsearch's filter cache. When you run the same filter multiple times, Elasticsearch can cache the result—a bitset representing which documents match that filter. Subsequent queries can reuse that bitset without re-evaluating the filter condition.

Query context results are not cached this way. Scores are recalculated because they can change based on index updates, index settings, or even the overall query structure.

In a CRM system, imagine filtering all customers by "country": "US" and "status": "active". The first query evaluates both filters and caches the bitsets. The next query that uses the same filters hits the cache. The tenth query still hits the cache. Over thousands of requests per minute, that cache hit saves milliseconds per query, which compounds to hours of saved CPU per day.

The cache size is controlled per shard and evicts least-recently-used entries when memory pressure rises. You typically don't need to tune it manually, but you can inspect cache performance via the node stats API:

json
{
  "query": {
    "bool": {
      "filter": [
        {
          "term": {
            "country": "US"
          }
        },
        {
          "term": {
            "status": "active"
          }
        }
      ]
    }
  }
}

This query has no scoring work, only filtering work. Both conditions are cached after the first execution. Every subsequent request using these same filters benefits from that cache.

Put exact conditions in filter

The rule of thumb is simple: if a condition is exact and not about relevance ranking, use filter context. If relevance or ranking matters, use query context.

In a SaaS analytics dashboard, you might search for events with filtering rules like:

json
{
  "query": {
    "bool": {
      "must": [
        {
          "match": {
            "event_description": "payment failed"
          }
        }
      ],
      "filter": [
        {
          "term": {
            "environment": "production"
          }
        },
        {
          "range": {
            "timestamp": {
              "gte": "now-7d"
            }
          }
        },
        {
          "terms": {
            "severity": ["critical", "high"]
          }
        }
      ]
    }
  }
}

The event description uses match in query context because you want events sorted by relevance—maybe "payment failed" should rank higher than "payment timeout". The environment, timestamp, and severity are exact constraints. They narrow the candidate set but have no relevance scores. Filter context is the right choice.

Here's the same query structured incorrectly, using query context for everything:

json
{
  "query": {
    "bool": {
      "must": [
        {
          "match": {
            "event_description": "payment failed"
          }
        },
        {
          "match": {
            "environment": "production"
          }
        },
        {
          "match": {
            "timestamp": "now-7d"
          }
        },
        {
          "match": {
            "severity": "critical"
          }
        }
      ]
    }
  }
}

Now Elasticsearch calculates relevance scores for the environment field, the timestamp field, and the severity field. None of those scores matter—you're not sorting by how "productiony" an event is. You're wasting CPU on scoring that will never influence the final ranking. Move them to filter and reclaim that performance.

Common mistakes

Using query context for all boolean logic: Developers new to Elasticsearch often throw everything into a must or should clause, not realizing that filter is available. This incurs scoring overhead for exact conditions that could be cached.

Forgetting that match is query context: The match clause always runs in query context. If you need an exact term match with no scoring, use term in a filter clause instead. The difference is significant for high-cardinality fields or high-volume queries.

Mixing stemming and filters: The term query doesn't apply stemming; it matches the exact token in the index. If your field is analyzed with a stemmer, term will not match the stemmed form. Use keyword field type for fields you intend to filter on.

Assuming caching is automatic: Filter caching is automatic and transparent, but only for filters that are stable. Filters with now in a range query may not cache as effectively because the range changes every millisecond. Consider using cache: false hint for filters you know will rarely repeat, to avoid evicting more valuable cached filters.

Key takeaways

  • Query context calculates relevance scores on every matching document; filter context is binary yes-or-no checking with no scoring.
  • Use query context for full-text search, fuzzy matching, and any condition where ranking or relevance matters.
  • Use filter context for exact conditions: status values, date ranges, IDs, tags, and any constraint that's about inclusion or exclusion, not ranking.
  • Filters are cached by Elasticsearch; repeated filter evaluations hit the bitset cache instead of re-running the condition.
  • Move exact conditions out of must clauses and into filter to lower CPU usage and improve query latency, especially under high volume.
  • The same logical constraint can be expressed in query or filter context; the performance difference is substantial but the results are semantically identical.
ShareXLinkedInReddit

Found this useful? Support the writing.

Buy me a coffee
Written by
Expona Admin
Site administrator

Comments

Loading comments…

Leave a comment

Comments are reviewed before they appear. Be kind and stay on topic.

Related articles