Comment lire cette page. Chaque section est repliée sous son titre : clique sur « Afficher … » pour l'ouvrir, et referme-la quand tu as fini pour garder la page lisible. Ordre de lecture : Objectif, puis En bref (les commandes à taper), puis Le jeu de données (à lire avant toute requête), puis les requêtes Elasticsearch et Neo4j, classées de la plus simple (
GET _cat/indices,MATCH (n) RETURN n) à la plus impressionnante, avec une explication après chacune. Le pas à pas détaillé, avec la sortie attendue de chaque commande et les pannes à provoquer, est en annexe : annexe A pour Windows (PowerShell), annexe B pour Linux, macOS, WSL 2 et Git Bash. Ouvre une seule annexe, celle de ton système. L'annexe C, commune, regroupe les cas où ça coince.
Tu rejoins l'équipe qui construit le moteur de recherche de la plateforme de cours en ligne. Ta cheffe te tend le kit du labo : « Demain matin, je veux un labo qui tourne sur ton poste, les données chargées, et la certitude que tu sais le dépanner sans m'appeler. » Tu vas donc démarrer la stack, prouver que les trois moteurs contiennent ce qu'ils doivent, puis arrêter un service exprès pour voir comment la panne se lit dans etat, dans le navigateur et dans le journal, et le remettre en route. Reconnaître « ce service est arrêté » en dix secondes, c'est ce qui évite des heures de recherche au mauvais endroit.
Les neuf étapes de ce schéma sont détaillées, avec la sortie attendue de chaque commande, dans l'annexe A (Windows) ou l'annexe B (Linux, macOS) en bas de page.
Kit du labo : https://github.com/hrhouma2/aiopsatlas-recherche-graphes-labo-fr
Tu clones le kit dans un dossier lab1, tu vérifies que Docker est prêt, tu démarres les trois services (Elasticsearch, Kibana, Neo4j), tu ouvres leurs trois pages web, puis tu charges les données. Tu lances importer et charger-graphe deux fois : la seconde ne doit rien changer aux compteurs, c'est la preuve que le chargement est rejouable sans doublon. À la fin, etat doit afficher (healthy) partout, acces 12000 avis 609 cours 504 et nœuds : 872. Commence par exécuter ce bloc.
Windows (PowerShell)
git clone https://github.com/hrhouma2/aiopsatlas-recherche-graphes-labo-fr.git lab1
cd lab1
ls # explorer le contenu : docker-compose.yml, labo.ps1, labo.sh, elasticsearch/, neo4j/, outils/
.\labo.ps1 prerequis
.\labo.ps1 demarrer
.\labo.ps1 etatVérifier les trois URL dans le navigateur :
Kibana http://localhost:5601 (Dev Tools : menu ☰ → Management → Dev Tools)
Elasticsearch http://localhost:9200
Neo4j Browser http://localhost:7474 (utilisateur neo4j · mot de passe aiopsatlas2026).\labo.ps1 importer
.\labo.ps1 charger-graphe
.\labo.ps1 importer # 2e passage : mêmes compteurs, rien ne double
.\labo.ps1 charger-graphe
.\labo.ps1 etat # attendu : acces 12000 avis 609 cours 504 nœuds : 872Si PowerShell refuse .\labo.ps1 (« l'exécution de scripts est désactivée ») : Set-ExecutionPolicy -Scope CurrentUser RemoteSigned, réponds O, relance.
Linux, macOS, WSL 2, Git Bash
git clone https://github.com/hrhouma2/aiopsatlas-recherche-graphes-labo-fr.git lab1
cd lab1
ls # explorer le contenu : docker-compose.yml, labo.sh, labo.ps1, elasticsearch/, neo4j/, outils/
./labo.sh prerequis
./labo.sh demarrer
./labo.sh etatVérifier les trois URL dans le navigateur :
Kibana http://localhost:5601 (Dev Tools : menu ☰ → Management → Dev Tools)
Elasticsearch http://localhost:9200
Neo4j Browser http://localhost:7474 (utilisateur neo4j · mot de passe aiopsatlas2026)./labo.sh importer
./labo.sh charger-graphe
./labo.sh importer # 2e passage : mêmes compteurs, rien ne double
./labo.sh charger-graphe
./labo.sh etat # attendu : acces 12000 avis 609 cours 504 nœuds : 872Avant de taper une seule requête, regarde les données. Tout le labo tourne autour d'une plateforme de cours en ligne fictive : un catalogue de cours, les avis laissés par les étudiants, le journal du serveur web qui sert les pages, et les liens entre étudiants, professeurs, cours et compétences. Les mêmes données sont chargées dans Elasticsearch (pour chercher) et dans Neo4j (pour suivre les liens). Elles sont dans le kit, en clair, dans deux dossiers :
lab1/
├── elasticsearch/donnees/ ← ce qui va dans Elasticsearch (3 fichiers NDJSON)
│ ├── cours.ndjson 504 cours
│ ├── avis.ndjson 609 avis
│ └── acces.ndjson 12 000 lignes de journal web
└── neo4j/import/ ← ce qui va dans Neo4j (8 fichiers CSV)
├── cours.csv 504 cours → nœuds Cours
├── etudiants.csv 300 étudiants → nœuds Etudiant
├── professeurs.csv 30 professeurs → nœuds Professeur
├── competences.csv 22 compétences → nœuds Competence
├── villes.csv 16 villes → nœuds Ville
├── inscriptions.csv 1 654 inscriptions → relations INSCRIT_A
├── couvre.csv 994 liens → relations COUVRE
└── prerequis.csv 230 liens → relations PREREQUIS_DEOuvre-les toi-même, ça prend dix secondes et tu sauras exactement ce que tu manipules :
# Windows (PowerShell), depuis le dossier lab1
Get-Content elasticsearch\donnees\cours.ndjson -TotalCount 2
Get-Content elasticsearch\donnees\avis.ndjson -TotalCount 2
Get-Content elasticsearch\donnees\acces.ndjson -TotalCount 2
Get-Content neo4j\import\cours.csv -TotalCount 3
Get-Content neo4j\import\inscriptions.csv -TotalCount 3# Linux, macOS, WSL 2, Git Bash, depuis le dossier lab1
head -n 2 elasticsearch/donnees/cours.ndjson
head -n 2 elasticsearch/donnees/avis.ndjson
head -n 2 elasticsearch/donnees/acces.ndjson
head -n 3 neo4j/import/cours.csv
head -n 3 neo4j/import/inscriptions.csvUn fichier .ndjson (newline-delimited JSON) contient un objet JSON par ligne. Dans le kit, les lignes vont par deux : une ligne d'instruction (« range ce qui suit dans l'index cours sous l'identifiant C0001 »), puis le document lui-même. C'est le format que la commande importer envoie à Elasticsearch en bloc.
{"index":{"_index":"cours","_id":"C0001"}}
{"id":"C0001","titre":"Docker expliqué simplement","description":"…","categorie":"DevOps", … }cours : 504 documents, un par cours du catalogueLe premier document, tel qu'il est dans Elasticsearch :
{
"id": "C0001",
"titre": "Docker expliqué simplement",
"description": "Dans ce cours accessible sans prérequis, vous apprenez à automatiser vos applications avec Docker. …",
"categorie": "DevOps",
"sujet": "Docker",
"niveau": "debutant",
"langue": "en",
"prix": 129,
"gratuit": false,
"duree_heures": 5,
"tags": ["docker", "linux", "helm", "devops"],
"date_publication": "2024-08-14",
"note_moyenne": 4.4,
"nb_avis": 327,
"professeur": { "id": "P001", "nom": "Karim Caron", "ville": "Gatineau" },
"competences": ["Conteneurisation", "Intégration continue"]
}| Champ | Exemple | Ce que c'est |
|---|---|---|
id | C0001 | Identifiant du cours. C comme cours, puis un numéro. C'est aussi l'_id du document. |
titre | Docker expliqué simplement | Le titre. Texte libre, c'est là-dessus qu'on fera les recherches par mots. |
description | Dans ce cours… | Un paragraphe de présentation. Texte libre aussi. |
categorie | DevOps | Une des six grandes familles : Cloud, DevOps, Données, Développement web, IA, Sécurité. 84 cours chacune. |
sujet | Docker | Plus précis que la catégorie : Docker, Kubernetes, Neo4j, Elasticsearch… |
niveau | debutant | debutant, intermediaire ou avance. |
langue | en | fr ou en. |
prix | 129 | En dollars. 0 pour un cours gratuit. |
gratuit | false | Vrai ou faux. |
duree_heures | 5 | Durée totale du cours. |
tags | ["docker", "linux", …] | Une liste de mots-clés. Un champ peut contenir plusieurs valeurs. |
date_publication | 2024-08-14 | Une date. |
note_moyenne | 4.4 | Moyenne des notes reçues, sur 5. |
nb_avis | 327 | Nombre d'avis reçus. |
professeur | { "id": "P001", "nom": …, "ville": … } | Un objet dans l'objet : le professeur est décrit directement dans la fiche du cours, avec son identifiant, son nom et sa ville. |
competences | ["Conteneurisation", …] | Liste des compétences que le cours couvre. |
avis : 609 documents, un par avis laissé par un étudiant{
"id": "A00001",
"cours_id": "C0028",
"etudiant": "Nathan",
"ville": "Sherbrooke",
"pays": "Canada",
"note": 3,
"texte": "Les vidéos sont bonnes, la partie théorique est dense.",
"date": "2024-06-21",
"utile": 33
}| Champ | Exemple | Ce que c'est |
|---|---|---|
id | A00001 | Identifiant de l'avis. A comme avis. |
cours_id | C0028 | Le cours concerné. C'est le lien vers l'index cours : ce C0028 est l'id d'un document de cours. |
etudiant | Nathan | Prénom de l'auteur. |
ville, pays | Sherbrooke, Canada | D'où il écrit. |
note | 3 | La note donnée, de 1 à 5. |
texte | Les vidéos sont bonnes… | Le commentaire. Texte libre. |
date | 2024-06-21 | Date de l'avis. |
utile | 33 | Nombre de personnes qui ont trouvé cet avis utile. |
acces : 12 000 documents, une ligne par requête reçue par le serveur web{
"id": "L000001",
"@timestamp": "2026-08-10T08:17:19.000Z",
"methode": "GET",
"chemin": "/robots.txt",
"cours_id": null,
"categorie": null,
"statut": 200,
"octets": 108506,
"duree_ms": 100,
"ip": "108.190.166.1",
"pays": "CA",
"appareil": "desktop",
"navigateur": "Edge",
"referent": "google"
}| Champ | Exemple | Ce que c'est |
|---|---|---|
id | L000001 | Identifiant de la ligne. L comme ligne de journal (log). |
@timestamp | 2026-08-10T08:17:19.000Z | Date et heure exactes de la requête. Le @ est une convention : c'est le champ de temps que Kibana repère automatiquement. |
methode | GET | GET ou POST. |
chemin | /robots.txt | L'adresse demandée sur le site : /, /cours, /catalogue, /contact, /tarifs, ou la page d'un cours. |
cours_id | null ou C0042 | Le cours consulté, si la page en concerne un ; null (vide) sinon, comme ici pour /robots.txt. Lien vers l'index cours. |
categorie | null ou DevOps | Catégorie du cours consulté, recopiée pour simplifier les graphiques. |
statut | 200 | Le code HTTP de la réponse : 200 OK, 301 et 304 redirection ou cache, 404 introuvable, 500 et 503 erreur serveur. |
octets | 108506 | Taille de la réponse envoyée. |
duree_ms | 100 | Temps de réponse en millisecondes. |
ip | 108.190.166.1 | Adresse IP du visiteur. |
pays | CA | Pays du visiteur, code à deux lettres. |
appareil | desktop | desktop, mobile ou tablette. |
navigateur | Edge | Chrome, Firefox, Safari, Edge… |
referent | google | D'où venait le visiteur : direct, google, linkedin, youtube ou newsletter. |
Les trois index sont liés par cours_id : un avis parle d'un cours, une ligne de journal consulte un cours. Mais Elasticsearch ne fait pas de jointure : chaque index se cherche séparément. Pour suivre les liens, on passe dans Neo4j.
Neo4j reçoit les mêmes cours (mêmes identifiants C0001…, mêmes titres), plus ce qui n'est pas dans Elasticsearch : les étudiants, leurs inscriptions, les professeurs, les compétences, les villes, et les prérequis entre cours. Le modèle tient en un dessin :
Les cinq flèches se lisent comme des phrases : un étudiant est inscrit à un cours ; un professeur enseigne un cours ; un cours couvre une compétence ; un cours **est prérequis d'**un autre cours ; un étudiant ou un professeur habite une ville. Au total 872 nœuds et 3 712 relations.
Les données arrivent sous forme de fichiers CSV classiques, une ligne d'en-tête puis une ligne par élément :
cours.csv id,titre,categorie,sujet,niveau,prix,duree_heures,date_publication,professeur_id
C0001,Docker expliqué simplement,DevOps,Docker,debutant,129,5,2024-08-14,P001
etudiants.csv id,prenom,nom,ville,pays,inscription_le,interet
E0001,Nathan,Ben Ali,Sherbrooke,Canada,2023-05-15,DevOps
professeurs.csv id,prenom,nom,ville,pays,specialite,annees_experience
P001,Karim,Caron,Gatineau,Canada,DevOps,23
competences.csv id,nom
K01,Conteneurisation
villes.csv nom,pays,latitude,longitude
Montréal,Canada,45.5019,-73.5674
inscriptions.csv etudiant_id,cours_id,date,progression,note
E0001,C0028,2024-04-01,100,3
couvre.csv cours_id,competence_id
C0001,K01
prerequis.csv prerequis_id,cours_id
C0001,C0003Regarde comment ça se traduit en graphe. Les cinq premiers fichiers deviennent des nœuds ; chaque ligne est un nœud, chaque colonne une propriété. Les trois derniers deviennent des relations ; chaque ligne relie deux nœuds par leurs identifiants. La ligne E0001,C0028,2024-04-01,100,3 de inscriptions.csv devient la flèche (Etudiant E0001)-[:INSCRIT_A {date, progression, note}]->(Cours C0028) : Nathan Ben Ali est inscrit au cours C0028, l'a terminé à 100 %, et lui a mis 3. Compare avec le premier avis de l'index avis : c'est le même Nathan, le même cours C0028, la même note 3.
| Nœud | Propriétés | Exemple |
|---|---|---|
Cours | id, titre, categorie, sujet, niveau, prix, duree_heures, date_publication | C0001, « Docker expliqué simplement », 129 $ |
Etudiant | id, prenom, nom, interet, inscription_le | E0001, Nathan Ben Ali, intérêt DevOps |
Professeur | id, prenom, nom, specialite, annees_experience | P001, Karim Caron, DevOps, 23 ans |
Competence | id, nom | K01, Conteneurisation |
Ville | nom, pays, latitude, longitude | Montréal, Canada |
| Relation | De → vers | Propriétés | Sens |
|---|---|---|---|
INSCRIT_A | Etudiant → Cours | date, progression, note | L'étudiant suit ce cours |
ENSEIGNE | Professeur → Cours | — | Ce professeur donne ce cours (un seul par cours) |
COUVRE | Cours → Competence | — | Ce cours apprend cette compétence |
PREREQUIS_DE | Cours → Cours | — | Il faut avoir suivi le premier avant le second |
HABITE | Etudiant ou Professeur → Ville | — | Où la personne vit |
Toutes les données utilisent la même convention : une lettre, puis un numéro. Quand tu vois un identifiant, tu sais tout de suite de quoi on parle.
| Préfixe | Chose | Exemple | Où on le trouve |
|---|---|---|---|
C | Cours | C0001 | Elasticsearch cours, avis.cours_id, acces.cours_id, Neo4j Cours |
A | Avis | A00001 | Elasticsearch avis |
L | Ligne de journal | L000001 | Elasticsearch acces |
E | Étudiant | E0001 | Neo4j Etudiant |
P | Professeur | P001 | cours.professeur.id dans Elasticsearch, Neo4j Professeur |
K | Compétence | K01 | Neo4j Competence |
Retiens un seul fil pour toute la suite : le cours C0001, « Docker expliqué simplement », enseigné par P001, Karim Caron. Tu le retrouveras dans Elasticsearch (requête E9), dans Neo4j (requêtes N7 et N13), et il servira à prouver que les deux moteurs contiennent bien la même chose.
Les compteurs de etat prouvent que les données sont là ; les requêtes ci-dessous les font voir. Elles sont toutes en lecture seule : tu peux les rejouer autant de fois que tu veux, rien ne sera modifié. Elles sont identiques sous Windows et sous Linux, tout se passe dans le navigateur.
La règle de cette section : une seule nouveauté par requête. On commence par « montre-moi ce qu'il y a », sans aucun paramètre, et on ajoute une notion à chaque pas. Après chaque requête, une explication courte ; quand il faut creuser, un bloc dépliable « Pour bien comprendre ». Colle les requêtes une par une, dans l'ordre, et lis la réponse avant de passer à la suivante.
Ouvre http://localhost:5601, puis menu ☰ → Management → Dev Tools (ou « Outils de développement »). Le volet de gauche est un éditeur : colle une requête, place le curseur dessus, puis Ctrl+Entrée ou le triangle ▶. La réponse s'affiche à droite, avec le code HTTP (200 - OK) en bas.
Une image à garder en tête pour toute la suite : Elasticsearch est une grande armoire.
L'armoire = Elasticsearch
Un tiroir = un index (le tiroir « cours », le tiroir « avis », le tiroir « acces »)
Une fiche = un document (une fiche par cours, une fiche par avis, une fiche par ligne de journal)
_cat/indices = lire les étiquettes collées sur les tiroirs : nom, état, nombre de fiches, épaisseur
_search = ouvrir un tiroir et lire les fiches qui sont dedansLes requêtes E1 à E4 regardent les étiquettes des tiroirs. À partir de E5, on ouvre les tiroirs. Ne confonds jamais les deux : c'est l'erreur numéro un des débutants.
GET _cat/indicesCe que la requête demande : « Elasticsearch, montre-moi la liste de tous tes tiroirs. » Réponse sur la machine du cours :
green open avis W9j_JrwJT4mdzpWcS5k7xg 1 0 609 0 74.6kb 74.6kb 74.6kb
green open .internal.alerts-security.alerts-default-000001 YJq3VGeRQKqD0fqJW6Fw3w 1 0 0 0 249b 249b 249b
green open acces aii68fsfQXKyt5wqkE1mPA 1 0 12000 0 1.5mb 1.5mb 1.5mb
green open cours pmq403ZgSZWeHJY9uNw1Qw 1 0 504 0 183.6kb 183.6kb 183.6kbQuatre lignes = quatre tiroirs. Trois sont à toi : avis, acces, cours. Le quatrième, celui dont le nom commence par un point (.internal.alerts-security…), n'est pas à toi : Kibana l'a créé tout seul pour son usage interne (la gestion d'alertes de sécurité). Il est vide (0 fiches), il ne gêne pas, ne le supprime pas. Dans la suite, on l'écartera de l'affichage.
Ce qui est pénible ici : il y a des chiffres partout et aucun titre de colonne. C'est comme un tableau Excel sans ligne d'en-tête. On corrige ça en E2.
Pour lire la requête elle-même, mot par mot :
| Morceau | Ce que ça veut dire |
|---|---|
GET | « Je veux lire quelque chose. » Je ne modifie rien, je ne supprime rien, je regarde. Sans risque. |
_cat | « Réponds-moi en tableau texte », lisible par un humain, pas en JSON. Le tiret bas au début signale une commande d'Elasticsearch lui-même, pas un nom d'index. |
indices | « … le tableau des index. » C'est le pluriel anglais d'index. |
Un index, c'est un tiroir : un endroit où l'on range des fiches qui se ressemblent. Toutes les fiches « cours » vont dans le tiroir cours, toutes les fiches « avis » dans le tiroir avis, et chaque ligne du journal du serveur web est une fiche du tiroir acces. Si tu connais SQL, l'index est ce que SQL appelle une table :
| Elasticsearch | Dans l'image | SQL |
|---|---|---|
Index cours | Le tiroir « cours » | Table cours |
| Document JSON | Une fiche dans le tiroir | Ligne |
| Champ | Une case sur la fiche (titre, prix…) | Colonne |
_id du document | Le numéro écrit en haut de la fiche | Clé primaire |
Les trois tiroirs du labo :
Elasticsearch
├── index cours → 504 documents (un document = un cours du catalogue)
├── index avis → 609 documents (un document = un avis laissé par un étudiant)
└── index acces → 12 000 documents (un document = une ligne de journal du serveur web)Deux pièges de vocabulaire. Le singulier est un index, et le pluriel est des index en français (Elasticsearch écrit indices, le pluriel anglais). Et ce n'est pas un « indice » au sens mathématique ou policier : c'est un index comme celui à la fin d'un livre, la liste qui dit à quelle page se trouve chaque mot. Elasticsearch fait exactement ça, en très grand.
Maintenant, prenons une seule ligne de la réponse E1 et lisons-la valeur par valeur, de gauche à droite :
green open avis W9j_JrwJT4mdzpWcS5k7xg 1 0 609 0 74.6kb 74.6kb 74.6kb| Valeur | Ce que ça veut dire |
|---|---|
green | Le tiroir est en bon état. Vert = tout va bien. |
open | Le tiroir est ouvert : on peut lire et écrire dedans. (Le contraire serait close.) |
avis | Le nom du tiroir. |
W9j_JrwJT4mdzpWcS5k7xg | Un numéro de série technique, généré par Elasticsearch. Tu ne t'en serviras jamais. |
1 | Le tiroir est en un seul morceau (un shard principal). Un gros index peut être découpé en plusieurs morceaux répartis sur plusieurs machines ; ici, non. |
0 | Zéro copie de sécurité (replica). Normal dans un labo à une seule machine : une copie n'aurait nulle part où aller. |
609 | Le tiroir contient 609 fiches. C'est la valeur qu'on regarde en premier. |
0 | Zéro fiche marquée « à jeter » en attente de nettoyage. |
74.6kb | Place totale prise sur le disque. |
74.6kb | Place prise par le morceau principal seul (identique : il n'y a qu'un morceau et pas de copie). |
74.6kb | Taille des données elles-mêmes. |
Trois fois 74.6kb parce que, avec un seul morceau et zéro copie, les trois façons de mesurer donnent le même résultat. Sur un vrai cluster avec des copies, store.size serait plus grand que pri.store.size.
En résumé, tes trois tiroirs :
avis → 609 fiches → 74,6 Ko
acces → 12 000 fiches → 1,5 Mo
cours → 504 fiches → 183,6 Ko?vGET _cat/indices?vhealth status index uuid pri rep docs.count docs.deleted store.size pri.store.size dataset.size
green open avis W9j_JrwJT4mdzpWcS5k7xg 1 0 609 0 74.6kb 74.6kb 74.6kb
green open .internal.alerts-security.alerts-default-000001 YJq3VGeRQKqD0fqJW6Fw3w 1 0 0 0 249b 249b 249b
green open acces aii68fsfQXKyt5wqkE1mPA 1 0 12000 0 1.5mb 1.5mb 1.5mb
green open cours pmq403ZgSZWeHJY9uNw1Qw 1 0 504 0 183.6kb 183.6kb 183.6kbMême réponse qu'en E1, plus une ligne de titres au-dessus. C'est tout ce que fait ?v. Maintenant tu n'as plus à deviner : la colonne docs.count est le nombre de fiches (609, 0, 12000, 504), la colonne store.size est la place sur le disque.
?v, ça veut dire quoi, exactement ?
? dit : « ce qui suit, ce sont des options ». Il sépare la commande (_cat/indices) de ses réglages.v est l'option verbose, « bavard » : « affiche aussi les titres des colonnes ».Donc ?v = « réponds-moi avec les en-têtes, pour que je comprenne ce que je lis ». Prends l'habitude de le mettre toujours sur les commandes _cat. Sans ?v, tu as des chiffres ; avec ?v, tu as de l'information.
| Colonne | En clair |
|---|---|
health | Santé du tiroir. green : tout va bien. yellow : les fiches sont là, mais certaines copies de sécurité manquent. red : certaines fiches sont inaccessibles. |
status | open : utilisable. close : fermé, on ne peut ni lire ni écrire. |
index | Le nom du tiroir. |
uuid | Numéro de série technique. On ne s'en sert jamais à la main. |
pri | En combien de morceaux (primary shards) le tiroir est découpé. 1 dans le labo. |
rep | Combien de copies de sécurité (replicas) de chaque morceau. 0 dans le labo. |
docs.count | Nombre de fiches. La colonne à regarder en premier. |
docs.deleted | Fiches marquées « à jeter » pas encore nettoyées du disque. |
store.size | Place totale sur le disque. |
pri.store.size | Place des morceaux principaux seuls, sans les copies. |
dataset.size | Taille des données elles-mêmes. |
Pourquoi tout est green dans le labo : rep vaut 0, donc il n'y a aucune copie à faire, donc aucune copie ne peut manquer. Sur un vrai cluster, rep 1 avec une seule machine donnerait yellow, parce que la copie n'a nulle part où aller.
GET _cat/indices/cours?vhealth status index uuid pri rep docs.count docs.deleted store.size pri.store.size dataset.size
green open cours pmq403ZgSZWeHJY9uNw1Qw 1 0 504 0 183.6kb 183.6kb 183.6kbCe que la requête demande : « Montre-moi l'étiquette du tiroir cours. » Une seule nouveauté : le nom du tiroir ajouté après _cat/indices/. Une seule ligne en réponse, celle de cours, avec ses 504 fiches. Ce qui suit le dernier / sert de filtre.
Fais pareil pour les deux autres tiroirs :
GET _cat/indices/avis?v
GET _cat/indices/acces?v« Montre-moi l'étiquette du tiroir des avis » : 609 dans docs.count. « Montre-moi l'étiquette du tiroir des journaux d'accès » : 12000. Dans Dev Tools, quand plusieurs requêtes sont collées à la suite, seule celle où se trouve le curseur est envoyée : place-le sur la ligne voulue avant Ctrl+Entrée.
GET _cat/indices/acces,avis,cours?v&s=index&h=index,health,docs.count,store.sizeindex health docs.count store.size
acces green 12000 1.5mb
avis green 609 74.6kb
cours green 504 183.6kbCe que la requête demande : « Montre-moi un tableau résumé des tiroirs acces, avis et cours. Trie-les par nom, et n'affiche que leur nom, leur santé, leur nombre de fiches et leur taille. » Quatre lignes, quatre colonnes, tout ce qu'il faut et rien de plus. C'est la requête à garder sous la main pour vérifier le labo en un coup d'œil.
Elle a l'air compliquée parce qu'elle est longue, mais c'est juste E3 avec trois options de plus. Décomposition, morceau par morceau :
| Morceau | En clair |
|---|---|
GET | Je demande une information, je ne modifie rien. |
_cat/indices | Les étiquettes des tiroirs, en tableau lisible. |
/acces,avis,cours | Seulement ces trois tiroirs (noms séparés par des virgules, sans espace). Le tiroir interne de Kibana disparaît. |
?v | Avec les titres de colonnes. |
&s=index | s comme sort : trie les lignes par ordre alphabétique de la colonne index. |
&h=index,health,docs.count,store.size | h comme headers : n'affiche que ces colonnes, dans cet ordre. |
Une règle et une seule pour les options : la première commence par ?, toutes les suivantes par &. C'est pour ça qu'on lit ?v&s=…&h=….
Et les quatre colonnes choisies :
| Colonne | En clair |
|---|---|
index | Le nom du tiroir. |
health | Sa santé : green tout fonctionne ; yellow les fiches existent mais des copies de sécurité manquent ; red certaines fiches sont indisponibles. |
docs.count | Combien de fiches il contient. |
store.size | Combien de place il prend sur le disque. |
En une phrase : tes trois tiroirs sont ouverts, en bonne santé, et contiennent exactement les fiches attendues, 12 000, 609 et 504.
Sans h=, Elasticsearch renvoie onze colonnes dont la plupart ne t'apprennent rien au quotidien. Sans s=, l'ordre des lignes est arbitraire et change d'un appel à l'autre (regarde E2 : avis arrivait avant acces). En nommant les trois tiroirs, tu écartes aussi celui de Kibana. Le résultat tient sur quatre lignes et se compare d'un coup d'œil avec les chiffres attendus. C'est exactement ce que fait la commande etat du kit en coulisses.
Variante utile si tu veux aussi voir que les tiroirs sont bien ouverts : ajoute status dans la liste h= :
GET _cat/indices/acces,avis,cours?v&s=index&h=index,health,status,docs.count,store.sizeindex health status docs.count store.size
acces green open 12000 1.5mb
avis green open 609 74.6kb
cours green open 504 183.6kbBilan de E1 à E4 : tu n'as encore lu aucune fiche. Tu as seulement regardé les étiquettes sur les tiroirs. Tu sais qu'il y a 504 cours, mais tu n'as pas encore vu un seul titre de cours. C'est ce qu'on fait maintenant.
GET cours/_count{
"count": 504,
"_shards": { "total": 1, "successful": 1, "skipped": 0, "failed": 0 }
}Ce que la requête demande : « Compte les fiches du tiroir cours. » Réponse : 504. Regarde bien la forme de la requête, elle est nouvelle : on ne parle plus à l'armoire (_cat/…), on parle à un tiroir. Le nom du tiroir vient en premier (cours), puis ce qu'on veut lui faire (_count), séparés par un /. Toutes les requêtes qui suivent auront cette forme : nom-du-tiroir/_action.
Autre changement : la réponse n'est plus un tableau texte mais du JSON, avec des accolades et des guillemets. C'est la forme normale des réponses d'Elasticsearch ; _cat était l'exception. Ignore la partie _shards, elle dit seulement « le morceau du tiroir a répondu, rien n'a échoué ».
Essaie GET avis/_count (609) et GET acces/_count (12000). En SQL : SELECT COUNT(*) FROM cours.
GET cours/_searchCe que la requête demande : « Ouvre le tiroir cours et montre-moi les fiches qui sont dedans. » C'est la première fois que tu vois un vrai cours : son titre, son prix, son professeur.
La réponse est longue : c'est normal, elle contient dix fiches complètes. Elasticsearch ne te donne que les 10 premières, même si le tiroir en contient 504 ; c'est une protection, pour ne pas t'envoyer 504 fiches d'un coup sans que tu l'aies demandé. Regarde la structure plutôt que le contenu :
{
"took": 1,
"timed_out": false,
"_shards": { … },
"hits": {
"total": { "value": 504, "relation": "eq" },
"max_score": 1,
"hits": [
{
"_index": "cours",
"_id": "C0001",
"_score": 1,
"_source": {
"id": "C0001",
"titre": "Docker expliqué simplement",
"categorie": "DevOps",
"niveau": "debutant",
"prix": 129,
"tags": ["docker", "linux", "helm", "devops"],
"professeur": { "id": "P001", "nom": "Karim Caron", "ville": "Gatineau" },
…
}
},
… 9 autres documents …
]
}
}_search veut dire « cherche » ; sans autre précision, ça veut dire « donne-moi des fiches, n'importe lesquelles ». Les deux chiffres à repérer : "total": { "value": 504 } (il y a 504 fiches dans le tiroir) et la liste hits qui n'en contient que 10 (celles qu'on te montre). Chaque fiche est dans _source, avec toutes ses cases : titre, prix, categorie…
Fais pareil avec les deux autres tiroirs : GET avis/_search (« ouvre le tiroir avis, montre-moi les 10 premiers avis ») et GET acces/_search (les 10 premières lignes de journal). En SQL : SELECT * FROM cours LIMIT 10.
La différence essentielle, à ne plus jamais confondre.
GET _cat/indices/cours?vregarde l'étiquette du tiroir : son état, son nombre de fiches, sa taille. Une ligne. Tu ne vois aucun cours.
GET cours/_searchouvre le tiroir et lit les fiches : titres, prix, catégories, professeurs. Dix fiches. Tu vois les cours eux-mêmes.La première répond « il y a 504 cours ». La seconde répond « voici des cours ».
| Clé | En clair |
|---|---|
took | Le temps qu'Elasticsearch a mis, en millisecondes (ici 1 ms). |
hits.total.value | Le nombre total de fiches qui correspondent : 504. Même si on ne t'en montre que 10, il te dit combien il y en a en tout. |
hits.hits | La liste des fiches qu'on te montre : les 10 premières par défaut. |
_index | Le tiroir d'où vient la fiche (cours). |
_id | Le numéro écrit en haut de la fiche (C0001, C0002…). |
_score | Une note de pertinence. Vaut 1 partout ici parce qu'on n'a rien cherché de précis : toutes les fiches se valent. |
_source | La fiche elle-même, telle qu'elle a été rangée dans le tiroir, avec toutes ses cases. |
Le mot hit veut dire « touche », comme au tir : une fiche « touchée » par la requête. Le champ professeur est une fiche dans la fiche (un JSON dans le JSON) : le professeur est écrit directement sur la fiche du cours, avec son nom et sa ville. SQL ne fait pas ça naturellement, il faudrait une deuxième table et une jointure.
GET cours/_search
{
"size": 20
}Ce que la requête demande : « Ouvre le tiroir cours et montre-moi 20 fiches au lieu des 10 habituelles. »
Une seule nouveauté, mais importante : la requête a maintenant deux parties. La première ligne (GET cours/_search) dit quoi faire ; le bloc entre accolades en dessous, qu'on appelle le corps, donne des précisions. Ici la précision est "size": 20 : « taille du lot = 20 ». C'est tout. size veut simplement dire « combien de fiches tu veux qu'on te montre ».
Vérifie dans la réponse : hits.total.value vaut toujours 504 (le tiroir n'a pas changé), mais la liste hits contient maintenant 20 fiches au lieu de 10. En SQL : LIMIT 20.
En HTTP classique, GET n'a pas de corps ; Elasticsearch l'accepte quand même parce que c'est pratique dans Dev Tools. Si un outil refuse, POST cours/_search avec le même corps fait exactement la même chose. Autre limite à connaître : size ne peut pas dépasser 10 000 d'un coup (paramètre index.max_result_window) ; pour parcourir plus, on pagine. Pour nos 504 cours, "size": 504 marcherait, mais la réponse ferait des milliers de lignes : ce n'est pas comme ça qu'on lit des données, les agrégations (E13) sont faites pour ça.
GET cours/_search
{
"size": 3,
"_source": ["titre", "prix"]
}"hits": [
{ "_id": "C0001", "_source": { "titre": "Docker expliqué simplement", "prix": 129 } },
{ "_id": "C0002", "_source": { "titre": "Docker avancé : industrialiser des conteneurs en production", "prix": 29 } },
{ "_id": "C0003", "_source": { "titre": "Docker : le guide complet", "prix": 19 } }
]Ce que la requête demande : « Montre-moi 3 fiches, mais seulement les cases titre et prix de chaque fiche. » Nouveauté : _source avec une liste de champs. Au lieu de la fiche entière, on ne garde que les cases qui nous intéressent. La réponse devient lisible d'un coup d'œil. En SQL : SELECT titre, prix FROM cours LIMIT 3. On utilisera _source dans presque toutes les requêtes suivantes, justement pour garder des réponses courtes.
GET cours/_doc/C0001{
"_index": "cours",
"_id": "C0001",
"_version": 6,
"found": true,
"_source": {
"id": "C0001",
"titre": "Docker expliqué simplement",
"categorie": "DevOps",
"sujet": "Docker",
"niveau": "debutant",
"prix": 129,
"note_moyenne": 4.4,
"nb_avis": 327,
"professeur": { "id": "P001", "nom": "Karim Caron", "ville": "Gatineau" },
…
}
}Pas de recherche ici : on demande le document dont l'identifiant est C0001, et Elasticsearch le rend directement ("found": true). C'est l'accès le plus rapide qui existe. En SQL : SELECT * FROM cours WHERE id = 'C0001'. Retiens ce cours, « Docker expliqué simplement », par Karim Caron : on le retrouvera dans Neo4j tout à l'heure, pour prouver que les deux moteurs contiennent les mêmes données.
_id (avec un tiret bas) est l'identifiant technique du document dans Elasticsearch ; id (sans tiret) est un champ ordinaire à l'intérieur de _source. Le kit les a volontairement rendus identiques (C0001 des deux côtés) pour que ce soit lisible. C'est aussi ce qui rend importer rejouable : envoyer deux fois un document avec le même _id remplace le premier au lieu d'en créer un second, d'où _version: 6 (le document a été réécrit six fois sur la machine du cours, sans jamais être dupliqué).
GET cours/_search
{
"query": { "match": { "titre": "kubernetes" } },
"_source": ["titre"]
}"hits": {
"total": { "value": 7, "relation": "eq" },
"hits": [
{ "_score": 5.0897474, "_source": { "titre": "Les bases de Kubernetes" } },
{ "_score": 5.0897474, "_source": { "titre": "Kubernetes en pratique" } },
{ "_score": 5.0897474, "_source": { "titre": "Kubernetes pour les débutants" } },
…
]
}Nouveauté : query, la partie du corps qui dit quoi chercher. match est la recherche de base : « les documents dont le champ titre contient le mot kubernetes ». Sept cours répondent, et pour la première fois _score n'est plus 1 : c'est la pertinence, et les résultats sont classés du plus pertinent au moins pertinent. En SQL, l'équivalent approximatif serait WHERE titre LIKE '%kubernetes%', mais regarde bien : on a tapé kubernetes en minuscules et on trouve « Kubernetes » avec une majuscule. LIKE ne l'aurait pas fait.
Au moment où un cours est indexé, Elasticsearch découpe son titre en mots, les met en minuscules, retire les accents et ramène chaque mot à sa racine (« conteneurs » devient « conteneur ») : c'est l'analyse, faite ici par l'analyseur french défini dans le mapping du kit. Quand tu cherches, ta requête subit le même traitement, puis Elasticsearch compare mot à mot. Résultat : majuscules, accents et pluriels ne comptent plus. Le _score monte quand le mot est rare dans l'index et fréquent dans le document. Le module 3 y consacre plusieurs leçons ; ici, retiens seulement : match cherche des mots, pas des suites de caractères.
D'abord la preuve que, par défaut, une vraie faute ne trouve rien. « kubrenetes » (deux lettres inversées) :
GET cours/_count
{
"query": { "match": { "titre": "kubrenetes" } }
}{ "count": 0, … }Puis la même requête avec une seule ligne de plus, fuzziness, qui autorise une ou deux lettres d'écart :
GET cours/_search
{
"query": { "match": { "titre": { "query": "kubrenetes", "fuzziness": "AUTO" } } },
"size": 3,
"_source": ["titre", "niveau", "prix"]
}"total": { "value": 7, "relation": "eq" },
"hits": [
{ "_score": 4.4535294, "_source": { "titre": "Les bases de Kubernetes", "niveau": "debutant", "prix": 0 } },
{ "_score": 4.4535294, "_source": { "titre": "Kubernetes en pratique", "niveau": "debutant", "prix": 129 } },
{ "_score": 4.4535294, "_source": { "titre": "Kubernetes pour les débutants", "niveau": "debutant", "prix": 29 } }
]Les 7 cours Kubernetes reviennent, malgré la faute. C'est le moment qui surprend toujours en salle, et c'est la raison d'être d'Elasticsearch dans une barre de recherche : l'utilisateur tape mal, le moteur comprend quand même. Un LIKE '%kubrenetes%' en SQL n'aurait jamais rien rendu. Remarque la syntaxe : quand match a besoin d'options, la valeur du champ devient un objet { "query": …, "fuzziness": … } au lieu d'une simple chaîne.
fuzziness compte les modifications (une lettre ajoutée, retirée, changée ou échangée avec sa voisine) qu'on tolère entre le mot tapé et le mot indexé. AUTO adapte la tolérance à la longueur du mot : 0 faute pour un mot de 1 ou 2 lettres, 1 faute de 3 à 5 lettres, 2 fautes au-delà. « kubrenetes » a 10 lettres, donc 2 fautes admises ; il n'en a qu'une (l'échange re ↔ er) : trouvé. Le _score est un peu plus bas qu'en E10 (4,45 contre 5,09) : Elasticsearch pénalise légèrement les correspondances approximatives, ce qui garde les correspondances exactes en tête.
GET cours/_search
{
"query": { "range": { "nb_avis": { "gte": 10 } } },
"sort": [{ "note_moyenne": "desc" }, { "nb_avis": "desc" }],
"size": 5,
"_source": ["titre", "note_moyenne", "nb_avis", "categorie"]
}Deux nouveautés, qui se comprennent à la lecture. range avec gte (greater than or equal) : seulement les cours qui ont au moins 10 avis, pour écarter les notes basées sur un seul vote. sort : classer par note_moyenne décroissante, puis par nb_avis décroissant pour départager les égalités. En SQL : WHERE nb_avis >= 10 ORDER BY note_moyenne DESC, nb_avis DESC LIMIT 5. Quand on trie soi-même, _score devient null : la pertinence ne sert plus, c'est ton ordre qui compte.
GET cours/_search
{
"size": 0,
"aggs": {
"par_categorie": { "terms": { "field": "categorie" } }
}
}"aggregations": {
"par_categorie": {
"buckets": [
{ "key": "Cloud", "doc_count": 84 },
{ "key": "DevOps", "doc_count": 84 },
{ "key": "Données", "doc_count": 84 },
{ "key": "Développement web", "doc_count": 84 },
{ "key": "IA", "doc_count": 84 },
{ "key": "Sécurité", "doc_count": 84 }
]
}
}Nouveauté : aggs (agrégations). terms sur categorie fait un paquet (bucket) par valeur distincte et compte les documents dedans. size: 0 dit « ne me renvoie aucun document, seulement les compteurs » : la réponse est minuscule et instantanée, même sur des millions de lignes. En SQL : SELECT categorie, COUNT(*) FROM cours GROUP BY categorie. Résultat : six catégories de 84 cours chacune. par_categorie est un nom que tu choisis ; il sert seulement à retrouver le résultat dans la réponse.
Chaque graphique de Kibana, chaque camembert, chaque histogramme est une agrégation comme celle-là, exécutée par Elasticsearch et dessinée par Kibana. Quand tu construiras un tableau de bord au module 4, tu cliqueras sur « champ : categorie, agrégation : terms » et Kibana enverra exactement cette requête. Savoir la lire, c'est savoir ce que fait le tableau de bord en coulisses.
GET cours/_search
{
"size": 0,
"aggs": {
"par_categorie": {
"terms": { "field": "categorie" },
"aggs": { "par_niveau": { "terms": { "field": "niveau" } } }
}
}
}Même requête qu'E13, avec un second aggs à l'intérieur du premier. Dans chaque paquet de catégorie, on refait des paquets par niveau. Résultat : pour « Cloud », 45 débutant, 28 avancé, 11 intermédiaire ; et ainsi de suite pour les six catégories, en une seule passe. En SQL : GROUP BY categorie, niveau, mais la réponse est déjà hiérarchisée, prête pour un graphique empilé.
GET acces/_search
{
"size": 0,
"query": { "range": { "statut": { "gte": 500 } } },
"aggs": { "codes": { "terms": { "field": "statut" } } }
}"hits": { "total": { "value": 178, "relation": "eq" } },
"aggregations": {
"codes": {
"buckets": [
{ "key": 500, "doc_count": 122 },
{ "key": 503, "doc_count": 56 }
]
}
}Rien de nouveau : on combine range (E12) et terms (E13) sur l'index acces. Parmi les 12 000 lignes de journal, 178 ont un code HTTP ≥ 500 : 122 erreurs 500 et 56 503. C'est la question qu'un responsable de plateforme pose chaque matin, et elle est répondue en une milliseconde. En SQL : SELECT statut, COUNT(*) FROM acces WHERE statut >= 500 GROUP BY statut. C'est exactement ce que tu mettras sur un tableau de bord au module 4.
Ouvre http://localhost:7474. Écran de connexion : URL neo4j://localhost:7687, utilisateur neo4j, mot de passe aiopsatlas2026. En haut, une barre d'édition qui commence par neo4j$ : colle une seule requête, puis Ctrl+Entrée ou le triangle ▶. Le résultat apparaît dans un cadre dessous, avec des onglets à gauche : Graph (un dessin, quand le résultat contient des nœuds), Table (des lignes et des colonnes) et Text.
MATCH (n) RETURN nLe résultat est un nuage de bulles colorées, reliées par des flèches, que tu peux déplacer à la souris. Neo4j Browser affiche au plus 300 nœuds à la fois (un message en haut du résultat le signale) ; les 872 sont bien là, il n'en dessine qu'une partie pour rester lisible.
C'est la requête la plus simple de Cypher, le langage de Neo4j. MATCH veut dire « trouve », (n) désigne un nœud quelconque (les parenthèses dessinent un rond, comme une bulle) auquel on donne le nom n, et RETURN n veut dire « montre-le-moi ». En SQL, il n'y a pas d'équivalent : ce serait « SELECT * de toutes les tables à la fois », ce que SQL ne sait pas faire.
Un graphe est fait de deux choses : des nœuds (les bulles) et des relations (les flèches entre bulles). Chaque nœud porte une étiquette qui dit ce qu'il est (Cours, Etudiant…) et des propriétés (titre, prix…). Chaque relation porte un type (INSCRIT_A, ENSEIGNE…) et une direction.
| Neo4j | SQL |
|---|---|
Étiquette Cours | Table cours |
| Nœud | Ligne |
| Propriété | Colonne |
Relation INSCRIT_A | Table de liaison inscriptions + jointures |
Le graphe du labo :
Neo4j
├── 504 nœuds Cours
├── 300 nœuds Etudiant
├── 30 nœuds Professeur
├── 22 nœuds Competence
└── 16 nœuds Ville
= 872 nœuds, reliés par 3 712 relationsLes 504 cours sont les mêmes que les 504 documents de l'index Elasticsearch cours : ce sont deux moteurs qui rangent les mêmes données de deux façons, chacun pour répondre à des questions différentes.
MATCH (n) RETURN n LIMIT 25Une seule nouveauté : LIMIT 25, même mot qu'en SQL. Vingt-cinq bulles au lieu de trois cents : on voit enfin quelque chose. Survole une bulle : ses propriétés s'affichent en bas du cadre. Double-clique dessus : ses voisins se déplient.
MATCH (n) RETURN count(n) AS totaltotal
872count(n) compte au lieu de dessiner ; AS total nomme la colonne. Plus de dessin : le résultat passe tout seul en vue Table, puisqu'un chiffre ne se dessine pas. 872, le chiffre de etat. En SQL : SELECT COUNT(*).
MATCH (c:Cours) RETURN c LIMIT 5Nouveauté : :Cours après le nom de variable. C'est l'étiquette : « seulement les nœuds qui sont des cours ». Cinq bulles, toutes de la même couleur. En SQL : SELECT * FROM cours LIMIT 5. Par habitude, on appelle la variable par l'initiale de l'étiquette (c pour Cours, e pour Etudiant), mais n marcherait aussi.
MATCH (c:Cours) RETURN c.titre, c.prix LIMIT 5c.titre c.prix
"Docker expliqué simplement" 129.0
"Docker avancé : industrialiser des conteneurs en production" 29.0
"Docker : le guide complet" 19.0
…Nouveauté : c.titre, c.prix. Le point donne accès à une propriété du nœud. Quand on renvoie des propriétés au lieu de nœuds entiers, Neo4j Browser passe en vue Table. En SQL : SELECT titre, prix FROM cours LIMIT 5. Compare avec E8 : mêmes titres, mêmes prix, même ordre. Pour connaître toutes les propriétés d'un cours : MATCH (c:Cours) RETURN keys(c) LIMIT 1 répond prix, duree_heures, date_publication, sujet, niveau, categorie, id, titre.
MATCH (n) RETURN labels(n)[0] AS type, count(*) AS nombre ORDER BY nombre DESCtype nombre
"Cours" 504
"Etudiant" 300
"Professeur" 30
"Competence" 22
"Ville" 16labels(n) renvoie la liste des étiquettes du nœud (un nœud peut en avoir plusieurs ; ici une seule, d'où le [0], le premier élément). Le count(*) se regroupe automatiquement par tout ce qui n'est pas une agrégation : pas besoin d'écrire GROUP BY, Cypher le déduit. ORDER BY nombre DESC trie. La somme des cinq lignes fait 872. En SQL, il faudrait cinq SELECT COUNT(*) et des UNION.
MATCH (c:Cours {id: 'C0001'}) RETURN c.titre, c.prix, c.niveauc.titre c.prix c.niveau
"Docker expliqué simplement" 129.0 "debutant"Nouveauté : les accolades {id: 'C0001'} dans le motif. Elles filtrent sur une propriété, comme un WHERE id = 'C0001'. Et c'est le même cours qu'en E9 dans Elasticsearch : même titre, même prix. Voilà la preuve que les deux moteurs contiennent bien les mêmes données ; ce qui change, c'est ce qu'on peut leur demander.
MATCH (c:Cours) WHERE c.titre CONTAINS 'Kubernetes' RETURN c.titre ORDER BY c.titrec.titre
"Kubernetes : de zéro à la production"
"Kubernetes : le guide complet"
"Kubernetes avancé : superviser des conteneurs en production"
"Kubernetes en pratique"
"Kubernetes expliqué simplement"
"Kubernetes pour les débutants"
"Les bases de Kubernetes"WHERE s'écrit comme en SQL et CONTAINS cherche une suite de caractères. Sept cours, les mêmes qu'en E10. Mais essaie CONTAINS 'kubernetes' en minuscules : zéro résultat. Et CONTAINS 'kubrenetes' : zéro aussi. Neo4j compare des caractères, exactement ; il ne connaît ni la casse, ni les mots, ni les fautes. C'est précisément pour ça que le labo a les deux moteurs : la barre de recherche, c'est Elasticsearch ; les liens entre les choses, c'est Neo4j.
MATCH (c:Cours) RETURN c.titre, c.prix ORDER BY c.prix DESC LIMIT 5c.titre c.prix
"Helm par la pratique : superviser un pipeline CI/CD" 199.0
"Atelier GitHub Actions : vos applications" 199.0
"Docker pour les débutants" 199.0
…Rien de neuf : ORDER BY … DESC LIMIT 5, comme en SQL. Les cinq cours les plus chers, tous à 199 $.
MATCH (c:Cours) RETURN c.categorie AS categorie, count(*) AS nombre ORDER BY nombre DESCcategorie nombre
"DevOps" 84
"Données" 84
"IA" 84
"Développement web" 84
"Sécurité" 84
"Cloud" 84Exactement le résultat de l'agrégation E13 dans Elasticsearch : six catégories de 84. Même donnée, deux moteurs, deux syntaxes. Jusqu'ici, Neo4j n'a rien fait que SQL ne sache faire. Ça change à la requête suivante.
MATCH (p:Professeur)-[r:ENSEIGNE]->(c:Cours) RETURN p, r, c LIMIT 30Passe en vue Graph : des bulles « professeur » reliées par des flèches ENSEIGNE à des bulles « cours ». Attrape un professeur avec la souris, tu vois tous ses cours suivre.
Nouveauté : la flèche. (p:Professeur)-[r:ENSEIGNE]->(c:Cours) se lit littéralement « un professeur, qui enseigne, un cours ». Les parenthèses sont des nœuds, les crochets sont la relation, -> donne le sens. C'est un dessin ASCII de ce qu'on cherche, et Neo4j trouve tous les endroits du graphe qui ressemblent à ce dessin. En SQL, ce serait SELECT * FROM professeurs JOIN cours ON cours.professeur_id = professeurs.id, et ça ne se dessinerait pas.
En SQL, une relation entre deux lignes n'existe pas vraiment : elle est recalculée à chaque requête par une jointure, qui compare des identifiants. Avec dix jointures enchaînées, ça devient lent et illisible. Dans Neo4j, la relation est stockée comme une flèche physique entre deux nœuds : suivre une flèche coûte le même prix quel que soit le nombre de nœuds dans la base. C'est ce qui rend possibles les requêtes N14 à N16, qui enchaînent plusieurs sauts sans effort.
MATCH ()-[r]->() RETURN type(r) AS relation, count(*) AS nombre ORDER BY nombre DESCrelation nombre
"INSCRIT_A" 1654
"COUVRE" 994
"ENSEIGNE" 504
"HABITE" 330
"PREREQUIS_DE" 230() est un nœud quelconque dont on ne garde même pas le nom ; [r] une relation de n'importe quel type ; type(r) son type. Cinq types de relations, 3 712 au total : le chiffre affiché par le panneau Database information de Neo4j Browser. On lit le modèle du labo en une ligne : des étudiants inscrits à des cours, des cours qui couvrent des compétences, des professeurs qui enseignent des cours, des gens qui habitent des villes, et des cours prérequis d'autres cours. Le ENSEIGNE à 504 dit qu'il y a exactement une relation par cours : chaque cours a un et un seul professeur.
MATCH (p:Professeur {id: 'P001'})-[:ENSEIGNE]->(c:Cours)
RETURN p.prenom + ' ' + p.nom AS professeur, count(c) AS nb_coursprofesseur nb_cours
"Karim Caron" 11On combine N7 (le filtre {id: 'P001'}) et N11 (la flèche). P001, c'est le professeur de « Docker expliqué simplement » vu en E9 et N7 ; il enseigne 11 cours. Le + colle des chaînes de caractères, comme en SQL avec ||. Remarque [:ENSEIGNE] sans nom de variable : quand on n'a pas besoin de la relation dans le RETURN, on ne la nomme pas.
MATCH (debut:Cours {id: 'C0111'}), (fin:Cours {id: 'C0110'}),
chemin = shortestPath((debut)-[:PREREQUIS_DE*]-(fin))
RETURN length(chemin) AS sauts, [n IN nodes(chemin) | n.titre] AS parcourssauts parcours
5 ["Neo4j : de zéro à la production", "Neo4j en pratique", "Neo4j par la pratique : interroger des journaux applicatifs", "Neo4j avancé : modéliser des journaux applicatifs", "Neo4j : le guide complet", "Maîtriser Neo4j"]Première requête qui ne ressemble plus à rien en SQL. [:PREREQUIS_DE*] avec l'astérisque veut dire « en suivant cette relation autant de fois qu'il faut ». shortestPath demande le plus court des chemins possibles entre les deux cours. La réponse : 5 sauts, et la liste des six titres à suivre, dans l'ordre, pour aller du premier au dernier cours de la filière Neo4j. C'est un plan de formation calculé à la volée. En SQL, il faudrait une requête récursive de plusieurs dizaines de lignes, et elle serait lente.
nodes(chemin) donne la liste des nœuds traversés. [n IN nodes(chemin) | n.titre] se lit « pour chaque nœud n de cette liste, garde son titre » : c'est une façon compacte de transformer une liste de nœuds en liste de titres. length(chemin) compte les relations traversées (5 relations pour 6 nœuds). Le module 6 détaille ces fonctions ; ici l'important est le résultat : Neo4j a trouvé un itinéraire dans le graphe.
MATCH (x:Cours {id: 'C0213'})<-[:INSCRIT_A]-(e:Etudiant)-[:INSCRIT_A]->(autre:Cours)
WHERE autre <> x
RETURN autre.titre AS recommandation, autre.categorie AS categorie,
count(DISTINCT e) AS etudiants_communs
ORDER BY etudiants_communs DESC, recommandation
LIMIT 5recommandation categorie etudiants_communs
"Agents IA par la pratique : évaluer un pipeline de prédiction" "IA" 3
"Atelier NLP : des agents autonomes" "IA" 2
"Deep learning par la pratique : orchestrer un assistant …" "IA" 2
…Le moteur de recommandation d'un site marchand, en cinq lignes. Lis le motif de gauche à droite : on part du cours x, on remonte la flèche INSCRIT_A (elle pointe vers x, d'où le <-) jusqu'aux étudiants e qui le suivent, puis on redescend une autre flèche INSCRIT_A vers les autres cours autre de ces mêmes étudiants. WHERE autre <> x écarte le cours de départ lui-même. count(DISTINCT e) compte les étudiants en commun, sans doublon. Les cours en tête sont ceux que les étudiants de C0213 suivent le plus souvent en parallèle : ce sont les recommandations. En SQL : deux jointures sur la table d'inscriptions, un GROUP BY, et une requête qu'on relit trois fois avant de la comprendre.
MATCH chemin = (x:Cours {id: 'C0213'})<-[:INSCRIT_A]-(:Etudiant)-[:INSCRIT_A]->(autre:Cours)
WHERE autre <> x
RETURN chemin LIMIT 50Passe en vue Graph : le cours de départ au centre, ses étudiants autour, et les cours qu'ils partagent en périphérie. C'est la requête N15 sans le comptage : on renvoie les chemins entiers plutôt que des colonnes, et Neo4j Browser les dessine. C'est l'image à montrer quand quelqu'un demande « à quoi sert une base de graphe ».
Le message à faire passer. Quinze requêtes Elasticsearch, seize requêtes Cypher, et on a vu, dans l'ordre : lister, compter, afficher, filtrer, trier, regrouper, ce que SQL fait aussi ; puis pardonner une faute de frappe, agréger 12 000 lignes en une milliseconde, calculer un plus court chemin et produire des recommandations, ce que SQL fait mal ou pas du tout. Le même cours « Docker expliqué simplement » est apparu dans les deux moteurs : mêmes données, questions différentes. C'est exactement pourquoi ce labo existe, et tout le reste du cours détaille comment chacune de ces requêtes fonctionne.
Refais l'étape 7 de ton annexe avec docker compose stop neo4j : que devient la ligne Neo4j répond — nœuds : 872 dans etat ? Que dit Neo4j Browser, déjà connecté, quand tu relances la requête N3 (MATCH (n) RETURN count(n)) ? Quelle est la dernière ligne de journal neo4j ? Relance avec docker compose start neo4j, vérifie que les 872 nœuds sont toujours là sans rien recharger, et note lequel des deux services redémarre le plus vite.
Toutes les commandes de cette annexe se tapent dans PowerShell (Windows Terminal, ou PowerShell 7), avec .\labo.ps1 …. Les sorties reproduites sont celles de la machine du cours, sous Windows 11 et Docker Desktop.
docker-compose.yml et labo.ps1)..\labo.ps1 (« l'exécution de scripts est désactivée sur ce système ») : Set-ExecutionPolicy -Scope CurrentUser RemoteSigned, réponds O, relance..\labo.ps1 etat doit afficher (healthy) partout, "status":"green", acces 12000 avis 609 cours 504 et nœuds : 872.elasticsearch/requetes/01-pratique-demarrer-verifier-reparer.txt. Aucune n'écrit dans le cluster.Avant d'allumer quoi que ce soit, fais parler le script.
.\labo.ps1 prerequisPoint de contrôle : que des coches vertes et la phrase finale.
== Prérequis ==
✔ docker : Docker version 29.3.1, build c2be9cc
✔ le démon Docker répond
✔ docker compose : 5.1.1
✔ mémoire disponible pour Docker : 31 Go
✔ processeurs : 20
✔ port 9200 : utilisé par le labo lui-même
✔ port 5601 : utilisé par le labo lui-même
✔ port 7474 : utilisé par le labo lui-même
✔ port 7687 : utilisé par le labo lui-même
Tout est prêt. Lancez : .\labo.ps1 demarrerChez toi, versions et mémoire diffèrent, et si le labo n'a jamais tourné les quatre dernières lignes disent port 9200 : libre. Les deux lectures sont bonnes.
Si tu vois autre chose : une croix ✘ contient son remède dans la phrase (Docker Desktop pas lancé, mémoire sous 4 Go, port pris) ; corrige, relance, n'avance qu'avec la ligne verte. Pour la mémoire : Docker Desktop → Settings → Resources, ou le fichier %UserProfile%\.wslconfig si Docker Desktop utilise WSL 2.
Lance le démarrage et, cette fois, lis ce qui défile au lieu d'attendre la fin.
.\labo.ps1 demarrerPoint de contrôle : trois blocs == … == puis trois prêt.
== Téléchargement des images (long la première fois : ~6 Go, ~12 Go avec OpenSearch) ==
== Démarrage ==
== Attente que chaque service soit prêt ==
elasticsearch prêt (0 s)
kibana prêt (0 s)
neo4j ..... prêt (15 s)
Le labo est prêt.
Kibana http://localhost:5601 (Dev Tools : menu ☰ → Management → Dev Tools)
Elasticsearch http://localhost:9200
Neo4j Browser http://localhost:7474 (utilisateur neo4j · mot de passe aiopsatlas2026)
Étape suivante : .\labo.ps1 importer puis .\labo.ps1 charger-grapheLe premier bloc est vide si les images sont déjà là ; le deuxième contient les lignes de Compose (Container labo-elasticsearch Started, ou Running s'il tournait déjà) ; le troisième ajoute un point toutes les trois secondes jusqu'à prêt (… s). Labo froid : une à deux minutes ; labo déjà en marche : prêt (0 s).
Si tu vois autre chose : unhealthy, exited ou délai dépassé → .\labo.ps1 journal <service> et le catalogue de la leçon 04 ; cause la plus fréquente sous Windows : la mémoire allouée à Docker Desktop (Exited (137)).
etatTu vas taper etat plusieurs fois ; apprends d'abord à en extraire les quatre chiffres qui comptent.
.\labo.ps1 etatPoint de contrôle : sortie réelle de la machine du cours, où le profil OpenSearch du module 5 est actif. Chez toi, les deux lignes labo-opensearch… et ✔ OpenSearch sont remplacées par — OpenSearch non démarré (profil optionnel : .\labo.ps1 demarrer opensearch).
== Conteneurs ==
NAME STATUS PORTS
labo-elasticsearch Up 10 hours (healthy) 0.0.0.0:9200->9200/tcp, [::]:9200->9200/tcp
labo-kibana Up 10 hours (healthy) 0.0.0.0:5601->5601/tcp, [::]:5601->5601/tcp
labo-neo4j Up 37 seconds (healthy) 0.0.0.0:7474->7474/tcp, [::]:7474->7474/tcp, 0.0.0.0:7687->7687/tcp, [::]:7687->7687/tcp
labo-opensearch Up 10 hours (healthy) 0.0.0.0:9201->9200/tcp, [::]:9201->9200/tcp
labo-opensearch-dashboards Up 10 hours 0.0.0.0:5602->5601/tcp, [::]:5602->5601/tcp
== Services ==
✔ Elasticsearch : {"status":"green","number_of_nodes":1}
index : acces 12000 avis 609 cours 504
✔ Kibana répond (http://localhost:5601)
✔ Neo4j répond — nœuds : 872
✔ OpenSearch : {"status":"green","number_of_nodes":1}Note les quatre chiffres attendus : 12000, 609, 504, 872. Sur un labo tout neuf tu lis index : aucun index du labo et nœuds : 0 : normal, l'étape A.4 les remplit.
Si tu vois autre chose : ✘ Kibana ne répond pas encore dans la minute qui suit demarrer → Kibana finit de créer ses index internes ; retape etat trente secondes plus tard.
Charge les index puis le graphe, et relance les deux commandes une seconde fois : les compteurs ne doivent pas changer d'une unité.
.\labo.ps1 importer
.\labo.ps1 charger-graphe
.\labo.ps1 importer
.\labo.ps1 charger-graphePoint de contrôle : au second passage, importer signale que les index existent déjà et rend les mêmes compteurs :
== Import dans elasticsearch ==
— index cours existe déjà — conservé
✔ données cours chargées
— index avis existe déjà — conservé
✔ données avis chargées
— index acces existe déjà — conservé
✔ données acces chargées
index docs.count store.size
acces 12000 1.5mb
avis 609 74.8kb
cours 504 183.6kb
Import terminé. Attendu : cours = 504, avis = 609, acces = 12000.et charger-graphe donne le même bilan qu'au premier :
== Chargement du graphe Neo4j ==
✔ contraintes et index en place
etiquette, noeuds
"Competence", 22
"Cours", 504
"Etudiant", 300
"Professeur", 30
"Ville", 16
Graphe chargé. Attendu : Competence 22, Cours 504, Etudiant 300, Professeur 30, Ville 16.Les documents portent leur propre _id (C0001, A00001…), donc un second _bulk remplace chaque document au lieu de l'ajouter ; côté Neo4j, tout est en MERGE. Seul le store.size peut bouger de quelques kilo-octets (Lucene garde un moment les anciennes versions) ; les docs.count, jamais.
Si tu vois autre chose : un docs.count différent de 504 / 609 / 12000 → quelqu'un a écrit dans ces index ; .\labo.ps1 reinitialiser puis demarrer, importer, charger-graphe te rend l'état de référence.
Le script dit que tout est bon ; vérifie-le sans lui. Ouvre http://localhost:5601, menu ☰ → Management → Outils de développement, et envoie ces requêtes une par une (Ctrl + Entrée ou le bouton ▶ « Cliquer pour envoyer la requête »).
GET _cluster/healthPoint de contrôle : "status": "green", "unassigned_shards": 0, et 200 - OK en bas à droite du panneau de réponse.
{
"cluster_name": "labo",
"status": "green",
"timed_out": false,
"number_of_nodes": 1,
"number_of_data_nodes": 1,
"active_primary_shards": 53,
"active_shards": 53,
…
"unassigned_shards": 0,
…
"active_shards_percent_as_number": 100
}GET _cat/indices/cours,avis,acces?v&s=indexPoint de contrôle : trois lignes green, pri 1, rep 0, et les trois compteurs dans docs.count.
health status index uuid pri rep docs.count docs.deleted store.size pri.store.size dataset.size
green open acces aii68fsfQXKyt5wqkE1mPA 1 0 12000 0 1.5mb 1.5mb 1.5mb
green open avis W9j_JrwJT4mdzpWcS5k7xg 1 0 609 0 74.8kb 74.8kb 74.8kb
green open cours pmq403ZgSZWeHJY9uNw1Qw 1 0 504 0 183.6kb 183.6kb 183.6kbTermine par GET cours/_count, GET avis/_count, GET acces/_count : "count": 504, 609, 12000. Tu as les mêmes chiffres par deux chemins indépendants : le script (curl dans le conteneur) et Dev Tools (via Kibana). S'ils divergent un jour, c'est le chemin qui est en cause, pas les données.
Si tu vois autre chose : "status": "yellow" → un index a des réplicas non alloués, impossible avec les mappings du kit (number_of_replicas: 0) ; GET _cat/indices?v&health=yellow désigne le coupable, en général un index créé à la main.
Même exercice pour Neo4j. Ouvre http://localhost:7474, connecte-toi (neo4j / aiopsatlas2026, URL localhost:7687), tape dans l'éditeur neo4j$ et clique Run :
MATCH (n) RETURN labels(n)[0] AS label, count(*) ORDER BY labelPoint de contrôle : un cadre avec deux vues, Table et Raw (pas de Graph : la requête renvoie des chiffres, pas des nœuds), deux colonnes label et count(*), cinq lignes triées, et en bas à droite Started streaming 5 records after … ms and completed after … ms.
label count(*)
"Competence" 22
"Cours" 504
"Etudiant" 300
"Professeur" 30
"Ville" 1622 + 504 + 300 + 30 + 16 = 872, le chiffre de etat. Le panneau Database information (icône Database overview, la première de la barre latérale) affiche Nodes (872) et Relationships (3,712).
Si tu vois autre chose : une sixième étiquette inconnue → des nœuds créés hors kit (le module 6 t'apprendra à les supprimer proprement) ; Nodes (0) → tu as sauté charger-graphe, retour à l'étape A.4.
Tu sais à quoi ressemble un labo sain ; provoque une panne dont tu connais la cause pour apprendre à la lire. Arrête uniquement Kibana, avec Compose (pas arreter, qui stopperait tout) :
docker compose stop kibana Container labo-kibana Stopping
Container labo-kibana StoppedPuis les trois gestes de la leçon 04, dans l'ordre : etat, le navigateur, journal.
Point de contrôle 1, .\labo.ps1 etat : la ligne labo-kibana a disparu du bloc == Conteneurs == (Compose n'affiche par défaut que les conteneurs en cours d'exécution) et le bloc == Services == marque une croix :
== Conteneurs ==
NAME STATUS PORTS
labo-elasticsearch Up 10 hours (healthy) 0.0.0.0:9200->9200/tcp, [::]:9200->9200/tcp
labo-neo4j Up 3 minutes (healthy) 0.0.0.0:7474->7474/tcp, [::]:7474->7474/tcp, 0.0.0.0:7687->7687/tcp, [::]:7687->7687/tcp
labo-opensearch Up 10 hours (healthy) 0.0.0.0:9201->9200/tcp, [::]:9201->9200/tcp
labo-opensearch-dashboards Up 10 hours 0.0.0.0:5602->5601/tcp, [::]:5602->5601/tcp
== Services ==
✔ Elasticsearch : {"status":"green","number_of_nodes":1}
index : acces 12000 avis 609 cours 504
✘ Kibana ne répond pas encore
✔ Neo4j répond — nœuds : 872
✔ OpenSearch : {"status":"green","number_of_nodes":1}Pour voir quand même le conteneur arrêté : docker compose ps -a affiche labo-kibana Exited (0) 31 seconds ago. Le 0 dit « arrêt propre, demandé » ; un 137 dirait « tué, mémoire ».
Point de contrôle 2, le navigateur : recharge http://localhost:5601. Pas de « Kibana server is not ready yet » (cette phrase vient de Kibana, or il n'y a plus de Kibana pour la dire) mais l'erreur de connexion du navigateur lui-même : dans Chrome ou Edge, « Ce site est inaccessible », code ERR_CONNECTION_REFUSED. Personne n'écoute sur le port 5601. À retenir : page Kibana qui s'excuse = Kibana tourne mais attend Elasticsearch ; erreur du navigateur = Kibana ne tourne pas.
Point de contrôle 3, .\labo.ps1 journal kibana : les cent dernières lignes se terminent par un arrêt propre, daté à la seconde où tu as tapé stop :
labo-kibana | [2026-09-09T23:44:06.893+00:00][INFO ][root] SIGTERM received - initiating shutdown
labo-kibana | [2026-09-09T23:44:06.894+00:00][INFO ][root] Kibana is shutting down
labo-kibana | [2026-09-09T23:44:06.902+00:00][INFO ][plugins-system.standard] Stopping all plugins.
…
labo-kibana | [2026-09-09T23:44:07.265+00:00][INFO ][plugins-system.standard] All plugins stopped.SIGTERM received signe un arrêt demandé (par toi, par docker compose stop, par un redémarrage de Docker Desktop). Aucune ligne ERROR ni FATAL : rien à réparer, il faut juste relancer. Les dizaines de lignes at OperatorSubscriber… entre les deux sont une pile d'appels émise par un plugin pendant l'arrêt : du bruit.
Si tu vois autre chose : no configuration file provided: not found → tu n'es pas dans le dossier du kit ; no such service: kibana → tu as tapé le nom du conteneur (labo-kibana) au lieu du service Compose (kibana).
Relance le service et attends que son healthcheck repasse au vert : 40 à 60 secondes, le temps de se reconnecter à Elasticsearch et de vérifier ses index internes.
docker compose start kibana Container labo-elasticsearch Waiting
Container labo-elasticsearch Healthy
Container labo-kibana Starting
Container labo-kibana StartedCompose a d'abord vérifié qu'Elasticsearch était Healthy (le depends_on … service_healthy de la leçon 03), puis a démarré Kibana. Surveille toutes les cinq secondes :
docker inspect --format '{{.State.Health.Status}}' labo-kibanaPoint de contrôle : starting pendant 40 à 50 secondes, puis healthy (sur la machine du cours : starting de 0 à 45 s, healthy à 50 s). etat remontre alors la ligne :
labo-kibana Up 52 seconds (healthy) 0.0.0.0:5601->5601/tcp, [::]:5601->5601/tcp
…
✔ Kibana répond (http://localhost:5601)et journal kibana se termine par les lignes qu'on veut voir :
labo-kibana | [2026-09-09T23:47:49.408+00:00][INFO ][http.server.Kibana] http server running at http://0.0.0.0:5601
labo-kibana | [2026-09-09T23:47:50.374+00:00][INFO ][status] Kibana is now availableRouvre Dev Tools (si tu tombes sur « Kibana server is not ready yet », tu as été plus rapide que le healthy : attends dix secondes) et envoie la requête que etat lui-même utilise :
GET _cluster/health?filter_path=status,number_of_nodes{
"status": "green",
"number_of_nodes": 1
}Elasticsearch a répondu green pendant toute la panne : Kibana est une fenêtre sur les données, pas les données. Arrêter Kibana n'a rien effacé ni réindexé.
Si tu vois autre chose : unhealthy après deux minutes → journal kibana et cherche ECONNREFUSED (Elasticsearch tombé entre-temps) ; port is already allocated → un autre programme a pris le port 5601 pendant l'arrêt (leçon 04, panne 1).
Tu reconnais un service arrêté ; apprends maintenant à reconnaître une requête fausse sur un service sain, la confusion la plus fréquente en salle. Dans Dev Tools, écris une requête de recherche qui renvoie 404 avec "type": "index_not_found_exception", puis explique en une phrase pourquoi le cluster reste green.
Indice : Elasticsearch ne devine jamais le nom d'un index. Choisis-en un qui n'existe pas, avec le préfixe pratique- ; rien à créer, rien à écrire.
GET pratique-inexistant/_searchRéponse, avec le badge 404 - Not Found en bas à droite du panneau de sortie :
{
"error": {
"root_cause": [
{
"type": "index_not_found_exception",
"reason": "no such index [pratique-inexistant]",
"resource.type": "index_or_alias",
"resource.id": "pratique-inexistant",
"index_uuid": "_na_",
"index": "pratique-inexistant"
}
],
"type": "index_not_found_exception",
"reason": "no such index [pratique-inexistant]",
…
},
"status": 404
}Explication attendue : le 404 est une réponse normale et complète d'Elasticsearch : « j'ai compris ta requête, mais cette ressource n'existe pas ». Le service tourne, le cluster reste green ; il n'y a que le nom à corriger (GET _cat/indices?v donne la liste). À l'étape A.7, il n'y avait aucune réponse du tout.
Deux variantes à essayer : GET pratique-inexistant/_count?ignore_unavailable=true renvoie 200 et "count": 0 (tu demandes d'ignorer l'index absent) ; GET cours/_serch renvoie 400 avec "no handler found for uri [/cours/_serch] and method [GET]". Un 400 : « je ne comprends pas la requête » ; un 404 : « je comprends, mais ça n'existe pas ».
Une seule commande prouve que tout est fait : etat, avec Kibana revenu et les quatre chiffres.
.\labo.ps1 etat== Conteneurs ==
NAME STATUS PORTS
labo-elasticsearch Up 10 hours (healthy) 0.0.0.0:9200->9200/tcp, [::]:9200->9200/tcp
labo-kibana Up About a minute (healthy) 0.0.0.0:5601->5601/tcp, [::]:5601->5601/tcp
labo-neo4j Up 7 minutes (healthy) 0.0.0.0:7474->7474/tcp, [::]:7474->7474/tcp, 0.0.0.0:7687->7687/tcp, [::]:7687->7687/tcp
labo-opensearch Up 10 hours (healthy) 0.0.0.0:9201->9200/tcp, [::]:9201->9200/tcp
labo-opensearch-dashboards Up 10 hours 0.0.0.0:5602->5601/tcp, [::]:5602->5601/tcp
== Services ==
✔ Elasticsearch : {"status":"green","number_of_nodes":1}
index : acces 12000 avis 609 cours 504
✔ Kibana répond (http://localhost:5601)
✔ Neo4j répond — nœuds : 872
✔ OpenSearch : {"status":"green","number_of_nodes":1}(Sans le profil OpenSearch, les deux conteneurs labo-opensearch… n'apparaissent pas et la dernière ligne dit — OpenSearch non démarré … : c'est l'état attendu jusqu'au module 5.)
prerequis se termine par Tout est prêt.labo-elasticsearch, labo-kibana et labo-neo4j sont Up … (healthy), Kibana compris.etat affiche "status":"green", acces 12000 avis 609 cours 504, nœuds : 872, inchangés après le second importer / charger-graphe.etat, le navigateur et journal kibana quand Kibana est arrêté, et en quoi ça diffère d'un 404.etat ci-dessus (copie ou capture) comme livrable.Cette pratique ne crée rien : ni index, ni nœud, ni objet Kibana. Deux choses à garantir : que Kibana tourne (sinon docker compose start kibana depuis le dossier du kit), et qu'aucun index de travail ne traîne :
GET _cat/indices/pratique-*?vRéponse attendue : la ligne d'en-tête seule (health status index uuid pri rep docs.count …). Ne touche pas à cours, avis, acces ni au graphe : ils servent à tous les modules suivants.
Toutes les commandes de cette annexe se tapent dans un terminal bash (ou zsh), avec ./labo.sh …. Les sorties sont identiques à celles de Windows au nom du script près : le kit est le même, seuls les lanceurs changent.
docker sur Linux : docker info doit répondre), le kit cloné, un terminal ouvert dans le dossier du kit (celui qui contient docker-compose.yml et labo.sh).Permission denied sur ./labo.sh : chmod +x labo.sh, une seule fois../labo.sh etat doit afficher (healthy) partout, "status":"green", acces 12000 avis 609 cours 504 et nœuds : 872.elasticsearch/requetes/01-pratique-demarrer-verifier-reparer.txt. Aucune n'écrit dans le cluster.Avant d'allumer quoi que ce soit, fais parler le script.
./labo.sh prerequisPoint de contrôle : que des coches vertes et la phrase finale.
== Prérequis ==
✔ docker : Docker version 29.3.1, build c2be9cc
✔ le démon Docker répond
✔ docker compose : 5.1.1
✔ mémoire disponible pour Docker : 31 Go
✔ processeurs : 20
✔ port 9200 : utilisé par le labo lui-même
✔ port 5601 : utilisé par le labo lui-même
✔ port 7474 : utilisé par le labo lui-même
✔ port 7687 : utilisé par le labo lui-même
Tout est prêt. Lancez : ./labo.sh demarrerChez toi, versions et mémoire diffèrent, et si le labo n'a jamais tourné les quatre dernières lignes disent port 9200 : libre. Les deux lectures sont bonnes.
Si tu vois autre chose : une croix ✘ contient son remède dans la phrase (Docker pas lancé, mémoire sous 4 Go, port pris) ; corrige, relance, n'avance qu'avec la ligne verte. Sous Linux natif, la mémoire est celle de la machine ; sous macOS et WSL 2, c'est celle allouée dans Docker Desktop → Settings → Resources.
Lance le démarrage et, cette fois, lis ce qui défile au lieu d'attendre la fin.
./labo.sh demarrerPoint de contrôle : trois blocs == … == puis trois prêt.
== Téléchargement des images (long la première fois : ~6 Go, ~12 Go avec OpenSearch) ==
== Démarrage ==
== Attente que chaque service soit prêt ==
elasticsearch prêt (0 s)
kibana prêt (0 s)
neo4j ..... prêt (15 s)
Le labo est prêt.
Kibana http://localhost:5601 (Dev Tools : menu ☰ → Management → Dev Tools)
Elasticsearch http://localhost:9200
Neo4j Browser http://localhost:7474 (utilisateur neo4j · mot de passe aiopsatlas2026)
Étape suivante : ./labo.sh importer puis ./labo.sh charger-grapheLe premier bloc est vide si les images sont déjà là ; le deuxième contient les lignes de Compose (Container labo-elasticsearch Started, ou Running s'il tournait déjà) ; le troisième ajoute un point toutes les trois secondes jusqu'à prêt (… s). Labo froid : une à deux minutes ; labo déjà en marche : prêt (0 s).
Si tu vois autre chose : unhealthy, exited ou délai dépassé → ./labo.sh journal <service> et le catalogue de la leçon 04 ; causes les plus fréquentes : la mémoire (Exited (137)) et, sous Linux natif, vm.max_map_count trop bas (sudo sysctl -w vm.max_map_count=262144, puis ./labo.sh demarrer).
etatTu vas taper etat plusieurs fois ; apprends d'abord à en extraire les quatre chiffres qui comptent.
./labo.sh etatPoint de contrôle : sortie réelle de la machine du cours, où le profil OpenSearch du module 5 est actif. Chez toi, les deux lignes labo-opensearch… et ✔ OpenSearch sont remplacées par — OpenSearch non démarré (profil optionnel : ./labo.sh demarrer opensearch).
== Conteneurs ==
NAME STATUS PORTS
labo-elasticsearch Up 10 hours (healthy) 0.0.0.0:9200->9200/tcp, [::]:9200->9200/tcp
labo-kibana Up 10 hours (healthy) 0.0.0.0:5601->5601/tcp, [::]:5601->5601/tcp
labo-neo4j Up 37 seconds (healthy) 0.0.0.0:7474->7474/tcp, [::]:7474->7474/tcp, 0.0.0.0:7687->7687/tcp, [::]:7687->7687/tcp
labo-opensearch Up 10 hours (healthy) 0.0.0.0:9201->9200/tcp, [::]:9201->9200/tcp
labo-opensearch-dashboards Up 10 hours 0.0.0.0:5602->5601/tcp, [::]:5602->5601/tcp
== Services ==
✔ Elasticsearch : {"status":"green","number_of_nodes":1}
index : acces 12000 avis 609 cours 504
✔ Kibana répond (http://localhost:5601)
✔ Neo4j répond — nœuds : 872
✔ OpenSearch : {"status":"green","number_of_nodes":1}Note les quatre chiffres attendus : 12000, 609, 504, 872. Sur un labo tout neuf tu lis index : aucun index du labo et nœuds : 0 : normal, l'étape B.4 les remplit.
Si tu vois autre chose : ✘ Kibana ne répond pas encore dans la minute qui suit demarrer → Kibana finit de créer ses index internes ; retape etat trente secondes plus tard.
Charge les index puis le graphe, et relance les deux commandes une seconde fois : les compteurs ne doivent pas changer d'une unité.
./labo.sh importer
./labo.sh charger-graphe
./labo.sh importer
./labo.sh charger-graphePoint de contrôle : au second passage, importer signale que les index existent déjà et rend les mêmes compteurs :
== Import dans elasticsearch ==
— index cours existe déjà — conservé
✔ données cours chargées
— index avis existe déjà — conservé
✔ données avis chargées
— index acces existe déjà — conservé
✔ données acces chargées
index docs.count store.size
acces 12000 1.5mb
avis 609 74.8kb
cours 504 183.6kb
Import terminé. Attendu : cours = 504, avis = 609, acces = 12000.et charger-graphe donne le même bilan qu'au premier :
== Chargement du graphe Neo4j ==
✔ contraintes et index en place
etiquette, noeuds
"Competence", 22
"Cours", 504
"Etudiant", 300
"Professeur", 30
"Ville", 16
Graphe chargé. Attendu : Competence 22, Cours 504, Etudiant 300, Professeur 30, Ville 16.Les documents portent leur propre _id (C0001, A00001…), donc un second _bulk remplace chaque document au lieu de l'ajouter ; côté Neo4j, tout est en MERGE. Seul le store.size peut bouger de quelques kilo-octets (Lucene garde un moment les anciennes versions) ; les docs.count, jamais.
Si tu vois autre chose : un docs.count différent de 504 / 609 / 12000 → quelqu'un a écrit dans ces index ; ./labo.sh reinitialiser puis demarrer, importer, charger-graphe te rend l'état de référence.
Le script dit que tout est bon ; vérifie-le sans lui. Ouvre http://localhost:5601, menu ☰ → Management → Outils de développement, et envoie ces requêtes une par une (Ctrl + Entrée, ou Cmd + Entrée sur macOS, ou le bouton ▶ « Cliquer pour envoyer la requête »).
GET _cluster/healthPoint de contrôle : "status": "green", "unassigned_shards": 0, et 200 - OK en bas à droite du panneau de réponse.
{
"cluster_name": "labo",
"status": "green",
"timed_out": false,
"number_of_nodes": 1,
"number_of_data_nodes": 1,
"active_primary_shards": 53,
"active_shards": 53,
…
"unassigned_shards": 0,
…
"active_shards_percent_as_number": 100
}GET _cat/indices/cours,avis,acces?v&s=indexPoint de contrôle : trois lignes green, pri 1, rep 0, et les trois compteurs dans docs.count.
health status index uuid pri rep docs.count docs.deleted store.size pri.store.size dataset.size
green open acces aii68fsfQXKyt5wqkE1mPA 1 0 12000 0 1.5mb 1.5mb 1.5mb
green open avis W9j_JrwJT4mdzpWcS5k7xg 1 0 609 0 74.8kb 74.8kb 74.8kb
green open cours pmq403ZgSZWeHJY9uNw1Qw 1 0 504 0 183.6kb 183.6kb 183.6kbTermine par GET cours/_count, GET avis/_count, GET acces/_count : "count": 504, 609, 12000. Tu as les mêmes chiffres par deux chemins indépendants : le script (curl dans le conteneur) et Dev Tools (via Kibana). S'ils divergent un jour, c'est le chemin qui est en cause, pas les données.
Sous bash, tu as même un troisième chemin, sans navigateur :
curl -s 'http://localhost:9200/_cat/indices/cours,avis,acces?v&s=index'Si tu vois autre chose : "status": "yellow" → un index a des réplicas non alloués, impossible avec les mappings du kit (number_of_replicas: 0) ; GET _cat/indices?v&health=yellow désigne le coupable, en général un index créé à la main.
Même exercice pour Neo4j. Ouvre http://localhost:7474, connecte-toi (neo4j / aiopsatlas2026, URL localhost:7687), tape dans l'éditeur neo4j$ et clique Run :
MATCH (n) RETURN labels(n)[0] AS label, count(*) ORDER BY labelPoint de contrôle : un cadre avec deux vues, Table et Raw (pas de Graph : la requête renvoie des chiffres, pas des nœuds), deux colonnes label et count(*), cinq lignes triées, et en bas à droite Started streaming 5 records after … ms and completed after … ms.
label count(*)
"Competence" 22
"Cours" 504
"Etudiant" 300
"Professeur" 30
"Ville" 1622 + 504 + 300 + 30 + 16 = 872, le chiffre de etat. Le panneau Database information (icône Database overview, la première de la barre latérale) affiche Nodes (872) et Relationships (3,712).
Si tu vois autre chose : une sixième étiquette inconnue → des nœuds créés hors kit (le module 6 t'apprendra à les supprimer proprement) ; Nodes (0) → tu as sauté charger-graphe, retour à l'étape B.4.
Tu sais à quoi ressemble un labo sain ; provoque une panne dont tu connais la cause pour apprendre à la lire. Arrête uniquement Kibana, avec Compose (pas arreter, qui stopperait tout) :
docker compose stop kibana Container labo-kibana Stopping
Container labo-kibana StoppedPuis les trois gestes de la leçon 04, dans l'ordre : etat, le navigateur, journal.
Point de contrôle 1, ./labo.sh etat : la ligne labo-kibana a disparu du bloc == Conteneurs == (Compose n'affiche par défaut que les conteneurs en cours d'exécution) et le bloc == Services == marque une croix :
== Conteneurs ==
NAME STATUS PORTS
labo-elasticsearch Up 10 hours (healthy) 0.0.0.0:9200->9200/tcp, [::]:9200->9200/tcp
labo-neo4j Up 3 minutes (healthy) 0.0.0.0:7474->7474/tcp, [::]:7474->7474/tcp, 0.0.0.0:7687->7687/tcp, [::]:7687->7687/tcp
labo-opensearch Up 10 hours (healthy) 0.0.0.0:9201->9200/tcp, [::]:9201->9200/tcp
labo-opensearch-dashboards Up 10 hours 0.0.0.0:5602->5601/tcp, [::]:5602->5601/tcp
== Services ==
✔ Elasticsearch : {"status":"green","number_of_nodes":1}
index : acces 12000 avis 609 cours 504
✘ Kibana ne répond pas encore
✔ Neo4j répond — nœuds : 872
✔ OpenSearch : {"status":"green","number_of_nodes":1}Pour voir quand même le conteneur arrêté : docker compose ps -a affiche labo-kibana Exited (0) 31 seconds ago. Le 0 dit « arrêt propre, demandé » ; un 137 dirait « tué, mémoire ».
Point de contrôle 2, le navigateur : recharge http://localhost:5601. Pas de « Kibana server is not ready yet » (cette phrase vient de Kibana, or il n'y a plus de Kibana pour la dire) mais l'erreur de connexion du navigateur lui-même : dans Chrome, « Ce site est inaccessible », code ERR_CONNECTION_REFUSED ; dans Firefox, « Impossible de se connecter ». En ligne de commande, curl -s http://localhost:5601 || echo REFUSE affiche REFUSE : personne n'écoute sur le port 5601. À retenir : page Kibana qui s'excuse = Kibana tourne mais attend Elasticsearch ; erreur du navigateur = Kibana ne tourne pas.
Point de contrôle 3, ./labo.sh journal kibana : les cent dernières lignes se terminent par un arrêt propre, daté à la seconde où tu as tapé stop :
labo-kibana | [2026-09-09T23:44:06.893+00:00][INFO ][root] SIGTERM received - initiating shutdown
labo-kibana | [2026-09-09T23:44:06.894+00:00][INFO ][root] Kibana is shutting down
labo-kibana | [2026-09-09T23:44:06.902+00:00][INFO ][plugins-system.standard] Stopping all plugins.
…
labo-kibana | [2026-09-09T23:44:07.265+00:00][INFO ][plugins-system.standard] All plugins stopped.SIGTERM received signe un arrêt demandé (par toi, par docker compose stop, par un redémarrage de Docker). Aucune ligne ERROR ni FATAL : rien à réparer, il faut juste relancer. Les dizaines de lignes at OperatorSubscriber… entre les deux sont une pile d'appels émise par un plugin pendant l'arrêt : du bruit.
Si tu vois autre chose : no configuration file provided: not found → tu n'es pas dans le dossier du kit ; no such service: kibana → tu as tapé le nom du conteneur (labo-kibana) au lieu du service Compose (kibana).
Relance le service et attends que son healthcheck repasse au vert : 40 à 60 secondes, le temps de se reconnecter à Elasticsearch et de vérifier ses index internes.
docker compose start kibana Container labo-elasticsearch Waiting
Container labo-elasticsearch Healthy
Container labo-kibana Starting
Container labo-kibana StartedCompose a d'abord vérifié qu'Elasticsearch était Healthy (le depends_on … service_healthy de la leçon 03), puis a démarré Kibana. Surveille toutes les cinq secondes :
docker inspect --format '{{.State.Health.Status}}' labo-kibanaou, pour ne pas retaper, watch -n 5 docker inspect --format '{{.State.Health.Status}}' labo-kibana (Ctrl + C pour sortir ; watch est absent de macOS par défaut, retape la commande à la main).
Point de contrôle : starting pendant 40 à 50 secondes, puis healthy (sur la machine du cours : starting de 0 à 45 s, healthy à 50 s). etat remontre alors la ligne :
labo-kibana Up 52 seconds (healthy) 0.0.0.0:5601->5601/tcp, [::]:5601->5601/tcp
…
✔ Kibana répond (http://localhost:5601)et journal kibana se termine par les lignes qu'on veut voir :
labo-kibana | [2026-09-09T23:47:49.408+00:00][INFO ][http.server.Kibana] http server running at http://0.0.0.0:5601
labo-kibana | [2026-09-09T23:47:50.374+00:00][INFO ][status] Kibana is now availableRouvre Dev Tools (si tu tombes sur « Kibana server is not ready yet », tu as été plus rapide que le healthy : attends dix secondes) et envoie la requête que etat lui-même utilise :
GET _cluster/health?filter_path=status,number_of_nodes{
"status": "green",
"number_of_nodes": 1
}Elasticsearch a répondu green pendant toute la panne : Kibana est une fenêtre sur les données, pas les données. Arrêter Kibana n'a rien effacé ni réindexé.
Si tu vois autre chose : unhealthy après deux minutes → journal kibana et cherche ECONNREFUSED (Elasticsearch tombé entre-temps) ; port is already allocated → un autre programme a pris le port 5601 pendant l'arrêt (leçon 04, panne 1).
Tu reconnais un service arrêté ; apprends maintenant à reconnaître une requête fausse sur un service sain, la confusion la plus fréquente en salle. Dans Dev Tools, écris une requête de recherche qui renvoie 404 avec "type": "index_not_found_exception", puis explique en une phrase pourquoi le cluster reste green.
Indice : Elasticsearch ne devine jamais le nom d'un index. Choisis-en un qui n'existe pas, avec le préfixe pratique- ; rien à créer, rien à écrire.
GET pratique-inexistant/_searchRéponse, avec le badge 404 - Not Found en bas à droite du panneau de sortie :
{
"error": {
"root_cause": [
{
"type": "index_not_found_exception",
"reason": "no such index [pratique-inexistant]",
"resource.type": "index_or_alias",
"resource.id": "pratique-inexistant",
"index_uuid": "_na_",
"index": "pratique-inexistant"
}
],
"type": "index_not_found_exception",
"reason": "no such index [pratique-inexistant]",
…
},
"status": 404
}La même chose en ligne de commande, pour voir le code HTTP nu : curl -s -o /dev/null -w '%{http_code}\n' http://localhost:9200/pratique-inexistant/_search affiche 404.
Explication attendue : le 404 est une réponse normale et complète d'Elasticsearch : « j'ai compris ta requête, mais cette ressource n'existe pas ». Le service tourne, le cluster reste green ; il n'y a que le nom à corriger (GET _cat/indices?v donne la liste). À l'étape B.7, il n'y avait aucune réponse du tout.
Deux variantes à essayer : GET pratique-inexistant/_count?ignore_unavailable=true renvoie 200 et "count": 0 (tu demandes d'ignorer l'index absent) ; GET cours/_serch renvoie 400 avec "no handler found for uri [/cours/_serch] and method [GET]". Un 400 : « je ne comprends pas la requête » ; un 404 : « je comprends, mais ça n'existe pas ».
Une seule commande prouve que tout est fait : etat, avec Kibana revenu et les quatre chiffres.
./labo.sh etat== Conteneurs ==
NAME STATUS PORTS
labo-elasticsearch Up 10 hours (healthy) 0.0.0.0:9200->9200/tcp, [::]:9200->9200/tcp
labo-kibana Up About a minute (healthy) 0.0.0.0:5601->5601/tcp, [::]:5601->5601/tcp
labo-neo4j Up 7 minutes (healthy) 0.0.0.0:7474->7474/tcp, [::]:7474->7474/tcp, 0.0.0.0:7687->7687/tcp, [::]:7687->7687/tcp
labo-opensearch Up 10 hours (healthy) 0.0.0.0:9201->9200/tcp, [::]:9201->9200/tcp
labo-opensearch-dashboards Up 10 hours 0.0.0.0:5602->5601/tcp, [::]:5602->5601/tcp
== Services ==
✔ Elasticsearch : {"status":"green","number_of_nodes":1}
index : acces 12000 avis 609 cours 504
✔ Kibana répond (http://localhost:5601)
✔ Neo4j répond — nœuds : 872
✔ OpenSearch : {"status":"green","number_of_nodes":1}(Sans le profil OpenSearch, les deux conteneurs labo-opensearch… n'apparaissent pas et la dernière ligne dit — OpenSearch non démarré … : c'est l'état attendu jusqu'au module 5.)
prerequis se termine par Tout est prêt.labo-elasticsearch, labo-kibana et labo-neo4j sont Up … (healthy), Kibana compris.etat affiche "status":"green", acces 12000 avis 609 cours 504, nœuds : 872, inchangés après le second importer / charger-graphe.etat, le navigateur et journal kibana quand Kibana est arrêté, et en quoi ça diffère d'un 404.etat ci-dessus (copie ou capture) comme livrable.Cette pratique ne crée rien : ni index, ni nœud, ni objet Kibana. Deux choses à garantir : que Kibana tourne (sinon docker compose start kibana depuis le dossier du kit), et qu'aucun index de travail ne traîne :
GET _cat/indices/pratique-*?vRéponse attendue : la ligne d'en-tête seule (health status index uuid pri rep docs.count …). Ne touche pas à cours, avis, acces ni au graphe : ils servent à tous les modules suivants.
docker compose stop kibana répond no configuration file provided: not found → Compose cherche docker-compose.yml dans le dossier courant. cd vers la racine du kit (celle qui contient labo.sh et labo.ps1) et recommence. Le script se repositionne tout seul ; les commandes docker compose tapées à la main, non.
Après stop, etat ne montre plus labo-kibana et tu crois l'avoir supprimé → Non : docker compose ps masque les conteneurs arrêtés. docker compose ps -a le liste en Exited (0), et docker compose start kibana le relance avec ses données. Un conteneur vraiment supprimé n'apparaîtrait pas même avec -a ; demarrer le recréerait.
Kibana reste starting puis passe unhealthy, et journal kibana répète Unable to retrieve version information from Elasticsearch nodes. connect ECONNREFUSED 172.x.x.x:9200 → Kibana est revenu mais Elasticsearch est tombé entre-temps (souvent Exited (137), la mémoire). Répare Elasticsearch d'abord (leçon 04, panne 2) ; Kibana se reconnecte seul.
Dev Tools renvoie 400 au lieu du 404 attendu, avec no handler found for uri [/pratique-inexistant/_serch] and method [GET] → La faute porte sur l'API (_serch), pas sur l'index. Elasticsearch valide d'abord la route, puis l'index : corrige en _search et le 404 apparaît.
Windows seulement — sous PowerShell 5.1, demarrer affiche en rouge docker : Image docker.elastic.co/kibana/kibana:9.5.3 Pulling … NativeCommandError mais finit par Le labo est prêt → Ça n'arrive que si tu rediriges la sortie (2>&1, | Tee-Object) : Compose écrit sa progression sur le flux d'erreur et PowerShell 5.1 l'habille en exception. Pas une erreur ; lance le script sans redirection, ou passe à PowerShell 7.
Windows seulement — .\labo.ps1 est refusé : « l'exécution de scripts est désactivée sur ce système » → Set-ExecutionPolicy -Scope CurrentUser RemoteSigned, réponds O, relance. Une seule fois par machine.
Linux natif seulement — Elasticsearch sort en Exited (78) et journal elasticsearch dit max virtual memory areas vm.max_map_count [65530] is too low → sudo sysctl -w vm.max_map_count=262144 puis ./labo.sh demarrer. Pour que ça survive au redémarrage : ajoute vm.max_map_count=262144 dans /etc/sysctl.conf.
macOS et bash — ./labo.sh répond Permission denied → chmod +x labo.sh, une seule fois. Si bash: ./labo.sh: /bin/bash^M: bad interpreter, le fichier a des fins de ligne Windows : git config core.autocrlf input puis re-clone, ou sed -i '' 's/\r$//' labo.sh.