Sujet de TP : Neo4j
S1 2026-2027 - IUT - University 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
URI = "neo4j://localhost:7687"
AUTH = ("neo4j", "password") # À adapter selon vos identifiants
def init_db(tx):
# Réinitialisation complète du graphe
tx.run("MATCH (n) DETACH DELETE n")
# Création des contraintes et index de base
tx.run("CREATE CONSTRAINT IF NOT EXISTS FOR (u:User) REQUIRE u.id IS UNIQUE")
tx.run("CREATE CONSTRAINT IF NOT EXISTS FOR (a:Artist) REQUIRE a.id IS UNIQUE")
tx.run("CREATE CONSTRAINT IF NOT EXISTS FOR (t:Track) REQUIRE t.isrc IS UNIQUE")
tx.run("CREATE CONSTRAINT IF NOT EXISTS FOR (g:Genre) REQUIRE g.name IS UNIQUE")
# Création des Genres
tx.run("""
UNWIND ['Electro', 'Synthwave', 'French Touch', 'Rock', 'Indie Pop'] AS g_name
CREATE (:Genre {name: g_name})
""")
# Création des Artistes
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'})
""")
# Collaborations entre Artistes
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)
""")
# Morceaux et genres
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)
""")
# Utilisateurs, suivis et écoutes
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)
""")
with GraphDatabase.driver(URI, auth=AUTH) as driver:
with driver.session() as session:
session.execute_write(init_db)
print("Graphe StreamPulse initialisé avec succès !")
Partie 1 : Connexion, Inspection du Graphe et Prédicats simples (~1h30)
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 PyMongo/Neo4j.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é et affiche le nom de la base de données courante. - 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. (Indice : Plusieurs syntaxes Cypher sont possibles, soit via des clauses WHERE soit via le filtrage direct dans le pattern).
Partie 2 : Lecture de motifs, Traversées et Projections (~2h)
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 exécutés par l'artiste
"Daft Punk". Affiche le titre du morceau et son année de sortie. - Profil d'écoute : Liste les artistes écourté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 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) (~2h30)
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égations, Fonctions d'assemblage et Algorithmes de Graphe en Cypher (~2h30)
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 (
sum(play_count)). - Aggregation avec collections (
collect) : Pour chaque artiste, retourne son nom ainsi que la liste (tableau) 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 écourtés par chaque utilisateur. (Indice : Nécessite une traversée User -> LISTENED_TO -> Track -> IN_GENRE suivie d'une agrégation appropriée).
- 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 & Cypher Avancé (~2h)
Documentation
MATCH (u1:User)-[:LISTENED_TO]->(t:Track)<-[:LISTENED_TO]-(u2:User): Pattern de filtrage collaboratif (Utilisateurs aux goûts similaires).WHERE NOT (u1)-[:LISTENED_TO]->(t2): Exclusion des éléments déjà consommés par l'utilisateur cible.CREATE INDEX FOR (n:Label) ON (n.prop): Crée un index B-Tree simple pour accélérer la recherche par propriété.EXPLAIN/PROFILE: Analyse le plan d'exécution d'une requête Cypher et mesure le nombre d'opérations mémoire (db hits).SHOW INDEXES: Affiche la liste des index et contraintes actifs sur l'instance Neo4j.
Exercices
Partie A : Moteur de Recommandation
- 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.
Partie B : Performance & Diagnostics
- Analyse de plan d'exécution (
PROFILE) :
- Exécute une requête cherchant un utilisateur par son nom (
MATCH (u:User {name: 'Alice'}) RETURN u). - Préfixe la requête avec
PROFILE. Note le nombre deDbHits(accès base).
- Création d'index :
- Crée un index sur la propriété
namedes nœudsUser. - Relance la requête avec
PROFILEet compare la stratégie d'exécution (NodeByLabelScanvsNodeIndexSeek).