Asking Elasticsearch: match, filter, bool and aggregations
After this lesson you will turn every Elasticsearch sentence of the exam — "includes the words", "removing the ones", "must not affect the final score", "prioritising", "the count for each" — into the right clause of a bool query or a terms aggregation, and predict the hits and scores that come back.
The lecture searches New York's restaurant inspections — the nyc-restaurants index of lesson 25 — with one query built in three steps (pp. 604–606). Step one keeps the inspections between 23 February 2020 and 23 February 2021: 9510 hits, each with score 1.0. Step two throws out the violation code 02G: 8923 hits, still 1.0. Step three adds a clause that the slide labels OR: the borough is Queens.
An OR can only let more documents in; read as a condition, the clause would keep Queens alone. Write down your guess for the number of hits.
The slide prints 8923 — not one more, not one fewer. Yet the best score jumps from 1.0 to 2.487945, and a Queens restaurant moves to the top. 2.487945 is 1.0 plus 1.4879451, a score printed on another slide (p. 597). The last section puts the two numbers together; by then you will have predicted them yourself.
match: the words you meant
A search is one HTTP request to the _search endpoint of an index, with a JSON body; the slide frames the request line as "Query" and the JSON as "Body" (p. 595).
# the shape of every search (p. 595)
GET /nyc-restaurants/_search
{
"query": {
"match": {
"FIELD": "TEXT"
}
}
}
Read it aloud: from nyc-restaurants, get the documents whose FIELD matches TEXT. The session calls match, term and range leaf query clauses — they "look for a particular value in a specific field" — and bool a compound query clause, which wraps others "to combine multiple queries logically" (p. 1641). In SQL a leaf is one condition of a WHERE; bool is the AND, OR and NOT around the conditions.
The lecture's first real searches are two match queries. One on INSPECTION TYPE brings back inspections, the first scored 0.53188044 (p. 596); one on the borough, "BORO": "Queens", brings back restaurants, the first scored 1.4879451 (p. 597). With "queens" in lower case the complete answer is (p. 598):
# the response to a match on "BORO": "queens" (p. 598)
{
"took" : 0,
"timed_out" : false,
"_shards" : { "total" : 1, "successful" : 1, "skipped" : 0, "failed" : 0 },
"hits" : {
"total" : { "value" : 0, "relation" : "eq" },
"max_score" : null,
"hits" : [ ]
}
}
Learn to read hits: total.value counts the matches, and relation says whether that count is exact ("eq") or a lower bound ("gte"); max_score is the best score, null when nothing matched; the array hits lists the documents, best first and 10 at most by default, each with its _score and its stored JSON under _source.
Why nothing? "Borough (BORO) was mapped as a keyword. A keyword does not have any analyser!" (p. 598). A match query sends its text through the field's analyzer: "Whenever a text field is provided, it is analyzed before matching" (p. 1644). On a text field that gives lower-case words, and by default a document qualifies when it contains at least one of them; on a keyword field nothing is cut, and the whole value must equal the stored one byte for byte, capital Q included. Lesson 25 shows how the mapping takes that decision; here you see what it does to your queries.
match analyses its text with the field's own analyzer: separate words on a text field, the whole value on a keyword field.
How many of the words?
The operator decides how many of the analysed words a document needs: OR, "at least 1 word among the ones returned by the analyzer", is "the DEFAULT"; AND wants "all the words" (p. 1646). The session tries both on its Steam video games, index steam_overviews (p. 1647):
# the AND variant of p. 1647: overview contains free, to and play
GET /steam_overviews/_search
{
"query": {
"match": {
"overview": {
"query": "free-to-play",
"operator": "and"
}
}
}
}
The analyzer cuts "free-to-play" into free, to and play: with "or", the slide's request, 61 games match; with "and", 35. The best score is 1.4712923 both times (p. 1648): the top game contains all three words, and its score, a sum over them, cannot change. The operator decides who qualifies, not how a qualifying document is scored.
Options live inside the field's object, next to "query"; when there are none, the compact form is enough, as in "Extract all games which tags field contains the "Free-to-Play" tag" (p. 1665):
# games whose tags contain free, to or play (p. 1665)
GET /steam_overviews/_search
{
"query": {
"match": {
"tags": "Free-to-Play"
}
}
}
One field per match, and a count without documents
Name two fields in one match and Elasticsearch refuses: "[match] query doesn't support multiple fields, found [SCORE] and [BORO]", status 400 — "You can only match 1 field at time" (p. 600). Two conditions are two clauses inside a bool (section 3). When only the number matters, the same body goes to _count (p. 599):
# how many inspections scored 13 (p. 599)
GET /nyc-restaurants/_count
{
"query": {
"match": {
"SCORE": 13
}
}
}
The answer is "count" : 32930: no hits, no scores. The number comes back in section 4.
Exact conditions and the filter context
For an exact value the session has the term query, which returns "documents that contain an exact term in a provided field". Underlined on the same slide, DO NOT in red: "DO NOT use term queries for text fields. Remember that text fields are analyzed" (p. 1649).
# games published by exactly "Ubisoft " (p. 1649)
GET /steam_overviews/_search
{
"query": {
"term": {
"publisher": {
"value": "Ubisoft "
}
}
}
}
The space inside "Ubisoft " is not a typo. The publishers of this dataset end with a space — the stored game reads "publisher": "Daybreak Game Company " (p. 1640) — and a term compares bytes: without the space, nothing matches. The compact form { "term": { "publisher": "Ubisoft " } } is the session's answer to "Extract all the games which publisher is "Ubisoft"" (p. 1664). Fig. 1 shows what the warning about text fields protects you from.
term: the bytes you typed
match: the words you meant
term looks up "Male" exactly as typed and finds nothing; match first runs the field's analyzer, turns "Male" into male, and finds three documents. On a keyword field — or on the Gender.keyword sub-field of section 6 — the whole value "Male" is stored as one term, and term is the right tool.Two parameters "must be added within the field, together with the value parameter" (p. 1651): case_insensitive (default false) lets a term ignore case, and boost "is a float number that multiplies the relevance score of a query", useful when the scores of several clauses add up.
# "Ubisoft " in any case, with the parameter of p. 1651
GET /steam_overviews/_search
{
"query": {
"term": {
"publisher": {
"value": "ubisoft ",
"case_insensitive": true
}
}
}
}
The third leaf, range, takes the bounds gt, gte, lt and lte and is "usually employed with numerical fields or dates" (p. 1650):
# published in the last ten days, score doubled (p. 1650)
GET /steam_overviews/_search
{
"query": {
"range": {
"publish_date": {
"gte": "now-10d/d",
"lte": "now",
"boost": 2.0
}
}
}
}
The Steam index has no publish_date (p. 1638 lists five fields): the slide shows the syntax only. "now-10d/d" is date math: an anchor, now; a step back of ten days, -10d; a rounding to the day, /d (p. 1652).
Two questions: how well, or whether
"By default, Elasticsearch sorts matching search results by Relevance Score, which measures how well each document matches a query" (p. 1642) — today a BM25 score, the descendant of the TF-IDF of lesson 24. Not every condition has a "how well". The lecture's slide on filters lists five properties (p. 601): "Exact match", "For fields that are not analyzed", "No relevance", "The filter is either satisfied or not", "Cacheable". Its example wraps a term in the filter of a bool (p. 602):
# inspections with violation code 04M, unscored (p. 602)
GET /nyc-restaurants/_search
{
"query": {
"bool": {
"filter": {
"term": {
"VIOLATION CODE": "04M"
}
}
}
}
}
8711 hits, "max_score" : 0.0, and "_score" : 0.0 on every hit. The session names the two worlds (p. 1643). Query context asks "How well does this document match this query clause?": it decides whether the document matches, and the relevance score IS computed. Filter context asks "Does this document match this query clause?": it decides the same thing, but the score IS NOT computed; it is "Applied with the filter or must_not parameter". Fig. 2 sends one clause through both.
Why keep two contexts? A yes/no answer depends neither on the other clauses nor on the statistics of the index, so Elasticsearch can store it as a bit set and reuse it in the next query, and it skips the scoring work. That is the lecture's "It's more efficient than doing only queries" (p. 608). A filter is also how you ask for no score at all, as in "Extract all games which publisher is exactly "Lag Studios" without computing the relevance score" (p. 1667):
# publisher exactly "Lag Studios ", no score (p. 1667)
GET /steam_overviews/_search
{
"query": {
"bool": {
"filter": [
{ "term": { "publisher": "Lag Studios " } }
]
}
}
}
The lecture's last filter is prefix: the documents with a word that starts with the given string (p. 607).
# descriptions with a word starting with "flies" (p. 607)
GET /nyc-restaurants/_search
{
"query": {
"bool": {
"filter": {
"prefix": {
"VIOLATION DESCRIPTION": "flies"
}
}
}
}
}
The first hit's description begins "Filth flies": the prefix was tested word by word on an analysed text field, not on the whole value. The prefix itself is not analysed, like a term: "Flies" would find nothing. The response reads "value" : 10000, "relation" : "gte" — at least ten thousand hits. Elasticsearch counts exactly only up to 10000 unless asked to go on (background, from the Elasticsearch documentation).
bool: four clauses, two questions
Real questions carry several conditions, and the one-field rule forbids packing them into one match. The lecture answers with a skeleton of three arrays, framed and labelled (p. 603): must — AND, must_not — NOT, should — OR. The fourth array, filter, is where the lecture's own filters sat (pp. 602, 607); the session gives the rule of each in a table (p. 1653):
must: "The query must appear in matching documents and it will contribute to the score."filter: "The query must appear in matching documents. However, unlike must the score of the query will be ignored."should: "The query should appear in the matching document. If so, it will contribute to the score. If should is the only clause included, at least one of the conditions must be satisfied."must_not: "The query must not appear in the matching documents. The scoring is ignored."
Two questions sort the four (Fig. 3): must a document match the clause, and does the clause add to its score?
bool as answers to two questions. The two empty cells explain why there are exactly four: a clause that neither restricts nor scores would do nothing, and a document that matches a must_not clause is gone, so nothing is left to score.Each array holds a list of clauses — "Each of the clauses described may contain multiple conditions" (p. 1654) — and a query may use "all or just a few of the clauses" (p. 1655):
# Multiplayer games by exactly "Ubisoft ", Action first (p. 1655)
GET /steam_overviews/_search
{
"query": {
"bool": {
"must": [
{ "match": { "tags": "Multiplayer" } }
],
"filter": [
{ "term": { "publisher": "Ubisoft " } }
],
"should": [
{ "match": { "tags": "Action" } }
]
}
}
}
Read it clause by clause. Tagged Multiplayer: required and scored. Published by exactly "Ubisoft ": required, no score. Tagged Action: not required, but its score is added, so the Action games rise. A hit's _score is the sum of the scores of its must clauses and of the should clauses it matches, each multiplied by its boost; filter and must_not add nothing.
Next to a must or a filter, should never removes a document: it only adds score.
That second life of should is the session's "prioritizing" — "Extract all games which tags field contains the "Free-to-Play" tag, prioritizing those whose developer is "Daybreak Game Company"" (p. 1666):
# Free-to-Play games, Daybreak's first (p. 1666)
GET /steam_overviews/_search
{
"query": {
"bool": {
"must": [
{ "match": { "tags": "Free-to-Play" } }
],
"should": [
{
"term": {
"developer": {
"value": "Daybreak Game Company "
}
}
}
]
}
}
}
Both widgets search the twelve dishes of Table 1, ours, in the style of the restaurant of the exam of 6 Sep 2024. Glance at the descriptions and recipes: the tasks ask you to predict.
dishes of both widgets. name, description and recipe are text (with name.keyword as a keyword sub-field), course is keyword, price is double.| id | name | course | price | description | recipe |
|---|---|---|---|---|---|
| 1 | Salmon tartare | starter | 14.00 | Raw salmon with lime | Dice the salmon and stir in the lime juice |
| 2 | Grilled salmon | main course | 18.00 | Salmon fillet with butter and herbs | Grill the fillet and brush it with melted butter |
| 3 | Salmon pasta | first course | 12.00 | Fresh pasta with smoked salmon, cream and chives | Boil the pasta then stir in the salmon and the cream |
| 4 | Salmon salad | starter | 9.50 | Smoked salmon and salmon roe on fresh leaves | Toss the leaves and lay the salmon on top |
| 5 | Focaccia ligure | starter | 6.00 | Flat bread with olive oil and coarse salt | Knead the dough, let it rise and bake it |
| 6 | Risotto alla milanese | first course | 12.00 | Saffron risotto finished with butter | Toast the rice, add the broth and stir until creamy |
| 7 | Trofie al pesto | first course | 11.00 | Ligurian pasta with basil pesto | Boil the trofie and stir in the pesto |
| 8 | Minestrone | first course | 8.00 | Thick vegetable soup | Boil the vegetables slowly with the beans |
| 9 | Cod with potatoes | main course | 16.00 | Baked cod with potatoes and olives | Bake the cod on a bed of potatoes |
| 10 | Tiramisu | dessert | 7.00 | Coffee and mascarpone dessert | Dip the biscuits in coffee and stir the mascarpone with sugar |
| 11 | Butter cookies | dessert | 5.00 | Shortbread made with plenty of butter | Mix butter, sugar and flour, then bake |
| 12 | Panna cotta | dessert | 6.00 | Cooked cream with berry sauce | Boil the cream with sugar and let it set |
"Filter and Query combination"
The lecture sums up the gain in three lines (p. 608): "You can combine filter and queries", "It's more efficient than doing only queries", "You can increase the relevance of a document at query time". Its example, though, labels its clauses in a way that would mislead you at the exam.
# the lecture's "Filter and Query combination" (p. 609)
GET /nyc-restaurants/_search
{
"query": {
"bool": {
"must": [
{
"match": {
"SCORE": "20"
}
},
{
"term": {
"BORO": {
"value": "Manhattan"
}
}
}
]
}
}
}
One more address trick (p. 610): "Elasticsearch allows to search in multiple index at the same time". /_search searches every index, index_1,index_2/_search a list of them, and prefix*/_search every index whose name starts with the prefix.
Counting without reading: aggregations
The lecture starts from SQL (p. 611): SELECT score, count(*) FROM nyc-restaurants GROUP BY score becomes
# inspections per score, SQL's GROUP BY (p. 611)
GET /nyc-restaurants/_search
{
"size": 0,
"aggs": {
"score_groups": {
"terms": {
"field": "SCORE"
}
}
}
}
Read it line by line. "size": 0: send back no documents — "size=0 means we don't want actual documents, just the aggregation results" (p. 1656). "aggs": the aggregations, a sibling of "query". "score_groups": a name you choose, which the response repeats. terms: a bucket aggregation — one bucket per distinct value of SCORE, each with its doc_count. The response (p. 612):
# the first four buckets of the response (p. 612)
"aggregations" : {
"score_groups" : {
"doc_count_error_upper_bound" : 0,
"sum_other_doc_count" : 184789,
"buckets" : [
{ "key" : 12, "doc_count" : 41375 },
{ "key" : 13, "doc_count" : 32930 },
{ "key" : 10, "doc_count" : 21892 },
{ "key" : 11, "doc_count" : 21330 },
…
Bucket 13 holds 32930 inspections — the _count of section 1. The buckets arrive sorted by doc_count, largest first. And "sum_other_doc_count" : 184789 says that 184789 inspections fell in buckets that were not returned at all: a terms aggregation returns only the 10 biggest buckets unless its own size is raised (background, from the Elasticsearch documentation). So the slide's request is GROUP BY with ORDER BY count DESC LIMIT 10. "doc_count_error_upper_bound" : 0 says the counts are exact; on an index with several shards they can be approximate.
course, with "size": 2 inside terms. Buckets are ranked by doc_count (a tie, dessert and starter with 3, goes to the smaller key); the cut keeps two, and the five documents of the others survive only as a number. On p. 612 the same cut keeps ten scores and leaves 184789 inspections in sum_other_doc_count."With "size": 0 no buckets come back." No. The top-level size is the number of hits — 10 by default — and 0 drops them. The number of buckets is the size inside terms, also 10 by default. The session's sentence "The size parameter defines how many results are returned in the aggregation" (p. 1656) blurs the two; its parenthesis — "size=0 means we don't want actual documents, just the aggregation results" — is the exact meaning of the top-level one.
The lecture lists four families of aggregations: "Metrics aggregations", "Bucket aggregations", "Pipeline aggregations", "Matrix aggregations" (p. 613). The session gives three, with definitions (p. 1656). Metric aggregations "calculate metrics from field values (count, min, max, avg, sum)". Bucket aggregations "group documents into buckets, based on field values (terms), ranges (range), or other criteria (filters, etc..)". Pipeline aggregations "take input from other aggregations instead of documents or fields". The lecture's fourth, matrix, computes statistics over several fields at once; today's documentation counts the session's three families and files those statistics under metrics (background). Know both lists: the course material prints both.
On the 64 Steam games the session's games_per_publisher gives "Daybreak Game Company " 3 games and "sum_other_doc_count": 44 (p. 1657): the ten biggest publishers own only 20 games. Three combinations complete the toolkit. First, a query selects the documents and the aggregation groups those: "Aggregations can be combined with regular queries to filter the data and then perform the aggregation" (p. 1658). The session's exercise "For each developer, extract the count of the games whose tags field contains the "Free-to-Play" tag" (p. 1668):
# Free-to-Play games per developer (p. 1668)
GET /steam_overviews/_search
{
"size": 0,
"query": {
"bool": {
"must": [
{ "match": { "tags": "Free-to-Play" } }
]
}
},
"aggs": {
"f2p_per_devs": {
"terms": {
"field": "developer"
}
}
}
}
The same slide adds an N.B.: "term aggregations only work on keywords (for texts, one can use field_name.keyword to use the keyword version of the field)" (p. 1658) — its "term aggregations" are the terms aggregations of the code, not the term query. Second, several aggregations can sit side by side in one aggs, each its own GROUP BY (p. 1659); with "size": 0 the response has "hits": [] and "max_score": null, while total.value still reports 64. Third, a bucket aggregation can carry sub-aggregations, "computed for each aggregation by considering the documents within each one of them" (p. 1660):
# average price inside each publisher bucket (p. 1660)
GET /steam_overviews/_search
{
"size": 0,
"aggs": {
"games_per_publisher": {
"terms": {
"field": "publisher"
},
"aggs": {
"average_games_per_publisher": {
"avg": {
"field": "price"
}
}
}
}
}
}
Every bucket answers "value": null — "Price does not exists in the mapping, so it is set to null" (p. 1660): the syntax is right, the index has no price. Despite its name, the sub-aggregation averages a price, not games: in pandas it would be df.groupby("publisher")["price"].mean(), kept to the ten largest groups. The widget's dishes have prices, so try it there.
From the exam sentence to the request
Seven of the nine past papers carry an Elasticsearch exercise of 3 or 4 points, always on one entity of the paper's ER diagram, always in two moves. First: "Provide the complete mapping of the index for the considered entity (i.e., field name, field type, the structure of the mapping, etc.)", with a stub "PUT …" or "PUT /" to complete. Then one or two queries. The session's own mapping exercise shows the structure: "Define the full mapping describing a Person with 10 shards and 3 replicas" (p. 1663).
# a Person index with 10 shards and 3 replicas (p. 1663)
PUT /people
{
"settings": {
"number_of_shards": 10,
"number_of_replicas": 3
},
"mappings": {
"properties": {
"personal_id": { "type": "keyword" },
"name": { "type": "text" },
"surname": { "type": "text" },
"birth date": {
"type": "date",
"format": "yyyy-MM-dd"
},
"address": { "type": "text" },
"eye_color": { "type": "keyword" },
"height": { "type": "integer" }
}
}
}
settings belongs in an answer only when the paper asks for shards or replicas; mappings, properties and one entry with its type per attribute are what every paper wants. Choose each type for the queries that follow — lesson 25 has the full list: text for anything searched by its words, keyword for a value matched whole or counted by, integer or double for numbers, date with its format, boolean for a Yes/No attribute. A terms count on a text field fails, so the field you will count by must be keyword. The queries then follow a small phrase book (Table 2).
| The paper says | You write | Papers |
|---|---|---|
| "includes the words "A" and "B"", "must include the words", "contains the words" | two match clauses in must, or one with "operator": "and" | 6 Jun 2024, 24 Jun 2024, 11 Feb 2025, 12 Sep 2025 |
| "removing the ones whose …", "do not include", "does not contain" | that clause in must_not | 6 Jun 2024, 6 Sep 2024, 12 Sep 2025 |
| "The condition on … should not / must not affect the final score" | that clause in filter (a must_not never scores anyway) | 6 Jun 2024, 6 Sep 2024, 13 Jun 2025 |
| "assigning a higher score to …", "prioritising / prioritizing …", "if … is found, assign a higher score" | that clause in should, next to a must | 24 Jun 2024, 6 Sep 2024, 11 Feb 2025, 13 Jun 2025, 12 Sep 2025 |
| "greater than", "exceeds", "at least" | range: gt for "greater than" and "exceeds", gte for "at least" | 6 Sep 2024, 11 Feb 2025, 13 Jun 2025 |
| "whose name is …", "male", "music type is classical" | term on a keyword field, or match on a text field | 6 Sep 2024, 11 Feb 2025, 13 Jun 2025, 12 Sep 2025 |
| "the count of … for each …", "the count of indoor and outdoor …", "the total number of … for each …" | "size": 0 and a terms aggregation on that field | 6 Jun 2024, 24 Jun 2024, 30 May 2025 |
One sentence, read clause by clause
The exam of 11 Feb 2025 followed Formula 1 races. Its Pilot is a Team Member, ISA (P, E), which is a Person, ISA (T, E): Personal_ID, Name, Surname, Birth_Date and Gender come from Person; Championships_Won and Career (Textual) are the pilot's own (the paper prints Career twice). The first sub-question, the mapping (1 PT), is solved in lesson 25: gender is a keyword, championships_won an integer, career text.
Then (2 PT): "Write a query to return male pilots who won at least 10 championships and whose career must include the words "Champion" and "Car", assigning a higher score to the ones whose career includes the word "Young"." Cut it at the clauses. "Male" and "at least 10 championships" are yes/no conditions with no "how well": filter. "Must include the words" is about words: two match clauses in must. "Assigning a higher score": should.
# male pilots, 10+ championships, Champion and Car; Young first
GET /pilots/_search
{
"query": {
"bool": {
"must": [
{ "match": { "career": "Champion" } },
{ "match": { "career": "Car" } }
],
"filter": [
{ "term": { "gender": "Male" } },
{ "range": { "championships_won": { "gte": 10 } } }
],
"should": [
{ "match": { "career": "Young" } }
]
}
}
}
The paper does not say that gender or the championships must leave the score alone, so must would be correct for them too: a term on a keyword field and a range give every hit the same score, which cannot change the order — task 4 of the first widget shows it. filter says what you mean, and costs less.
The counting move is the shortest. The exam of 30 May 2025 (its header misprints the date as "June 30, 2025") followed the production and sales of puzzles; its Puzzle has Puzzle ID, Name, Collection Name (e.g., Museum Collection), Number of Pieces, Width, Height and Description. It asks: "Write a query to compute the total number of puzzles for each collection name. (2 PT)". With collection_name mapped as keyword:
# puzzles per collection name, exam of 30 May 2025
GET /puzzles/_search
{
"size": 0,
"aggs": {
"puzzles_per_collection": {
"terms": {
"field": "collection_name"
}
}
}
}
Thirteen session exercises, and the hook
The session ends with thirteen exercises on the index employees50k, whose attributes it lists with their types (p. 1669): Address, Designation, FirstName, Gender, Interests, LastName and MaritalStatus as Text; Age and Salary as Integer; DateOfJoining as Date. The strings behave as dynamically mapped ones, text with a keyword sub-field: the last solution counts on Gender.keyword (p. 1682). Write each request before you open its answer.
The solutions on pp. 1670–1681 are typed with typographic quotes (“query”) and Swiss thousands separators (80’000). In the safe exam browser type JSON: straight quotes, 80000, a comma between two members and none after the last — pp. 1678–1680 miss the comma after the must array, and pp. 1678–1679 leave one after the last array.
"Write a query to collect all the employees whose first name is "Bob"." (p. 1670)
Check yourself
GET /employees50k/_search { "query": { "match": { "FirstName": "Bob" } } }As printed, quotes apart. A
matchon a text field: "bob" would find him too."… whose salary is greater than 80’000 and less than 95’000." (p. 1671)
Check yourself
GET /employees50k/_search { "query": { "range": { "Salary": { "gt": 80000, "lt": 95000 } } } }"Greater than … less than" are strict bounds:
gtandlt."… whose age is 31." (p. 1672)
Check yourself
GET /employees50k/_search { "query": { "term": { "Age": 31 } } }The slide adds: "Disclaimer: This solution is not optimal (although it works). The best one would be a range query with gte and lte 31." Both forms are correct answers:
termis fine on a number — the warning of p. 1649 is about text fields."… who joined later than 03/09/2015." (p. 1673)
Check yourself
GET /employees50k/_search { "query": { "range": { "DateOfJoining": { "gt": "2015-09-03" } } } }The slide writes
"03-09-2015"with the note "Any date format would be fine". It is not: a date in arangeis parsed with the field's format, and the default one reads 2015-09-03 but refuses 03-09-2015 with a parse error (add"format": "dd-MM-yyyy"to keep the slide's string). The papers write days first, so 03/09/2015 is 3 September."… whose gender is exactly "Male"." (p. 1674)
Check yourself
GET /employees50k/_search { "query": { "term": { "Gender.keyword": "Male" } } }The slide's
{ "term": { "Gender": "Male" } }finds nothing: Gender is text, and its index holds "male" (Fig. 1). "Exactly" is atermon the keyword sub-field."… whose first name is "Moses" and whose last name is "Daschofsky"." (p. 1675)
Check yourself
GET /employees50k/_search { "query": { "bool": { "must": [ { "match": { "FirstName": "Moses" } }, { "match": { "LastName": "Daschofsky" } } ] } } }Two fields, two clauses: the one-field rule of p. 600.
"… whose interests contains one or more words among "Paragliding", "Kayaking", and "Playing". The more words are found, the higher the final score." (p. 1676)
Check yourself
GET /employees50k/_search { "query": { "match": { "Interests": { "query": "Paragliding Kayaking Playing", "operator": "or" } } } }One
matchwith OR does both jobs: one word is enough to qualify, and the score is a sum over the words found. OR is the default, so{ "match": { "Interests": "Paragliding Kayaking Playing" } }is the same request."… whose name is "Elden", assigning a higher score those whose designation is "Delivery Manager"." (p. 1677)
Check yourself
GET /employees50k/_search { "query": { "bool": { "must": [ { "match": { "FirstName": "Elden" } } ], "should": [ { "match": { "Designation": "Delivery Manager" } } ] } } }As printed. The OR
matchalso gives a plain "Manager" a smaller bonus; the delivery managers collect both words."… whose last name is "Weatherly" and whose salary is greater than 50’000. The name should not affect the final score." (p. 1678)
Check yourself
GET /employees50k/_search { "query": { "bool": { "must": [ { "range": { "Salary": { "gt": 50000 } } } ], "filter": [ { "match": { "LastName": "Weatherly" } } ] } } }The exam's sentence, word for word: the name goes in
filter, where amatchis still analysed but scores nothing."… whose designation is "Manager", assigning a higher score to those whose gender is "Female" and those who are "Married"." (p. 1679)
Check yourself
GET /employees50k/_search { "query": { "bool": { "must": [ { "match": { "Designation": "Manager" } } ], "should": [ { "term": { "Gender.keyword": "Female" } }, { "term": { "MaritalStatus.keyword": "Married" } } ] } } }Two bonuses, two
shouldclauses: the married women come first, then those with one bonus, then the rest."… whose designation is "Manager" or "Delivery Manager" and whose salary is not higher than 150’000. Assign a higher score to those whose interests includes "Blogging"." (p. 1680)
Check yourself
GET /employees50k/_search { "query": { "bool": { "must": [ { "match": { "Designation": { "query": "Delivery Manager", "operator": "or" } } }, { "range": { "Salary": { "lte": 150000 } } } ], "should": [ { "match": { "Interests": "Blogging" } } ] } } }"Not higher than" is
lte. The OR match keeps every designation with "delivery" or "manager", which covers both values — and any other manager. The strict form names the two values:{ "terms": { "Designation.keyword": ["Manager", "Delivery Manager"] } }inmust, the IN of section 2. The slide repeats the slips of p. 1676:"operator"beside the field and the field name "Interest".
"Write a query to count the number of employees based on their gender and designation separately." (p. 1681)
Check yourself
GET /employees50k/_search { "size": 0, "aggs": { "employees_per_gender": { "terms": { "field": "Gender.keyword" } }, "employees_per_designation": { "terms": { "field": "Designation.keyword" } } } }"Separately" means two sibling aggregations (p. 1659), not one inside the other.
"Write a query to count the number of employees based on their gender. Then, compute the number of people based on their age for each gender." (p. 1682)
Check yourself
GET /employees50k/_search { "size": 0, "aggs": { "employees_per_gender": { "terms": { "field": "Gender.keyword" }, "aggs": { "employees_per_gender_per_age": { "terms": { "field": "Age" } } } } } }As printed — the only solution typed with straight quotes. A sub-aggregation runs inside each gender bucket; Age is a number and needs no
.keyword. The innertermsreturns the 10 most frequent ages per gender unless its ownsizeis raised.
Back to the hook
Now the three steps of pp. 604–606 read themselves (Fig. 5). Steps one and two hold a must, the date range, and a must_not, the code 02G. A range gives every match the same score, 1.0, and must_not scores nothing: 9510 hits, then 8923, all at 1.0. Step three adds a should next to a must, so the new clause is optional: no inspection joins, none leaves — 8923. But every Queens inspection collects the score of the should on top of the range's 1.0. That clause is a term on the keyword field BORO, which scores every Queens document alike: 1.4879451, the score of the match on "Queens" printed on p. 597. The best score is therefore 1.0 + 1.4879451 = 2.4879451, displayed as 2.487945 (p. 606).
Before: must and must_not (p. 605)
After: plus a should (p. 606)
should with nothing beside it — this one, next to a must, is the exam's "prioritising".Exam-style questions
Five Elasticsearch exercises of the past papers, each after the part of its ER diagram that it uses (the mapping of the 12 Sep 2025 paper is in lesson 25), then two theory questions in the forms of the written exam. No solutions are published: the model answers are ours, written in the session's idioms and checked against Elasticsearch's syntax.
Exam of 6 Jun 2024, exercise 4 — Elasticsearch (4 PT). ER (tracking natural disasters), the part used: Location (Postal Code, City, Country, Description); Occurr: Location 0:N – Natural Disaster 1:N.
"Consider the Location entity. 4.1. Provide the complete mapping of the index (i.e., field name, field type, the structure of the mapping, etc.) (1 PT) PUT …"
"Write a query to return the list of all the locations whose description includes the words "Beautiful" and "Town", removing the ones whose name includes the word "Milano". The condition on the word "Town" should not affect the final score. (1.5 PT)"
"Write a query to return the count of the locations for each country. (1.5 PT)"
Show model answer
PUT /locations
{
"mappings": {
"properties": {
"postal_code": { "type": "keyword" },
"city": { "type": "text" },
"country": { "type": "keyword" },
"description": { "type": "text" }
}
}
}
GET /locations/_search
{
"query": {
"bool": {
"must": [
{ "match": { "description": "Beautiful" } }
],
"filter": [
{ "match": { "description": "Town" } }
],
"must_not": [
{ "match": { "city": "Milano" } }
]
}
}
}
GET /locations/_search
{
"size": 0,
"aggs": {
"locations_per_country": {
"terms": { "field": "country" }
}
}
}
Location has no Name attribute: I assume the paper means the city.
Three choices decide this exercise. Postal code and country are keyword — one matched whole, one counted by — and the description is text. "Town … should not affect the final score" sends that word to filter, where a match is still analysed but scores nothing; "Beautiful" stays in must. The readme of the papers warns that an exercise may name something its diagram lacks and tells you to "consider all the necessary assumptions to solve the exercise": write yours in one line, as above. Add "size": 250 inside terms if every country must appear, not only the ten largest.
Exam of 24 Jun 2024, exercise 4 — Elasticsearch (4 PT). ER (tracking Disney's digital products), the part used: Location (ID, Name, Description, Furniture (Text), Indoor (Yes/No)); Located: Script 1:N – Location 1:1.
"Consider the Location entity. 4.1. Provide the complete mapping of the index (i.e., field name, field type, the structure of the mapping, etc.) (1 PT) PUT …"
"Write a query to return the list of Locations whose description includes the words "Stadium" and "Basketball", assigning a higher score to those including the words "Planet" in their furniture and "Space" in their description. (1.5 PT)"
"Write a query to return the count of indoor and outdoor locations. (1.5 PT)"
Show model answer
PUT /locations
{
"mappings": {
"properties": {
"id": { "type": "keyword" },
"name": { "type": "text" },
"description": { "type": "text" },
"furniture": { "type": "text" },
"indoor": { "type": "boolean" }
}
}
}
GET /locations/_search
{
"query": {
"bool": {
"must": [
{ "match": { "description": "Stadium" } },
{ "match": { "description": "Basketball" } }
],
"should": [
{ "match": { "furniture": "Planet" } },
{ "match": { "description": "Space" } }
]
}
}
}
GET /locations/_search
{
"size": 0,
"aggs": {
"indoor_and_outdoor": {
"terms": { "field": "indoor" }
}
}
}
The aggregation returns two buckets, one for true (indoor) and one for false (outdoor), each with its doc_count; a boolean bucket prints its key as 1 or 0, with "key_as_string" "true" or "false".
Indoor is a Yes/No attribute: boolean, and its terms aggregation has exactly two buckets. "Includes the words "Stadium" and "Basketball"" needs both words: two match clauses in must, or one with "operator": "and" — a compact match with both words would accept either. The two bonuses sit on two different fields, so they are two should clauses: one match cannot name two fields.
Exam of 6 Sep 2024, exercise 4 — Elasticsearch (4 PT). ER (a restaurant), the part used: Dish (Dish_ID, Name, Price, Recipe, Description); Cook: Dish 1:N – Chef 1:N; Use: Dish 1:N – Ingredient 1:N.
"Consider the Dish entity. 4.1. Provide the complete mapping of the index (i.e., field name, field type, the structure of the mapping, etc.) (1 PT) PUT …"
"Write a query to return the list of all the dishes whose description and name include the word "salmon", prioritising the one whose recipe includes the words "stir" or "boil". (1.5 PT)"
"Write a query to return the list of all the dishes whose price exceeds 10.00€, whose name is "focaccia ligure", and whose descriptions do not include the word "butter". The condition on the description attribute must not affect the final score. (1.5 PT)"
Show model answer
PUT /dishes
{
"mappings": {
"properties": {
"dish_id": { "type": "keyword" },
"name": { "type": "text" },
"price": { "type": "double" },
"recipe": { "type": "text" },
"description": { "type": "text" }
}
}
}
GET /dishes/_search
{
"query": {
"bool": {
"must": [
{ "match": { "description": "salmon" } },
{ "match": { "name": "salmon" } }
],
"should": [
{ "match": { "recipe": "stir boil" } }
]
}
}
}
GET /dishes/_search
{
"query": {
"bool": {
"must": [
{ "range": { "price": { "gt": 10.00 } } },
{
"match": {
"name": {
"query": "focaccia ligure",
"operator": "and"
}
}
}
],
"must_not": [
{ "match": { "description": "butter" } }
]
}
}
}
Query 1: "description and name include the word" is two fields, so two match clauses in must; "stir" or "boil" is one match, OR by default, in should. Query 2: the description condition is a negation, and must_not never scores — that already satisfies "must not affect the final score". With name mapped as text, "name is "focaccia ligure"" is a match with "operator": "and"; had you mapped it as keyword, it would be a term on the exact stored value, capitals included. Whatever you choose, the query must agree with your own mapping.
Exam of 13 Jun 2025, exercise 5 — Elasticsearch (3 PT). (The paper's header misprints its date as "Jan. 21, 2025".) ER (tracking concert attendances and performances), the part used: Track (Title, Duration (minutes), Composer (e.g., Mozart), Music Type (e.g., classical)); Perform: Orchestra (0,N) – Track (1,N).
"Consider the Track entity. Provide the complete mapping of the index for the considered entity (i.e., field name, field type, the structure of the mapping, etc.) (1 PT) PUT /"
"Write a query to return the documents whose composer is "Bach", whose duration is greater than 8 minutes, and whose music type is classical, prioritizing those with duration greater than 11 minutes. The condition on the composer must not affect the final score (2 PT)"
Show model answer
PUT /tracks
{
"mappings": {
"properties": {
"title": { "type": "text" },
"duration": { "type": "integer" },
"composer": { "type": "keyword" },
"music_type": { "type": "keyword" }
}
}
}
GET /tracks/_search
{
"query": {
"bool": {
"must": [
{ "range": { "duration": { "gt": 8 } } },
{ "term": { "music_type": "classical" } }
],
"filter": [
{ "term": { "composer": "Bach" } }
],
"should": [
{ "range": { "duration": { "gt": 11 } } }
]
}
}
}
The composer goes to filter, as the paper demands. The 8-minute threshold and the music type are required: must (in filter they would be correct too — the paper only forbids scoring the composer). The 11-minute range in should adds 1.0 to the long tracks, which therefore come first: "prioritizing". Composer and music type are keywords so that term finds "Bach" and "classical" whole; with text fields you would write match.
Exam of 12 Sep 2025, exercise 3, second sub-question — Elasticsearch (2 PT). ER (tracking farmers and their sales at local markets), the part used: People (Personal ID, Name, Surname, Age, Address, State) and its child Farmer ((…), Biography, Experienced (Yes/No)), ISA (P, O); Cultivate: Farmer (1, N) – Crop (1, N).
The first sub-question, the complete mapping of the Farmer index (1 PT), is solved in lesson 25; this one uses its field names.
"Write a query to return the farmers whose name is "Bill", whose biography contains the words "cactus" and "aloe", and does not contain the word "potato". Additionally, if the "crop" word is found, assign a higher score to the farmer. (2 PT)"
Show model answer
GET /farmers/_search
{
"query": {
"bool": {
"must": [
{ "match": { "name": "Bill" } },
{ "match": { "biography": "cactus" } },
{ "match": { "biography": "aloe" } }
],
"must_not": [
{ "match": { "biography": "potato" } }
],
"should": [
{ "match": { "biography": "crop" } }
]
}
}
}
Every word condition is on a text field, hence match: "contains the words "cactus" and "aloe"" — both required, two clauses in must; "does not contain" — must_not; "if the "crop" word is found, assign a higher score" — should. Mapping the name as text lets "Bill" match whatever its case.
Explain the difference between query context and filter context in Elasticsearch. Which clauses of a bool query run in each context, and when would you place a condition in filter context?
Show model answer
In query context a clause answers the question "how well does this document match?": it decides whether the document matches and also computes a relevance score, and the hits are sorted by that score. A leaf query placed directly under query, and the must and should clauses of a bool query, run in query context. In filter context a clause answers only "does this document match?": it decides membership, but no score is computed. The filter and must_not clauses of a bool query run in filter context. A condition belongs in filter context when it must hold but should not influence the ranking — an exact category, a date range, or any condition that the task says "must not affect the final score". Filtering is also cheaper: no scoring is done, and the yes/no result can be cached and reused by later queries.
A complete answer names the two questions (how well / does), says that the score is computed in one and not in the other, places all four clauses — must_not is in filter context too, which is often forgotten — and gives a reason to filter: ranking versus a pure condition, and the cache.
A bool query with a must clause (a range on the inspection date) and a must_not clause returns 8923 hits, all with score 1.0. A should clause, a term on the borough "Queens", is added. Which statement describes the new response?
Show model answer
The same 8923 hits. Because the query has a must clause, the should clause is optional: it removes and adds no document, but it adds its score to the inspections in Queens, which rise to the top (best score 1.0 + 1.4879451 = 2.487945).
This is the lecture's own example (pp. 604–606). The slide labels should "OR", and the trap is the answer "more hits". Wrong closed answers may be penalised at the exam, so if you hesitate, remember that should is an OR only when it is alone.
Check yourself
A bool query has a match on the description in must and a range on the price in must. You move the range to filter. What happens?
A bool query contains only should clauses. Which statements are true? Select all that apply.
Alone, should is an OR that scores; beside must or filter, it is a bonus.
The text field description of a dish holds "Salmon fillet with butter and herbs". What does { "term": { "description": "Salmon" } } return?
The response of p. 607 reads "total" : { "value" : 10000, "relation" : "gte" }. What do you know about the number of matching documents?
You send "size": 0 and a terms aggregation, with no size inside it, on a keyword field with 25 distinct values. What comes back?
Which requests does Elasticsearch refuse with an error? Select all that apply.
The dangerous slip is the one without an error: a term on a text field returns an empty list that looks like a real answer.
A bool query has a filter clause and a should clause, and no must. Which dishes come back?
Key take-aways
- A search is
GET /index/_searchwith a JSON body whosequeryis one leaf clause —match,term,range,prefix— or aboolthat combines them;_countreturns only the number, and onematchlooks at one field. matchanalyses its text with the field's analyzer: words joined by OR (or by AND with"operator": "and") on a text field, the whole value on a keyword field.termnever analyses and does not belong on text fields;rangetakesgt,gte,lt,lteand date math such asnow-10d/d.- Query context asks how well a document matches and computes a score; filter context asks only whether it matches, scores 0 and can be cached.
filterandmust_notrun in filter context. mustis a required, scored AND;filtera required AND without score;must_nota NOT without score;shoulda bonus, and an OR only when it stands alone. Scores add up — 2.487945 = 1.0 + 1.4879451 on p. 606 — andboostmultiplies a clause's share."size": 0and a namedtermsaggregation are GROUP BY with counts: the 10 biggest buckets come back, the rest only insum_other_doc_count; a query filters first, sub-aggregations run inside each bucket, and a text field must be counted throughfield.keyword.- The exam exercise is a mapping written for the queries that follow, then sentences translated clause by clause: "includes the words" →
must, "removing" →must_not, "must not affect the final score" →filter, "prioritising" →should, "the count for each" →terms.
What to watch and read
-
Query DSL: query and filter context
Elastic documentation
The short official page behind p. 1643: "How well does this document match?" against "Does this document match?", and the note that frequently used filters are cached automatically.
-
Boolean query
Elastic documentation
The four clauses in the vendor's words, the rule for
minimum_should_match(1 whenshouldis alone, 0 besidemustorfilter) and the remark that filter clauses return a score of 0 — section 3 in reference form. -
Terms aggregation
Elastic documentation
The top-10 default and its
size,sum_other_doc_count, the count error across shards, and the rule that a text field cannot be aggregated by default. Read it next to the second widget. -
Running full text queries and combined queries
Lisa Jung, Official Elastic Community workshop notes
Every request of the workshop in the same console syntax as the slides:
matchwith and withoutoperator,match_phrase, andboolwith all four clauses. Good extra practice once the phrase book feels natural. -
start-local
Elastic, Elasticsearch and Kibana on your laptop
One command brings up Elasticsearch and Kibana in Docker. Paste the mappings and queries of the exam section into Kibana's console, add a few documents with
POST /dishes/_doc, and check your predictions against the real engine.