Exercises

Hands-on tasks for the Elasticsearch for Developers course. They follow the chapter order, so you can do them as you go.

Sample dataset. Several exercises use the shop-orders-demo index. Load it once: 1. Run the mapping in sample-data/shop-orders-demo-mapping.txt (a PUT shop-orders-demo). 2. Run the bulk file sample-data/shop-orders-demo-data.txt (a big POST _bulk). 3. Verify: GET shop-orders-demo/_count should return a few hundred orders.


Day 1 — Fundamentals

1) Environment & first contact (Chapter 01)

  1. Start a local stack with the docker-compose.yml from Chapter 01 (docker compose up -d), or use an Elastic Cloud trial.
  2. Open Kibana → Dev Tools and run:
    • GET / — confirm the version
    • GET /_cluster/health — note the status (green / yellow / red)
    • GET /_cat/nodes?v
    • GET /_cat/indices?v
  3. Why is a brand-new single-node cluster often yellow? (Hint: replicas.)

2) CRUD (Chapter 02)

  1. Create the following documents in a new index subscription_list:

    {
      "name": "First customer",
      "e-mail": "first-customer@icloud.com",
      "age": 31
    }
    {
      "name": "Second customer",
      "e-mail": "second-customer@gmail.com",
      "age": 47
    }
  2. List all documents in index subscription_list.

  3. Modify the e-mail address of the first customer to first@outlook.com using a partial update (_update), and check the result.

  4. Delete the second customer and list all documents in the subscription_list index again.

  5. Re-index the first customer with the same _id but a changed age. Look at _version in the response — what happened?


3) Indices, aliases, reindexing (Chapter 02 + a peek at Chapter 07)

  1. Create a document in index manufacturer with ID 1 and fields:

    • name: Apple
    • revenue: 274515
    • country: United States
  2. Create another document in index manufacturer, but this time let Elasticsearch generate the ID:

    • name: Toyota Group
    • revenue: 256721
    • country: Japan

    Now imagine the load on the index has increased and you need more shards.

  3. Create a new empty index manufacturer-2 with settings:

    • number_of_shards: 5
    • number_of_replicas: 1
  4. Reindex data from manufacturer to manufacturer-2 (_reindex).

  5. Delete the index manufacturer.

  6. Create an alias manufacturer that points to the index manufacturer-2.

  7. List all documents through the alias manufacturer.

Takeaway: the app keeps using the stable name manufacturer while the physical index changed underneath it.


4) Bulk API (Chapter 02)

  1. Using a single POST _bulk request, in one call:
    • index two new products into an index catalog
    • update one of them
    • delete a document by id
  2. Inspect the response: did any item fail? Where would you look (errors, per-item status)?

5) Basic search (Chapter 03)

Use shop-orders-demo.

  1. Return all orders (match_all) and read the response envelope: how many hits in hits.total.value, and what is relation?
  2. Find orders with order_status exactly delivered (which query — term or match? why?).
  3. Find orders delivered to country_code CZ or SK (terms).
  4. Find orders with total_amount between 1000 and 5000 (range).
  5. Full-text search the notes field for a word (e.g. reklamace) with match. Then try the same word with a term query — explain the different result.
  6. Build a single bool query that returns orders that:
    • are delivered (filter on order_status)
    • to HU (filter on country_code)
    • have total_amount2000 (range in filter)
    • are not returned (must_not on returned)
  7. Return only the fields order_id, total_amount, delivery.city (_source filtering), sorted by total_amount descending, page size 5.

6) Mappings (Chapter 04)

  1. Create an index template called comments that applies the following mapping to an index comments (created in the next step):

    Field Type
    title text
    text text
    author text + keyword (multi-field)
    email keyword
    visible boolean
    score a decimal number
    created date accepting only the format yyyy-MM-dd
  2. Store the following document into index comments:

    {
      "title": "Test comment",
      "text": "Hello world!",
      "author": "Hicham Sharp",
      "email": "hicham@sharp-family.zzz",
      "visible": true,
      "score": 12.7,
      "created": "2022-02-18"
    }
  3. Check the mappings of comments (GET comments/_mapping) — they should match step 1.

  4. Try to index a document with "created": "18-02-2022". What happens, and why?


7) Analyzers (Chapter 04)

  1. Use the _analyze API to tokenize the text The Wireless Headphones, 2-pack! with the standard analyzer. How many tokens, and what are they?
  2. Run the same text through the whitespace analyzer and compare.
  3. Run headphones are amazing through the english analyzer — notice stemming (headphonesheadphon) and stopword removal (are).
  4. Run POST shop-orders-demo/_analyze against the notes field (Czech analyzer) on a Czech sentence and observe stemming/stopwords.

Takeaway: the analyzer decides what is actually searchable — not the raw string.


Day 2 — Advanced

8) Custom analyzer (Chapter 05)

  1. Create a new index my_en_analyzer. Start from the built-in english analyzer settings (see the ES docs: Language analyzers → english analyzer) and extend it:

    • add a character filter that strips HTML tags from the input,
    • add a custom synonym token filter that converts the word failure into error.
  2. Create a mapping with a single field:

    • name: message
    • type: text
    • analyzer: the custom analyzer from step 1.
  3. Index these documents:

    POST my_en_analyzer/_doc
    { "message": "An error occured" }
    
    POST my_en_analyzer/_doc
    { "message": "There was a <b>failure</b> during processing request" }
  4. Search for error in message. Both documents should match. Explain why (synonyms + html_strip).


9) Sorting, pagination, and highlighting (Chapter 03 + 05)

  1. Store the following documents:

    POST book/_doc
    { "title": "The Forever Dog", "publish": "2020-01-20" }
    
    POST book/_doc
    { "title": "The Book Your Dog Wishes", "publish": "2018-01-01" }
    
    POST book/_doc
    { "title": "The Complete Dog Breed Book", "publish": "2018-01-01" }
  2. Search for Dog and:

    • sort results by publish date, oldest first,
    • if two books share the same publish date, sort them alphabetically by title.
  3. Highlight the matches using the HTML tag <b></b>.


10) Nested queries (Chapter 05)

In shop-orders-demo, items is a nested field.

  1. Find orders that contain an item where category is Elektronika and unit_price > 1000 in the same item (use a nested query — a plain bool would give false matches).
  2. Add inner_hits so the response shows which item(s) matched.
  3. (Bonus) Explain what would go wrong if items were a plain object instead of nested.

11) Autocomplete & typo tolerance (Chapter 05)

  1. Build a small index product_suggest with a completion-type field suggest.
  2. Index a handful of product names (e.g. Bezdrátová sluchátka AirWave 300, Kávovar AromaOne, Běžecké boty TrailPro).
  3. Use the completion suggester to autocomplete the prefix Káv.
  4. Enable fuzzy and try a prefix with a typo (e.g. Kavov). Does it still suggest Kávovar?

12) Custom relevance (Chapter 05)

In shop-orders-demo:

  1. Use function_score to search notes for a term, but boost orders with a higher rating (field_value_factor). Guard against missing rating.
  2. Use a gauss decay function so more recent orders (created_at) score higher.
  3. (Bonus) Rewrite the rating boost as a script_score query using a Painless script such as _score * Math.log(2 + doc['rating'].value).

13) Aggregations (Chapter 06)

Use shop-orders-demo (and/or kibana_sample_data_ecommerce if you have it installed).

  1. List the customer.id values (or e-mail in the Kibana sample) that created the most orders (terms).
  2. For each channel, compute the average total_amount (terms + avg).
  3. Orders per month (date_histogram on created_at, calendar_interval: month).
  4. Unique customers (cardinality on customer.id).
  5. p50 / p95 / p99 of total_amount (percentiles).
  6. Multi-level: for each country_code, the average total_amount and the number of unique customers.
  7. Nested aggregation: dive into items, then terms by category, then sum of unit_price. (Use reverse_nested to also count distinct parent orders per category.)

14) Pipeline aggregations (Chapter 06)

On a monthly date_histogram of revenue (sum of total_amount):

  1. Add a cumulative_sum of monthly revenue.
  2. Add a derivative (month-over-month change).
  3. Add a 3-month moving_fn average (MovingFunctions.unweightedAvg(values)).
  4. Add a bucket_script computing the average discount ratio per month (discount_amount / subtotal_amount).
  5. (Bonus) Use bucket_selector to keep only months whose revenue exceeded a threshold.

15) ILM & data streams (Chapter 07)

ILM rollover + retention (classic alias):

  1. Create an ILM policy logs-hot-delete with a hot phase that rolls over at max_primary_shard_size: 1mb (tiny, so you can trigger it by hand) and a delete phase at min_age: 30d.
  2. Create an index template logs-template (pattern logs-*) that wires in index.lifecycle.name and index.lifecycle.rollover_alias: logs-write, plus a strict mapping with @timestamp, service, level, message.
  3. Bootstrap logs-000001 with a write alias logs-write (is_write_index: true) and a read alias logs-read.
  4. Index ~20 documents into logs-write, then force a rollover: POST logs-write/_rollover { "conditions": { "max_primary_shard_size": "1mb" } }.
  5. Verify with GET logs-000001/_ilm/explain and confirm logs-000002 exists and the write alias moved.
  6. Query logs-read — both generations should be visible.

Data stream (modern):

  1. Create a data-stream index template logs-app-template (pattern logs-app*, data_stream: {}, @timestamp mapped, the same ILM policy attached) — note there is no rollover alias and no bootstrap index.
  2. Index a few documents directly into logs-app (each with an @timestamp). Inspect the stream: GET _data_stream/logs-app and GET _cat/indices/.ds-logs-app*?v.
  3. Trigger POST logs-app/_rollover; confirm a new backing index (...-000002) and that writes advanced.
  4. Search logs-app with a @timestamp range filter. Then try a direct update by _id against logs-app — explain why it is rejected.

Takeaway: the data stream gives you the same rollover pipeline as steps 1–6 behind a single name, with no alias to bootstrap — at the cost of being append-only.


16) Performance & profiling (Chapter 07)

  1. Run an aggregation without a time/range filter, then with one (created_at range). Compare took.
  2. Add "profile": true to a query and find where time is spent (query vs aggregation vs fetch). Try the Search Profiler UI in Dev Tools.
  3. Take a field that is text-only and try to aggregate/sort on it — observe the failure. Fix it by aggregating on the .keyword multi-field.
  4. Create a component template (shared mappings) and compose it into an index template; verify with _index_template/_simulate_index/<name>.

17) Operations & integration (Chapter 08)

  1. Cluster health: create an index with number_of_replicas: 1 on a single-node cluster → it goes yellow. Use GET _cluster/allocation/explain to see why, then set replicas to 0 and watch it return to green.
  2. Snapshots: register a filesystem snapshot repository, take a snapshot of shop-orders-demo, then restore it under a new name using rename_pattern / rename_replacement (so you don’t clobber the live index).
  3. Ingest pipeline: create a pipeline that, for an incoming order, sets an @timestamp, lowercases a field, and converts a stringified amount to a number. Test it with _simulate, then index a document through it (?pipeline=...).
  4. Decide: for each scenario, would you use an ingest pipeline, Beats, or Logstash? (a) shipping app log files, (b) light field normalization inside ES, (c) parsing many heterogeneous sources with buffering and multiple outputs.