Build predictable constraints on brands, prices, dates and indexed values.
course-products examples use the teaching mapping and product from chapter 5, “Mapping and field types”. Exercise walkthroughs below use the trainer’s separate public dataset.
- Terms and identifiers
- Boundaries and time
- Checking absence
Build predictable constraints on brands, prices, dates and indexed values.
The course targets Elasticsearch 9.4.3. Exercise examples use the public products dataset; other requests illustrate separate teaching indices and may require setup or privileges unavailable in the trainer. Responses are shortened fragments; scores may vary. Check deployment and license requirements for advanced features.
Terms and identifiers
term matches one indexed value without full-text analysis; terms accepts any value from a list. Brands normally use keyword. Case and normalization must agree with mapping. ids searches _id rather than a custom sku field. Exact queries do not split ordinary-looking input into words.
Check your understandingBoundaries and time
range uses gt, gte, lt and lte. Inclusive starts and exclusive ends prevent double-counting boundaries across adjacent intervals. Date math such as now-7d/d rounds boundaries; time_zone affects local boundary interpretation, while now represents the current instant. Distinguish a calendar week from the last 168 hours.
Check your understandingChecking absence
exists checks for an indexed value. A missing rating is commonly expressed as must_not exists. Absence can result from null, empty arrays and mapping options such as ignore_above. A key in _source and presence in searchable structures are different properties. Validate mandatory business data before indexing.
Brands form an OR condition, combined with the price range using AND.
{
"query": {
"bool": {
"filter": [
{
"terms": {
"brand": [
"Sony",
"Bose"
]
}
},
{
"range": {
"price": {
"gte": 50,
"lt": 200
}
}
}
]
}
}
}How to read this result
Brands form an OR condition, combined with the price range using AND.
Catalog reasoning example
Test prices 49.99, 50, 199.99 and 200 against [50,200): only 50 and 199.99 should pass. A missing price should not pass range. Test date boundaries in the chosen time zone similarly. Small counterexamples detect gte/lte mistakes more reliably than a total count over arbitrary data. For exists, compare null, an empty array and an empty string separately.
A valid zero-result response
{
"query": {
"term": {
"sku": "missing-sku"
}
}
}Elasticsearch response
{
"hits": {
"total": {
"value": 0,
"relation": "eq"
},
"hits": []
}
}How to read this result
Zero matches is a successful search result, not an HTTP error.
Find an exact brand
term searches for an indexed value without analyzing the input. For a brand identifier, select brand.keyword: it stores the complete value, including case. Sony and sony are different keyword values in this mapping. The brand field itself is text, so its indexed tokens are not the same thing as the original full brand string.
The same term syntax works for boolean and numeric fields: use true rather than the string "true", and a number rather than a formatted price string. Query fields use indexed paths, whereas _source contains original document fields: brand.keyword is a mapping subfield, not a separate property in the source JSON.
Try a different scenario
{
"query": {
"term": {
"brand.keyword": "Sony"
}
},
"size": 2,
"_source": [
"name",
"brand"
],
"sort": [
{
"id": "asc"
}
]
}Elasticsearch response
{
"hits": {
"total": {
"value": 50,
"relation": "eq"
},
"hits": [
{
"_id": "3",
"_score": null,
"_source": {
"name": "Sony Travel Speaker 3",
"brand": "Sony"
}
},
{
"_id": "9",
"_score": null,
"_source": {
"name": "Sony Premium Travel Laptop 9",
"brand": "Sony"
}
}
]
}
}How to read this result
Every returned hit has brand Sony. The returned _source uses brand, although the matching condition addresses brand.keyword. The small size does not restrict the number of matches counted by total.
Match several statuses
terms tests membership in a list: a document matches if the field equals any listed value. This is logical OR over exact values of one field, not a requirement to contain every value. Our status is a single keyword. Put values in an array even if only two alternatives are needed.
Use terms for a compact exact-value selection; use bool when alternatives involve different fields or combine multiple conditions. A list does not create independent query blocks. Missing fields do not match a supplied ordinary value, so missing status is not automatically part of the selection.
Try a different scenario
{
"query": {
"terms": {
"status": [
"preorder",
"discontinued"
]
}
},
"size": 2,
"_source": [
"name",
"status"
],
"sort": [
{
"id": "asc"
}
]
}Elasticsearch response
{
"hits": {
"total": {
"value": 200,
"relation": "eq"
},
"hits": [
{
"_id": "1",
"_score": null,
"_source": {
"name": "Wireless",
"status": "preorder"
}
},
{
"_id": "2",
"_score": null,
"_source": {
"name": "Reference device",
"status": "discontinued"
}
}
]
}
}How to read this result
The page may contain either status. Check total to see the whole selected population; no document is required to have both statuses.
Filter a price range
range compares indexed values rather than strings. gte and lte include the boundary; gt and lt exclude it. State both bounds in the same range object when one field must lie inside an interval. The example selects prices from 200 through 800, including both endpoints.
Translate natural-language limits before writing JSON: "below" means lt, "at least" means gte. Numeric field types handle fractional values, so 499.99 and 500 can fall on opposite sides of a boundary. The same query family supports dates, but price should receive numeric values, not strings with currency symbols.
| Comparison | Includes boundary? |
|---|---|
| gt / lt | No |
| gte / lte | Yes |
Try a different scenario
{
"query": {
"range": {
"price": {
"gte": 200,
"lte": 800
}
}
},
"size": 2,
"_source": [
"name",
"price"
],
"sort": [
{
"price": "asc"
},
{
"id": "asc"
}
]
}Elasticsearch response
{
"hits": {
"total": {
"value": 60,
"relation": "eq"
},
"hits": [
{
"_id": "5",
"_score": null,
"_source": {
"name": "Dell USB Cable 5",
"price": 499
}
},
{
"_id": "15",
"_score": null,
"_source": {
"name": "Sony USB Cable 15",
"price": 499
}
}
]
}
}How to read this result
The page is ordered from the cheapest matching item; total counts the entire interval. A product priced exactly 800 would be included, unlike an lt: 800 condition.
Find rated products
exists checks whether a field has an indexed value. In our mapping, an absent rating and a rating set to null both fail this test. A numeric zero is a valid value and must not be treated as missing. An empty text string is also not the same as a missing field.
An existing source property is not a universal guarantee of an indexed value: indexing options can change that. For these exercises the mapping is fixed and has no null_value substitution. To select documents without a rating, invert exists with bool.must_not; you will use that combination in the advanced lesson.
Try a different scenario
{
"query": {
"exists": {
"field": "rating"
}
},
"size": 2,
"_source": [
"name",
"rating"
],
"sort": [
{
"id": "asc"
}
]
}Elasticsearch response
{
"hits": {
"total": {
"value": 258,
"relation": "eq"
},
"hits": [
{
"_id": "1",
"_score": null,
"_source": {
"name": "Wireless",
"rating": 3.1
}
},
{
"_id": "2",
"_score": null,
"_source": {
"name": "Reference device",
"rating": 3.2
}
}
]
}
}How to read this result
Each hit contains a numeric rating. total is smaller than the index size because some products do not have an indexed rating.
Check your understanding
Answer before expanding each explanation.
1. Does terms require all values?
No, any listed value is sufficient.
2. What query fits a SKU?
term on a keyword field, or ids when the SKU is the _id.
3. How do adjacent date windows avoid overlap?
Use gte for the start and lt for the end.
4. How do you find missing ratings?
Use must_not exists on rating, considering the mapping.
5. Why does term on name not search a phrase?
It matches one term while a text field is normally tokenized.
Key takeaways
- No, any listed value is sufficient.
- term on a keyword field, or ids when the SKU is the _id.
- Use gte for the start and lt for the end.
- Use must_not exists on rating, considering the mapping.
- It matches one term while a text field is normally tokenized.
Official reference
Ready to put this into practice?
Confirm reading to save your place. Reading and exercise progress are tracked separately in this browser using your guest cookie.