Bool queries: concepts, trade-offs and production lessons
When you need to combine multiple search conditions—filtering products by category while boosting recent matches, or excluding out-of-stock items while searching by name—the bool query becomes your foundation. We're going to walk through how to structure bool queries the right way, explore the trade-offs between must, filter, should, and must_not clauses, and share lessons from production systems where getting this wrong cost latency and relevance.
Why bool queries matter
The bool query is Elasticsearch's primary tool for combining conditions. Unlike simple term or match queries, bool lets you layer logic: some clauses must match, others should match to boost scores, and some must be excluded entirely. The challenge isn't the syntax—it's knowing which clause to use and how they interact with scoring, caching, and query performance.
In production, we've seen teams use must for everything, then wonder why their queries are slow and their relevance scores feel flat. We've also seen teams over-engineer bool queries with deeply nested logic that becomes unmaintainable. The sweet spot is understanding the semantic difference between each clause and the performance implications of your choice.
Must: matches and scoring
A must clause requires a document to match the condition. It also contributes to the relevance score—documents matching more must clauses rank higher. Use must when a condition is semantically required to your query intent.
Example: you're searching an e-commerce catalog and want to find mugs with "travel" in the name or description. The match on the term "mug" is central to your query; documents without it shouldn't appear at all.
{
"query": {
"bool": {
"must": [
{ "match": { "name": "mug" } },
{ "match": { "description": "travel" } }
]
}
}
}Both must clauses fire scoring functions. A document matching both will score higher than one matching only the name. This is useful when you want relevance refinement, but it carries a cost: Elasticsearch evaluates the scoring logic for every document that passes the bool filter, which can slow queries if the must clause matches millions of documents.
In production, we've learned to reserve must for genuinely required conditions. If you need to narrow results but don't care about score contribution, filter is almost always better.
Filter: matches without scoring, with caching
A filter clause requires documents to match, just like must, but it does not contribute to the relevance score. Every document either matches the filter or doesn't; there's no scoring computation. Elasticsearch also caches filter results, making repeated filters dramatically faster.
Use filter for categorical or range-based constraints: status, category, price range, inventory level, date ranges. These are business logic constraints, not relevance factors.
{
"query": {
"bool": {
"must": [
{ "match": { "name": "mug" } }
],
"filter": [
{ "term": { "tags": "kitchen" } },
{ "range": { "price": { "lte": 20 } } },
{ "term": { "status": "in_stock" } }
]
}
}
}This query finds mugs (must clause, affects score) that are in the kitchen category, cost $20 or less, and are in stock (all filter clauses, no score contribution). The filter clauses are evaluated first and often hit the cache; only documents passing all filters enter the scoring pipeline for the must clause.
The performance difference is substantial. In a catalog of millions of products, a filter on category and price eliminates 90% of documents before scoring runs. Scoring only fires for the remaining 10%, and if the same filter combination runs again, Elasticsearch serves the result from cache.
A common pattern in production: wrap your business logic constraints in filter, and use must only for the core text search. This keeps queries fast and scores meaningful.
Should: optional boosts
A should clause matches optionally. Documents don't have to match any should clause to be returned, but if they do, they receive a relevance boost. In the absence of must clauses, should clauses apply a minimum threshold (by default, one match is enough to boost).
Use should when you want to promote certain results without requiring them. Common scenarios: boosting recent documents, promoting high-rated items, or surfacing documents with specific tags.
{
"query": {
"bool": {
"must": [
{ "match": { "name": "mug" } }
],
"filter": [
{ "term": { "status": "in_stock" } }
],
"should": [
{ "range": { "publish_date": { "gte": "now-7d" } } },
{ "range": { "rating": { "gte": 4.5 } } }
]
}
}
}This query returns mugs in stock (must + filter). If they're recent or highly rated, they rank higher. A mug matching both should clauses scores significantly better than one matching neither, but all are returned.
A key insight from production: if you have must clauses, should boosts are additive to the base score and are typically subtle. If you have no must clauses (only should clauses), the semantics change—at least one should clause must match for a document to be returned. We learned this the hard way when a search stopped returning results because the should logic became the gate instead of the boost.
Must_not: exclusions
A must_not clause excludes documents matching the condition. Documents matching must_not never appear in results, regardless of other clauses. must_not does not contribute to scoring.
Use must_not for negative constraints: exclude archived items, exclude flagged-for-deletion documents, or exclude competitor brands from your results.
{
"query": {
"bool": {
"must": [
{ "match": { "name": "mug" } }
],
"filter": [
{ "term": { "status": "in_stock" } }
],
"must_not": [
{ "term": { "archived": true } },
{ "terms": { "brand": ["competitor_a", "competitor_b"] } }
]
}
}
}This query returns in-stock mugs, boosting by name relevance, but excludes archived items and two specific brands. Every document matching a must_not clause is filtered out before scoring.
One production lesson: must_not can be expensive if the excluded condition matches many documents. Elasticsearch still evaluates it, and the exclusion cost grows with cardinality. If you're excluding 50% of your dataset, you're doing extra work. In those cases, consider inverting the logic: use a positive filter instead of a negative exclusion.
Common mistakes
Using must for filters. We see this constantly: teams use must clauses for status, category, and price range. These should be filter clauses. You gain no relevance value from scoring against "status equals active," but you lose caching and spend CPU on scoring. Move business logic to filter.
Over-nesting bool queries. Some teams build bool queries inside bool queries, sometimes multiple levels deep. This is legal but often unnecessary. Flatten your conditions into a single bool query with multiple must, filter, should, and must_not clauses. Deeply nested structures are harder to debug and often slower.
Forgetting should behavior without must. A bool query with only should clauses requires at least one match. A bool with must clauses treats should as a pure boost. These are different gates. We've deployed queries that worked in testing (where one should matched) but failed in production (where none matched).
Ignoring cache opportunities. Filter clauses are cached, but only if the filter is idempotent and self-contained. Complex nested queries, scripts, or function_score queries inside filter clauses often can't be cached. Keep filters simple and cacheable.
Must_not on high-cardinality fields. If your must_not excludes millions of documents, you've added latency without much benefit. Invert the logic to a positive filter when possible.
Production trade-offs
In high-traffic systems, the must vs. filter decision has real latency implications. A query with multiple must clauses on high-cardinality fields can score millions of documents, adding 100+ milliseconds to response time. Moving those conditions to filter drops the scoring load to documents that pass the filter gate, often reducing latency to 10–20 milliseconds.
Caching is the second lever. Cached filter results are nearly free on repeated queries. In our e-commerce platform, the same category and price filters run thousands of times per minute. Caching filters reduced our query latency by 60% compared to using must clauses for the same conditions.
The trade-off is relevance. If your filter gates out relevant results too aggressively, you've solved performance at the cost of recall. You need both: fast queries and correct results. The solution is testing: profile your queries with real data, measure latency and recall, and adjust the bool structure based on what you observe.
Key takeaways
- Use must for conditions that are semantically required and contribute to relevance scoring. Reserve it for the core search intent.
- Use filter for business logic constraints: category, status, price range, dates. Filters are cached and faster; move as much logic as possible from must to filter.
- Use should to boost results without requiring them. In the presence of must clauses, should is a ranking signal; without must, should becomes a gate.
- Use must_not for negative constraints, but invert the logic if the exclusion would filter out most of your dataset.
- Keep bool queries flat. Avoid deep nesting unless the logic genuinely requires it.
- Profile queries in production. Measure latency, recall, and cache hit rates. Bool structure decisions should be driven by observed behavior, not assumptions.






Comments
Loading comments…