Base de données graphes - Neo4J

Pourquoi Neo4J ? 

Éprouvé, utilisé par de nombreux acteurs de confiance tel que IBM, Microsoft, LinkedIn, Neo4J met à disposition de très bon outil et une documentation limpide. Le langage de requête utilisé au sein de Neo4J, Cypher, même s’il est actuellement moins répandu que SPARQL, est simple et puissant. on s’y fait très vite.

 

Mais le point essentiel est que Neo4J a un moteur de stockage et un moteur de traitement de graphe natif. Cela lui confère l’avantage d’être normalement une des bases de données les plus indiquée et optimisée pour traiter les données sous cette forme.

 

Le moteur de traitement de Neo4J est basé sur la philosophie « filtrer au plus tôt ».

Exemples avec Neo4J et Cypher

Rentrons dans le vif du sujet avec Cypher, le langage de requête par défaut pour le traitement des données dans Neo4J.

Créer des noeuds

CREATE 

(p1:Person {firstname:"bob", age:21}),
(
p2:Person {firstname:"jak", age:22}),
(
p3:Person {firstname:"anna", age:24})
RETURN p1, p2, p3

Créer des relations entre des noeuds

MATCH 

(bob:Person {firstname:"bob"}),
(
jak:Person {firstname:"jak"}),
(
anna:Person {firstname:"anna"})


CREATE 

(bob)-[:FRIEND]->(jak),
(
jak)-[:FRIEND]->(bob),
(
bob)-[:LOVES]->(anna),
(
jak)-[:LOVES]->(anna),
(
anna)-[:FRIEND]->(bob),
(
anna)-[:LOVES]->(jak)


RETURN jak, bob, anna

Retrouver les noeuds avec un label précis

MATCH (p:Person) RETURN p

 

Renvoie la même figure qu’au dessus.

Traverser le graphe

Qui aime anna ?

MATCH (p:Person)-[:LOVES]->(anna:Person {firstname:"anna"})
RETURN p

Quelle personne aime bob ?

MATCH (bob:Person {firstname:"bob"})-[:LOVES]->(p:Person)
RETURN p

Quel couple pourrait se former ?

MATCH (p1:Person)-[:LOVES]->(p2:Person)-[:LOVES]->(p1)
RETURN p1, p2

 

 

 

 

 

 

 

 

 

Je suis sûr que vous remarquerez la limpidité du langage et la simplicité pour traduire ce dont vous avez besoin vers une demande formelle.

 

Remarque : j’ai utilisé des relations bidirectionnelles explicites pour l’exemple. Dans un cas réel, définir la relation dans un sens suffit, il suffit ensuite de requêter correctement le graphe pour traverser les relations dans le sens voulu.

Schéma optionnel

Par le biais de la définition de contraintes et d’index, l’utilisateur défini le schéma. Ces opérations sont optionnelles mais bien sûr recommandées afin d’obtenir les meilleures performances mais surtout d’assurer l’intégrité et la cohérence des données.

Index

Les index permettent de retrouver l’information de manière plus rapide. Il s’agit sensiblement du même concept que dans le modèle relationnel ou document. Ainsi, il est possible de créer un index sur une propriété ou plusieurs. Dans le second cas, on parle d’index composite.

 

Exemple d’index simple :

 

CREATE INDEX ON :Person(firstname)

 

Exemple d’index composite :

 

CREATE INDEX ON :Person(firstname, surname)

 

A condition que vous n’ayez pas mis autant d’index qu’il y a de propriétés sur votre noeud, la recherche d’éléments pour un label donné devrait être plus rapide.

Contraintes

Contrainte d'unicité

Elle permet de définir des contraintes pour s’assurer que la valeur d’une propriété donnée soit unique.

 

Par exemple, définissons qu’une personne à un numéro de sécurité social unique :

 

CREATE CONSTRAINT ON (p:Person) ASSERT p.ssn IS UNIQUE

        

Vous remarquerez que la contrainte peut-être ajouté même si aucun nœud n’as pour le moment défini la propriété.

 

Ainsi je fourni, (un faux) numéro de sécurité social à jak :

 

MATCH (jak:Person {firstname:"jak"})
SET jak.ssn = 188067501615780
RETURN jak

        

Et comme bob est amoureux de anna, il tente d’usurper l’identité de jak dont elle est amoureuse :

 

MATCH (bob:Person {firstname:"bob"})
SET bob.ssn = 188067501615780
RETURN bob

 

Neo.ClientError.Schema.ConstraintValidationFailed: Node(1) already exists with label `Person` and property `ssn` = 188067501615780

 

Mais voilà que notre pauvre bob est rattrapé par notre contrainte !

Contrainte d'existence

Elle permet de semi-structurer un nœud ou une relation par une contrainte d’existence qui assure qu’une propriété est présente ou doit être présente.

 

Par exemple, on souhaite qu’un nœud étiqueté « Person », contienne au moins une propriété définissant le prénom :

 

CREATE CONSTRAINT ON (p:Person) ASSERT exists(p.firstname)

 

Ainsi si une personne anonyme souhaite s’intégrer à notre groupe d’ami, voilà ce qui se passera :

 

CREATE (p:Person {age:35})
RETURN p

 

Neo.ClientError.Schema.ConstraintValidationFailed: Node(3) with label `Person` must have the property `firstname`

Clé unique

Elle permet de s’assurer d’une part de l’existence des propriétés composants la clé, d’autre par de l’unicité de leur combinaison. Elle combine donc la contrainte d’unicité et d’existence. La clé peut-être, comme pour les index, simple ou composite.

 

Par exemple :

 

CREATE CONSTRAINT ON (p:Person) ASSERT (p.firstname, p.age) IS NODE KEY

Les API Neo4J

Les API Neo4J permettent d’interagir avec une base de données Graphe (Neo4J) par programmation. Il en existe de nombreuses officielles maintenues par la société elle même.

Exemple d'utilisation de l'API pour NodeJS

Le petit exemple ci-dessous permet de lire les nœuds étiquetés « Person ».

 

const neo4j = require('neo4j-driver').v1;
const [user, password] = ["neo4j", "admin"];
const uri = "bolt://localhost";
const driver = neo4j.driver(uri, neo4j.auth.basic(user, password));
const session = driver.session();

//Création d'une transaction de lecture avec promesse de résultat (asynchrone)
const resultPromise = session.readTransaction(tx => tx.run('MATCH (p:Person) RETURN p'));

//Une fois la réponse obtenue on la traite
resultPromise.then(result => {

 
//Fermeture de la session en cours
 session.close();

 
//Récupération du premier résultat
 const record = result.records[0];
 
//Récupération du premier noeud
 const node = record.get(0);
 
//Affichage de la propriété "firstname" du noeud
 console.log(node.properties.firstname);

 
//Fermeture du driver
 driver.close();
 
});

Conclusion : expérience personnelle

Personnellement j’ai essayé de nombreux modèles de données dans le cadre de la création et de la persistance de modèles de connaissances. Mon but premier était la transformation d’un MCD en MPD et le développement de bibliothèques permettant leur manipulation à travers le paradigme objet. Je me suis donc naturellement concentré sur l’utilisation d’ORM – Object Relational Mapping. L’ORM existe pour répondre à la problématique de l' »impedance mismatch » inhérente à la discordance entre le modèle relationnel et le modèle objet.

 

Après avoir expérimenté de nombreux ORM comme Propel, Doctrine, Eloquent de Laravel, NHibernate, Entity Framework et après en avoir conçu un pour Objective-C adapté à des problématiques particulières, j’ai réalisé que le modèle relationnel est peu adapté pour la persistance du modèle objet.

 

C’est ainsi que je me suis principalement penché sur ce type de base de données après avoir étudié de nombreuses possibilités afin de répondre au souhait global d’effectuer un mapping le plus transparent possible et le moins coûteux d’un modèle de données vers le modèle objet.

 

Les bases orientées documents apportent une meilleure représentation. La proximité entre le document et l’objet permet de réduire les opérations de mapping. Celui-ci disparaît ainsi totalement dans le cas de Javascript lorsque le résultat obtenu est en JSON – Javascript Object Notation – format littéral des objets en Javascript. Cela assure donc à ce modèle une très bonne compatibilité avec la programmation orientée objet et le typage dynamique. Cependant, je pense qu’il manque un concept clair définissant les relations entre les données.

 

C’est ainsi que je conclu, après de nombreuses expérimentation, que les bases de données orientées graphes sont les plus à même de représenter correctement le modèle orientée objet. Elles réduisent considérablement l' « impedance mismatch » car les relations directe entre les nœuds reflètent clairement les relations entre les objets. De plus on utilise à 100% le potentiel de ce type de base de données car elle propose la même complexité que le modèle orientée objet lorsqu’il s’agit de recouper des données, en parcourant les objet de relation en relation on traversera le graphe car les requêtes sont d’ordre transitives.

 

Afin de nuancer mes propos, j’ajouterais qu’il souffre du même manque que le modèle relationnel et orienté document pour la représentation de l’héritage et par conséquent du polymorphisme. Ce sujet bien plus vaste qu’un simple paragraphe mériterait d’être débattu dans un prochain article.

B.L