Lesson 0006 · Query DSL
bool queryThe workhorse compound query — four clause types, one scoring rule, one gotcha.
From memory: of the four bool keys, which two run in query context
(produce a _score) and which two run in filter context (no score, cacheable)?
— Scored: must, should. Unscored + cached: filter, must_not.
That single split (Lesson 0001) is the backbone of everything below.
Before the new material, one carry-over you needed two passes on last time:
IDF's n equals what, physically? — the length of the term's postings
list (its document frequency). Say it, then read on.
bool combines clauses under four keys. Each key answers two questions at once: does
it filter the result set? and does it affect the score?
— Query DSL: bool query
| Clause | Meaning | Scores? | Cached? | SQL-ish |
|---|---|---|---|---|
| must | must match | yes | no | AND (scored) |
| should | preferably matches | yes | no | OR / boost |
| filter | must match | no | yes | AND (unscored) |
| must_not | must not match | no | yes | AND NOT |
Note that must and filter are the same logical constraint — both
require the clause to match. The only difference is scoring: must contributes to
_score; filter ignores it and becomes cacheable. “The filter
and must_not clauses… include or exclude results without impacting the score.”
— bool query
“The bool query takes a more-matches-is-better approach, so the score from each
matching must or should clause will be added together to
provide the final _score.”
— bool query
Each scored clause computes BM25 (0002); bool just
sums them. So should clauses are how you say “also nice if it matches this” — they
lift the score of docs that satisfy them.
should means depends on its neighboursThis is the single most-missed bool behaviour, driven by the
minimum_should_match default:
“If the bool query includes at least one should clause and no
must or filter clauses, the default value is 1. Otherwise,
the default value is 0.”
— bool query
must or filter present → should is a
pure optional booster. It does not shrink the result set; it only re-ranks.
Adding a should here never removes a hit.must/filter → at least one
should must match, so should becomes the required matcher (an OR).“I added a should and got fewer results” (or “more”) is almost always this rule biting.
Put each concern in the clause that fits its scoring and caching intent — this is the design skill:
{ "query": { "bool": {
"must": { "match": { "title": "tuning" } }, // relevance — scored
"filter": [ { "term": { "status": "published" } }, // yes/no — cached
{ "range": { "year": { "gte": 2020 } } } ],
"must_not": { "match": { "title": "deprecated" } }, // exclude — cached
"should": { "match": { "title": "elasticsearch" } } // boost — optional
} } }
Full-text relevance → must/should. Exact/range/enum constraints →
filter (cheap, cached, per 0001).
Exclusions → must_not.
From memory — effortful recall is the point. Feedback is immediate.
Which two clause types contribute to _score?
must and should run in query context and their BM25 scores are summed. filter and must_not run in filter context — no score.A filter clause affects results by:
filter is the same "must match" constraint as must, but its score is ignored — which also makes it cacheable.With a must clause present, should clauses become:
must/filter exists, minimum_should_match defaults to 0, so should only re-ranks — it never filters the result set.With no must or filter, how many should must match?
should clauses, minimum_should_match defaults to 1 — at least one must match, so should acts like OR.must_not and filter are cheap on repeat because they are:
should behave two waysOn your live cluster. Seed a tiny index:
PUT /lab_bool
{ "mappings": { "properties": {
"title": { "type": "text" },
"status": { "type": "keyword" },
"year": { "type": "integer" }
}}}
POST /lab_bool/_bulk
{"index":{"_id":1}}
{"title":"elasticsearch tuning guide","status":"published","year":2021}
{"index":{"_id":2}}
{"title":"elasticsearch scaling guide","status":"published","year":2022}
{"index":{"_id":3}}
{"title":"postgres tuning guide","status":"published","year":2019}
{"index":{"_id":4}}
{"title":"elasticsearch basics","status":"draft","year":2023}
GET /lab_bool/_search
{ "query": { "bool": {
"must": { "match": { "title": "guide" } },
"filter": [ { "term": { "status": "published" } },
{ "range": { "year": { "gte": 2020 } } } ],
"should": { "match": { "title": "tuning" } }
} } }
Predict before running: docs 1 and 2 survive (3 is pre-2020, 4 has no “guide” and is draft), and
should pushes doc 1 above doc 2 because it also matches “tuning” — but both still
appear. The should re-ranked; it did not filter.
must/filter — same should, new meaningGET /lab_bool/_search
{ "query": { "bool": {
"should": [ { "match": { "title": "tuning" } },
{ "match": { "title": "scaling" } } ]
} } }
Now minimum_should_match is 1, so only docs matching “tuning” or “scaling” return
(1, 2, 3 — not 4). Same should clauses, opposite role. Then
DELETE /lab_bool and bring any surprise to your teacher.
Query DSL — Boolean query (Elastic Docs, current).
The authoritative reference for the four occurrence types, the minimum_should_match
default, and the more-matches-is-better scoring rule. Current-version, so the syntax is safe to copy.