Recommandations de collĂšgues

Rappel de l’épisode prĂ©cĂ©dent

L’exercice posĂ© en fin d’article prĂ©cĂ©dent, dans le cadre d’un moteur de recommandations pour un rĂ©seau social professionnel, Ă©tait le suivant : 

“trouve-moi tous les contacts de mes contacts, qui connaissent (sont en contact avec) quelqu’un avec qui j’ai dĂ©jĂ  travaillĂ© (avec qui je ne suis pas dĂ©jĂ  en contact)”

Petit rappel du graphe :

  • les utilisateurs auront un label CONTACT

  • les entreprises auront un label COMPANY

  • les noeuds ont (pour simplifier) une propriĂ©tĂ© name qui contient nom et prĂ©nom

  • le fait d’ĂȘtre en contact est matĂ©rialisĂ© par 
    (:CONTACT)-[:IN_CONTACT_WITH]-(:CONTACT)
    
  • le fait de travailler pour une entreprise s’écrit : 
    (:CONTACT)-[:WORKED_IN]->(:COMPANY)
    

Envisageons le problÚme avec humilité, et tùchons de le résoudre bloc par bloc.

Anciens collĂšgues

Trouvons donc mes anciens collĂšgues :

MATCH (me:CONTACT)-[:WORKED_IN]->(:COMPANY)<-[:WORKED_IN]-(colleagues:CONTACT)
WHERE me.name = {name}
RETURN me, colleagues

Pas mal, mais je voudrais Ă©viter d’inclure ceux avec qui je suis dĂ©jĂ  en contact.

Pour se faire, il suffit de vĂ©rifier l’absence de la relation entre les collĂšgues et moi :

MATCH (me:CONTACT)-[:WORKED_IN]->(:COMPANY)<-[:WORKED_IN]-(colleagues:CONTACT)
WHERE me.name = {name} AND NOT (me-[:IN_CONTACT_WITH]-colleagues)
RETURN me, colleagues

Allons encore plus loin : vous ne souhaitez maintenant en rĂ©sultat que les collĂšgues qui ont travaillĂ© dans l’entreprise en mĂȘme temps que vous. En d’autres termes, les personnes qui ont terminĂ© (voire pas terminĂ©) leur contrat aprĂšs vos dĂ©buts et qui ont commencĂ© avant que vous partiez (pour peu que vous soyez parti).

Ici, les spécifications ne sont pas complÚtes, vous retournez donc vers
les autorités compétentes et en concluez que cette notion de date est
portée par la relation WORKED_IN, avec deux attributs de type timestamp
beginning et end (end est optionnel si le collĂšgue y travaille toujours). Pratique d’avoir des propriĂ©tĂ©s sur les relations, non ?

Maintenant, les deux relations WORKED_IN nous intĂ©ressent puisqu’elles reprĂ©sentent respectivement vos dates de prĂ©sence et les dates de prĂ©sence de vos collĂšgues dans les entreprises communes. Profitons-en, d’ailleurs, pour sĂ©parer le MATCH en deux sous-patterns, afin d’amĂ©liorer la lisibilitĂ© de la requĂȘte.

MATCH (me:CONTACT)-[myStay:WORKED_IN]->(company:COMPANY),
company<-[theirStay:WORKED_IN]-(colleagues:CONTACT)
WHERE me.name = {name} AND NOT (me-[:IN_CONTACT_WITH]-colleagues)
RETURN me, colleagues

Exprimons maintenant les contraintes de chevauchement :

MATCH (me:CONTACT)-[myStay:WORKED_IN]->(company:COMPANY),
company<-[theirStay:WORKED_IN]-(colleagues:CONTACT)
WHERE me.name = {name} AND NOT (me-[:IN_CONTACT_WITH]-colleagues)
AND myStay.beginning < theirStay.end
AND theirStay.beginning < myStay.end
RETURN me, colleagues


 sans oublier le fait que les personnes peuvent encore ĂȘtre prĂ©sentes dans l’entreprise (auquel cas la propriĂ©tĂ© end ne sera tout simplement pas renseignĂ©e).

MATCH (me:CONTACT)-[myStay:WORKED_IN]->(company:COMPANY),
company<-[theirStay:WORKED_IN]-(colleagues:CONTACT)
WHERE me.name = {name} AND NOT (me-[:IN_CONTACT_WITH]-colleagues)
AND (NOT HAS(theirStay.end) OR myStay.beginning < theirStay.end)
AND (NOT HAS(myStay.end) OR theirStay.beginning < myStay.end)
RETURN me, colleagues

Filtrage des collĂšgues par contact

Maintenant que avons sous la main une requĂȘte de taille dĂ©jĂ  relativement importante, deux stratĂ©gies se prĂ©sentent pour continuer :

  1. nous continuons Ă  l’enrichir au risque de la rendre complĂštement spĂ©cifique et potentiellement illisible

  2. nous la chaĂźnons avec une autre-requĂȘte

Vous l’aurez compris, nous allons privilĂ©gier la seconde piste. De plus, cela va nous permettre d’introduire la clause WITH, qui agit Ă  l’instar d’un “pipe” qui sert de glue entre diffĂ©rentes commandes sous Unix.

Deux Ă©lĂ©ments nous intĂ©ressent dans la requĂȘte prĂ©cĂ©dente, ceux qui sont spĂ©cifiĂ©s dans la clause RETURN. Afin de pouvoir les rĂ©utiliser dans la requĂȘte suivante, nous allons simplement remplacer RETURN par WITH et filtrer par collĂšgues :

MATCH (me:CONTACT)-[myStay:WORKED_IN]->(company:COMPANY),
company<-[theirStay:WORKED_IN]-(colleagues:CONTACT)
WHERE me.name = {name} AND NOT (me-[:IN_CONTACT_WITH]-colleagues)
AND (NOT HAS(theirStay.end) OR myStay.beginning < theirStay.end)
AND (NOT HAS(myStay.end) OR theirStay.beginning < myStay.end)
WITH me, colleagues
WHERE (me-[:IN_CONTACT_WITH]-(:CONTACT)-[:IN_CONTACT_WITH]-colleagues)
RETURN me, colleagues

Sympa, non ?

Allez, une derniĂšre pour la route : pour que de retourner n associations 1-1 comme c’est le cas actuellement, les autoritĂ©s compĂ©tentes m’ont demandĂ© de directement retourner une association 1-n avec les collĂšgues triĂ©s par nom. Autrement dit : retourner la collection agrĂ©gĂ©e de collĂšgues.

Petite subtilitĂ© : rien ne vous garantit l’ordre des collĂšgues qui vous a Ă©tĂ© retournĂ©. Nous pouvons utiliser la clause ORDER BY juste aprĂšs WITH, afin de s’assurer que les collĂšgues sont triĂ©s (l’opĂ©ration de filtrage qui suit n’y changera rien).

MATCH (me:CONTACT)-[myStay:WORKED_IN]->(company:COMPANY),
company<-[theirStay:WORKED_IN]-(colleagues:CONTACT)
WHERE me.name = {name} AND NOT (me-[:IN_CONTACT_WITH]-colleagues)
AND (NOT HAS(theirStay.end) OR myStay.beginning < theirStay.end)
AND (NOT HAS(myStay.end) OR theirStay.beginning < myStay.end)
WITH me, colleagues
ORDER BY colleagues.name
WHERE (me-[:IN_CONTACT_WITH]-(:CONTACT)-[:IN_CONTACT_WITH]-colleagues)
RETURN me, colleagues

Reste maintenant à agréger :

MATCH (me:CONTACT)-[myStay:WORKED_IN]->(company:COMPANY),
company<-[theirStay:WORKED_IN]-(colleagues:CONTACT)
WHERE me.name = {name} AND NOT (me-[:IN_CONTACT_WITH]-colleagues)
AND (NOT HAS(theirStay.end) OR myStay.beginning < theirStay.end)
AND (NOT HAS(myStay.end) OR theirStay.beginning < myStay.end)
WITH me, colleagues
ORDER BY colleagues.name
WHERE (me-[:IN_CONTACT_WITH]-(:CONTACT)-[:IN_CONTACT_WITH]-colleagues)

Et le tour est joué !

Les avantages d’avoir dĂ©coupĂ© la requĂȘte comme suit sont multiples :

  • la requĂȘte, dĂ©coupĂ©e en blocs distincts, est nettement plus lisible

  • elle est Ă©galement plus maintenable puisque chaque bloc se voit confier une partie bien identifiĂ©e du problĂšme Ă  rĂ©soudre

  • et surtout, puisque l’on retourne toujours le contact concernĂ© et ses suggestions de contact, je peux me permettre d’opĂ©rer Ă  la “fire and forget” puisque le rĂ©sultat de la requĂȘte contient tout le contexte nĂ©cessaire Ă  son interprĂ©tation

Le mot de la fin

Et dire que Cypher a dĂ©marrĂ© comme une idĂ©e de time-off. Il y a 1.5 ans (version Neo4J 1.4 ou 1.5), seules des requĂȘtes assez limitĂ©es et en lecture Ă©taient possibles. Ce langage ne cesse de m’enthousiasmer : il reste vraiment accessible aux nĂ©ophytes et son Ă©ventail d’opĂ©rations possibles va bientĂŽt faire de lui un langage Turing-Complete :-)

Qu’on se le dise, Cypher va devenir la voie privilĂ©giĂ©e pour requĂȘter de la donnĂ©e sur Neo4j. Cela est somme toute logique, on attend d’une base de donnĂ©es qu’elle offre un langage de requĂȘtage.

Reste peut-ĂȘtre un dernier chaĂźnon pour complĂ©ter le tableau presque parfait : un protocole d’échange avec moins d’overhead que l’API REST standard pour communiquer avec une instance Neo4j distante, comme le dĂ©plorait SĂ©bastien Deleuze lors d’un Ă©change Ă  Soft-Shake.

Le prochain article aura pour thĂšme : Neo4J sous le capot.

Post-Scriptum : un jeu de donnĂ©es pour vĂ©rifier la requĂȘte

Essayez donc la requĂȘte finale sur http://console.neo4j.org et les requĂȘtes intermĂ©diaires en remplaçant {name} par ‘Florent’ sur le jeu de donnĂ©es fourni ci-dessous.