Taller fundamental 1 — Elasticsearch: un índice, un documento, una consulta GET

Práctica guiada13 min
Duración
20 min
Módulo
1/7
Requisitos previos
el laboratorio está corriendo (etat muestra (healthy) en todas partes), Kibana Dev Tools abierto
Vas a construir
un índice propio, pratique-mini, con dos documentos que vas a leer, completar, buscar y luego eliminar
Entregable
la respuesta de GET pratique-mini/_doc/1 tras el paso 5, con sus tres campos

Cómo leer esta página. Ocho pasos, una consulta a la vez. Para cada uno: la consulta que escribes, la respuesta exacta del laboratorio, y qué mirar en ella. Escribe tú mismo cada consulta (sin copiar y pegar): es escribiendo PUT, GET, _doc que las palabras se fijan. Los bloques «Para entender bien» son opcionales; ábrelos si un paso te deja una duda. Si el laboratorio no está iniciado, vuelve a la práctica guiada: la sección En resumen da los comandos, kit incluido (https://github.com/hrhouma2/aiopsatlas-recherche-graphes-labo-fr).

Objetivo

La práctica guiada te hizo cargar 504 cursos, 609 reseñas y 12 000 líneas de registro de una sola vez, con un script. Viste los números, pero aún no has escrito nada tú mismo. Aquí, partes de cero: un índice vacío que creas, una primera ficha que guardas dentro, que vuelves a leer, que completas, luego una segunda, una búsqueda, una eliminación. Al final, sabes qué es un índice y un documento porque fabricaste uno, no porque te lo dijeron.

El vocabulario en una imagen

Un índice es un armario. Un documento es una ficha guardada en el armario: un pequeño texto JSON con campos. Cada ficha tiene un número, su _id, que permite encontrarla directamente sin buscar. No tienes que declarar las columnas de antemano: la primera ficha guardada crea el mapping (el plano del armario) por sí sola.

ElasticsearchBase SQL clásicaEn esta práctica
índicetablapratique-mini
documentofila{"titre": "Mon premier document"}
campocolumnatitre, auteur, note
_idclave primaria1, 2
mappingesquema de la tabla (CREATE TABLE …)creado automáticamente en el paso 3
_sourcela fila tal como la escribistelo que GET _doc/1 te devuelve

Dónde escribir, y cómo leer una consulta

Abre http://localhost:5601, menú ☰ → ManagementDev Tools. El panel izquierdo recibe las consultas, el de la derecha muestra la respuesta. Envías con Ctrl + Enter (Cmd + Enter en macOS) o el botón ▶ a la derecha de la línea.

Toda consulta tiene la misma forma: un verbo, una ruta, y a veces un cuerpo JSON debajo.

VerboQué haceEquivalente SQL
GETleer, sin cambiar nadaSELECT
PUTcrear, o reemplazar por completoCREATE TABLE, INSERT (o reemplazar la fila)
POSTactuar: actualizar, buscar con un cuerpoUPDATE
DELETEeliminarDROP TABLE, DELETE

La ruta dice sobre qué se actúa: pratique-mini (el armario), pratique-mini/_doc/1 (la ficha número 1 del armario), pratique-mini/_search (buscar en el armario). Las palabras que empiezan con _ son comandos de Elasticsearch, no nombres tuyos.

Paso 1 — Crear el armario, vacío

text
PUT pratique-mini

Qué pide la consulta: crea un índice llamado pratique-mini. Nada más: sin columnas, sin contenido.

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

A observar: "acknowledged": true, «está hecho», y el nombre como eco. La insignia en la parte superior derecha de la respuesta dice 200 - OK.

Para entender bien
  • ¿Por qué pratique- delante? Todos los índices que creas en este curso llevan este prefijo. Así, GET _cat/indices/pratique-*?v lista todo lo que es tuyo y nada más, y cours, avis, acces permanecen intactos.
  • Un nombre de índice va en minúsculas, sin espacio, sin mayúscula ni /. Pratique-Mini sería rechazado.
  • Una segunda vez la misma consulta responde 400 con resource_already_exists_exception: el armario ya existe. No es un fallo, es una respuesta.

Paso 2 — Verlo, y corregir su color

text
GET _cat/indices/pratique-mini?v

Qué pide la consulta: una línea de resumen sobre este índice, con la línea de encabezado (?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

A observar: docs.count 0, el armario está vacío. Y health yellow con rep 1: Elasticsearch previó una copia de respaldo (una réplica) de tu índice en una segunda máquina, y el laboratorio solo tiene una. La copia no puede colocarse en ningún lado, de ahí el amarillo. Los tres índices del kit están en verde porque su mapping fija number_of_replicas: 0. Haz lo mismo, en una consulta:

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

Vuelve a escribir 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

A observar: green, rep 0. Tu uuid será diferente: es el identificador interno del índice, elegido al azar en la creación.

Para entender bien
  • Amarillo no es roto. Un índice amarillo se lee y se escribe normalmente. Es una advertencia: «la copia de respaldo que pediste no existe». En una sola máquina, no puede existir.
  • Mientras tu índice estaba amarillo, GET _cluster/health decía "status": "yellow" para todo el cluster: el color del cluster es el peor color de sus índices. Es la explicación del fallo «yellow» del catálogo de la lección 04.
  • GET pratique-mini (sin _cat) devuelve la ficha completa del índice: "mappings": { } (vacío, ninguna ficha guardada) y "settings" con number_of_replicas.

Paso 3 — Guardar una primera ficha

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

Qué pide la consulta: en el armario pratique-mini, guarda una ficha (_doc) número 1 que contiene un campo titre.

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

A observar: "result": "created" y "_version": 1: primera versión de la ficha 1. Equivalente SQL: INSERT INTO pratique_mini (id, titre) VALUES (1, 'Mon premier document'), salvo que no fue necesario ningún CREATE TABLE.

Para entender bien
  • El mapping acaba de nacer. Escribe GET pratique-mini/_mapping: el campo titre ahora está declarado de tipo text (para buscar palabras dentro) con un subcampo titre.keyword (para ordenar o filtrar por el valor exacto). Elasticsearch lo dedujo del valor "Mon premier document", una cadena.
  • El 1 de _doc/1 lo elegiste tú. Los documentos del kit hacen lo mismo (C0001, A00001…). Si escribes POST pratique-mini/_doc sin número, Elasticsearch inventa un _id de veinte caracteres; práctico para registros, molesto para una ficha que quieres encontrar a mano.
  • _shards, _seq_no, _primary_term son contabilidad interna (en cuántos fragmentos se confirmó la escritura, qué número de orden). No los necesitas en este curso.

Paso 4 — Volver a leer la ficha

text
GET pratique-mini/_doc/1

Qué pide la consulta: dame la ficha número 1 de pratique-mini, directamente, sin buscar.

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

A observar: "found": true, y _source, tu ficha tal como la escribiste, al carácter exacto. Equivalente SQL: SELECT * FROM pratique_mini WHERE id = 1.

Prueba con una ficha que no existe: GET pratique-mini/_doc/3.

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

Sin error, sin _source: "found": false, insignia 404 - Not Found. Elasticsearch entendió la pregunta; la respuesta es «no hay nada en ese número».

Paso 5 — Añadir valores a la ficha

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

Qué pide la consulta: actualiza (_update) la ficha 1 añadiendo estos dos campos. Lo que no se menciona (titre) queda igual.

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

A observar: "result": "updated", "_version": 2. Vuelve a leer la ficha con 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"
  }
}

Tres campos. El título sigue ahí. Equivalente SQL: UPDATE pratique_mini SET auteur = 'Alice', note = 5 WHERE id = 1, con la diferencia de que en SQL las columnas auteur y note habrían tenido que existir antes. Esta es tu respuesta-entregable: guárdala.

Para entender bien
  • La palabra doc en el cuerpo significa «aquí están los campos a fusionar». Sin ella, _update no sabe qué hacer.
  • El mapping ha crecido. GET pratique-mini/_mapping ahora muestra auteur (text + keyword, como titre) y note de tipo long, un entero. Elasticsearch adivinó el tipo a partir de 5. Si hubieras escrito "note": "5" entre comillas, habría declarado texto, y ya no podrías calcular un promedio sobre ese campo. Es el tema de la lección sobre el mapping, en el módulo 2.
  • _version cuenta las escrituras en esta ficha, no las lecturas: GET nunca la incrementa.

Paso 6 — La trampa: PUT reemplaza todo

Vuelve a enviar exactamente la consulta del paso 3:

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
}

A observar: "result": "updated" (no created: la ficha 1 ya existía) y "_version": 3. Luego vuelve a leerla:

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

auteur y note han desaparecido. PUT _doc/1 no modifica la ficha 1: la reemplaza por lo que envías. Para completar sin perder, es POST _update/1 con doc. Recuerda la regla con los dos verbos: PUT reemplaza, _update completa. Vuelve a poner los dos campos con la consulta del paso 5 antes de continuar (obtienes _version: 4).

Paso 7 — Una segunda ficha, contar, buscar

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

Respuesta: "_id": "2", "result": "created", "_version": 1. Luego cuenta:

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

Equivalente SQL: SELECT COUNT(*) FROM pratique_mini. Ahora, mira todo lo que hay dentro:

text
GET pratique-mini/_search

Qué pide la consulta: busca en pratique-mini, sin criterio, así que todo.

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"
        }
      }
    ]
  }
}

A observar: hits.total.value: 2 (cuántas fichas responden) luego hits.hits, la lista de fichas, cada una con su _id y su _source. El orden de las dos puede variar: sin criterio, todas tienen el mismo _score de 1.0. Equivalente SQL: SELECT * FROM pratique_mini.

Por último, busca una palabra:

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

Qué pide la consulta: las fichas cuyo campo titre contiene la palabra 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"
        }
      }
    ]
  }
}

A observar: una sola ficha, la 1, y un _score que ya no es 1.0: es la relevancia, «cuánto responde esta ficha a la pregunta». El módulo 3 está dedicado a este número. Equivalente SQL aproximado: SELECT * FROM pratique_mini WHERE titre LIKE '%premier%', salvo que match también encontraría Premier con mayúscula, y el módulo 2 te explicará por qué.

Para entender bien
  • ¿_count devuelve 0 o 1 justo después de una escritura? Elasticsearch hace visibles los documentos nuevos para la búsqueda cada segundo, no al instante. Vuelve a lanzar _count: está actualizado. GET _doc/1, en cambio, siempre es inmediato porque no busca, va directo al número. Si quieres forzar la visibilidad inmediata en una prueba: PUT pratique-mini/_doc/2?refresh=true.
  • took es el tiempo de la búsqueda en milisegundos. timed_out: false: terminó dentro de los plazos.
  • ¿Por qué GET con un cuerpo? Es una particularidad de Elasticsearch: la búsqueda es una lectura, por lo tanto GET, pero la pregunta cabe en un cuerpo JSON. POST pratique-mini/_search con el mismo cuerpo también funciona; ambos se aceptan.

Paso 8 — Eliminar una ficha, luego el armario

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
}

A observar: "result": "deleted". GET pratique-mini/_count devuelve 1 (después de un segundo). Equivalente SQL: DELETE FROM pratique_mini WHERE id = 2.

Luego elimina el armario entero, fichas incluidas:

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

Prueba de que ya no existe, 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
}

A observar: la diferencia con el paso 4. Ficha ausente en un armario presente: "found": false, sin error. Armario ausente: index_not_found_exception, 404. Ambas son respuestas normales de un servicio que está corriendo. Equivalente SQL: DROP TABLE pratique_mini.

Verificación final

text
GET _cat/indices/pratique-*?v

Respuesta esperada: solo la línea de encabezado. Nada tuyo permanece en el cluster, y GET _cat/indices/cours,avis,acces?v sigue mostrando 504, 609, 12000.

  • Creaste pratique-mini y sabes por qué estaba amarillo, luego verde.
  • Guardaste la ficha 1 con PUT _doc/1 y la volviste a leer con GET _doc/1.
  • Añadiste auteur y note con POST _update/1 sin perder titre.
  • Viste PUT _doc/1 borrar los dos campos, y sabes decir la regla: PUT reemplaza, _update completa.
  • _count dijo 2, _search listó las dos fichas, match solo conservó una.
  • Eliminaste la ficha 2 y luego el índice, y pratique-* está vacío.
  • Guardaste la respuesta de GET pratique-mini/_doc/1 con tres campos (paso 5) como entregable.

Si algo falla

Mostrar los casos frecuentes
  • 400 con Unexpected character o was expecting double-quote to start field name → el JSON del cuerpo está mal formado: cada nombre de campo y cada texto entre comillas dobles ", una coma entre los campos, sin coma después del último. Dev Tools subraya el lugar.
  • 400 con resource_already_exists_exception en PUT pratique-mini → el índice ya existe (relanzaste el paso 1). Continúa en el paso 2, o DELETE pratique-mini para volver a empezar de cero.
  • 400 con no handler found for uri → error de tipeo en una palabra con _: _serch, _doc/ olvidado, _udpate. Elasticsearch valida la ruta antes que todo lo demás.
  • 405 con Incorrect HTTP method for uri [/pratique-mini/_update/1] and method [GET], allowed: [POST] → verbo equivocado para esta ruta. El mensaje mismo dice cuál se acepta.
  • 400 con [UpdateRequest] unknown field [titre] → enviaste los campos directamente a _update, sin envolverlos en "doc": { … }. Añade la envoltura.
  • _count o _search no ven la ficha que acabas de escribir → espera un segundo y vuelve a lanzar (ver «Para entender bien» del paso 7). GET _doc/1 la ve de inmediato.
  • "result": "noop" en _update → los valores enviados ya eran los de la ficha; nada que cambiar, _version no se movió. No es un error.
  • "status": "yellow" persiste en GET _cluster/health después del paso 2 → otro índice tuyo todavía tiene rep 1. GET _cat/indices?v&health=yellow lo señala; aplícale el mismo _settings, o elimínalo.
  • Dev Tools muestra «Kibana server is not ready yet» → Kibana se está reiniciando o esperando a Elasticsearch; .\labo.ps1 etat o ./labo.sh etat, luego la lección 04.