Module 10 — Bases de graphes, Neo4j et premiers pas en Cypher
Inès dessine au tableau une question que Veille aimerait résoudre pour ses clients : « qui écrit dans les mêmes catégories que Lee Moran, et à quelle distance d'écriture ? ». En SQL, la requête devient un empilement de JOIN récursifs. Le même problème, dans un graphe, tient sur trois lignes. Sami installe Neo4j en local via le kit ; ce module lui apprend le vocabulaire, le langage Cypher, et lui fait construire un mini-graphe de l'équipe qui servira de terrain d'exercice.
Quand le graphe bat le relationnel
Une base relationnelle est excellente pour agréger des colonnes, moins bonne pour suivre des relations en profondeur variable. Trois indices que le problème est un problème de graphe :
- Une question fait intervenir un chemin : « existe-t-il un lien entre A et B ? », « à quelle distance ? ».
- La profondeur n'est pas fixe : « qui encadre qui, à un ou deux niveaux ? », « recommandations à deux sauts ».
- Les entités jouent plusieurs rôles : un auteur peut aussi être une source, une catégorie peut être aussi un centre d'intérêt.
En SQL, un chemin de longueur 3 se traduit par trois JOIN explicites. En Cypher, on écrit (a)-[*1..3]->(b) et le moteur trouve le chemin. Le gain n'est pas un gain d'expressivité seulement : le moteur de graphe stocke les relations comme des pointeurs et les traverse en temps constant, contrairement à une jointure qui repose sur un index sur la clé étrangère.
Le modèle à propriétés
Neo4j suit le property graph model. Quatre briques suffisent.
- Nœud — l'entité. Représenté entre parenthèses :
(p). - Étiquette — le type du nœud, précédé de
::(:Personne),(:Ville). Un nœud peut porter plusieurs étiquettes. - Relation typée et orientée — le lien, entre crochets et flèches :
-[:ENCADRE]->. Le type est obligatoire, la direction aussi (même si on peut la parcourir dans les deux sens à la lecture). - Propriété — une paire clé/valeur portée par un nœud ou une relation :
{nom: 'Inès', anciennete: 6}.
On assemble ces briques en motifs en ASCII-art :
(p:Personne)-[:HABITE {depuis: 2018}]->(v:Ville {nom: 'Montréal'})
Cette ligne se lit « il existe un nœud p étiqueté Personne en relation HABITE avec le nœud v étiqueté Ville de nom Montréal, la relation portant la propriété depuis à 2018 ». La même ligne sert à lire (dans un MATCH) et à écrire (dans un CREATE ou un MERGE). C'est le premier grand pari du langage : un seul vocabulaire pour interroger et pour construire.
Ouvrir Neo4j Browser
Le kit expose Neo4j Community 5.26 sur http://localhost:7474 (HTTP) et bolt://localhost:7687 (protocole binaire des clients). Ouvrez le navigateur.
- URL de connexion :
bolt://localhost:7687 - Utilisateur :
neo4j - Mot de passe :
veille2026
Une fois connecté, la barre du haut accepte trois familles d'entrées. Les requêtes Cypher (tout ce qui commence par MATCH, CREATE, MERGE, RETURN…). Les commandes du Browser, préfixées par : : :play (mini-tutoriels intégrés), :schema (liste des étiquettes, contraintes, index), :sysinfo (état du serveur, tailles de heap, page cache), :history (historique). Enfin, EXPLAIN et PROFILE (module 12) préfixent une requête pour visualiser son plan d'exécution.
Après un RETURN, le panneau de résultat propose trois icônes : graphe (visualisation), tableau (colonnes), texte (JSON brut). Le tableau est idéal pour vérifier des valeurs ; le graphe est essentiel pour comprendre la topologie.
Le script du kit : 10-premiers-pas.cypher
Le kit livre un mini-graphe de onze nœuds et quatorze relations qui décrit l'équipe Veille. On l'exécute d'une seule commande depuis le shell hôte.
./lab.sh cypher 10-premiers-pas.cypher
Le fichier est monté en lecture seule dans le conteneur veille-neo4j sous /cypher. Voyons-le bloc par bloc.
Bloc 1 — les contraintes d'unicité
CREATE CONSTRAINT personne_nom IF NOT EXISTS FOR (p:Personne) REQUIRE p.nom IS UNIQUE;
CREATE CONSTRAINT ville_nom IF NOT EXISTS FOR (v:Ville) REQUIRE v.nom IS UNIQUE;
CREATE CONSTRAINT competence_nom IF NOT EXISTS FOR (c:Competence) REQUIRE c.nom IS UNIQUE;
Trois contraintes d'unicité sur la propriété nom, une par étiquette. Deux effets. D'abord, elles garantissent l'unicité : Neo4j refusera d'insérer deux Personne avec le même nom. Ensuite, elles créent implicitement un index sur nom : les futurs MATCH (p:Personne {nom: 'Inès'}) seront des accès directs, pas des scans. IF NOT EXISTS rend la commande idempotente — on peut relancer le script sans erreur.
Depuis Neo4j 5, on écrit CREATE CONSTRAINT nom IF NOT EXISTS FOR (n:Label) REQUIRE n.prop IS UNIQUE. La forme ancienne CREATE CONSTRAINT ON ... ASSERT est dépréciée et ne fonctionne plus dans un projet neuf.
Bloc 2 — les nœuds
MERGE (:Personne {nom: 'Inès', role: 'lead data', anciennete: 6});
MERGE (:Personne {nom: 'Sami', role: 'data engineer', anciennete: 1});
MERGE (:Personne {nom: 'Léa', role: 'analyste', anciennete: 3});
MERGE (:Personne {nom: 'Karim', role: 'développeur', anciennete: 4});
Chaque MERGE tente d'abord un MATCH sur le motif et, s'il n'existe pas, exécute un CREATE. C'est le pattern upsert de Cypher. Grâce aux contraintes du bloc 1, la recherche est instantanée. Les trois villes et les quatre compétences suivent le même schéma :
MERGE (:Ville {nom: 'Montréal', pays: 'Canada'});
MERGE (:Competence {nom: 'Elasticsearch'});
// et ainsi de suite
Bloc 3 — les relations
Les relations se posent après les nœuds, avec le motif MATCH ... MERGE (a)-[:REL]->(b) :
MATCH (p:Personne {nom: 'Inès'}), (v:Ville {nom: 'Montréal'})
MERGE (p)-[:HABITE {depuis: 2018}]->(v);
Le MATCH retrouve les deux nœuds (accès par index grâce à la contrainte), et le MERGE crée la relation si elle n'existe pas encore. La propriété depuis porte l'année d'installation. Les relations MAITRISE portent un niveau de 1 à 5 :
MATCH (p:Personne {nom: 'Inès'}), (c:Competence {nom: 'Elasticsearch'})
MERGE (p)-[:MAITRISE {niveau: 5}]->(c);
Enfin, ENCADRE matérialise le mentorat :
MATCH (a:Personne {nom: 'Inès'}), (b:Personne {nom: 'Sami'})
MERGE (a)-[:ENCADRE]->(b);
Bloc 4 — le bilan
MATCH (n) WITH count(n) AS noeuds
MATCH ()-[r]->() RETURN noeuds, count(r) AS relations;
Résultat attendu à la fin de l'exécution :
noeuds | relations
-------+----------
11 | 14
Onze nœuds : 4 personnes, 3 villes, 4 compétences. Quatorze relations : 4 HABITE, 7 MAITRISE, 3 ENCADRE.
Pourquoi MERGE plutôt que CREATE
CREATE crée sans regarder. Relancer le script deux fois avec CREATE (:Personne {nom: 'Inès'}) insérerait deux Inès distincts, avec deux id internes différents. Passé sur un dataset, cette erreur devient invisible et coûteuse.
MERGE est l'équivalent d'un INSERT ... ON CONFLICT DO NOTHING doublé d'un SELECT implicite : la ligne existe, on la garde ; elle n'existe pas, on la crée. Le script du kit est donc idempotent : le relancer produit toujours le même graphe.
MERGE (a)-[:HABITE]->(b) crée la relation et les deux nœuds s'ils n'existent pas. Pour éviter d'inventer un nœud, on fait d'abord deux MATCH puis un MERGE uniquement sur la relation, comme dans le script. C'est le patron sûr.
Premières requêtes de lecture
Le graphe est en place. Ouvrez le Browser et lancez les requêtes suivantes.
Voir toute l'équipe :
MATCH (p:Personne) RETURN p.nom, p.role, p.anciennete ORDER BY p.anciennete DESC;
p.nom | p.role | p.anciennete
-------+----------------+-------------
Inès | lead data | 6
Karim | développeur | 4
Léa | analyste | 3
Sami | data engineer | 1
Qui habite Montréal — motif à deux nœuds avec filtre sur la ville :
MATCH (p:Personne)-[:HABITE]->(:Ville {nom: 'Montréal'})
RETURN p.nom;
Attendu : Inès, Sami.
Qui maîtrise Elasticsearch au niveau supérieur ou égal à 4 — le filtre porte sur la relation :
MATCH (p:Personne)-[r:MAITRISE]->(:Competence {nom: 'Elasticsearch'})
WHERE r.niveau >= 4
RETURN p.nom, r.niveau ORDER BY r.niveau DESC;
Attendu : Inès niveau 5.
Qui encadre qui — on lit la relation dans le sens naturel :
MATCH (mentor:Personne)-[:ENCADRE]->(mentore:Personne)
RETURN mentor.nom, mentore.nom;
Attendu : Inès → Sami, Inès → Léa, Karim → Sami.
Chaîne d'encadrement à deux niveaux — chemin de longueur variable de 1 à 2 :
MATCH chemin = (mentor:Personne)-[:ENCADRE*1..2]->(mentore:Personne)
RETURN mentor.nom, mentore.nom, length(chemin) AS profondeur
ORDER BY profondeur, mentor.nom;
Ici la profondeur reste 1 (aucun mentoré n'encadre à son tour). Le motif *1..2 prépare le module 12 : quand on chargera le graphe News, les chemins prendront tout leur sens.
*min..max(a)-[:REL*1..3]->(b) demande à Neo4j de suivre 1, 2 ou 3 relations REL orientées dans l'ordre. C'est ce qui manque cruellement à SQL. À l'usage, garder une borne haute évite les requêtes qui explosent : *1.. sans borne s'écrit techniquement mais coûte cher.
ORDER BY, LIMIT, WHERE et amis
Cypher reprend les mots-clés familiers de SQL avec la même sémantique.
MATCH (p:Personne)
WHERE p.anciennete > 2 AND p.role CONTAINS 'data'
RETURN p.nom, p.role
ORDER BY p.anciennete DESC
LIMIT 3;
Créer avec CREATE, modifier avec SET, supprimer avec DELETE ou DETACH DELETE. Cette dernière est cruciale : DELETE échoue si le nœud a encore des relations, DETACH DELETE supprime le nœud et toutes ses relations en une passe.
// Ajouter une compétence, corriger un rôle, retirer un mentorat
CREATE (:Competence {nom: 'Docker'});
MATCH (p:Personne {nom: 'Sami'}) SET p.role = 'data engineer senior';
MATCH (:Personne {nom: 'Karim'})-[r:ENCADRE]->(:Personne {nom: 'Sami'}) DELETE r;
Ne jamais faire MATCH (n) DELETE n sur une base réelle : il faut soit un DETACH DELETE, soit un balayage par lots — c'est exactement ce que fait 99-reset.cypher avec apoc.periodic.iterate (module 11).
Le shell interactif
Neo4j Browser est confortable pour explorer et visualiser. Pour scripter des requêtes ou intégrer à un pipeline, le kit expose un shell natif :
./lab.sh cypher-shell
On peut y coller un bloc Cypher, terminer par ; et voir la réponse en tableau. Les commandes :begin, :commit, :rollback donnent la main sur les transactions ; :exit sort du shell.
À vous
Exercice 1 — Écrivez la requête qui liste les compétences maîtrisées par au moins deux personnes de l'équipe Veille, avec le nombre de personnes.
Solution
MATCH (:Personne)-[:MAITRISE]->(c:Competence)
WITH c, count(*) AS n
WHERE n >= 2
RETURN c.nom, n ORDER BY n DESC, c.nom;
Sur le mini-graphe : Neo4j est maîtrisé par Inès et Karim (2), Python par Sami et Karim (2), Elasticsearch par Inès et Sami (2). Kibana n'apparaît pas.
Exercice 2 — Trouvez qui, à Montréal, maîtrise à la fois Elasticsearch et Neo4j (à n'importe quel niveau).
Solution
MATCH (p:Personne)-[:HABITE]->(:Ville {nom: 'Montréal'})
MATCH (p)-[:MAITRISE]->(:Competence {nom: 'Elasticsearch'})
MATCH (p)-[:MAITRISE]->(:Competence {nom: 'Neo4j'})
RETURN p.nom;
Attendu : Inès (Sami n'a pas Neo4j dans le mini-graphe).
Exercice 3 — Ajoutez la compétence « Docker » et affectez-la à Sami avec un niveau 2, puis interrogez à nouveau qui maîtrise Docker au niveau supérieur ou égal à 2.
Solution
MERGE (c:Competence {nom: 'Docker'});
MATCH (p:Personne {nom: 'Sami'}), (c:Competence {nom: 'Docker'})
MERGE (p)-[:MAITRISE {niveau: 2}]->(c);
MATCH (p:Personne)-[r:MAITRISE]->(:Competence {nom: 'Docker'})
WHERE r.niveau >= 2
RETURN p.nom, r.niveau;
Attendu : Sami niveau 2.
Points à retenir
- Un graphe est quatre briques : nœud, étiquette, relation typée orientée, propriété.
- Cypher utilise les motifs ASCII-art pour lire et pour écrire :
(a)-[:REL]->(b). - Poser une contrainte d'unicité avant les données donne un index gratuit et rend le script idempotent.
MERGEfait un upsert ;CREATEinsère aveuglément ; pour un script relançable,MERGE.DETACH DELETEsupprime un nœud et ses relations en une passe ;DELETEseul refuse si le nœud a des relations.- Le mini-graphe Veille contient 11 nœuds et 14 relations : 4 personnes, 3 villes, 4 compétences, 4
HABITE, 7MAITRISE, 3ENCADRE. *1..2sur une relation ouvre les chemins de longueur variable — le seul mot-clé qui change tout par rapport à SQL.
Si ça ne marche pas
- Neo4j Browser refuse le mot de passe → le volume
neo4j-dataa été créé avec un ancien mot de passe, changerNEO4J_PASSWORDdans.envne suffit pas →./lab.sh resetpuis./lab.sh up. ./lab.sh cypher 10-premiers-pas.cypherretourne « neo4j/cypher/... introuvable » → vous êtes hors du répertoire du kit →cddanskits/42-elasticsearch-neo4j/avant de relancer.:schemaaffiche « No constraints » → le script n'a pas été exécuté ou une erreur a stoppé le premier bloc →./lab.sh logs neo4j; relancer le script après correction.- Le graphe affiche cinq personnes au lieu de quatre → un
CREATEa été utilisé au lieu deMERGEen atelier →./lab.sh cypher 99-reset.cypherpuis relancer10-premiers-pas.cypher.
Pour aller plus loin
- Manuel Cypher officiel : https://neo4j.com/docs/cypher-manual/current/
- Guide
MERGE: https://neo4j.com/docs/cypher-manual/current/clauses/merge/ - Contraintes et index : https://neo4j.com/docs/cypher-manual/current/constraints/
- Neo4j Browser (commandes
:play,:schema,:sysinfo) : https://neo4j.com/docs/browser-manual/current/
Module suivant : modéliser un graphe et le charger avec LOAD CSV, les contraintes, les index et le motif CALL { ... } IN TRANSACTIONS, pour passer du mini-graphe de l'équipe au vrai graphe News.