Blog tech

Introduction à Neo4J

Rédigé par Théo Delaune | 31 mars 2016

Neo4J est l’une des base de données leader sur le marché du stockage des données au format graphe. Elle enregistre les données sous un format objet représenté comme un nœud et les lie ensemble avec des arêtes.

L’attrait principal pour ce type de sauvegarde ne fait que s’accroître ces derniers temps, car elle rend très performante les recherches sur de multiples relations entre objets. Nous pouvons par exemple prendre le cas d’un système de calcul d’itinéraires ou de relations entre individus pour lequel ce modèle de graphe convient parfaitement.

Nous verrons dans cet article un cas d’utilisation sur la gestion d’itinéraires de transport public.

Neo4J

Neo4J est une base de données orientée graphe, elle vit le jour en 2000 et appartient à la société Neo Technology.

Elle garantit que les transactions sont ACID (atomicité, cohérence, isolation et durabilité), chaque transaction est fiable et unique au sein de cette base. L’un des points mis en avant par Neo Technology est la possibilité d’exécuter plusieurs millions de requêtes par seconde sur des relations entre nœuds.

Neo4J intègre son propre langage de requêtage, le Cypher. Il peut être appelé grâce à l’API REST fournie, ou via l’une des différentes implémentations disponibles pour les principaux langages, dont Ruby.

Les données peuvent être chargées grâce à l’ETL propre à ce système, ce qui permet d’importer facilement et en masse une grande quantité de données provenant de fichiers CSV. Les données ainsi insérées peuvent être ensuite consultées sur un navigateur, par l’intermédiaire du panneau d’administration de notre instance Neo4J.

Elle est proposée en deux éditions, l’édition community, sous licence GPLv3, et l’édition commerciale, qui offre des options en plus comme la réplication d’un cluster Neo4J ou la possibilité de faire des sauvegardes à chaud.

Cette base de données utilise un graphe de type propriété comme modèle de stockage des objets, nous reviendrons dans la suite de l’article sur les caractéristiques de ce modèle spécifique de graphe.

Un graphe ?

Un graphe est un ensemble d’objets stockés sous forme de nœuds et liés entre eux par des relations qui sont nommées, une illustration vaut mieux qu’un long discours:

Voici la représentation au format GUI d’un graphe. Sur cet exemple nous avons stocké deux objets dans la base de données: un objet de type Staff qui représente un employé, et un objet de type Company qui représente une entreprise.

Je souhaite également enregistrer le fait que le Staff travaille pour la Company, nous avons besoin de créer une relation WORK_IN pour spécifier ce qui les lie ensemble et dans quel sens. Nous pouvons voir que l’objet Theo travaille pour Synbioz.

Nous entendons par graphe orienté le fait que les relations aient un sens. Dans le cas présent, de l’objet de type Staff vers l’objet Company.

Modèle de graphe par propriétés

Neo4J utilise un graphe de type propriété, c’est à dire qu’un nœud stockant un objet peut avoir plusieurs propriétés au format clé-valeur. Pour mon nœud Theo de type Staff cela nous donne par exemple :

Theo:Staff =>
name: 'Théo'
joined: 2014
job: developer
city: Nantes

Une relation entre deux nœuds, aussi appelée arête peut, dans le cas d’un graphe de type propriété, avoir également des caractéristiques au format clé-valeur:

WORK_IN =>
type: remote

En partant de cet exemple nous pouvons parfaitement imaginer l’ajout d’un autre membre à Synbioz mais avec des propriétés différentes telle qu’une autre ville ou travaillant autrement qu’en remote.

Chaque nœud peut aussi être complété d’un label pour lui spécifier son contexte ou y attacher des méta-données, dans notre cas les labels sont Staff et Company.

Il est par contre impossible de supprimer un nœud lorsqu’une relation le maintient à un autre nœud, la seule solution dans ce cas est de supprimer également la relation qui lie le nœud à supprimer à un autre. Ceci dans le but d’avoir des relations atomiques et non orphelines.

Forces et faiblesses

Forces Faiblesses
Performant sur des requêtes de type relation Inapproprié pour des requêtes de type relationnel sans foreign key
Sans schéma (type NoSQL) Oblige à architecturer correctement les modèles de données
Système de requêtage plus plaisant que le SQL Migration moins aisées
Simplicité de mise en place Stockage d’objets complexes

Installation et lancement de Neo4J

Nous allons ici installer la version Community de Neo4J, je vous invite à vous rendre sur cette page pour télécharger la version correspondante à votre système.

Vous retrouverez également sur cette page les différentes instructions d’installation et de mise en place pour que votre installation soit fonctionnelle.

Une fois installé, vous avez deux choix pour lancer Neo4J : le mode console pour que le processus reste au premier plan, ou en tâche de fond avec start. Sous Linux voici comme lancer le service selon les deux méthodes :

# On suppose que Neo4J est installé dans le répertoire /opt/
# mode console au premier plan
./opt/neo4j-community-2.3.2/bin/neo4j console

# lancement du service en tâche de fond
./opt/neo4j-community-2.3.2/bin/neo4j start

Vous pouvez maintenant avoir accès à l’interface d’administration de votre base de données à l’adresse http://localhost:7474.

Lors de votre première connexion il vous sera demandé de changer votre mot de passe administrateur, une fois cela fait vous devriez normalement arriver sur cette interface:

N’hésitez pas à parcourir cette interface pour y découvrir les fonctionnalités qu’elle propose. Vous trouverez sur le menu à gauche:

  • Les informations sur votre base de données
  • Vos favoris
  • L’aide de Neo4J qui comprend de la documentation ainsi que des exemples de données et d’interactions.

L’exemple Staff → Company

Nous allons partir de l’exemple présenté précédemment qui lie un employé avec une entreprise. Dans un premier temps nous allons définir nos nœuds comme ceci :

Theo:Staff =>
name: 'Théo'
joined: 2014
job: developer
city: Nantes

Jon:Staff =>
name: 'Jonathan'
joined: 2013
job: project manager
city: Lille

Synbioz:Company =>
born: 2007

Nous allons maintenant transposer ces nœuds en langage Cypher utilisé pour écrire nos requêtes au sein de Neo4J.

CREATE (Theo:Staff {title:'Theo', joined:2014, job: 'Developer', city: 'Nantes'})
CREATE (Jon:Staff {title:'Jonathan', joined:2013, job: 'Project Manager', city: 'Lille'})
CREATE (Synbioz:Company {name:'Synbioz', born:2007})

Vous pouvez exécuter ces commandes au sein de l’interface de Neo4J dans la barre du haut, une fois ces instructions écrites il ne vous reste plus qu’à cliquer sur play pour enregistrer en base ces informations.

Une fois ces instructions enregistrées vous pouvez vérifier si elles sont bien en base de données avec une requête Cypher:

MATCH (n:Staff) RETURN n LIMIT 25

Vous pouvez également sélectionner le type de label sur votre gauche, ce qui vous donnera, normalement, la même requête que juste au dessus.

Vous pouvez maintenant ajouter les relations qui lient Synbioz à Jon et Theo:

MATCH (n:Staff)
MATCH (m:Company {name: 'Synbioz'})
CREATE (n)-[:WORK_IN]->(m)

Ces commandes vont rechercher tous les nœuds avec le label Staff et avec le label Company. Comme il n’y a qu’un seul nœud Company avec un attribut name égal à Synbioz, les deux Staff vont être liés au seul nœud Synbioz.

Voici le rendu que nous devrions avoir:

Gestion d’itinéraires de transport public

Nous allons nous intéresser à un exemple un peu plus complexe en essayant de définir, grâce aux requêtes Neo4J, le plus court chemin pour atteindre un terminus depuis un point de départ donné.

Nous allons procéder avant tout à l’initialisation de nos arrêts en prenant pour exemple des arrêts de bus et de tram, par la suite nous utiliserons une requête Cypher pour définir le plus court chemin grâce à un algorithme implémenté dans Neo4J.

A savoir que Neo4J embarque plusieurs algorithmes issus de la théorie des graphes, comme le Shortest Path ou Dijkstra qui permettent de parcourir les nœuds présents dans la base de données.

Nous ne rentrerons ici pas dans le détail de chaque algorithme, mais nous les mettrons juste en place pour définir nos trajets.

Définition de nos nœuds et relations

Nous allons instancier chaque nœud avec un nom d’arrêt et il aura pour label Stop. Pour les relations nous définissons un label TRAVEL, qui contient comme attribut le type de transport utilisé, dans ce cas le bus ou le tram.

CREATE (Jamet:Stop {name:'Jamet'})
CREATE (Egalite:Stop {name:'Egalité'})
CREATE (Chaffault:Stop {name:'Du Chaffault'})
CREATE (Maritime:Stop {name:'Gare Maritime'})
CREATE (Chantier:Stop {name:'Chantiers Navals'})
CREATE (Mediatheque:Stop {name:'Médiathèque'})
CREATE (Commerce:Stop {name:'Commerce'})

CREATE
  (Jamet)-[:TRAVEL {type: 'tram'}]->(Egalite),
  (Egalite)-[:TRAVEL {type: 'tram'}]->(Chaffault),
  (Chaffault)-[:TRAVEL {type: 'tram'}]->(Maritime),
  (Maritime)-[:TRAVEL {type: 'tram'}]->(Chantier),
  (Chantier)-[:TRAVEL {type: 'tram'}]->(Mediatheque),
  (Mediatheque)-[:TRAVEL {type: 'tram'}]->(Commerce),
  (Jamet)<-[:TRAVEL {type: 'tram'}]-(Egalite),
  (Egalite)<-[:TRAVEL {type: 'tram'}]-(Chaffault),
  (Chaffault)<-[:TRAVEL {type: 'tram'}]-(Maritime),
  (Maritime)<-[:TRAVEL {type: 'tram'}]-(Chantier),
  (Chantier)<-[:TRAVEL {type: 'tram'}]-(Mediatheque),
  (Mediatheque)<-[:TRAVEL {type: 'tram'}]-(Commerce),
  (Egalite)-[:TRAVEL {type: 'bus'}]->(Commerce)

Nous vérifions si nos données sont correctes en affichant toutes les relations de type TRAVEL:

MATCH ()-[r:TRAVEL]->() RETURN r LIMIT 25

Nous obtenons alors une représentation de ce graphe:

Le plus court chemin

Maintenant que nos données sont entrées en base de données, nous pouvons utiliser la fonction shortestPath au sein d’une requête Cypher pour déterminer le chemin le plus court qu’importe la direction choisie.

MATCH (Jamet:Stop {name:'Jamet'}), (Commerce:Stop {name:'Commerce'}), p = shortestPath((Jamet)-[*]-(Commerce)) RETURN p

Sens de parcours

Dans le cas où nous voulons spécifier une direction, nous pouvons le spécifier avec -> qui définit le sens de parcours, comme lors de la création d’une relation :

MATCH (Jamet:Stop {name:'Jamet'}), (Commerce:Stop {name:'Commerce'}), p = shortestPath((Commerce)-[*]->(Jamet)) RETURN p

Parcours en tram

Si nous souhaitons trouver le plus court itinéraire entre deux arrêts n’étant pas du type bus, nous pouvons le spécifier grâce à une clause WHERE

MATCH (Jamet:Stop {name:'Jamet'}), (Commerce:Stop {name:'Commerce'}), p = shortestPath((Jamet)-[*]-(Commerce))
WHERE NONE (r IN rels(p) WHERE r.type= "bus")
RETURN p

A savoir que la relation TRAVEL en bus est également affichée, sur le mode d’affichage graphe. Sur le mode rows nous avons uniquement nos arrêts correspondant à un trajet en tram comme souhaité:

"row": [
            [
              { "name": "Jamet" },
              { "type": "tram" },
              { "name": "Egalité" },
              { "type": "tram" },
              { "name": "Du Chaffault" },
              { "type": "tram" },
              { "name": "Gare Maritime" },
              { "type": "tram" },
              { "name": "Chantiers Navals" },
              { "type": "tram" },
              { "name": "Médiathèque" },
              { "type": "tram" },
              { "name": "Commerce" }
            ]
          ],
    ...

Conclusion

Nous avons découvert aujourd’hui une base de données orientée graphe. Bien qu’attrayante, elle n’est pas toujours à privilégier face à une base relationnelle. Tout va dépendre du contexte de votre application et de sa structure de données.

Il existe d’autres base de données orientées graphe, comme OrientDB et ArangoDB, pour ne citer que les plus connues.

Je vous invite à découvrir plus en profondeur ce type de base de données. Dans un prochain article nous verrons comment interagir avec Neo4J par le biais d’une application Ruby on Rails.

L’équipe Synbioz.

Libres d’être ensemble.