Composing complex searches: the bool query with must, should, must_not, and filter; boosting query, constant_score, dis_max; filter caching and strategies for using filter vs query for optimal performance.

The basic queries from episode 6 are fine for simple cases, but the real world is rarely that simple: users want "plain shirts, black, under 100 thousand, a specific brand, but not kids' sizes". A single query DSL clause isn't enough — you need compound queries: combining several queries into one with boolean logic and relevance control.
Episode 8 covers the main compound queries: bool with must, should, must_not, and filter; boosting, constant_score, and dis_max; plus the filter caching strategy that determines search performance in production.
bool is the most important compound query in Elasticsearch — like WHERE + OR + NOT in SQL, but far more expressive. It has four clauses:
| Clause | Behavior | Relevance score |
|---|---|---|
must | Documents must match (AND logic) | Computed |
should | Documents ideally match (OR logic) | Computed |
must_not | Documents must not match | Not computed |
filter | Documents must match, results are cached | Not computed |
{
"query": {
"bool": {
"must": [
{ "match": { "name": "kaos polos" } }
],
"should": [
{ "match": { "description": "bahan katun" } }
],
"must_not": [
{ "term": { "size.keyword": "kids" } }
],
"filter": [
{ "term": { "category.keyword": "fashion" } },
{ "range": { "price": { "lte": 100000 } } }
]
}
}
}Let's read it slowly: documents must contain the words "kaos polos" in their name, are more relevant if the description mentions "bahan katun", must not be of kids' size, and must be in the fashion category with a price below 100 thousand. Note the should: without must/filter, at least one should must match; with must/filter, should only becomes a relevance bonus.
The should clause boosts the score of documents that match it. This is a common trick for setting priority: find the required matches with must, then use should to push the most relevant documents to the top. You can control this further with minimum_should_match if you need to require a minimum number of matching should clauses.
Sometimes we don't want to exclude documents, only lower their priority. boosting consists of positive (what you're searching for, with a boost) and negative (what gets penalized, with a negative_boost between 0 and 1):
{
"query": {
"boosting": {
"positive": {
"match": { "name": "kaos" }
},
"negative": {
"term": { "rating.keyword": "low" }
},
"negative_boost": 0.2
}
}
}Documents with a "low" rating still appear, but their score is multiplied by 0.2 — effectively sinking below other results.
constant_score wraps a query and gives it a constant score — usually combined with filter. Since filters don't compute scores, the result uses a default score that can be set with boost:
{
"query": {
"constant_score": {
"filter": {
"term": { "category.keyword": "fashion" }
},
"boost": 1.5
}
}
}When to use it? When you only need filtering without caring how relevant something is — for example a list of products in a single category sorted by price rather than score. The results are uniform and fast.
dis_max (disjunction max) combines several queries over several fields, but takes the highest score among them instead of summing them. This is ideal when the same word can appear in different fields and you don't want a "doubled" score to distort ranking:
{
"query": {
"dis_max": {
"queries": [
{ "match": { "name": "kaos" } },
{ "match": { "brand": "kaos" } }
],
"tie_breaker": 0.3
}
}
}If a document matches both name and brand, dis_max doesn't sum them — it takes the highest, then adds tie_breaker (0–1) times the second score as a small bonus. The result is fairer ranking than bool must, which sums scores.
Tip
Your choices: bool for AND/OR logic with full control, dis_max for picking the best field without penalizing multiple matches, constant_score for pure filtering. Most production searches use bool as their outer framework.
This is the main reason filters are cached: results from the same query run repeatedly — for example the same category filter on every page — can be cached in memory so subsequent queries don't recompute them. Scored queries are not cached this way. Under high load, moving rarely-changing criteria into filter can drastically reduce search latency.
| Criteria | Right Place |
|---|---|
| Category, status, price, date | filter — yes/no, cached |
| Free-text search | must or should — needs scoring |
| Exclusions without scoring | must_not |
| Mandatory constraints without affecting score | filter |
Rule of thumb: if a criterion doesn't change relevance ranking, put it in filter. Filters that rarely change (category, price range) deliver the biggest cache benefit; filters that constantly change (for example a dynamic user-location filter) won't benefit from the cache as much.
Warning
The filter cache uses JVM memory. Too many distinct filters actually pressure the cache and trigger pressure — a sign that the filters are too "unique" to be useful. Monitor it via the filter_cache node stats; we'll discuss these metrics in episode 21.
must for criteria that don't need scoring. Category and price in must force wasteful score computation — move them to filter.
must_not using match instead of term. For keyword fields, term is more appropriate; match analyzes text and can produce surprises.
Putting the entire filter set in query_string. Query strings are prone to parsing errors; break them into a structured bool.
Too many should clauses without minimum_should_match. Results can become too loose or unpredictable.
Disproportionate boosting. Extreme boost values make results nonsensical — increase gradually and evaluate.
In episode 8 you mastered compound queries: bool with must, should, must_not, and filter; boosting to lower priority; constant_score for constant-score filtering; dis_max for picking the best field; plus the filter caching strategy and when to use filter vs query.
Key takeaways:
bool is the main framework for production queries — master all four clauses.should gives a relevance bonus when must/filter already exist.filter is cached and doesn't compute scores — put yes/no criteria there.dis_max takes the best score, great for words that can appear in several fields.Relevant search alone isn't enough — you also want the data picture: total sales, average price per category, monthly trends. That's the job of aggregations. In episode 9 we'll cover aggregations: metrics (sum, avg, stats, cardinality, percentiles), buckets (terms, histogram, date_histogram, nested), and pipeline (moving average, derivative, cumulative sum, bucket sorting). See you there!