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
| Clause | SQL equivalent | Scoring? | Cached? |
|---|---|---|---|
must | AND | Yes (contributes to score) | No |
should | OR (or boost) | Yes (contributes to score) | No |
must_not | NOT | No | Yes |
filter | AND | No | Yes |
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.
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:
- No scoring math — BM25 calculations aren’t cheap. Skipping them saves CPU.
- 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.