Sujet de TP : Neo4j
S1 2026-2027 - IUT - Université de Lille, BUT3 SD
TP Neo4j
Modèle de données Graphe & Initialisation
Le graphe StreamPulse représente les connexions entre Utilisateurs, Artistes, Morceaux (Tracks), Playlists et Genres.
Nœuds (Labels) :
(:User {id, name, country, premium: boolean})(:Artist {id, name, country})(:Track {isrc, title, year, duration_sec})(:Genre {name})(:Playlist {id, title, public: boolean})
Relations (Types) :
(:User)-[:FOLLOWS {since_year}]->(:User)(:User)-[:LISTENED_TO {play_count, last_played}]->(:Track)(:User)-[:CREATED]->(:Playlist)(:Playlist)-[:CONTAINS {position}]->(:Track)(:Artist)-[:PERFORMED]->(:Track)(:Artist)-[:COLLABORATED_WITH {count, first_year}]->(:Artist)(:Track)-[:IN_GENRE]->(:Genre)
Script d'initialisation (setup_graph.py)
Ce script se connecte à Neo4j, vide la base active et génère un graphe de départ via des requêtes Cypher paramétrées :
from neo4j import GraphDatabase
# Configuration de la connexion
URI = "neo4j://localhost:7687"
AUTH = ("neo4j", "password") # À adapter selon vos identifiants Neo4j
# Reinitialisation
def reset_database(driver):
with driver.session() as session:
def _clear_db(tx):
tx.run("MATCH (n) DETACH DELETE n")
session.execute_write(_clear_db)
def _populate_db(tx):
tx.run("""
UNWIND ['Electro', 'Synthwave', 'French Touch', 'Rock', 'Indie Pop'] AS g_name
CREATE (:Genre {name: g_name})
""")
tx.run("""
CREATE (a1:Artist {id: 'A1', name: 'Daft Punk', country: 'France'}),
(a2:Artist {id: 'A2', name: 'Kavinsky', country: 'France'}),
(a3:Artist {id: 'A3', name: 'Justice', country: 'France'}),
(a4:Artist {id: 'A4', name: 'The Weeknd', country: 'Canada'}),
(a5:Artist {id: 'A5', name: 'Phoenix', country: 'France'})
""")
tx.run("""
MATCH (a1:Artist {id: 'A1'}), (a3:Artist {id: 'A3'}), (a4:Artist {id: 'A4'}), (a2:Artist {id: 'A2'})
CREATE (a1)-[:COLLABORATED_WITH {count: 3, first_year: 2016}]->(a4),
(a1)-[:COLLABORATED_WITH {count: 2, first_year: 2007}]->(a3),
(a2)-[:COLLABORATED_WITH {count: 1, first_year: 2013}]->(a4),
(a4)-[:COLLABORATED_WITH {count: 1, first_year: 2013}]->(a2)
""")
tx.run("""
MATCH (g_e:Genre {name: 'Electro'}), (g_s:Genre {name: 'Synthwave'}), (g_ft:Genre {name: 'French Touch'})
MATCH (a1:Artist {id: 'A1'}), (a2:Artist {id: 'A2'}), (a4:Artist {id: 'A4'})
CREATE (t1:Track {isrc: 'TR001', title: 'Starboy', year: 2016, duration_sec: 230}),
(t2:Track {isrc: 'TR002', title: 'Nightcall', year: 2010, duration_sec: 259}),
(t3:Track {isrc: 'TR003', title: 'One More Time', year: 2000, duration_sec: 320})
CREATE (a1)-[:PERFORMED]->(t1), (a4)-[:PERFORMED]->(t1),
(a2)-[:PERFORMED]->(t2),
(a1)-[:PERFORMED]->(t3),
(t1)-[:IN_GENRE]->(g_e), (t1)-[:IN_GENRE]->(g_s),
(t2)-[:IN_GENRE]->(g_s),
(t3)-[:IN_GENRE]->(g_ft), (t3)-[:IN_GENRE]->(g_e)
""")
tx.run("""
CREATE (u1:User {id: 'U1', name: 'Alice', country: 'France', premium: true}),
(u2:User {id: 'U2', name: 'Bob', country: 'France', premium: false}),
(u3:User {id: 'U3', name: 'Charlie', country: 'USA', premium: true}),
(u4:User {id: 'U4', name: 'David', country: 'Germany', premium: false})
WITH u1, u2, u3, u4
MATCH (t1:Track {isrc: 'TR001'}), (t2:Track {isrc: 'TR002'}), (t3:Track {isrc: 'TR003'})
CREATE (u1)-[:FOLLOWS {since_year: 2021}]->(u2),
(u2)-[:FOLLOWS {since_year: 2022}]->(u3),
(u1)-[:FOLLOWS {since_year: 2023}]->(u3),
(u3)-[:FOLLOWS {since_year: 2020}]->(u4),
(u1)-[:LISTENED_TO {play_count: 45, last_played: '2024-01-10'}]->(t1),
(u1)-[:LISTENED_TO {play_count: 12, last_played: '2024-01-12'}]->(t2),
(u2)-[:LISTENED_TO {play_count: 88, last_played: '2024-01-15'}]->(t2),
(u3)-[:LISTENED_TO {play_count: 30, last_played: '2024-01-01'}]->(t1),
(u3)-[:LISTENED_TO {play_count: 100, last_played: '2024-01-14'}]->(t3)
""")
session.execute_write(_populate_db)
with GraphDatabase.driver(URI, auth=AUTH) as driver:
reset_database(driver)Partie 1 : Connexion, Inspection du Graphe et Prédicats simples
Documentation
GraphDatabase.driver(uri, auth=...): Crée l'instance de pilote principale pour se connecter au cluster ou serveur Neo4j.driver.verify_connectivity(): Vérifie que le serveur est joignable et que les identifiants sont valides sans exécuter de requête Cypher.session.run(query, parameters): Exécute une requête Cypher et renvoie un objetResult(itérable d'objetsRecord).MATCH (n:Label) RETURN count(n): Compte le nombre de nœuds portant un label spécifique dans le graphe.MATCH ()-[r:TYPE]->() RETURN count(r): Compte le nombre total de relations d'un type donné.MATCH (n) DETACH DELETE n: Supprime l'ensemble des nœuds et leurs relations incidentes (attention : réinitialise tout le graphe !).record.get("key")ourecord["key"]: Extrait une valeur spécifique d'un enregistrement de résultat.driver.close(): Ferme manuellement le pool de connexions du driver (géré automatiquement avec un blocwith).
Exercices
Connexion & Vérification : Exécute
setup_graph.pypour initialiser la base. Crée un script d'inspection qui vérifie la connectivité.Volumétrie des Nœuds : Écris une fonction Python qui compte et affiche le nombre total de nœuds pour chaque label (
User,Artist,Track,Genre).Volumétrie des Relations : Affiche le nombre total de relations de type
LISTENED_TOetCOLLABORATED_WITH.Comptage Filtré : Compte combien d'utilisateurs possèdent un compte
premium: true. Utiliser deux méthodes:A. Via une clause WHERE.
B. Via le filtrage direct dans le pattern.
Partie 2 : Lecture de motifs, Traversées et Projections
Documentation
MATCH (a)-[:REL]->(b): Spécifie un motif (pattern) orienté de recherche dans le graphe.MATCH (a)-[:REL*1..2]->(b): Recherche des chemins de longueur variable (entre 1 et 2 sauts).WHERE n.prop = $val: Filtre les nœuds ou relations selon des conditions logiques (AND,OR,NOT,>,<).OPTIONAL MATCH (a)-[:REL]->(b): Réalise l'équivalent d'unLEFT JOINrelationnel : renvoienullsi le motif n'existe pas sans exclurea.RETURN a.prop AS name, labels(a): Projette des propriétés spécifiques et retourne les métadonnées de nœuds.ORDER BY n.prop DESC SKIP x LIMIT y: Trie et pagine les résultats d'une requête Cypher.MATCH (a)-[:REL]-(b): Recherche un motif non orienté (traverse la relation quel que soit son sens).
Exercices
- Traversée simple : Récupère tous les morceaux joués par l'artiste
"Daft Punk". Affiche le titre du morceau et son année de sortie. - Profil d'écoute : Liste les artistes écoutés par l'utilisateur
"Alice", ainsi que le nombre d'écoutes (play_count) associé à chaque morceau. (Motif : User -> Track <- Artist). - Exploration de relations optionnelles : Pour TOUS les artistes du graphe, affiche leur nom et les titres qu'ils ont interprétés. Si un artiste n'a interprété aucun morceau enregistrés dans la base (ex:
"Phoenix"), son nom doit tout de même apparaître avec la mentionNone. - Chemins de collaboration (Sens & Non-orienté) : Trouve les artistes avec lesquels
"Daft Punk"a collaboré directement. Modifie ensuite la requête pour trouver les artistes avec lesquels"Daft Punk"a collaboré de manière directe OU indirecte à 2 sauts près (-[:COLLABORATED_WITH*1..2]-). - Pagination & Tri : Liste les 2 pistes les plus écoutées par l'ensemble des utilisateurs (somme des
play_count), triées par ordre décroissant.
Partie 3 : Écriture, Modulations de structure et Idempotence (CREATE vs MERGE)
Documentation
CREATE (n:Label {props}): Crée inconditionnellement un nouveau nœud ou une nouvelle relation (risque de doublons si exécuté plusieurs fois).MERGE (n:Label {id: $id}) ON CREATE SET n.created = timestamp() ON MATCH SET n.last_seen = timestamp(): Garantit l'existence d'un motif (crée si absent, réutilise si présent).SET n.prop = $val, n:NewLabel: Ajoute ou modifie des propriétés ou ajoute un Label à un nœud existant.REMOVE n.prop, n:Label: Supprime une propriété ou un Label d'un nœud.DELETE r/DETACH DELETE n: Supprime des relations ou un nœud avec ses relations associées.SET n += $dict: Met à jour les propriétés d'un nœud à partir d'un dictionnaire Python sans écraser les autres propriétés.
Exercices
- Création simple (
CREATE) : Crée un nouvel utilisateur"Eva"(id: 'U5',country: 'France',premium: true). - Création idempotent (
MERGE) : Écris une fonction Python qui ajoute un genre musical passé en paramètre. Exécute cette fonction deux fois avec le genre"Synthwave". Vérifie dans la base qu'aucun doublon n'a été créé. - Création de relation conditionnelle : Fais en sorte que l'utilisateur
"Eva"suive"Alice"depuis l'année 2024. Si la relation existe déjà, mets seulement à jour le champsince_yearsans recréer la relation. - Mise à jour de propriétés d'écoute : L'utilisateur
"Alice"réécoute la piste"TR002"(Nightcall). Incrémente sonplay_countde 1 et mets à jour la propriétélast_playedavec la date du jour ('2026-08-05'). - Restructuration / Suppression : L'utilisateur
"David"décide de supprimer son compte. Supprime l'utilisateurU4ainsi que toutes ses relations sortantes et entrantes du graphe.
Partie 4 : Agrégation et Fonctions d'assemblage
Documentation
WITH clause1, clause2: Chaîne les étapes d'une requête Cypher en passant les résultats intermédiaires au bloc suivant (équivalent des étapes d'un pipeline d'agrégation).count(x),avg(x),sum(x),collect(x): Fonctions d'agrégation Cypher (collect(x)transforme des lignes en une liste/tableau Python).SIZE(pattern)ouCOUNT { (n)-[:REL]->() }: Compte le nombre de relations correspondant à un pattern sans dupliquer les lignes.UNWIND list AS item: Transforme une liste ou un tableau en une série de lignes individuelles (l'inverse decollect).DISTINCT x: Élimine les doublons au sein d'une agrégation ou d'une projection.WHERE ALL / ANY / SINGLE (x IN list WHERE condition): Prédicats sur les collections et tableaux.
Exercices
- Agrégation globale : Calcule pour chaque utilisateur le nombre total de morceaux écoutés et le volume total d'écoutes.
- Aggregation avec collections (
collect) : Pour chaque artiste, retourne son nom ainsi que la liste de tous les titres de ses morceaux. - Degré de réseau (In-degree / Out-degree) : Écris une requête Cypher qui calcule pour chaque utilisateur son nombre de followers (abonnés) et son nombre de followings (abonnements). Identifie l'utilisateur le plus influent du réseau.
- Diversité musicale par utilisateur : Calcule le nombre de genres distincts écoutés par chaque utilisateur.
- Filtrage post-agrégation avec
WITH: Écris une requête utilisantWITHqui calcule le total d'écoutes par genre musical, puis ne conserve dans le résultat que les genres comptabilisant au moins 50 écoutes au total. - Décomposition de collections (
UNWIND) : À partir d'une liste Python de nouveaux tags distribuée ainsi :['Indie', 'Alternative', 'Lo-Fi'], utiliseUNWINDpour rattacher ces 3 genres d'un seul coup au morceau"TR002".
Partie 5 : Recommandation
Exercices
Recommandation basée sur le réseau social ("Ce que vos amis écoutent") : Écris une requête de recommandation pour l'utilisateur
"Alice". La requête doit trouver les morceaux écoutés par les personnes qu'Alice suit (FOLLOWS), mais qu'Alice n'a pas encore écoutés. Affiche le titre du morceau recommandé et le nombre d'amis qui l'ont écouté.Filtrage Collaboratif Item-Item ("Les utilisateurs qui ont écouté X ont aussi écouté...") : L'utilisateur
"Bob"vient d'écouter le morceau"Nightcall"(TR002). Trouve les autres utilisateurs qui ont aussi écouté"Nightcall". Identifie les autres morceaux que ces utilisateurs ont écoutés (exclure ceux que Bob a déjà écoutés). Ordonne les résultats par popularité auprès de ces utilisateurs similaires.