Exploiter les données d’HopWork avec Neo4j (timeoff 2nd journée)

neo4jSecond jour de timeoff Lateral-Thoughts, je tente toujours de me tenir à un billet par jour. Et aujourd’hui ce post concernera Neo4J.

Neo4j fait partie des bases de données orientée graphe. Chez LT nous avons la chance d’avoir Florent Biville qui maîtrise bien le sujet et j’ai un peu profité de lui, en tout bien tout honneur, pour me faire un peu guider dessus.

Une base de données graphe par définition c’est une base qui traite de relation. Eh oui, toutes les bases nosql n’ont pas abandonné la composante relationnelle. Et c’est bien ce qui m’intéresse ici car je veux jouer avec les profils inscrits sur HopWork en les reliant via leurs compétences.

Objectif pour moi, répondre à des questions simples :

  • les mots clés les plus utilisés. Cette requête sert pour la mise en jambe car elle ne profite pas des relations
  • les mots clés les plus souvent associés
  • chercher un profil “proche” vis à vis d’un autre en prenant en compte uniquement les mots clés

 

Le chargement

Première étape, on va charger les données à partir de MongoDB. Pour cela on va utiliser un simple main Java :

 

       GraphDatabaseService graphDb = new GraphDatabaseFactory().newEmbeddedDatabase( "C:/temp/graphdb" );
       ExecutionEngine executionEngine = new ExecutionEngine(graphDb);

       MongoURI mongoURI = new MongoURI("mongodb://localhost/workable");
       DB db = mongoURI.connectDB();
       Jongo jongo = new Jongo(db);
       MongoCollection profiles = jongo.getCollection("profiles");

       profiles.find().as(Profile.class).forEach(profile -> {
            Map<String, Object> params = new HashMap<>();
            params.put("id", profile.getKey().toString());
           executionEngine.execute("MERGE (p:PROFILE {key: {id}}) ON CREATE set p.created  = 'true' ", params);

           profile.getInfos().getKeyWords().forEach(keyword -> {
               params.put("keyword", keyword);
               executionEngine.execute("MERGE (k:KEYWORD {label: {keyword} }) ON CREATE set k.created  = 'true' ", params);
               ExecutionResult relationResult = executionEngine.execute("MATCH (p:PROFILE {key: {id} }), (k:KEYWORD {label: {keyword} }) " +
                       "CREATE UNIQUE p-[:SPECIALISES_IN]-> k", params);
           });
       });
       graphDb.shutdown();

Ici on utilise un executionEngine pour exécuter des requêtes CYPHER, le langage de requêtage pour Neo4J.

On utilise Jongo pour interroger la base MongoDB.

On remarquera l’utilisation de Java 8 pour itérer sur les collections, faut bien fêter la sortie de Java8 🙂

 

Les requêtes CYPHER utilisées pour insérer les noeuds profiles et les noeuds keywords ont pour but d’éviter d’insérer des doublons :

MERGE (k:KEYWORD {label: {keyword} }) ON CREATE set k.created  = 'true'

 

La requête pour insérer les relations entre keywords et profiles utilise CREATE UNIQUE pour éviter d’ajouter une relation déjà existante :

 

MATCH (p:PROFILE {key: {id} }), (k:KEYWORD {label: {keyword} })
CREATE UNIQUE p-[:SPECIALISES_IN]->k

Ensuite il suffit d’utiliser la console Neo4j pour afficher le résultat :

graph

Quelques requêtes

Désormais nous avons nos données, essayons quelques requêtes.

Pour exécuter ces requêtes, la console Neo4j va rapidement montrer ces limites. J’ai souhaité utiliser une autre console un peu plus sympathique, rabbithole. Malheureusement je n’ai pas réussi à la démarrer pour des erreurs cryptiques en tant que débutant. Je suis donc reparti sur une exécution en Java.

Quels sont les mots clés les plus utilisés :

MATCH (:PROFILE)-[s:SPECIALISES_IN]->(k:KEYWORD)
RETURN k, count(s) AS relCount
ORDER BY relCount DESC
LIMIT 10;

 

Le résultat sur les données de production HopWork:

+--------------------------------------------------------+
| k                                           | relCount |
+--------------------------------------------------------+
| Node[67]{created:"true",label:"web"}        | 183      |
| Node[403]{created:"true",label:"php"}       | 148      |
| Node[58]{created:"true",label:"webdesign"}  | 145      |
| Node[39]{created:"true",label:"javascript"} | 127      |
| Node[451]{created:"true",label:"wordpress"} | 122      |
| Node[50]{created:"true",label:"logo"}       | 107      |
| Node[120]{created:"true",label:"css"}       | 103      |
| Node[18]{created:"true",label:"graphiste"}  | 101      |
| Node[125]{created:"true",label:"PHP"}       | 100      |
| Node[1]{created:"true",label:"Java"}        | 100      |
+--------------------------------------------------------+

 

On remarque évidemment tout de suite que PHP et php sont comptées différemment. Bon, je m’en doutais lors de l’insertion, il y a une grosse phase de normalisation à faire sur les mots clés. Déjà il aurait fallu s’occuper des différences de casses, mais aussi des synonymes (J2EE JEE) ou des regroupements (php relié à php5, php6 etc…).

 

Tant pis pour l’instant.

 

Quels sont les couples de mots clés les plus souvent associés :

MATCH (k2:KEYWORD)<-[:SPECIALISES_IN]-(:PROFILE)-[:SPECIALISES_IN]->(k1:KEYWORD)
WITH CASE k1.label > k2.label
WHEN true THEN k1.label + '_' + k2.label
ELSE k2.label + '_' + k1.label
END AS result
RETURN DISTINCT(result), COUNT(result)/2 AS count
ORDER BY count DESC
LIMIT 10;

Bon là clairement c’est une requête très peu optimisé et le temps d’exécution sur les données d’HopWork est… infini. Je vais faire appel à un ami (Florent) pour l’améliorer 🙂

 

Et la dernière requête, partant de mon profil, qui est le plus proche de moi sur HopWork en comptant le nombre de mots clés en commun (faite par Florent) :

MATCH (currentProfile:PROFILE { id:123 })-[:SPECIALISES_IN]->(k:KEYWORD)<-[:SPECIALISES_IN]-(otherProfile:PROFILE)
RETURN otherProfile, collect(k), COUNT(k) AS keywordCount
ORDER BY keywordCount DESC LIMIT 6;

+———————————————————————————————————————————–+

| otherProfile                                              | collect(k.label)                                       | keywordCount |

+———————————————————————————————————————————–+

| Node[4205]{key: »Raphael« ,created: »true »} | [« javascript », »mongodb », »elasticsearch », »performance »] | 4            |

| Node[3302]{key: »Sylvain« ,created: »true »} | [« javascript », »elasticsearch », »nosql », »mongodb »]       | 4            |

| Node[6351]{key: »Katia« ,created: »true »} | [« nosql », »javascript », »mongodb »]                       | 3            |

| Node[3265]{key: »Zahir« ,created: »true »} | [« mongodb », »backbone », »javascript »]                    | 3            |

| Node[3452]{key: »Maxence« ,created: »true »} | [« elasticsearch », »mongodb », »javascript »]               | 3            |

| Node[4869]{key: »Arnault« ,created: »true »} | [« mongodb », »elasticsearch », »performance »]              | 3            |

+———————————————————————————————————————————–+

(J’ai remplacé les id par les liens vers les profils HopWork pour les besoins du billet)

Avis au bout d’une journée

Dans l’ensemble, on voit un peu comment pourrait se dessiner un moteur de recommandation basé sur un graphe des données dans HopWork. L’outil est intéressant, un peu raide à prendre en main car les infos restent assez parcellaires sur le net. Je prends juste l’exemple des paramètres nommés dans une requête CYPHER, rien dans la doc n’explique le format attendu.

Je suis bien conscient que je suis resté très en surface mais l’outil et ses applications font que j’y retournerais sans doute très prochainement.

Laisser un commentaire