Bool Query

intermediate elasticsearch query-dsl bool filter-context

In simple language, bool is how we combine multiple conditions in a single query. Think of it like SQL’s WHERE a AND b AND NOT c OR d — but with a twist: each clause type changes whether the result affects the relevance score or not.

Almost every real-world ES query is a bool query under the hood. Master this and we’ve mastered Query DSL.

The four clauses

ClauseSQL equivalentScoring?Cached?
mustANDYes (contributes to score)No
shouldOR (or boost)Yes (contributes to score)No
must_notNOTNoYes
filterANDNoYes

The key insight — must and filter do the same logical thing (both require a match). The only difference is must computes a relevance score, filter doesn’t. Filter is cached and skips scoring math, so it’s significantly faster.

Query context vs Filter context
QUERY CONTEXT (must, should)
• Computes _score (BM25)
• Not cached
• "How well does this match?"
Use for: search relevance
FILTER CONTEXT (filter, must_not)
• No scoring (score = 0)
• Cached in bitset
• "Does this match — yes/no?"
Use for: exact filters, ranges

The canonical example

Let’s say we’re building a product search. The user typed “wireless headphones” and selected filters: in stock, price under 200, brand is Sony or Bose.

GET /products/_search
{
  "query": {
    "bool": {
      "must": [
        { "match": { "title": "wireless headphones" } }
      ],
      "filter": [
        { "term":  { "in_stock": true } },
        { "range": { "price": { "lte": 200 } } }
      ],
      "should": [
        { "term": { "brand": "sony" } },
        { "term": { "brand": "bose" } }
      ],
      "must_not": [
        { "term": { "discontinued": true } }
      ]
    }
  }
}

Breaking it down:

  • must — the title MUST match “wireless headphones” (and this drives relevance).
  • filter — must be in stock AND price ≤ 200 (no score impact, cached).
  • should — bonus points if brand is sony or bose (boosts score, doesn’t exclude others).
  • must_not — exclude discontinued products.

The “should” gotcha — minimum_should_match

By default, if a bool query has no must or filter clauses, then at least one should clause must match. If must or filter exists, should becomes a pure score booster — it doesn’t have to match anything.

To force at least N should-clauses to match:

{
  "bool": {
    "should": [
      { "term": { "tags": "premium" } },
      { "term": { "tags": "featured" } },
      { "term": { "tags": "bestseller" } }
    ],
    "minimum_should_match": 2
  }
}

Now at least 2 of those tags must match. Super useful for “match any 2 of these criteria” logic.

Why filter is faster

Two reasons:

  1. No scoring math — BM25 calculations aren’t cheap. Skipping them saves CPU.
  2. Bitset caching — ES caches the set of matching doc IDs as a bitmap. Next time we filter in_stock: true, it’s a lookup, not a search.

Rule of thumb — if we don’t care about relevance for a clause, put it in filter. The classic mistake is using must for exact filters like booleans, dates, and IDs.

must_not is also a filter

must_not runs in filter context too — it’s cached and doesn’t affect scoring. Use it freely for exclusions.

{
  "bool": {
    "filter": [
      { "term": { "status": "active" } }
    ],
    "must_not": [
      { "term":  { "is_test_account": true } },
      { "range": { "deleted_at": { "exists": true } } }
    ]
  }
}

Quick rules

  • User-typed text → must (we want scoring).
  • Exact filters (booleans, IDs, ranges, dates) → filter.
  • Optional boosts → should.
  • Exclusions → must_not.
  • Need “at least N of these” → should + minimum_should_match.