Lesson 0001 · Relevance & Query DSL
One distinction that shows up in almost every real query — and almost every interview.
You already write Elasticsearch queries. This lesson sharpens one thing you half-know into something you can defend: every clause you write runs in one of two contexts, and choosing the right one is simultaneously a relevance decision and a performance decision. Interviewers probe this precisely because it reveals whether you understand what the engine is actually doing.
Every clause answers one of two questions:
| Context | Question it asks | Output |
|---|---|---|
| Query context | “How well does this document match?” | A relevance _score |
| Filter context | “Does this document match — yes or no?” | No score. Just inclusion. |
Query context computes a _score (default: BM25 — see Lesson 0002).
Filter context skips scoring entirely: a document is either in the set or it isn't.
— Elastic Docs: Query and filter context
The bool query is where this becomes concrete. Its four keys split cleanly across the two contexts:
| bool key | Context | Must match? | Affects score? |
|---|---|---|---|
must | query | Yes | Yes |
should | query | Optional* | Yes |
filter | filter | Yes | No |
must_not | filter | Must not | No |
* should is where people trip. See “The should trap” below.
must and filter do the same matching — both require the clause to match.
The only difference is that must spends effort computing a score and filter doesn't.
So if a clause shouldn't influence ranking, putting it in must is pure waste.
Constraints like status = published, a date range, or a permission check are
binary facts, not signals of relevance. If you score them, an exact-match term on a
rare status value can distort ranking. Filtering them keeps _score reflecting only
what the user actually searched for.
Filter-context clauses are eligible for the node query cache: Elasticsearch remembers which documents matched as a compact bitset, keyed by the filter. Reuse that filter on the next request and it's a cache hit — no re-evaluation, no scoring math. — Elastic Docs
That's why the standard pattern is: full-text in must, constraints in
filter.
GET /articles/_search
{
"query": {
"bool": {
"must": { "match": { "title": "elasticsearch tuning" } }, // scored (relevance)
"filter": [
{ "term": { "status": "published" } }, // yes/no, cacheable
{ "range": { "published_at": { "gte": "2024-01-01" } } }
],
"must_not": { "term": { "archived": true } } // exclusion, no score
}
}
}
The match in must ranks the results. Everything in filter
and must_not just carves the candidate set — cheaply and cache-friendly.
What does should mean depends on its neighbors. With no
must/filter, at least one should must match
(minimum_should_match defaults to 1) — it behaves like OR.
Add a must or filter, and should becomes optional
(default 0): a pure relevance boost for docs that happen to match.
Need a clause to filter (match/no-match, cached) but still return a score?
Wrap it in constant_score — every match gets the same fixed score (default 1.0),
with none of the BM25 cost.
Filters change which documents come back — they are not "free extras." Moving a
clause from must to filter never changes the result set
(matching is identical); it only removes that clause's contribution to _score
and makes it cacheable.
Answer from memory — the effortful recall is what builds retention. Feedback is immediate.
Which bool clause runs in filter context (no score computed)?
filter and must_not are filter context. must and should are query context and contribute to _score.A status: active constraint must not influence ranking. Where does it belong?
filter: same matching as must, but no wasted scoring and it becomes cacheable.Why are filter-context clauses often faster on a repeated query?
A bool has one must clause and two should clauses. Default minimum_should_match is:
must is present, should becomes optional (default 0) — it only boosts the score of docs that happen to match.Moving a matching clause from must to filter changes what?
_score, and it becomes cacheable.Recall is one thing; watching the numbers move is another. Run this against a
live cluster (Kibana Dev Tools shown; curl works too). The goal is to
see that must → filter changes the score but not the
result set.
PUT /lab_articles/_bulk
{"index":{"_id":1}}
{"title":"elasticsearch tuning guide","status":"published"}
{"index":{"_id":2}}
{"title":"elasticsearch relevance tuning tuning","status":"published"}
{"index":{"_id":3}}
{"title":"postgres tuning","status":"published"}
{"index":{"_id":4}}
{"title":"elasticsearch tuning","status":"draft"}
// A — constraint in must (scored)
GET /lab_articles/_search
{ "query": { "bool": { "must": [
{ "match": { "title": "elasticsearch tuning" } },
{ "term": { "status": "published" } }
]}}}
// B — constraint in filter (not scored)
GET /lab_articles/_search
{ "query": { "bool": {
"must": { "match": { "title": "elasticsearch tuning" } },
"filter": { "term": { "status": "published" } }
}}}
Before running B: will the set of returned docs change? Will the
_score values change? Commit to an answer, then run it.
Same hits, same order in A and B — docs 1, 2, 3 (doc 4 is
draft, excluded either way). Matching is identical.
Every _score is higher in A than in B, because the
status term adds its own contribution in query context. In B that clause
is a silent yes/no. That gap is the lesson, made numeric.
// must_not is filter context → excludes, never scores
GET /lab_articles/_search
{ "query": { "bool": {
"must": { "match": { "title": "tuning" } },
"must_not": { "match": { "title": "postgres" } }
}}}
// constant_score → matches like a filter, flat score of 1.0 (no BM25)
GET /lab_articles/_search
{ "query": { "constant_score": { "filter": { "match": { "title": "elasticsearch" } } } } }
Expect every hit in the last query to have exactly "_score": 1.0. When
you're done, DELETE /lab_articles to clean up — and bring your A-vs-B
_score numbers to your teacher if anything surprised you.
Elastic Docs — Query and filter context. Short, authoritative, and the reference every one of these claims traces back to. Read it end to end; it's under 500 words.