Elasticsearch is a search engine. An index is a collection of searchable documents, each with an identifier and typed fields. Our products index contains 300 fixed product documents. The mapping determines how a field is indexed: a number supports numeric comparison, while a text field is analyzed into terms. The original JSON is stored in _source. A field in the mapping need not be present in every document.
Each example below is a body for POST /products/_search. In the trainer, paste only the JSON: the server selects the physical index and adds total-hit tracking. Run Query explores data; Check Solution verifies both behavior and the required query construction. Example values deliberately differ from the exercises. Start with matching documents, then inspect why a particular document was included.
- Document: JSON
- Mapping: field types
- Query: conditions
- hits: matching documents
Mapping determines indexed values. The query selects documents; hits returns a page of matches.
Examples use the public products dataset on Elasticsearch 9.4.3. Responses are shortened JSON fragments: timing, shard and index metadata are omitted. Numeric scores are illustrative and can vary. Examples teach the method; exercise values may differ.
Retrieve all products
The root of a search body is a JSON object. query defines which documents match. match_all with an empty object imposes no restriction, so it is useful for exploring a dataset before adding conditions. size controls the number of returned hits, not how many documents match; sort makes this small demonstration reproducible. JSON requires double quotes and does not allow comments or a trailing comma.
hits.total.value reports the number of matching documents; relation: eq means this count is exact. hits.hits is the returned page. _id identifies a hit, _source contains requested original fields, and _score is the relevance metadata. took is measured in milliseconds and varies between runs. Our fragments omit took, shard information and index metadata; that omission is presentation, not another Elasticsearch response format.
| Field | Mapping | Purpose |
|---|---|---|
| id | integer | Stable product identifier and tie-breaker |
| name / description | text | Analyzed full-text search |
| brand / brand.keyword | text / keyword | Text search / exact brand value |
| category / status | keyword | Case-sensitive exact values |
| price / rating | double / float | Numeric values; rating is optional |
| in_stock / created_at | boolean / date | Availability / creation date |
Try a different scenario
{
"query": {
"match_all": {}
},
"size": 2,
"_source": [
"name",
"price"
],
"sort": [
{
"id": "asc"
}
]
}Elasticsearch response
{
"hits": {
"total": {
"value": 300,
"relation": "eq"
},
"hits": [
{
"_id": "1",
"_score": null,
"_source": {
"name": "Wireless",
"price": 100
}
},
{
"_id": "2",
"_score": null,
"_source": {
"name": "Reference device",
"price": 100
}
}
]
}
}How to read this result
Two hits are returned, but total is 300. The id order comes from sort, not from relevance. _source is limited to name and price to keep the example readable. Later lessons explain these output controls in detail.
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.
Key takeaways
- query selects documents; size limits the returned page.
- Use keyword for complete exact strings; preserve value case.
- term is one value; terms is any value in an array.
- Choose gt/gte/lt/lte from the wording of each boundary.
- exists means indexed presence; zero is not missing.
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.