Cas Pratique - Réseau Social & Système de Recommandation en Graphe

@joekakone

Joseph Konka @joekakone

03/03/2026 · 18 min

Concevez un réseau social professionnel et un moteur de recommandation d'amis et de compétences avec Neo4j, Cypher et le pilote Python officiel (neo4j-driver).

Cas Pratique : Réseau Social et Système de Recommandation en Graphe#

Dans les architectures logicielles actuelles, les moteurs de recommandation constituent le cœur de la valeur ajoutée des géants de la Tech. Qu'il s'agisse de LinkedIn recommandant de nouveaux contacts (« Personnes que vous pourriez connaître »), de Netflix suggérant des séries selon vos affinités ou d'Amazon croisant les historiques d'achat, les bases de données de graphes surpassent de plusieurs ordres de grandeur les approches tabulaires SQL.

Dans ce projet pratique intégrateur, nous allons concevoir le socle d'un réseau professionnel baptisé « GraphIn ». Nous construirons son modèle topologique, insérerons un réseau de membres, d'entreprises et de compétences, puis développerons en Cypher et en Python un moteur de recommandation d'amis et de matching d'offres d'emploi.


1. Modélisation du Graphe de Domaine « GraphIn »#

Le réseau métier s'articule autour de 4 types de nœuds interconnectés :

graph TD
    M1["(:Member)<br/>Alice, Bob..."] -->|"-[:CONNECTED_TO]-"| M2["(:Member)<br/>Claire, David..."]
    M1 -->|"-[:WORKS_AT]->"| C["(:Company)<br/>AlgoJungle, TechCorp..."]
    M1 -->|"-[:HAS_SKILL]->"| S["(:Skill)<br/>Python, Neo4j, Docker..."]
    JO["(:JobOffer)<br/>Data Engineer, DevOps..."] -->|"-[:POSTED_BY]->"| C
    JO -->|"-[:REQUIRES_SKILL]->"| S

Typologie des nœuds et relations :#

  • :Member : Profil utilisateur (nom, titre, ville, anciennete).
  • :Company : Entreprise employeuse (nom, secteur).
  • :Skill : Compétence technique (nom, domaine).
  • :JobOffer : Offre de recrutement (titre, salaire_k€).
  • Relations :
  • (:Member)-[:CONNECTED_TO {depuis: annee}]-(:Member) : Réseau d'amitié / collaboration bilatérale.
  • (:Member)-[:WORKS_AT {poste: str}]->(:Company) : Emploi actuel.
  • (:Member)-[:HAS_SKILL {niveau: 1..5}]->(:Skill) : Compétences maîtrisées.
  • (:JobOffer)-[:POSTED_BY]->(:Company) : Entreprise émettrice de l'offre.
  • (:JobOffer)-[:REQUIRES_SKILL {critique: bool}]->(:Skill) : Compétences attendues.

2. Peuplement de la base dans Neo4j Browser#

Connectez-vous sur votre interface Neo4j Browser (http://localhost:7474) et exécutez le script d'initialisation suivant pour injecter le graphe de données :

// 1. Nettoyage préventif
MATCH (n) DETACH DELETE n;

// 2. Création des Entreprises
CREATE (c1:Company {nom: 'AlgoJungle', secteur: 'EdTech'})
CREATE (c2:Company {nom: 'CloudScale', secteur: 'Cloud & DevOps'})
CREATE (c3:Company {nom: 'DataCorp', secteur: 'Finance'});

// 3. Création des Compétences
CREATE (s1:Skill {nom: 'Python', domaine: 'Data'})
CREATE (s2:Skill {nom: 'Neo4j', domaine: 'Database'})
CREATE (s3:Skill {nom: 'Docker', domaine: 'DevOps'})
CREATE (s4:Skill {nom: 'SQL', domaine: 'Database'})
CREATE (s5:Skill {nom: 'Kubernetes', domaine: 'DevOps'});

// 4. Création des Membres et de leurs compétences
CREATE (alice:Member {nom: 'Alice Martin', titre: 'Lead Data Engineer', ville: 'Paris'})
CREATE (bob:Member {nom: 'Bob Dubois', titre: 'DevOps Specialist', ville: 'Paris'})
CREATE (claire:Member {nom: 'Claire Laurent', titre: 'Data Scientist', ville: 'Lyon'})
CREATE (david:Member {nom: 'David Cohen', titre: 'Cloud Architect', ville: 'Marseille'})
CREATE (emma:Member {nom: 'Emma Bernard', titre: 'Graph Analyst', ville: 'Paris'})

// Association Membres -> Entreprises
CREATE (alice)-[:WORKS_AT {poste: 'Lead Data', depuis: 2022}]->(c1)
CREATE (bob)-[:WORKS_AT {poste: 'DevOps Engineer', depuis: 2023}]->(c2)
CREATE (claire)-[:WORKS_AT {poste: 'Data Scientist', depuis: 2021}]->(c3)
CREATE (david)-[:WORKS_AT {poste: 'Senior Architect', depuis: 2020}]->(c2)
CREATE (emma)-[:WORKS_AT {poste: 'Graph Consultant', depuis: 2024}]->(c1)

// Association Membres -> Compétences
CREATE (alice)-[:HAS_SKILL {niveau: 5}]->(s1) // Python
CREATE (alice)-[:HAS_SKILL {niveau: 5}]->(s2) // Neo4j
CREATE (alice)-[:HAS_SKILL {niveau: 4}]->(s4) // SQL

CREATE (bob)-[:HAS_SKILL {niveau: 5}]->(s3) // Docker
CREATE (bob)-[:HAS_SKILL {niveau: 4}]->(s5) // Kubernetes
CREATE (bob)-[:HAS_SKILL {niveau: 3}]->(s1) // Python

CREATE (claire)-[:HAS_SKILL {niveau: 4}]->(s1) // Python
CREATE (claire)-[:HAS_SKILL {niveau: 4}]->(s4) // SQL

CREATE (david)-[:HAS_SKILL {niveau: 5}]->(s3) // Docker
CREATE (david)-[:HAS_SKILL {niveau: 5}]->(s5) // Kubernetes

CREATE (emma)-[:HAS_SKILL {niveau: 5}]->(s2) // Neo4j
CREATE (emma)-[:HAS_SKILL {niveau: 4}]->(s1) // Python
CREATE (emma)-[:HAS_SKILL {niveau: 3}]->(s3) // Docker

// 5. Tissage du réseau de relations interpersonnelles
CREATE (alice)-[:CONNECTED_TO {annee: 2022}]->(bob)
CREATE (alice)-[:CONNECTED_TO {annee: 2023}]->(claire)
CREATE (bob)-[:CONNECTED_TO {annee: 2023}]->(david)
CREATE (bob)-[:CONNECTED_TO {annee: 2024}]->(emma)
CREATE (claire)-[:CONNECTED_TO {annee: 2024}]->(emma)

// 6. Publication d'offres d'emploi
CREATE (job1:JobOffer {titre: 'Senior Graph Data Engineer', salaire_k: 75})
CREATE (job1)-[:POSTED_BY]->(c1)
CREATE (job1)-[:REQUIRES_SKILL {obligatoire: true}]->(s1) // Python
CREATE (job1)-[:REQUIRES_SKILL {obligatoire: true}]->(s2) // Neo4j
CREATE (job1)-[:REQUIRES_SKILL {obligatoire: false}]->(s3) // Docker

CREATE (job2:JobOffer {titre: 'Ingénieur Kubernetes & Cloud', salaire_k: 70})
CREATE (job2)-[:POSTED_BY]->(c2)
CREATE (job2)-[:REQUIRES_SKILL {obligatoire: true}]->(s3) // Docker
CREATE (job2)-[:REQUIRES_SKILL {obligatoire: true}]->(s5) // Kubernetes

3. Conception des Algorithmes de Recommandation en Cypher#

Algorithme 1 : Recommandation d'Amis (People You May Know - PYMK)#

Le principe de la fermeture triadique postule que si \(A\) est connecté à \(B\) et que \(B\) est connecté à \(C\), il existe une forte probabilité naturelle que \(A\) et \(C\) se connaissent ou aient intérêt à se connecter.

Problématique : Recommander à Alice de nouveaux contacts qu'elle ne connaît pas encore, en les classant par le nombre d'amis communs :

MATCH (alice:Member {nom: 'Alice Martin'})-[:CONNECTED_TO]-(ami_commun:Member)-[:CONNECTED_TO]-(suggeré:Member)
// Exclure Alice elle-même
WHERE suggeré <> alice
// Exclure les personnes qu'Alice connaît déjà directement
AND NOT (alice)-[:CONNECTED_TO]-(suggeré)
RETURN 
    suggeré.nom AS Recommandation,
    suggeré.titre AS Poste,
    count(DISTINCT ami_commun) AS AmisCommuns,
    collect(ami_commun.nom) AS ParLIntermediaireDe
ORDER BY AmisCommuns DESC

Sortie obtenue :

Recommandation   | Poste              | AmisCommuns | ParLIntermediaireDe
"Emma Bernard"   | "Graph Consultant" | 2           | ["Bob Dubois", "Claire Laurent"]
"David Cohen"    | "Cloud Architect"  | 1           | ["Bob Dubois"]

Analyse : Emma Bernard arrive en tête car elle partage 2 relations communes avec Alice (Bob et Claire), ce qui en fait la suggestion la plus pertinente.


Algorithme 2 : Moteur de Recrutement & Matching de Compétences#

Comment trouver les candidats idéaux pour l'offre « Senior Graph Data Engineer » et calculer leur score d'adéquation en pourcentage ?

MATCH (job:JobOffer {titre: 'Senior Graph Data Engineer'})-[:REQUIRES_SKILL]->(comp_requise:Skill)
WITH job, count(comp_requise) AS TotalCompetencesRequises

MATCH (candidat:Member)-[h:HAS_SKILL]->(comp_detenue:Skill)<-[:REQUIRES_SKILL]-(job)
WITH 
    candidat, 
    job, 
    TotalCompetencesRequises, 
    count(comp_detenue) AS CompetencesValidees,
    collect(comp_detenue.nom) AS CompetencesEnCommun

RETURN 
    candidat.nom AS Candidat,
    candidat.titre AS PosteActuel,
    CompetencesValidees + " / " + TotalCompetencesRequises AS Match,
    round((toFloat(CompetencesValidees) / TotalCompetencesRequises) * 100, 1) AS ScoreAdequation,
    CompetencesEnCommun
ORDER BY ScoreAdequation DESC

Résultat :

Candidat        | ScoreAdequation | CompetencesEnCommun
"Alice Martin"  | 66.7 %          | ["Python", "Neo4j"]
"Emma Bernard"  | 100.0 %         | ["Neo4j", "Python", "Docker"]
"Bob Dubois"    | 66.7 %          | ["Docker", "Python"]
"Claire Laurent"| 33.3 %          | ["Python"]

Analyse : Emma Bernard possède 100 % des compétences requises (Python, Neo4j, Docker), se positionnant immédiatement comme la candidate prioritaire pour le poste.


Algorithme 3 : Plus Court Chemin d'Introduction (Shortest Path)#

Imaginons que Claire Laurent souhaite entrer en contact avec David Cohen pour un partenariat. Quel est le chemin relationnel le plus court pour obtenir une introduction ?

MATCH 
    (claire:Member {nom: 'Claire Laurent'}),
    (david:Member {nom: 'David Cohen'}),
    chemin = shortestPath((claire)-[:CONNECTED_TO*]-(david))
RETURN 
    [n IN nodes(chemin) | n.nom] AS CheminDIntroduction,
    length(chemin) AS DegresDeSeparation

Résultat :

CheminDIntroduction                          | DegresDeSeparation
["Claire Laurent", "Alice Martin", "Bob Dubois", "David Cohen"] | 3

En moins d'une milliseconde, Neo4j a calculé le graphe d'introduction optimal à travers le réseau !


4. Automatisation en Python avec neo4j-driver#

Pour interfacer vos applications backend avec Neo4j, le pilote officiel neo4j (compatible Python 3.10+) utilise le protocole binaire Bolt via le port 7687.

Installation#

pip install neo4j

Script décisionnel complet (graphin_recommender.py)#

Créez le script suivant dans votre environnement de développement :

"""
Plateforme décisionnelle de recommandation en graphe pour GraphIn.
Auteur : Joseph Konka (AlgoJungle)
"""

from neo4j import GraphDatabase, Driver
from typing import List, Dict, Any

# URI de connexion officielle Bolt
URI = "bolt://localhost:7687"
AUTH = ("neo4j", "Password123")


class GraphInEngine:
    def __init__(self, uri: str, auth: tuple):
        self._driver: Driver = GraphDatabase.driver(uri, auth=auth)

    def close(self):
        self._driver.close()

    def verifier_connexion(self):
        """Valide la communication avec le cluster Neo4j."""
        self._driver.verify_connectivity()
        print("✅ Connexion réussie à l'instance Neo4j (Protocole Bolt) !")

    def recommander_amis(self, nom_membre: str) -> List[Dict[str, Any]]:
        """Calcule les suggestions PYMK basées sur les amis communs."""
        query = """
        MATCH (cible:Member {nom: $nom})-[:CONNECTED_TO]-(ami_commun:Member)-[:CONNECTED_TO]-(suggere:Member)
        WHERE suggere <> cible AND NOT (cible)-[:CONNECTED_TO]-(suggere)
        RETURN 
            suggere.nom AS nom,
            suggere.titre AS titre,
            count(DISTINCT ami_commun) AS nb_communs,
            collect(DISTINCT ami_commun.nom) AS amis_communs
        ORDER BY nb_communs DESC
        """
        with self._driver.session() as session:
            resultat = session.run(query, nom=nom_membre)
            return [record.data() for record in resultat]

    def matcher_offres_emploi(self, titre_offre: str) -> List[Dict[str, Any]]:
        """Évalue les profils correspondant aux critères de l'offre."""
        query = """
        MATCH (job:JobOffer {titre: $titre})-[:REQUIRES_SKILL]->(s:Skill)
        WITH job, count(s) AS total_requis
        MATCH (m:Member)-[:HAS_SKILL]->(detenue:Skill)<-[:REQUIRES_SKILL]-(job)
        WITH m, job, total_requis, count(detenue) AS match_nb, collect(detenue.nom) AS skills
        RETURN 
            m.nom AS candidat,
            m.titre AS poste,
            round((toFloat(match_nb) / total_requis) * 100, 1) AS score,
            skills
        ORDER BY score DESC
        """
        with self._driver.session() as session:
            resultat = session.run(query, titre=titre_offre)
            return [record.data() for record in resultat]


def main():
    engine = GraphInEngine(URI, AUTH)

    try:
        engine.verifier_connexion()

        print("=" * 68)
        print("          🔗 GRAPHIN - MOTEUR DE RECOMMANDATION INTELLIGENT")
        print("=" * 68)

        # 1. Recommandations de contacts pour Alice
        membre_cible = "Alice Martin"
        print(f"\n👥 RECOMMANDATIONS DE CONTACTS POUR : {membre_cible.upper()}")
        print("-" * 68)

        recommandations = engine.recommander_amis(membre_cible)
        for r in recommandations:
            amis_str = ", ".join(r["amis_communs"])
            print(
                f"👉 {r['nom']:<16} ({r['titre']})\n"
                f"   Score affinité : {r['nb_communs']} amis communs [Par : {amis_str}]\n"
            )

        # 2. Matching pour l'offre d'emploi
        offre = "Senior Graph Data Engineer"
        print(f"💼 MATCHING CANDIDATS POUR L'OFFRE : '{offre}'")
        print("-" * 68)

        candidats = engine.matcher_offres_emploi(offre)
        print(f"{'Candidat':<18} | {'Poste Actuel':<22} | {'Adéquation':>10} | Compétences")
        print("-" * 68)
        for c in candidats:
            comp_str = ", ".join(c["skills"])
            print(
                f"{c['candidat']:<18} | {c['poste']:<22} | {c['score']:>9.1f}% | {comp_str}"
            )

        print("=" * 68 + "\n")

    except Exception as err:
        print(f"❌ Erreur applicative : {err}")
    finally:
        engine.close()


if __name__ == "__main__":
    main()

Exécution du script Python#

python graphin_recommender.py

Résultat en console :

✅ Connexion réussie à l'instance Neo4j (Protocole Bolt) !
====================================================================
          🔗 GRAPHIN - MOTEUR DE RECOMMANDATION INTELLIGENT
====================================================================

👥 RECOMMANDATIONS DE CONTACTS POUR : ALICE MARTIN
--------------------------------------------------------------------
👉 Emma Bernard     (Graph Consultant)
   Score affinité : 2 amis communs [Par : Bob Dubois, Claire Laurent]

👉 David Cohen      (Cloud Architect)
   Score affinité : 1 amis communs [Par : Bob Dubois]

💼 MATCHING CANDIDATS POUR L'OFFRE : 'Senior Graph Data Engineer'
--------------------------------------------------------------------
Candidat           | Poste Actuel           | Adéquation | Compétences
--------------------------------------------------------------------
Emma Bernard       | Graph Consultant       |    100.0% | Neo4j, Python, Docker
Alice Martin       | Lead Data Engineer     |     66.7% | Python, Neo4j
Bob Dubois         | DevOps Specialist      |     66.7% | Python, Docker
Claire Laurent     | Data Scientist         |     33.3% | Python
====================================================================


5. Défis d'extension#

Pour pousser l'expérimentation plus loin :

Défi 1 : Recommandation pondérée par affinité géographique#

  • Modifiez la requête de recommandation d'amis pour attribuer un bonus de score de \(+50\,\%\) si le contact recommandé réside dans la même ville que le membre cible.

Défi 2 : Détection de l'employeur le plus influent#

  • Écrivez une requête Cypher calculant quelle entreprise emploie le plus grand nombre de membres ayant une compétence commune particulière (ex: Python).

Commentaires

Inscrivez-vous à une formation complète

Voir le catalogue