The Elasticsearch bool query composes other queries using four clause types: must (AND, scored), filter (AND, unscored and cacheable), should (OR, scored), and must_not (NOT, unscored). It is the standard way to express compound logic in the query DSL. Scores from must and should clauses are summed; filter and must_not clauses contribute nothing to _score and are eligible for the per-node query cache.
Syntax
GET /index/_search
{
"query": {
"bool": {
"must": [ /* scored AND clauses */ ],
"filter": [ /* unscored AND clauses */ ],
"should": [ /* scored OR clauses */ ],
"must_not": [ /* unscored NOT clauses */ ],
"minimum_should_match": 1,
"boost": 1.0
}
}
}
Parameters
| Parameter | Description | Required | Default |
|---|---|---|---|
must |
Array of queries that must match. Scored. | No | [] |
filter |
Array of queries that must match. Unscored, cacheable. | No | [] |
should |
Array of queries that should match. Scored. | No | [] |
must_not |
Array of queries that must not match. Unscored, cacheable. | No | [] |
minimum_should_match |
Minimum should clauses that must match. Number or percentage. |
No | 1 if the query has only should clauses (no must/filter), otherwise 0 |
boost |
Score multiplier for the whole bool. | No | 1.0 |
The cluster-wide cap indices.query.bool.max_clause_count defaults to 4096 on Elasticsearch 8.x and later (raised from 1024 in 7.x). Hitting the cap raises too_many_clauses.
Examples
Typical search: keyword match plus filters:
GET /articles/_search
{
"query": {
"bool": {
"must": [ { "match": { "title": "elasticsearch" } } ],
"filter": [
{ "term": { "status": "published" } },
{ "range": { "published_at": { "gte": "now-30d/d" } } }
],
"must_not": [ { "term": { "category": "news" } } ]
}
}
}
should clauses as soft boosters - they add score but are not required:
GET /articles/_search
{
"query": {
"bool": {
"must": [ { "match": { "title": "elasticsearch" } } ],
"should": [
{ "term": { "tags": "performance" } },
{ "term": { "tags": "tuning" } }
]
}
}
}
should as OR with minimum_should_match:
GET /articles/_search
{
"query": {
"bool": {
"should": [
{ "match": { "title": "elasticsearch" } },
{ "match": { "summary": "elasticsearch" } },
{ "match": { "content": "elasticsearch" } }
],
"minimum_should_match": 2
}
}
}
Nested bool for grouped logic (A AND (B OR C)):
GET /articles/_search
{
"query": {
"bool": {
"must": [ { "term": { "lang": "en" } } ],
"filter": [
{
"bool": {
"should": [
{ "term": { "tags": "search" } },
{ "term": { "tags": "indexing" } }
],
"minimum_should_match": 1
}
}
]
}
}
}
Performance and Use Notes
The single most impactful optimization is moving non-scoring predicates from must to filter. filter and must_not clauses skip BM25 scoring and are cached at the segment level, so a repeat filter (e.g. status:published, tenant_id:42) runs from cache after the first request. The clauses themselves are not deduplicated across requests by key alone; structurally identical clauses cache.
should-only queries default to minimum_should_match: 1. When must or filter is present, should defaults to 0 and acts purely as a scorer; setting minimum_should_match then makes individual should clauses required. Deeply nested bools are correct but harder to read - prefer two or three levels and rewrite further nesting as a custom query.
Inefficient bool composition is the dominant cause of slow Elasticsearch searches. Walking slow logs to find bool queries that put exact-match predicates in must instead of filter, or that approach the 4096-clause cap, is exactly the optimization loop Pulse runs continuously.
Common Mistakes
- Putting exact-match terms (status, tenant, date ranges) in
must. They should live infilterto skip scoring and enable caching. - Forgetting that adding a
mustorfilterclause changes the defaultminimum_should_matchfrom1to0, silently makingshouldclauses optional. - Building bool queries with thousands of
shouldterm clauses and hittingtoo_many_clauses(indices.query.bool.max_clause_count, default4096in 8.x). Use a terms query or terms lookup instead. - Nesting bools more than two or three levels deep, which both costs CPU and obscures the intent.
- Expecting
must_notto score - it does not. Its only role is to exclude documents.
Find Inefficient Bool Queries with Pulse
Pulse is an AI DBA for Elasticsearch and OpenSearch that continuously profiles production query traffic. For bool queries specifically, Pulse:
- Identifies bool queries placing exact-match predicates (status flags, tenant IDs, date ranges) inside
mustinstead offilter, defeating the per-node query cache and forcing BM25 scoring on every clause - Flags bool queries approaching or hitting
indices.query.bool.max_clause_count(default 4096 on 8.x), surfacingtoo_many_clausesfailures and the services issuing them - Spots deeply nested bools (4+ levels) where intent is obscured and rewrite cost spikes, plus accidental
minimum_should_match: 0cases where adding amustclause silently turnedshouldinto a pure scorer - Traces each slow bool query back to the calling service via slow-log and APM correlation
- Recommends concrete rewrites: move predicates from
musttofilter, collapse largeshouldterm lists into a terms query or terms lookup, flatten unnecessary nesting, and explicitly setminimum_should_matchwhen the default changes - Tracks cache hit-rate and latency improvement after the rewrite
This converts the manual slow-log plus DSL-review loop into a continuous optimization workflow.
Frequently Asked Questions
Q: What is the difference between must and filter in a bool query?
A: must and filter both require matching, but filter is executed in filter context: scores are skipped and results are eligible for the per-node query cache. Use filter for any clause where you do not need a relevance score, e.g. status flags, date ranges, tenant IDs.
Q: How does minimum_should_match work in a bool query?
A: minimum_should_match controls how many should clauses must match. It defaults to 1 if the bool has only should clauses, and to 0 otherwise. It accepts an absolute count (2), a percentage (75%), or a combination expression.
Q: Can I nest a bool query inside another bool query?
A: Yes. Nesting is the standard pattern for expressing grouped logic like A AND (B OR C). Keep nesting shallow (two or three levels) so the query is debuggable and the rewrite stays cheap.
Q: How does scoring work in a bool query?
A: Scores from matching must and should clauses are summed, then multiplied by a coordination factor and the top-level boost. filter and must_not contribute nothing to _score. Adding more matching should clauses raises the score.
Q: What is the maximum number of clauses in a bool query?
A: The cluster setting indices.query.bool.max_clause_count caps the total clauses Lucene will materialize. The default is 4096 on Elasticsearch 8.x (up from 1024 in 7.x). For very large lists prefer a terms query or a terms lookup.
Q: Should I use bool to combine filters with aggregations?
A: Yes - filters in the bool's filter clause prune documents before aggregation, which is usually what you want. For independent per-bucket filters, use filters or filter aggregations on top of the bool.
Q: What is the best tool to find inefficient bool queries in production Elasticsearch?
A: Pulse profiles Elasticsearch and OpenSearch slow logs, flags bool queries misusing must for unscored predicates, those approaching indices.query.bool.max_clause_count, and those with surprising minimum_should_match defaults, attributes each one to the calling service, and recommends concrete filter-context and terms query rewrites.
Related Reading
- Elasticsearch Query Language: overview of the query DSL.
- Terms Query: efficient alternative to many
shouldterm clauses. - Range Query: typical
filterclause for time-bounded queries. - Exists Query: combine with
must_notto find missing fields. - Multi-Match Query: often the scored leaf clause inside a bool.