Neo4j Et Graphe Social
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 :
-
nous continuons Ă lâenrichir au risque de la rendre complĂštement spĂ©cifique et potentiellement illisible
-
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.