Foundational Workshop 1 — Elasticsearch: An Index, a Document, a GET Query

Guided practice13 min
Duration
20 min
Module
1/7
Prerequisites
the lab is running (etat shows (healthy) everywhere), Kibana Dev Tools open
You will build
an index of your own, pratique-mini, with two documents that you will read, complete, search, then delete
Deliverable
the response of GET pratique-mini/_doc/1 after step 5, with its three fields

How to read this page. Eight steps, one query at a time. For each: the query to type, the exact response from the lab, and what to look at inside it. Type each query yourself (no copy-paste): it is by writing PUT, GET, _doc that the words sink in. The "To understand better" blocks are optional; open them if a step leaves you in doubt. If the lab is not started, go back to the guided practice: the In Brief section gives the commands, kit included (https://github.com/hrhouma2/aiopsatlas-recherche-graphes-labo-fr).

Objective

The guided practice had you load 504 courses, 609 reviews, and 12,000 log lines in one go, with a script. You saw the numbers, but you have not written anything yourself yet. Here, you start from zero: an empty index that you create, a first record that you file in it, that you read back, that you complete, then a second one, a search, a deletion. At the end, you know what an index and a document are because you built one, not because you were told.

The Vocabulary in One Image

An index is a filing cabinet. A document is a card filed in the cabinet: a small JSON text with fields. Each card has a number, its _id, which lets you retrieve it directly without searching. You do not have to declare the columns in advance: the first card filed creates the mapping (the cabinet's blueprint) by itself.

ElasticsearchClassic SQL databaseIn this practice
indextablepratique-mini
documentrow{"titre": "Mon premier document"}
fieldcolumntitre, auteur, note
_idprimary key1, 2
mappingtable schema (CREATE TABLE …)created automatically at step 3
_sourcethe row exactly as you wrote itwhat GET _doc/1 gives you back

Where to Type, and How to Read a Query

Open http://localhost:5601, ☰ menu → ManagementDev Tools. The left panel receives the queries, the right one displays the response. You send with Ctrl + Enter (Cmd + Enter on macOS) or the ▶ button to the right of the line.

Every query has the same shape: a verb, a path, and sometimes a JSON body underneath.

VerbWhat it doesSQL equivalent
GETread, without changing anythingSELECT
PUTcreate, or replace entirelyCREATE TABLE, INSERT (or replace the row)
POSTact: update, search with a bodyUPDATE
DELETEdeleteDROP TABLE, DELETE

The path says what we act on: pratique-mini (the cabinet), pratique-mini/_doc/1 (card number 1 of the cabinet), pratique-mini/_search (search in the cabinet). Words that start with _ are Elasticsearch commands, not names of yours.

Step 1 — Create the Cabinet, Empty

text
PUT pratique-mini

What the query asks: create an index named pratique-mini. Nothing else: no columns, no content.

json
{
  "acknowledged": true,
  "shards_acknowledged": true,
  "index": "pratique-mini"
}

What to look at: "acknowledged": true, "done", and the name echoed back. The badge at the top right of the response says 200 - OK.

To understand better
  • Why pratique- in front? Every index you create in this course carries this prefix. That way GET _cat/indices/pratique-*?v lists everything that is yours and nothing else, and cours, avis, acces remain untouched.
  • An index name is lowercase, with no space, no capital letter, no /. Pratique-Mini would be refused.
  • A second time, the same query answers 400 with resource_already_exists_exception: the cabinet already exists. That is not a failure, it is a response.

Step 2 — See It, and Fix Its Color

text
GET _cat/indices/pratique-mini?v

What the query asks: one summary line about this index, with the header line (?v, verbose).

text
health status index         uuid                   pri rep docs.count docs.deleted store.size pri.store.size dataset.size
yellow open   pratique-mini YHjAfGXBTwSZcl7CLcc2jw   1   1          0            0       227b           227b         227b

What to look at: docs.count 0, the cabinet is empty. And health yellow with rep 1: Elasticsearch has planned a backup copy (a replica) of your index on a second machine, and the lab has only one. The copy cannot be placed anywhere, hence the yellow. The kit's three indexes are green because their mapping sets number_of_replicas: 0. Do the same, in one query:

text
PUT pratique-mini/_settings
{
  "index": { "number_of_replicas": 0 }
}
json
{
  "acknowledged": true
}

Retype GET _cat/indices/pratique-mini?v:

text
health status index         uuid                   pri rep docs.count docs.deleted store.size pri.store.size dataset.size
green  open   pratique-mini YHjAfGXBTwSZcl7CLcc2jw   1   0          0            0       227b           227b         227b

What to look at: green, rep 0. Your uuid will be different: it is the index's internal identifier, drawn at random on creation.

To understand better
  • Yellow is not broken. A yellow index reads and writes normally. It is a warning: "the backup copy you asked for does not exist". On a single machine, it cannot exist.
  • While your index was yellow, GET _cluster/health said "status": "yellow" for the whole cluster: the cluster's color is the worst color among its indexes. That is the explanation of the "yellow" failure in the lesson 04 catalog.
  • GET pratique-mini (without _cat) returns the index's complete record: "mappings": { } (empty, no card filed) and "settings" with number_of_replicas.

Step 3 — File a First Card

text
PUT pratique-mini/_doc/1
{
  "titre": "Mon premier document"
}

What the query asks: in the pratique-mini cabinet, file a card (_doc) number 1 that contains a titre field.

json
{
  "_index": "pratique-mini",
  "_id": "1",
  "_version": 1,
  "result": "created",
  "_shards": {
    "total": 1,
    "successful": 1,
    "failed": 0
  },
  "_seq_no": 0,
  "_primary_term": 1
}

What to look at: "result": "created" and "_version": 1: first version of card 1. SQL equivalent: INSERT INTO pratique_mini (id, titre) VALUES (1, 'Mon premier document'), except that no CREATE TABLE was necessary.

To understand better
  • The mapping was just born. Type GET pratique-mini/_mapping: the titre field is now declared with type text (to search for words inside it) with a titre.keyword sub-field (to sort or filter on the exact value). Elasticsearch inferred it from the value "Mon premier document", a string.
  • The 1 in _doc/1 is one you chose. The kit's documents do the same (C0001, A00001…). If you write POST pratique-mini/_doc with no number, Elasticsearch invents a twenty-character _id; handy for logs, painful for a card you want to find by hand.
  • _shards, _seq_no, _primary_term are internal bookkeeping (on how many pieces the write was confirmed, which sequence number). You do not need them in this course.

Step 4 — Read the Card Back

text
GET pratique-mini/_doc/1

What the query asks: give me card number 1 of pratique-mini, directly, without searching.

json
{
  "_index": "pratique-mini",
  "_id": "1",
  "_version": 1,
  "_seq_no": 0,
  "_primary_term": 1,
  "found": true,
  "_source": {
    "titre": "Mon premier document"
  }
}

What to look at: "found": true, and _source, your card exactly as you wrote it, to the character. SQL equivalent: SELECT * FROM pratique_mini WHERE id = 1.

Try a card that does not exist: GET pratique-mini/_doc/3.

json
{
  "_index": "pratique-mini",
  "_id": "3",
  "found": false
}

No error, no _source: "found": false, badge 404 - Not Found. Elasticsearch understood the question; the answer is "there is nothing at this number".

Step 5 — Add Values to the Card

text
POST pratique-mini/_update/1
{
  "doc": {
    "auteur": "Alice",
    "note": 5
  }
}

What the query asks: update (_update) card 1 by adding these two fields. What is not mentioned (titre) stays as it is.

json
{
  "_index": "pratique-mini",
  "_id": "1",
  "_version": 2,
  "result": "updated",
  "_shards": {
    "total": 1,
    "successful": 1,
    "failed": 0
  },
  "_seq_no": 1,
  "_primary_term": 1
}

What to look at: "result": "updated", "_version": 2. Read the card back with GET pratique-mini/_doc/1:

json
{
  "_index": "pratique-mini",
  "_id": "1",
  "_version": 2,
  "_seq_no": 1,
  "_primary_term": 1,
  "found": true,
  "_source": {
    "titre": "Mon premier document",
    "note": 5,
    "auteur": "Alice"
  }
}

Three fields. The title is still there. SQL equivalent: UPDATE pratique_mini SET auteur = 'Alice', note = 5 WHERE id = 1, with the difference that in SQL the auteur and note columns would have had to exist beforehand. This is your deliverable response: keep it.

To understand better
  • The word doc in the body means "here are the fields to merge". Without it, _update does not know what to do.
  • The mapping has grown. GET pratique-mini/_mapping now shows auteur (text + keyword, like titre) and note of type long, an integer. Elasticsearch guessed the type from 5. If you had written "note": "5" in quotes, it would have declared text, and you could no longer compute an average on it. That is the subject of the mapping lesson, in module 2.
  • _version counts the writes on this card, not the reads: GET never increments it.

Step 6 — The Trap: PUT Replaces Everything

Send exactly the query of step 3 again:

text
PUT pratique-mini/_doc/1
{
  "titre": "Mon premier document"
}
json
{
  "_index": "pratique-mini",
  "_id": "1",
  "_version": 3,
  "result": "updated",
  "_shards": {
    "total": 1,
    "successful": 1,
    "failed": 0
  },
  "_seq_no": 3,
  "_primary_term": 1
}

What to look at: "result": "updated" (not created: card 1 existed) and "_version": 3. Then read it back:

json
{
  "_index": "pratique-mini",
  "_id": "1",
  "_version": 3,
  "_seq_no": 3,
  "_primary_term": 1,
  "found": true,
  "_source": {
    "titre": "Mon premier document"
  }
}

auteur and note are gone. PUT _doc/1 does not modify card 1: it replaces it with what you send. To complete without losing anything, use POST _update/1 with doc. Remember the rule with the two verbs: PUT replaces, _update completes. Put the two fields back with the query of step 5 before continuing (you get _version: 4).

text
PUT pratique-mini/_doc/2
{
  "titre": "Deuxième document, écrit par Bob",
  "auteur": "Bob",
  "note": 3
}

Response: "_id": "2", "result": "created", "_version": 1. Then count:

text
GET pratique-mini/_count
json
{
  "count": 2,
  "_shards": {
    "total": 1,
    "successful": 1,
    "skipped": 0,
    "failed": 0
  }
}

SQL equivalent: SELECT COUNT(*) FROM pratique_mini. Now, see everything that is in it:

text
GET pratique-mini/_search

What the query asks: search in pratique-mini, with no criterion, so everything.

json
{
  "took": 2,
  "timed_out": false,
  "_shards": {
    "total": 1,
    "successful": 1,
    "skipped": 0,
    "failed": 0
  },
  "hits": {
    "total": {
      "value": 2,
      "relation": "eq"
    },
    "max_score": 1.0,
    "hits": [
      {
        "_index": "pratique-mini",
        "_id": "2",
        "_score": 1.0,
        "_source": {
          "titre": "Deuxième document, écrit par Bob",
          "auteur": "Bob",
          "note": 3
        }
      },
      {
        "_index": "pratique-mini",
        "_id": "1",
        "_score": 1.0,
        "_source": {
          "titre": "Mon premier document",
          "note": 5,
          "auteur": "Alice"
        }
      }
    ]
  }
}

What to look at: hits.total.value: 2 (how many cards match) then hits.hits, the list of cards, each with its _id and its _source. The order of the two may vary: with no criterion, all have the same _score of 1.0. SQL equivalent: SELECT * FROM pratique_mini.

Finally, search for a word:

text
GET pratique-mini/_search
{
  "query": {
    "match": {
      "titre": "premier"
    }
  }
}

What the query asks: the cards whose titre field contains the word premier.

json
{
  "took": 1,
  "timed_out": false,
  "_shards": {
    "total": 1,
    "successful": 1,
    "skipped": 0,
    "failed": 0
  },
  "hits": {
    "total": {
      "value": 1,
      "relation": "eq"
    },
    "max_score": 0.3788134,
    "hits": [
      {
        "_index": "pratique-mini",
        "_id": "1",
        "_score": 0.3788134,
        "_source": {
          "titre": "Mon premier document",
          "note": 5,
          "auteur": "Alice"
        }
      }
    ]
  }
}

What to look at: a single card, number 1, and a _score that is no longer 1.0: this is relevance, "how well this card answers the question". Module 3 is devoted to this number. Approximate SQL equivalent: SELECT * FROM pratique_mini WHERE titre LIKE '%premier%', except that match would also find Premier with a capital letter, and module 2 will explain why.

To understand better
  • _count returns 0 or 1 right after a write? Elasticsearch makes new cards visible to search every second, not at the very instant. Rerun _count: it is up to date. GET _doc/1, on the other hand, is always immediate because it does not search, it goes straight to the number. If you want to force immediate visibility in a test: PUT pratique-mini/_doc/2?refresh=true.
  • took is the search time in milliseconds. timed_out: false: it finished on time.
  • Why GET with a body? It is a peculiarity of Elasticsearch: a search is a read, hence GET, but the question fits in a JSON body. POST pratique-mini/_search with the same body works too; both are accepted.

Step 8 — Delete a Card, Then the Cabinet

text
DELETE pratique-mini/_doc/2
json
{
  "_index": "pratique-mini",
  "_id": "2",
  "_version": 2,
  "result": "deleted",
  "_shards": {
    "total": 1,
    "successful": 1,
    "failed": 0
  },
  "_seq_no": 4,
  "_primary_term": 1
}

What to look at: "result": "deleted". GET pratique-mini/_count returns 1 (after one second). SQL equivalent: DELETE FROM pratique_mini WHERE id = 2.

Then delete the entire cabinet, cards included:

text
DELETE pratique-mini
json
{
  "acknowledged": true
}

Proof that it no longer exists, GET pratique-mini/_doc/1:

json
{
  "error": {
    "root_cause": [
      {
        "type": "index_not_found_exception",
        "reason": "no such index [pratique-mini]",
        "resource.type": "index_or_alias",
        "resource.id": "pratique-mini",
        "index_uuid": "_na_",
        "index": "pratique-mini"
      }
    ],
    "type": "index_not_found_exception",
    "reason": "no such index [pratique-mini]",
    "resource.type": "index_or_alias",
    "resource.id": "pratique-mini",
    "index_uuid": "_na_",
    "index": "pratique-mini"
  },
  "status": 404
}

What to look at: the difference with step 4. Missing card in an existing cabinet: "found": false, no error. Missing cabinet: index_not_found_exception, 404. Both are normal responses from a running service. SQL equivalent: DROP TABLE pratique_mini.

Final Check

text
GET _cat/indices/pratique-*?v

Expected response: the header line alone. Nothing of yours is left lying around in the cluster, and GET _cat/indices/cours,avis,acces?v still shows 504, 609, 12000.

  • You created pratique-mini and you know why it was yellow, then green.
  • You filed card 1 with PUT _doc/1 and read it back with GET _doc/1.
  • You added auteur and note with POST _update/1 without losing titre.
  • You saw PUT _doc/1 erase the two fields, and you can state the rule: PUT replaces, _update completes.
  • _count said 2, _search listed both cards, match kept only one.
  • You deleted card 2 then the index, and pratique-* is empty.
  • You kept the three-field response of GET pratique-mini/_doc/1 (step 5) as the deliverable.

If Something Goes Wrong

Show the frequent cases
  • 400 with Unexpected character or was expecting double-quote to start field name → the body's JSON is malformed: every field name and every text in double quotes ", a comma between fields, no comma after the last one. Dev Tools underlines the spot.
  • 400 with resource_already_exists_exception on PUT pratique-mini → the index already exists (you reran step 1). Continue at step 2, or DELETE pratique-mini to start from scratch.
  • 400 with no handler found for uri → typo in a _ word: _serch, _doc/ forgotten, _udpate. Elasticsearch validates the path before anything else.
  • 405 with Incorrect HTTP method for uri [/pratique-mini/_update/1] and method [GET], allowed: [POST] → wrong verb for this path. The message itself says which one is accepted.
  • 400 with [UpdateRequest] unknown field [titre] → you sent the fields directly to _update, without wrapping them in "doc": { … }. Add the wrapper.
  • _count or _search do not see the card you just wrote → wait one second and rerun (see "To understand better" in step 7). GET _doc/1 sees it right away.
  • "result": "noop" on _update → the values sent were already those of the card; nothing to change, _version did not move. That is not an error.
  • "status": "yellow" persists in GET _cluster/health after step 2 → another index of yours still has rep 1. GET _cat/indices?v&health=yellow points it out; apply the same _settings to it, or delete it.
  • Dev Tools shows "Kibana server is not ready yet" → Kibana is restarting or waiting for Elasticsearch; .\labo.ps1 etat or ./labo.sh etat, then lesson 04.