SW-3-GraphOperations

Navigation : << 2-RDFBasics | Index | 4-SPARQL >>

Opérations sur les graphes RDF

Ce notebook manipule des graphes RDF au moyen de la bibliothèque dotNetRDF (open-source, dotnetrdf.org), qui implémente le modèle de données abstrait de RDF 1.1 (Cyganiak, Wood, Lanthaler (eds.), RDF 1.1 Concepts and Abstract Syntax, W3C Recommendation, 25 février 2014).

Objectifs pedagogiques

  1. Lire un graphe RDF depuis différents formats sérialisés (Turtle, NTriples, RDF/XML, JSON-LD, etc.) avec les parsers de VDS.RDF.Parsing.
  2. Ecrire un graphe dans un format sérialisé avec les writers de VDS.RDF.Writing, en comparant verbose vs compresse.
  3. Fusionner deux graphes RDF en respectant la sémantique d’ensemble (pas de doublons, renommage des noeuds blancs).
  4. Sélectionner des triplets par sujet / prédicat / objet, en composant avec LINQ.
  5. Manipuler des listes RDF (AssertList, RetractList, AddToList, RemoveFromList).
  6. Exercices : charger/explorer, fusionner/sérialiser, créer des listes personnalisées.

Plan du notebook

# Section Thème Sortie attendue
1 Lecture Parsers, StringParser, Handlers 3 méthodes de lecture + CountHandler
2 Écriture Writers, configuration NTriples / CompressingTurtle / RDF/XML
3 Fusion Merge, deduplication Graphes combines sans doublons
4 Sélection GetTriplesWithXxx + LINQ Sélection par sujet/prédicat/objet
5 Listes RDF AssertList/RetractList/Add/Remove Liste [1,2,3] + modifications
6 Exercices 3 exemples guides + 3 exercices Chargement, fusion, listes

Coût total : < 10 secondes (1 installation NuGet + 20 chargements + 5 écritures + 3 fusions + 6 sélections + 4 manipulations de listes).

Concepts clés : RDF 1.1 (triplets sujet-prédicat-objet), formats sérialisés (Turtle, NTriples, RDF/XML, JSON-LD), graphes orientes, sémantique d’ensemble, listes chaînées avec rdf:first/rdf:rest, LINQ comme langage de requête.

Références : Cyganiak, Wood, Lanthaler (eds.) 2014 (RDF 1.1 Concepts), Beckett et al. 2014 (RDF 1.1 Turtle), dotNetRDF library documentation (dotnetrdf.org).

Prérequis

  • Kernel .net-csharp (.NET Interactive 9.0, cf. ML/ML.Net/ML-1-Introduction-Python.ipynb).
  • Bibliothèque NuGet : dotNetRDF, 3.2.1 (bibliothèque .NET de reference pour RDF/Semantic Web). Le kernel télécharge cette dépendance à la volée via #r "nuget: dotNetRDF, 3.2.1".
  • Connaissances de RDF 1.1 et des formats sérialisés (cf. SW-1-RDFBasics et SW-2-RDFBasics).
#r "nuget: dotNetRDF, 3.2.1"
Installed Packages
  • dotNetRDF, 3.2.1

Lecture de l’installation dotNetRDF (cellule code[0]) :

#r "nuget: dotNetRDF, 3.2.1"

La commande magic #r "nuget: dotNetRDF, 3.2.1" télécharge le package NuGet dotNetRDF version 3.2.1 depuis nuget.org, le compile, et l’attache au contexte de session du kernel .net-csharp. Tous les using VDS.RDF.*; suivants peuvent referencer ses types.

Pourquoi dotNetRDF plutot que alternatives :

Bibliothèque Langage Avantage Inconvénient
dotNetRDF C#/.NET Mature, complète, LINQ-friendly Dernière release 3.2.1 (2023)
Apache Jena Java Ecosystem large, Fuseki + ARQ Pas .NET
RDFLib Python Standard Python, large Pas .NET
Redland C Bas niveau, très performant API complexe

Pour une application .NET, dotNetRDF est le choix naturel. C’est l’équivalent de RDFLib en .NET ou de Jena en Java.

Le pattern #r "nuget: ..." :

C’est une commande magic du kernel .net-csharp (cf. Microsoft.DotNet.Interactive). Le download prend ~10-30 secondes la première fois (mise en cache dans ~/.nuget/packages/). Les executions ulterieures reutilisent le cache.

Sortie attendue : chargement du package (pas de sortie directe visible, mais la cellule est marquée exec_count=1 après compilation).

Coût : ~15 secondes la première fois (download + compilation), < 1 ms en cache.

Importation des espaces de noms dotNetRDF et configuration de l’environnement de travail.

using VDS.RDF;
using VDS.RDF.Parsing;
using VDS.RDF.Writing;
using VDS.RDF.Parsing.Handlers;
using System;
using System.IO;
using System.Linq;
using System.Collections.Generic;

Console.WriteLine("dotNetRDF 3.2.1 pret.");
dotNetRDF 3.2.1 pret.

1. Lecture de fichiers RDF

Les parsers sont dans VDS.RDF.Parsing. Chacun implémente IRdfReader et supporte Load(IGraph, ...) avec un chemin de fichier, un StreamReader ou un TextReader. Formats supportés : NTriples, Turtle, Notation 3, RDF/XML, JSON-LD, RDF/JSON, RDFa, TriG, NQuads.

Chaque format correspond à une Recommandation W3C distincte de la famille RDF 1.1 : Turtle (Terse RDF Triple Language), NTriples, RDF/XML, JSON-LD (JavaScript Object Notation for Linking Data), etc.

Pourquoi cette variété de formats :

Le modèle de données abstrait de RDF 1.1 (triplets sujet-prédicat-objet) est unique, mais chaque format de sérialisation a ses propres conventions de syntaxe. Turtle est le plus compact et lisible par les humains ; NTriples est le plus simple à parser (1 triplet par ligne) ; RDF/XML est le plus ancien et le plus répandu historiquement ; JSON-LD est le plus naturel pour les applications web.

Trois méthodes de lecture complémentaires :

  1. TurtleParser + fichier : format explicite, lecture en streaming. Idéal pour les fichiers connus et les pipelines ETL.
  2. StringParser.Parse() : détection automatique du format depuis une chaîne. Idéal pour les formats inconnus ou les entrées utilisateurs.
  3. CountHandler + Parser : comptage sans chargement en mémoire. Idéal pour les fichiers très volumineux où la mémoire est contrainte.

Sortie attendue (cellule code[2]) : un graphe g chargé depuis Turtle, avec ses triplets accessibles via g.Triples.

Coût : ~1 seconde (téléchargement NuGet dotNetRDF 3.2.1 la première fois + parsing d’un petit fichier Turtle).

// 1.1 Chargement depuis un fichier Turtle
IGraph g = new Graph();
IGraph h = new Graph();
TurtleParser ttlparser = new TurtleParser();

ttlparser.Load(g, "data/Example.ttl");              // par chemin
ttlparser.Load(h, new StreamReader("data/Example.ttl")); // par stream

Console.WriteLine($"Fichier  : {g.Triples.Count} triplets");
Console.WriteLine($"Stream   : {h.Triples.Count} triplets");
Console.WriteLine($"Identique: {g.Triples.Count == h.Triples.Count}");
Fichier  : 4 triplets
Stream   : 4 triplets
Identique: True

Lecture du chargement TurtleParser (cellule code[2]) :

// 1.1 Chargement depuis un fichier Turtle
var g = new Graph();
var parser = new TurtleParser();
parser.Load(g, "data/example.ttl");
Console.WriteLine($"Nombre de triplets : {g.Triples.Count}");

Trois étapes du chargement :

  1. new Graph() : cree un graphe RDF vide en mémoire. Le graphe est un container pour les triplets, avec une table de hachage interne pour la recherche rapide.
  2. new TurtleParser() : cree un parser pour le format Turtle (.ttl). Le parser implémente IRdfReader avec une méthode Load(IGraph, ...) qui prend un chemin de fichier, un StreamReader, ou un TextReader.
  3. parser.Load(g, "data/example.ttl") : charge le fichier dans le graphe. Après cette ligne, g.Triples contient tous les triplets parses.

Pourquoi un graphe vide à l’initialisation :

C’est une convention de l’API : le graphe est cree vide, puis les triplets sont ajoutes par le parser. Cela permet de charger plusieurs fichiers dans le même graphe (via Merge()), ou de construire un graphe programme par programme.

Sortie attendue : les lignes commitees ci-dessus – « Fichier : 4 triplets / Stream : 4 triplets / Identique: True » (le même example.ttl lu par les deux voies).

Coût : ~0.1 seconde (parsing d’un fichier de 5-50 triplets).

Utilisation de StringParser pour la détection automatique de format et de NTriplesParser pour un parsing explicite.

// 1.2 StringParser (detection auto) vs Parser specifique
Graph g1 = new Graph();
StringParser.Parse(g1, "<http://example.org/a> <http://example.org/b> <http://example.org/c>.");
Console.WriteLine($"StringParser (auto)  : {g1.Triples.Count} triplet");

Graph g2 = new Graph();
new NTriplesParser().Load(g2, new StringReader("<http://example.org/a> <http://example.org/b> <http://example.org/c>."));
Console.WriteLine($"NTriplesParser       : {g2.Triples.Count} triplet");
StringParser (auto)  : 1 triplet
NTriplesParser       : 1 triplet

Comptage des triplets avec CountHandler sans charger le graphe en mémoire.

// 1.3 Handlers de lecture avancee (pas de chargement memoire)
CountHandler handler = new CountHandler();
new TurtleParser().Load(handler, "data/animals.ttl");
Console.WriteLine($"animals.ttl : {handler.Count} triplets (comptes sans charger en memoire)");
animals.ttl : 51 triplets (comptes sans charger en memoire)

Interpretation : Méthodes de lecture

Méthode Avantage Inconvénient
TurtleParser + fichier Format explicite, fiable Fichier requis
StringParser.Parse() Détection auto du format Peut échouer sur fragments ambigus
NTriplesParser + StringReader Format explicite, en mémoire Necessite de connaitre le format
CountHandler + Parser Comptage sans chargement mémoire Pas d’accès aux triplets

Pourquoi la détection automatique peut échouer :

StringParser.Parse() se base sur des heuristiques (présence de <...> pour XML, {...} pour JSON-LD, @prefix pour Turtle, etc.). Sur des fragments ambigus (par exemple, une chaîne qui pourrait être Turtle ou NTriples), la détection peut se tromper. C’est pourquoi les parsers spécifiques (TurtleParser, NTriplesParser) sont preferes quand le format est connu.

Pourquoi CountHandler est utile :

Pour les graphes de plusieurs millions de triplets, charger le graphe entier en mémoire peut saturer la RAM. CountHandler est un handler qui incrémente un compteur à chaque triplet rencontre, sans stocker le graphe. C’est l’équivalent d’un wc -l en streaming.

Trois exemples d’usage :

  1. ETL en streaming : lire un gros fichier Turtle, compter les triplets par type, sans tout charger en mémoire.
  2. Validation rapide : vérifier qu’un fichier RDF est bien forme (parsing sans erreur) sans traiter les triplets.
  3. Statistiques de graphe : compter les triplets par prédicat, par sujet, etc., pour construire un profil du graphe.

Coût : ~0.1 seconde (parsing d’un petit graphe en mémoire).


2. Écriture de graphes RDF

Les writers sont dans VDS.RDF.Writing et implementent IRdfWriter. Formats : NTriples, Turtle, RDF/XML, CompressingTurtle, RDF/JSON, CSV, TSV.

// 2.1 Comparaison de formats de sortie
IGraph g = new Graph();
new TurtleParser().Load(g, "data/Example.ttl");

Console.WriteLine("=== NTriples ===");
Console.WriteLine(VDS.RDF.Writing.StringWriter.Write(g, new NTriplesWriter()));

Console.WriteLine("=== Turtle Compresse ===");
Console.WriteLine(VDS.RDF.Writing.StringWriter.Write(g, new CompressingTurtleWriter()));
=== NTriples ===
<http://www.w3.org/TR/rdf-syntax-grammar> <http://purl.org/dc/elements/1.1/title> "RDF/XML Syntax Specification (Revised)"^^<http://www.w3.org/2001/XMLSchema#string> .
_:autos3 <http://example.org/stuff/1.0/fullname> "Dave Beckett"^^<http://www.w3.org/2001/XMLSchema#string> .
_:autos3 <http://example.org/stuff/1.0/homePage> <http://purl.org/net/dajobe/> .
<http://www.w3.org/TR/rdf-syntax-grammar> <http://example.org/stuff/1.0/editor> _:autos3 .

=== Turtle Compresse ===
@prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>.
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#>.
@prefix xsd: <http://www.w3.org/2001/XMLSchema#>.
@prefix dc: <http://purl.org/dc/elements/1.1/>.
@prefix ex: <http://example.org/stuff/1.0/>.

<http://www.w3.org/TR/rdf-syntax-grammar> ex:editor [ex:fullname "Dave Beckett" ; 
                                                     ex:homePage <http://purl.org/net/dajobe/>];
                                          dc:title "RDF/XML Syntax Specification (Revised)".

Interpretation : Comparaison des formats de sortie

NTriples : format verbeux – chaque triplet occupe une ligne complète avec les URIs en entier. Pas de préfixe, pas de compression. Idéal pour le debugging, le piping UNIX, les tests automatises.

Turtle compresse : format compact – les préfixes sont définis une fois (@prefix), les noeuds blancs sont regroupés avec ; (même sujet) et les propriétés sont imbriquees avec [ (blanks). Idéal pour les fichiers de données volumineux, les graphes publiés sur le web.

RDF/XML : format ancien – syntaxe XML stricte, namespaces obligatoires. Idéal pour l’interopérabilité avec les systèmes legac Java/Jena (Apache Jena, Sesame).

Tableau comparatif :

Format Compression Lisibilité Interopérabilité
NTriples 1.0x (baseline) Moyenne Universelle
Turtle compresse ~3-5x Élevée Standard W3C
RDF/XML ~2x Faible Legacy Java
JSON-LD ~2.5x Moyenne Web apps

Pourquoi CompressingTurtleWriter est le défaut moderne :

C’est le meilleur compromis compression/lisibilité, et il est standardisé W3C (Recommandation 2014). Il génère automatiquement les préfixes pour les URIs partagees, ce qui réduit significativement la taille du fichier.

Sortie attendue (cellule code[5]) : les deux sérialisations commitees du même graphe de 4 triplets (NTriples : une ligne par triplet ; Turtle compresse) ; le RDF/XML suit dans la cellule suivante.

Coût : ~0.1 seconde (3 sérialisations d’un graphe de 5-10 triplets).

Sérialisation du graphe en RDF/XML via deux méthodes equivalentes : helper statique et StringWriter explicite.

// 2.2 RDF/XML : deux methodes pour obtenir une chaine
var rdfxmlwriter = new RdfXmlWriter();

// Methode 1 : Helper (une ligne)
String data1 = VDS.RDF.Writing.StringWriter.Write(g, rdfxmlwriter);

// Methode 2 : System.IO.StringWriter (controle total)
var sw = new System.IO.StringWriter();
rdfxmlwriter.Save(g, sw);
String data2 = sw.ToString();

Console.WriteLine($"Helper   : {data1.Length} car.");
Console.WriteLine($"StringWriter: {data2.Length} car.");
Console.WriteLine($"Identiques  : {data1 == data2}");
Console.WriteLine($"\n=== RDF/XML (extrait) ===\n{data1.Substring(0, Math.Min(data1.Length, 400))}...");
Helper   : 1017 car.
StringWriter: 1017 car.
Identiques  : True

=== RDF/XML (extrait) ===
<?xml version="1.0" encoding="utf-16"?>
<!DOCTYPE rdf:RDF [
    <!ENTITY rdf 'http://www.w3.org/1999/02/22-rdf-syntax-ns#'>
    <!ENTITY rdfs 'http://www.w3.org/2000/01/rdf-schema#'>
    <!ENTITY xsd 'http://www.w3.org/2001/XMLSchema#'>
    <!ENTITY dc 'http://purl.org/dc/elements/1.1/'>
    <!ENTITY ex 'http://example.org/stuff/1.0/'>
]>
<rdf:RDF xmlns:rdfs="http://www.w3.org/2000/01/rdf-schema#" xmlns:x...

Configuration avancee des writers (compression, pretty-print) pour optimiser la sortie Turtle.

// 2.3 Configuration avancee des writers
public static string SaveConfigured(IGraph g, IRdfWriter writer)
{
    if (writer is IPrettyPrintingWriter ppw) ppw.PrettyPrintMode = true;
    if (writer is IHighSpeedWriter hsw) hsw.HighSpeedModePermitted = false;
    if (writer is ICompressingWriter cw) cw.CompressionLevel = WriterCompressionLevel.High;
    return VDS.RDF.Writing.StringWriter.Write(g, writer);
}

Console.WriteLine("=== Turtle haute compression ===");
Console.WriteLine(SaveConfigured(g, new CompressingTurtleWriter()));
=== Turtle haute compression ===
@prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>.
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#>.
@prefix xsd: <http://www.w3.org/2001/XMLSchema#>.
@prefix dc: <http://purl.org/dc/elements/1.1/>.
@prefix ex: <http://example.org/stuff/1.0/>.

<http://www.w3.org/TR/rdf-syntax-grammar> ex:editor [ex:fullname "Dave Beckett" ; 
                                                     ex:homePage <http://purl.org/net/dajobe/>];
                                          dc:title "RDF/XML Syntax Specification (Revised)".

Interpretation : Writers et configuration

Writer Format Lisibilité Taille
NTriplesWriter NTriples Faible Verbose
CompressingTurtleWriter Turtle Élevée Compact
RdfXmlWriter RDF/XML Moyenne Moyenne
Interface Capacite
IPrettyPrintingWriter Formatage lisible (indentation)
ICompressingWriter Compression des préfixes

Pourquoi configurer les writers :

Les writers ont des options par défaut raisonnables, mais pour des cas d’usage spécifiques (production, publication, debugging), il peut être utile de :

  1. Activer le pretty-printing : formatage lisible avec indentation (mais taille accrue).
  2. Activer la compression : préfixes partagés (mais parsing légèrement plus lent).
  3. Desactiver les namespaces : sortie NTriples pure, sans declaration de préfixes.

Trois exemples d’usage :

  1. Production : CompressingTurtleWriter pour minimiser la taille des fichiers sur disque et la bande passante lors du transfert.
  2. Debugging : NTriplesWriter pour une sortie 1-triplet-par-ligne facile à grepper.
  3. Publication Web : RdfXmlWriter ou JsonLdWriter pour l’interopérabilité avec les systèmes tiers.

Coût : ~0.1 seconde (1 sérialisation par writer teste).


3. Fusion de graphes

La méthode Merge() combine deux graphes en respectant la sémantique RDF : pas de doublons, renommage des noeuds blancs.

// 3.1 Fusion de deux graphes
Graph g1 = new Graph();
Graph g2 = new Graph();
var ttlP = new TurtleParser();

ttlP.Load(g1, "data/Example.ttl");
ttlP.Load(g2, "data/animals.ttl");
int somme = g1.Triples.Count + g2.Triples.Count;

Console.WriteLine($"Graphe 1 (Example.ttl) : {g1.Triples.Count} triplets");
Console.WriteLine($"Graphe 2 (animals.ttl) : {g2.Triples.Count} triplets");

g1.Merge(g2);
Console.WriteLine($"Apres fusion           : {g1.Triples.Count} triplets (somme theorique: {somme})");
Console.WriteLine($"Doublons supprimes     : {somme - g1.Triples.Count}");
Graphe 1 (Example.ttl) : 4 triplets
Graphe 2 (animals.ttl) : 51 triplets
Apres fusion           : 55 triplets (somme theorique: 55)
Doublons supprimes     : 0

Interpretation

  • Les triplets identiques ne sont pas dupliques (sémantique d’ensemble)
  • Les noeuds blancs sont renommés pour éviter les collisions
  • L’opération est asymétrique : g1.Merge(g2) modifie g1, pas g2

Pourquoi la sémantique d’ensemble est importante :

Un graphe RDF est un ensemble de triplets (pas une multiset). Quand on fusionne deux graphes, les triplets identiques ne sont pas dupliques – c’est la propriété d’ensemble. Cela reflete le modèle abstrait : un triplet <s, p, o> est soit present, soit absent.

Pourquoi les noeuds blancs sont renommés :

Les noeuds blancs (blank nodes) sont des identifiants locaux sans IRI. Deux graphes peuvent avoir des noeuds blancs avec le même label interne (_:b1) qui représentent en fait des entités differentes. Pour éviter les collisions, dotNetRDF renomme les noeuds blancs du graphe fusionne avec des nouveaux identifiants uniques.

Pourquoi l’opération est asymétrique :

g1.Merge(g2) ajoute les triplets de g2 à g1. g2 n’est pas modifié. C’est la convention standard pour les opérations de fusion : l’un des deux graphes est “absorbé” par l’autre. Si on veut une copie de g2 non modifiée, on peut utiliser g1.Merge(new Graph(g2)) ou appliquer la méthode d’extension GraphExtensions.Merge(g1, g2).

Trois cas d’usage :

  1. Agrégation de sources : combiner des graphes de plusieurs fournisseurs en un graphe unifié (par exemple, pour un moteur de recherche sémantique).
  2. Incremental loading : charger un graphe de base, puis ajouter des patchs successifs (nouvelles entités, corrections).
  3. Différentiel : calculer le graphe différentiel entre deux versions (par complement).

Coût : ~0.1 seconde (fusion de 2 graphes de 5-10 triplets).


4. Sélection de triplets

IGraph propose des méthodes GetTriplesWithXxx() retournant IEnumerable<Triple>, composables avec LINQ.

// 4.1 Selection par predicat et par sujet
Graph g = new Graph();
new TurtleParser().Load(g, "data/animals.ttl");

IUriNode rdfType = g.CreateUriNode("rdf:type");
Console.WriteLine("=== Triplets rdf:type ===");
foreach (Triple t in g.GetTriplesWithPredicate(rdfType))
    Console.WriteLine($"  {t}");

IUriNode rex = g.GetUriNode(new Uri("http://example.org/animals#rex"));
if (rex != null)
{
    Console.WriteLine("\n=== Proprietes de Rex ===");
    foreach (Triple t in g.GetTriplesWithSubject(rex))
        Console.WriteLine($"  {t}");
}
=== Triplets rdf:type ===
  http://example.org/animals#Animal , http://www.w3.org/1999/02/22-rdf-syntax-ns#type , http://www.w3.org/2000/01/rdf-schema#Class
  http://example.org/animals#Mammal , http://www.w3.org/1999/02/22-rdf-syntax-ns#type , http://www.w3.org/2000/01/rdf-schema#Class
  http://example.org/animals#Bird , http://www.w3.org/1999/02/22-rdf-syntax-ns#type , http://www.w3.org/2000/01/rdf-schema#Class
  http://example.org/animals#Dog , http://www.w3.org/1999/02/22-rdf-syntax-ns#type , http://www.w3.org/2000/01/rdf-schema#Class
  http://example.org/animals#Cat , http://www.w3.org/1999/02/22-rdf-syntax-ns#type , http://www.w3.org/2000/01/rdf-schema#Class
  http://example.org/animals#Parrot , http://www.w3.org/1999/02/22-rdf-syntax-ns#type , http://www.w3.org/2000/01/rdf-schema#Class
  http://example.org/animals#name , http://www.w3.org/1999/02/22-rdf-syntax-ns#type , http://www.w3.org/1999/02/22-rdf-syntax-ns#Property
  http://example.org/animals#age , http://www.w3.org/1999/02/22-rdf-syntax-ns#type , http://www.w3.org/1999/02/22-rdf-syntax-ns#Property
  http://example.org/animals#sound , http://www.w3.org/1999/02/22-rdf-syntax-ns#type , http://www.w3.org/1999/02/22-rdf-syntax-ns#Property
  http://example.org/animals#canFly , http://www.w3.org/1999/02/22-rdf-syntax-ns#type , http://www.w3.org/1999/02/22-rdf-syntax-ns#Property
  http://example.org/animals#rex , http://www.w3.org/1999/02/22-rdf-syntax-ns#type , http://example.org/animals#Dog
  http://example.org/animals#minou , http://www.w3.org/1999/02/22-rdf-syntax-ns#type , http://example.org/animals#Cat
  http://example.org/animals#coco , http://www.w3.org/1999/02/22-rdf-syntax-ns#type , http://example.org/animals#Parrot
  http://example.org/animals#buddy , http://www.w3.org/1999/02/22-rdf-syntax-ns#type , http://example.org/animals#Dog

=== Proprietes de Rex ===
  http://example.org/animals#rex , http://www.w3.org/1999/02/22-rdf-syntax-ns#type , http://example.org/animals#Dog
  http://example.org/animals#rex , http://example.org/animals#name , Rex^^http://www.w3.org/2001/XMLSchema#string
  http://example.org/animals#rex , http://example.org/animals#age , 5^^http://www.w3.org/2001/XMLSchema#integer
  http://example.org/animals#rex , http://example.org/animals#sound , Woof^^http://www.w3.org/2001/XMLSchema#string

Interpretation : Résultats de la sélection

Sélection par prédicat rdf:type : 14 triplets trouves – 6 déclarations de classes (Animal, Mammal, Bird, Dog, Cat, Parrot), 4 déclarations de propriétés (name, age, sound, canFly) et 4 instances (rex/Dog, minou/Cat, coco/Parrot, buddy/Dog).

Sélection par sujet ex:rex : 4 propriétés – type (Dog), name (Rex), age (5), sound (Woof). Rex est un Dog de 5 ans qui aboie.

Sélection combinee : en combinant GetTriplesWithPredicate + LINQ Where(), on peut filtrer par prédicat puis par valeur d’objet. Par exemple, “tous les animaux qui peuvent voler” = triplets.Where(t => t.Predicate.Equals(ns.canFly) && t.Object.Equals(true)).

Trois exemples de sélection avancee :

  1. Toutes les instances d’une classe : GetTriplesWithPredicate(rdfType).Where(t => t.Object.Equals(ns.Dog)) – liste tous les chiens.
  2. Toutes les propriétés d’une instance : GetTriplesWithSubject(ns.rex) – propriétés directes d’une entite.
  3. Toutes les valeurs d’une propriété : GetTriplesWithPredicate(ns.age).Select(t => t.Object) – ages de tous les animaux.

Pourquoi LINQ est adapte :

LINQ (Language Integrated Query) est un langage de requête en mémoire pour .NET. Il permet de composer des filtres, projections, et aggregations de manière déclarative, sans boucles impératives. C’est l’équivalent de Python comprehensions ou SQL pour les collections .NET.

Coût : ~0.1 seconde (sélection de 10-50 triplets).

Combinaison de sélections par prédicat et par objet avec des expressions LINQ pour filtrer les animaux.

// 4.2 Selection combinee et LINQ
IUriNode nameProp = g.CreateUriNode(new Uri("http://example.org/animals#name"));
Console.WriteLine("=== Noms des animaux ===");
foreach (Triple t in g.GetTriplesWithPredicate(nameProp))
    Console.WriteLine($"  {t.Subject} -> {t.Object}");

// LINQ : animaux de plus de 5 ans
IUriNode ageProp = g.CreateUriNode(new Uri("http://example.org/animals#age"));
var older = g.GetTriplesWithPredicate(ageProp)
    .Where(t => t.Object is ILiteralNode lit && int.TryParse(lit.Value, out int age) && age > 5)
    .Select(t => new { Animal = t.Subject, Age = t.Object });

Console.WriteLine("\n=== Animaux > 5 ans (LINQ) ===");
foreach (var a in older)
    Console.WriteLine($"  {a.Animal} : {a.Age}");
=== Noms des animaux ===
  http://example.org/animals#rex -> Rex^^http://www.w3.org/2001/XMLSchema#string
  http://example.org/animals#minou -> Minou^^http://www.w3.org/2001/XMLSchema#string
  http://example.org/animals#coco -> Coco^^http://www.w3.org/2001/XMLSchema#string
  http://example.org/animals#buddy -> Buddy^^http://www.w3.org/2001/XMLSchema#string

=== Animaux > 5 ans (LINQ) ===
  http://example.org/animals#coco : 12^^http://www.w3.org/2001/XMLSchema#integer
  http://example.org/animals#buddy : 7^^http://www.w3.org/2001/XMLSchema#integer

Interpretation : Méthodes de sélection

Méthode Pattern Équivalent SPARQL
GetTriplesWithSubject(s) s ? ? SELECT ?p ?o WHERE { s ?p ?o }
GetTriplesWithPredicate(p) ? p ? SELECT ?s ?o WHERE { ?s p ?o }
GetTriplesWithObject(o) ? ? o SELECT ?s ?p WHERE { ?s ?p o }
GetTriplesWithSubjectPredicate(s,p) s p ? SELECT ?o WHERE { s p ?o }

Pourquoi ces méthodes existent :

Elles correspondent aux patterns de triplets du modèle abstrait RDF 1.1. Pour chaque combinaison de variables fixées (sujet, prédicat, objet), il y a une méthode qui retourne les triplets correspondants.

Avantage par rapport à SPARQL :

  • Pas de parsing : les méthodes sont compilees, pas interpretees.
  • Integration LINQ : on peut chainer les filtres avec Where(), Select(), GroupBy().
  • Typage fort : les méthodes retournent IEnumerable<Triple> directement, pas une table de résultats à parser.

Limitation par rapport à SPARQL :

  • Pas de joins complexes : pour les requêtes multi-patterns (par exemple, “tous les X qui ont une propriété Y et une propriété Z”), il faut faire les joins manuellement en mémoire.
  • Pas de filtres avances : pas de FILTER, BIND, OPTIONAL. Pour ca, il faut utiliser SPARQL (cf. SW-4-SPARQL).

Coût : ~0.1 seconde (sélection + filtrage LINQ).


5. Listes RDF

Les listes RDF sont des structures chaînées encodees avec rdf:first et rdf:rest. dotNetRDF fournit des méthodes haut niveau dans VDS.RDF.Extensions.

// 5.1 Creer et lire une liste RDF
IGraph g = new Graph();
string rdfData = @"
@prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
@prefix ex: <http://example.org/ns#> .
ex:subj ex:pred _:b1 .
_:b1 rdf:first 1 . _:b1 rdf:rest _:b2 .
_:b2 rdf:first 2 . _:b2 rdf:rest _:b3 .
_:b3 rdf:first 3 . _:b3 rdf:rest rdf:nil .";

TurtleParser parser = new TurtleParser();
parser.Load(g, new StringReader(rdfData));

IUriNode predicate = g.CreateUriNode("ex:pred");
INode root = g.GetTriplesWithPredicate(predicate).First().Object;

Console.WriteLine($"GetListItems  : [{string.Join(", ", g.GetListItems(root))}]");
Console.WriteLine($"GetListNodes  : [{string.Join(", ", g.GetListNodes(root))}]");
Console.WriteLine($"GetListAsTriples : {g.GetListAsTriples(root).Count()} triplets");
GetListItems  : [1^^http://www.w3.org/2001/XMLSchema#integer, 2^^http://www.w3.org/2001/XMLSchema#integer, 3^^http://www.w3.org/2001/XMLSchema#integer]
GetListNodes  : [_:b1, _:b2, _:b3]
GetListAsTriples : 6 triplets

Interpretation : Structure interne

root -> [1 | rest] -> [2 | rest] -> [3 | rest] -> nil
Méthode Retourne Exemple
GetListItems(root) Valeurs (rdf:first) 1, 2, 3
GetListNodes(root) Noeuds intermédiaires _:b1, _:b2, _:b3
GetListAsTriples(root) Tous les triplets rdf:first + rdf:rest

Pourquoi les listes RDF sont chaînées :

En RDF 1.1, il n’y a pas de structure de liste native. Les listes sont encodees avec des triplets rdf:first (la valeur) et rdf:rest (le noeud suivant), terminées par un noeud spécial rdf:nil. C’est un choix de design minimaliste : tout en RDF est un triplet, donc les listes sont aussi encodees comme des triplets.

Avantage :

  • Pas de langage d’extension : les listes sont du RDF pur, parseable par n’importe quel outil RDF.
  • Coherent avec le modèle : les listes sont des graphes, donc toutes les opérations classiques (sélection, fusion, etc.) s’appliquent.

Inconvénient :

  • Verbeux : une liste de N éléments prend ~2N triplets (un pour rdf:first, un pour rdf:rest). Pour N=100, c’est 200 triplets.
  • Accès séquentiel : pour accéder au Kième élément, il faut traverser les K-1 premiers. Complexité O(N).

Trois alternatives :

  1. rdf:Seq : collections ordonnées natives (RDF 1.1 + schema.org), plus efficaces que les listes chaînées.
  2. JSON-LD arrays : syntaxe [...] directement convertible en listes RDF.
  3. Listes en mémoire : pour les performances, stocker la liste en mémoire et ne la sérialiser en RDF qu’au besoin.

Coût : ~0.1 seconde (creation + lecture d’une liste de 3 éléments).

// 5.2 AssertList et RetractList
INode newRoot = g.AssertList(new List<INode>() { (true).ToLiteral(g), (false).ToLiteral(g) });
Console.WriteLine($"AssertList    : [{string.Join(", ", g.GetListItems(newRoot))}]");

int avant = g.Triples.Count;
g.RetractList(newRoot);
Console.WriteLine($"RetractList   : {avant} -> {g.Triples.Count} triplets");
AssertList    : [true^^http://www.w3.org/2001/XMLSchema#boolean, false^^http://www.w3.org/2001/XMLSchema#boolean]
RetractList   : 11 -> 7 triplets

Interpretation : Creation et suppression de liste

Résultat obtenu : AssertList crée une liste [true, false] en générant des noeuds blancs et triplets associés. RetractList supprime tous les triplets de la liste.

Opération Triplets avant Triplets après Delta
AssertList 7 (1 ancrage + liste 1,2,3 du §5.1) 11 (7 + 4 nouveaux pour [true, false]) +4
RetractList 11 7 -4

Pourquoi AssertList génère des noeuds blancs :

Chaque élément de la liste est stocké dans un triplet rdf:first, et le chaînage est stocke dans un triplet rdf:rest. Les éléments intermédiaires (noeuds _:b1, _:b2, …) sont des noeuds blancs qui n’ont pas d’IRI – ils sont uniquement références dans le graphe.

Pourquoi RetractList supprime récursivement :

RetractList(root) commence par le noeud racine, recupere le rdf:rest suivant, et se rappelle récursivement sur le noeud suivant. A la fin, tous les triplets de la liste sont retirés du graphe. Le noeud racine lui-même n’est pas détruit (c’est à l’appelant de le gérer s’il est ancré ailleurs).

Trois cas d’usage :

  1. Représenter des collections : liste d’auteurs d’un livre, liste de tags d’un article, liste de dependances d’un paquet.
  2. Ordonner des éléments : les listes RDF preservent l’ordre (contrairement aux sacs RDF).
  3. Manipuler des structures : ajouter, supprimer, rechercher dans une liste en RDF.

Coût : ~0.1 seconde (AssertList + RetractList sur une liste de 3 éléments).

Ajout et suppression d’éléments dans une liste RDF existante avec AddToList et RemoveFromList.

// 5.3 AddToList et RemoveFromList
Console.WriteLine($"Avant         : [{string.Join(", ", g.GetListItems(root))}]");

g.AddToList(root, new List<INode>() { (true).ToLiteral(g), (false).ToLiteral(g) });
Console.WriteLine($"AddToList     : [{string.Join(", ", g.GetListItems(root))}]");

g.RemoveFromList(root, new List<INode>() { (true).ToLiteral(g), (false).ToLiteral(g) });
Console.WriteLine($"RemoveFromList: [{string.Join(", ", g.GetListItems(root))}]");
Avant         : [1^^http://www.w3.org/2001/XMLSchema#integer, 2^^http://www.w3.org/2001/XMLSchema#integer, 3^^http://www.w3.org/2001/XMLSchema#integer]
AddToList     : [1^^http://www.w3.org/2001/XMLSchema#integer, 2^^http://www.w3.org/2001/XMLSchema#integer, 3^^http://www.w3.org/2001/XMLSchema#integer, true^^http://www.w3.org/2001/XMLSchema#boolean, false^^http://www.w3.org/2001/XMLSchema#boolean]
RemoveFromList: [1^^http://www.w3.org/2001/XMLSchema#integer, 2^^http://www.w3.org/2001/XMLSchema#integer, 3^^http://www.w3.org/2001/XMLSchema#integer]

Interpretation : API des listes

Opération Méthode Comportement
Créer AssertList(elements) Génère noeuds blancs + triplets
Supprimer tout RetractList(root) Suppression récursive
Ajouter AddToList(root, elements) Ajoute en fin de liste
Retirer RemoveFromList(root, elements) Supprime toutes les occurrences

Pourquoi AddToList ajoute en fin :

C’est la convention pour les listes chaînées – pour ajouter en tête, il faudrait créer un nouveau noeud racine et transférer tous les éléments, ce qui est coûteux. Ajouter en fin est O(1) si on connaît le dernier noeud, ou O(N) sinon (on doit traverser toute la liste pour trouver le dernier).

Pourquoi RemoveFromList supprime toutes les occurrences :

Si un élément apparaît plusieurs fois dans la liste (par exemple, [1, 2, 1, 3]), la suppression de 1 doit supprimer les deux occurrences. C’est la sémantique de “toutes les occurrences” – alternative à RemoveFirst qui ne supprime que la première.

Trois nuances :

  1. AddToList crée de nouveaux noeuds blancs pour les nouveaux éléments, distincts des noeuds existants.
  2. RemoveFromList peut laisser des trous dans la liste si la suppression crée des noeuds blancs orphelins. Dans ce cas, un appel ultérieur à AssertList peut rééquilibrer.
  3. Les listes ne sont pas typées : on peut avoir une liste mixte [1, "hello", true]. Le typage est à la charge de l’ontologie (schema.org, FOAF, etc.).

Coût : ~0.1 seconde (chaque opération sur une liste de 3-5 éléments).


6. Exercices pratiques

Exemple guide 1 : Charger et explorer

Chargez data/animals.ttl et affichez : le nombre total de triplets, les triplets rdf:type, et les propriétés de ex:rex.

Algorithme :

// Chargement du graphe depuis animals.ttl avec TurtleParser
Graph g = new Graph();
new TurtleParser().Load(g, "data/animals.ttl");

// Affichage du nombre total de triplets dans le graphe chargé
Console.WriteLine($"Total triplets : {g.Triples.Count}");

// Sélection de tous les triplets dont le prédicat est rdf:type
// rdf:type indique la classe d'appartenance d'une ressource (ex : rex est de type Dog)
IUriNode rdfType = g.CreateUriNode("rdf:type");
Console.WriteLine("Triplets rdf:type :");
foreach (Triple t in g.GetTriplesWithPredicate(rdfType))
    Console.WriteLine($"  {t}");

// Récupération du nœud URI de ex:rex par son IRI complet
// GetTriplesWithSubject retourne toutes les propriétés dont rex est le sujet
IUriNode rex = g.GetUriNode(new Uri("http://example.org/animals#rex"));
Console.WriteLine("Proprietes de rex :");
foreach (Triple t in g.GetTriplesWithSubject(rex))
    Console.WriteLine($"  {t}");

Sortie attendue :

Total triplets : 51
Triplets rdf:type : 6 (classes) + 4 (proprietes) + 4 (instances) = 14
Proprietes de rex : 4 (type=Dog, name=Rex, age=5, sound=Woof)

Pourquoi cet exemple est fondamental :

C’est le workflow de base du Semantic Web en .NET : charger un graphe depuis un fichier sérialisé, l’interroger avec les méthodes de sélection, et afficher les résultats. La majorité des applications RDF commencent par ce pattern.

Trois extensions possibles :

  1. SPARQL : remplacer les sélections par une requête SPARQL (cf. SW-4-SPARQL).
  2. Inférence : ajouter un moteur d’inférence pour déduire de nouveaux triplets.
  3. Validation : utiliser SHACL ou ShEx pour valider la conformité du graphe à un schéma.

Coût : ~0.5 seconde (chargement + 3 sélections + affichage).

// Chargement du graphe depuis animals.ttl avec TurtleParser
Graph g = new Graph();
new TurtleParser().Load(g, "data/animals.ttl");

// Affichage du nombre total de triplets dans le graphe chargé
Console.WriteLine($"Total triplets : {g.Triples.Count}");

// Sélection de tous les triplets dont le prédicat est rdf:type
// rdf:type indique la classe d'appartenance d'une ressource (ex : rex est de type Dog)
IUriNode rdfType = g.CreateUriNode("rdf:type");
Console.WriteLine("Triplets rdf:type :");
foreach (Triple t in g.GetTriplesWithPredicate(rdfType))
    Console.WriteLine($"  {t}");

// Récupération du nœud URI de ex:rex par son IRI complet
// GetTriplesWithSubject retourne toutes les propriétés dont rex est le sujet
IUriNode rex = g.GetUriNode(new Uri("http://example.org/animals#rex"));
Console.WriteLine("Proprietes de rex :");
foreach (Triple t in g.GetTriplesWithSubject(rex))
    Console.WriteLine($"  {t}");
Total triplets : 51
Triplets rdf:type :
  http://example.org/animals#Animal , http://www.w3.org/1999/02/22-rdf-syntax-ns#type , http://www.w3.org/2000/01/rdf-schema#Class
  http://example.org/animals#Mammal , http://www.w3.org/1999/02/22-rdf-syntax-ns#type , http://www.w3.org/2000/01/rdf-schema#Class
  http://example.org/animals#Bird , http://www.w3.org/1999/02/22-rdf-syntax-ns#type , http://www.w3.org/2000/01/rdf-schema#Class
  http://example.org/animals#Dog , http://www.w3.org/1999/02/22-rdf-syntax-ns#type , http://www.w3.org/2000/01/rdf-schema#Class
  http://example.org/animals#Cat , http://www.w3.org/1999/02/22-rdf-syntax-ns#type , http://www.w3.org/2000/01/rdf-schema#Class
  http://example.org/animals#Parrot , http://www.w3.org/1999/02/22-rdf-syntax-ns#type , http://www.w3.org/2000/01/rdf-schema#Class
  http://example.org/animals#name , http://www.w3.org/1999/02/22-rdf-syntax-ns#type , http://www.w3.org/1999/02/22-rdf-syntax-ns#Property
  http://example.org/animals#age , http://www.w3.org/1999/02/22-rdf-syntax-ns#type , http://www.w3.org/1999/02/22-rdf-syntax-ns#Property
  http://example.org/animals#sound , http://www.w3.org/1999/02/22-rdf-syntax-ns#type , http://www.w3.org/1999/02/22-rdf-syntax-ns#Property
  http://example.org/animals#canFly , http://www.w3.org/1999/02/22-rdf-syntax-ns#type , http://www.w3.org/1999/02/22-rdf-syntax-ns#Property
  http://example.org/animals#rex , http://www.w3.org/1999/02/22-rdf-syntax-ns#type , http://example.org/animals#Dog
  http://example.org/animals#minou , http://www.w3.org/1999/02/22-rdf-syntax-ns#type , http://example.org/animals#Cat
  http://example.org/animals#coco , http://www.w3.org/1999/02/22-rdf-syntax-ns#type , http://example.org/animals#Parrot
  http://example.org/animals#buddy , http://www.w3.org/1999/02/22-rdf-syntax-ns#type , http://example.org/animals#Dog
Proprietes de rex :
  http://example.org/animals#rex , http://www.w3.org/1999/02/22-rdf-syntax-ns#type , http://example.org/animals#Dog
  http://example.org/animals#rex , http://example.org/animals#name , Rex^^http://www.w3.org/2001/XMLSchema#string
  http://example.org/animals#rex , http://example.org/animals#age , 5^^http://www.w3.org/2001/XMLSchema#integer
  http://example.org/animals#rex , http://example.org/animals#sound , Woof^^http://www.w3.org/2001/XMLSchema#string

Lecture de l’exemple 1 (charger et explorer) (cellule code[14]) :

// Chargement du graphe depuis animals.ttl avec TurtleParser
Graph g = new Graph();
new TurtleParser().Load(g, "data/animals.ttl");

// Affichage du nombre total de triplets dans le graphe chargé
Console.WriteLine($"Total triplets : {g.Triples.Count}");

// Sélection de tous les triplets dont le prédicat est rdf:type
// rdf:type indique la classe d'appartenance d'une ressource (ex : rex est de type Dog)
IUriNode rdfType = g.CreateUriNode("rdf:type");
Console.WriteLine("Triplets rdf:type :");
foreach (Triple t in g.GetTriplesWithPredicate(rdfType))
    Console.WriteLine($"  {t}");

// Récupération du nœud URI de ex:rex par son IRI complet
// GetTriplesWithSubject retourne toutes les propriétés dont rex est le sujet
IUriNode rex = g.GetUriNode(new Uri("http://example.org/animals#rex"));
Console.WriteLine("Proprietes de rex :");
foreach (Triple t in g.GetTriplesWithSubject(rex))
    Console.WriteLine($"  {t}");

Trois opérations de l’exemple :

  1. Chargement : new TurtleParser().Load(g, "data/animals.ttl") charge le fichier animals.ttl dans le graphe g.
  2. Comptage total : g.Triples.Count donne le nombre total de triplets.
  3. Sélection par prédicat : g.GetTriplesWithPredicate(rdfType) filtre les triplets ayant rdf:type comme prédicat.

Pourquoi cet exemple est le workflow de base :

C’est le pattern standard de toute application Semantic Web en .NET : charger un graphe sérialisé, l’interroger avec les méthodes de sélection, et afficher les résultats. La majorité des notebooks SW-* du depot commencent par ce pattern.

Trois extensions possibles :

  1. SPARQL : remplacer les sélections par une requête SPARQL (cf. SW-4-SPARQL).
  2. Inférence : ajouter un moteur d’inférence (RDFS, OWL) pour déduire de nouveaux triplets.
  3. Validation : utiliser SHACL ou ShEx pour valider la conformité du graphe à un schéma.

Coût : ~0.5 seconde (chargement + sélection + affichage).

Exercice 4 : Recherche avancee dans un graphe

Chargez data/animals.ttl et trouvez :

  1. Tous les animaux qui sont des chiens (rdf:type -> ex:Dog) en utilisant GetTriplesWithPredicateObject
  2. Le nombre d’animaux qui peuvent voler (prédicat ex:canFly avec valeur true)
  3. L’animal le plus âgé (maximum de la valeur du prédicat ex:age)

Indices : utilisez GetTriplesWithPredicateObject(predicate, object) pour les sélections combinees, et LINQ pour les aggregations.

Protocole attendu :

// 1. Tous les chiens
var dogs = g.GetTriplesWithPredicateObject(
    g.CreateUriNode("rdf:type"),
    g.CreateUriNode("ex:Dog")
).Select(t => t.Subject);

// 2. Animaux qui volent
var flying = g.GetTriplesWithPredicateObject(
    g.CreateUriNode("ex:canFly"),
    (true).ToLiteral(g)
).Count();

// 3. Animal le plus age
var ages = g.GetTriplesWithPredicate(g.CreateUriNode("ex:age"))
    .Select(t => new { Subject = t.Subject, Age = int.Parse(t.Object.ToString()) })
    .OrderByDescending(x => x.Age)
    .First();

Sortie attendue :

Chiens : rex (Rex), buddy (Buddy)
Animaux qui volent : 1 (coco le Perroquet)
Animal le plus age : coco (12 ans)

Pourquoi cet exercice est intéressant :

Il combine 3 types d’aggregations : - Sélection par prédicat-objet (chiens) - Comptage avec prédicat et valeur (volants) - Tri et max (plus age)

C’est le pattern classique des requêtes RDF : on selectionne, on filtre, on agrège.

Coût : ~5 minutes pour l’étudiant (3 sous-questions + LINQ).

// TODO etudiant : Chargez data/animals.ttl et trouvez :
// 1. Tous les animaux qui sont des chiens (rdf:type -> ex:Dog)
// 2. Le nombre d'animaux qui peuvent voler (predicat ex:canFly avec valeur true)
// 3. L'animal le plus age (maximum de ex:age)
//
// Indice : utilisez GetTriplesWithPredicateObject pour (1)
// et LINQ .Where() + .Max() pour (3)
Console.WriteLine("Exercice a completer");
Exercice a completer

Exemple guide 2 : Fusionner et sérialiser

Chargez data/Example.ttl et data/animals.ttl, fusionnez-les, et ecrivez le résultat en Turtle compresse.

Algorithme :

// 1. Charger les deux graphes
var g1 = new Graph();
new TurtleParser().Load(g1, "data/Example.ttl");

var g2 = new Graph();
new TurtleParser().Load(g2, "data/animals.ttl");

// 2. Fusionner (g1 absorbe g2)
g1.Merge(g2);
Console.WriteLine($"Apres fusion : {g1.Triples.Count} triplets");

// 3. Serialiser en Turtle compresse
string result = VDS.RDF.Writing.StringWriter.Write(g1, new CompressingTurtleWriter());
Console.WriteLine(result);

Sortie attendue :

Avant fusion - g1: 4, g2: 51
Apres fusion : 55 triplets (4 de Example + 51 de animals, zero doublon)
suivi de la serialisation Turtle compressee du resultat

Pourquoi cet exemple est pratique :

L’agrégation de graphes est un cas d’usage classique : on charge des sources hétérogènes (personnes, animaux, produits, etc.), on les fusionne en un graphe unifié, et on sérialise le résultat pour le publier ou le requêter.

Trois variantes de fusion :

  1. Merge direct : g1.Merge(g2) – asymétrique, modifie g1.
  2. Merge symétrique : créer un nouveau graphe, merger les deux sources.
  3. Merge avec dedup explicite : utiliser un HashSet<Triple> pour éviter les doublons.

Coût : ~0.5 seconde (2 chargements + 1 fusion + 1 sérialisation).

// Chargement des deux graphes : personnes (Example.ttl) et animaux (animals.ttl)
Graph g1 = new Graph();
Graph g2 = new Graph();
new TurtleParser().Load(g1, "data/Example.ttl");
new TurtleParser().Load(g2, "data/animals.ttl");

Console.WriteLine($"Avant fusion - g1: {g1.Triples.Count}, g2: {g2.Triples.Count}");
// Merge de g2 dans g1 : les triplets identiques ne sont pas dupliqués (sémantique d'ensemble RDF)
// Les nœuds blancs sont automatiquement renommés pour éviter les collisions
g1.Merge(g2);
Console.WriteLine($"Apres fusion : {g1.Triples.Count} triplets");

string result = VDS.RDF.Writing.StringWriter.Write(g1, new CompressingTurtleWriter());
Console.WriteLine(result);
Avant fusion - g1: 4, g2: 51
Apres fusion : 55 triplets
@prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>.
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#>.
@prefix xsd: <http://www.w3.org/2001/XMLSchema#>.
@prefix dc: <http://purl.org/dc/elements/1.1/>.
@prefix ex: <http://example.org/stuff/1.0/>.
@prefix ns0: <http://example.org/animals#>.

ns0:Animal a rdfs:Class;
           rdfs:comment "Classe racine de tous les animaux"@fr;
           rdfs:label "Animal"@fr.
ns0:Bird a rdfs:Class;
         rdfs:label "Oiseau"@fr;
         rdfs:subClassOf ns0:Animal.
ns0:Cat a rdfs:Class;
        rdfs:label "Chat"@fr;
        rdfs:subClassOf ns0:Mammal.
ns0:Dog a rdfs:Class;
        rdfs:label "Chien"@fr;
        rdfs:subClassOf ns0:Mammal.
ns0:Mammal a rdfs:Class;
           rdfs:label "Mammifere"@fr;
           rdfs:subClassOf ns0:Animal.
ns0:Parrot a rdfs:Class;
           rdfs:label "Perroquet"@fr;
           rdfs:subClassOf ns0:Bird.
ns0:age a rdf:Property;
        rdfs:domain ns0:Animal;
        rdfs:label "age"@fr;
        rdfs:range xsd:integer.
ns0:buddy ns0:age 7 ;
          ns0:name "Buddy";
          ns0:sound "Woof woof";
          a ns0:Dog.
ns0:canFly a rdf:Property;
           rdfs:domain ns0:Animal;
           rdfs:label "peut voler"@fr;
           rdfs:range xsd:boolean.
ns0:coco ns0:age 12 ;
         ns0:canFly true;
         ns0:name "Coco";
         ns0:sound "Coco veut un gateau";
         a ns0:Parrot.
ns0:minou ns0:age 3 ;
          ns0:name "Minou";
          ns0:sound "Miaou";
          a ns0:Cat.
ns0:name a rdf:Property;
         rdfs:domain ns0:Animal;
         rdfs:label "nom"@fr;
         rdfs:range xsd:string.
ns0:rex ns0:age 5 ;
        ns0:name "Rex";
        ns0:sound "Woof";
        a ns0:Dog.
ns0:sound a rdf:Property;
          rdfs:domain ns0:Animal;
          rdfs:label "cri"@fr;
          rdfs:range xsd:string.
<http://www.w3.org/TR/rdf-syntax-grammar> ex:editor [ex:fullname "Dave Beckett" ; 
                                                     ex:homePage <http://purl.org/net/dajobe/>];
                                          dc:title "RDF/XML Syntax Specification (Revised)".

Exercice 5 : Fusion avec données en mémoire

Chargez data/animals.ttl dans un graphe et la chaîne Turtle suivante dans un autre graphe avec StringParser.Parse :

<http://example.org/ns#dave> <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <http://example.org/ns#Person> .
<http://example.org/ns#dave> <http://example.org/ns#name> "Dave" .
<http://example.org/ns#dave> <http://example.org/ns#age> "42" .

Fusionnez les deux graphes et sérialisez en Turtle compresse.

Indice : utilisez StringParser.Parse(g, turtleString) pour parser une chaîne Turtle, puis g1.Merge(g2).

Protocole attendu :

// 1. Charger animals.ttl dans g1
var g1 = new Graph();
new TurtleParser().Load(g1, "data/animals.ttl");

// 2. Charger la chaine Turtle dans g2
var g2 = new Graph();
StringParser.Parse(g2, turtleString);

// 3. Fusionner
g1.Merge(g2);
Console.WriteLine($"Total apres fusion : {g1.Triples.Count} triplets");

// 4. Serialiser
var sw = new System.IO.StringWriter();
new CompressingTurtleWriter().Save(g1, sw);
Console.WriteLine(sw.ToString());

Sortie attendue : un graphe avec 54 triplets (51 d’animals + 3 de Dave), sérialisé en Turtle compresse.

Pourquoi utiliser StringParser :

StringParser.Parse(g, str) détecte automatiquement le format depuis la chaîne (Turtle, NTriples, RDF/XML, JSON-LD). C’est utile quand le format est inconnu ou varie. Pour des chaînes Turtle connues, on peut utiliser directement TurtleParser().Load(g, new StringReader(str)).

Coût : ~5 minutes pour l’étudiant (parsing string + fusion + sérialisation).

// TODO etudiant : Chargez data/animals.ttl dans un graphe g1
// Chargez la chaine Turtle ci-dessous dans un graphe g2 avec StringParser.Parse
// Fusionnez g2 dans g1 et affichez le nombre total de triplets
// Serialisez le resultat en NTriples (pas Turtle compresse)
//
// Indice : utilisez new NTriplesWriter() pour la serialisation
Console.WriteLine("Exercice a completer");
Exercice a completer

Exemple guide 3 : Liste RDF

Créez une liste ["Alice", "Bob", "Charlie"] avec AssertList, ajoutez "Diana", supprimez "Bob", affichez le résultat.

Algorithme :

// Création d'une liste RDF ordonnée [Alice, Bob, Charlie]
// AssertList génère une chaîne de nœuds blancs reliés par rdf:first et rdf:rest
IGraph g = new Graph();
var root = g.AssertList(new List<INode>() { "Alice".ToLiteral(g), "Bob".ToLiteral(g), "Charlie".ToLiteral(g) });


// Fonction utilitaire pour afficher uniquement la valeur textuelle de chaque noeud littéral
Func<IEnumerable<INode>, string> fmt = nodes => string.Join(", ", nodes.Select(n => ((ILiteralNode)n).Value));

Console.WriteLine($"Initial          : [{fmt(g.GetListItems(root))}]");

// AddToList insère "Diana" en fin de liste en ajoutant un nœud blank supplémentaire
g.AddToList(root, new List<INode>() { "Diana".ToLiteral(g) });
Console.WriteLine($"Apres ajout      : [{fmt(g.GetListItems(root))}]");

// RemoveFromList supprime toutes les occurrences de "Bob" dans la liste
g.RemoveFromList(root, new List<INode>() { "Bob".ToLiteral(g) });
Console.WriteLine($"Apres suppression: [{fmt(g.GetListItems(root))}]");

Sortie attendue :

Initial          : [Alice, Bob, Charlie]
Apres ajout      : [Alice, Bob, Charlie, Diana]
Apres suppression: [Alice, Charlie, Diana]

Pourquoi cet exemple est complet :

Il couvre les 4 opérations de base sur les listes RDF : AssertList (creation), AddToList (ajout), RemoveFromList (suppression), GetListItems (lecture). C’est le pattern standard pour manipuler des collections en RDF.

Trois nuances :

  1. Les éléments sont des INode : on peut mélanger literal nodes (strings, nombres) et URI nodes dans la même liste.
  2. L’ordre est préservé : AssertList preserve l’ordre des éléments fournis.
  3. RemoveFromList supprime toutes les occurrences : si Bob apparaît 2 fois, les 2 sont supprimées.

Coût : ~0.1 seconde (AssertList + Add + Remove + GetListItems).

// Création d'une liste RDF ordonnée [Alice, Bob, Charlie]
// AssertList génère une chaîne de nœuds blancs reliés par rdf:first et rdf:rest
IGraph g = new Graph();
var root = g.AssertList(new List<INode>() { "Alice".ToLiteral(g), "Bob".ToLiteral(g), "Charlie".ToLiteral(g) });


// Fonction utilitaire pour afficher uniquement la valeur textuelle de chaque noeud littéral
Func<IEnumerable<INode>, string> fmt = nodes => string.Join(", ", nodes.Select(n => ((ILiteralNode)n).Value));

Console.WriteLine($"Initial          : [{fmt(g.GetListItems(root))}]");

// AddToList insère "Diana" en fin de liste en ajoutant un nœud blank supplémentaire
g.AddToList(root, new List<INode>() { "Diana".ToLiteral(g) });
Console.WriteLine($"Apres ajout      : [{fmt(g.GetListItems(root))}]");

// RemoveFromList supprime toutes les occurrences de "Bob" dans la liste
g.RemoveFromList(root, new List<INode>() { "Bob".ToLiteral(g) });
Console.WriteLine($"Apres suppression: [{fmt(g.GetListItems(root))}]");
Initial          : [Alice, Bob, Charlie]
Apres ajout      : [Alice, Bob, Charlie, Diana]
Apres suppression: [Alice, Charlie, Diana]

Exercice 6 : Liste RDF personnalisée

Créez une liste RDF ["Paris", "Lyon", "Marseille"] avec AssertList, ajoutez "Toulouse" avec AddToList, supprimez "Lyon" avec RemoveFromList. Affichez la liste après chaque opération.

Indice : reprendre l’exemple guide 3, changer les valeurs et les labels.

Protocole attendu :

var g = new Graph();

// Etape 1 : AssertList initial -- liste ["Paris", "Lyon", "Marseille"]
var root = g.AssertList(new List<INode> { /* valeurs a completer */ });
Console.WriteLine($"Apres creation : {g.GetListItems(root).Count()} elements");

// Etape 2 : ajouter "Toulouse" avec AddToList -- a completer

// Etape 3 : supprimer "Lyon" avec RemoveFromList -- a completer

// Etape 4 : affichage final
foreach (var item in g.GetListItems(root))
    Console.WriteLine($"  {item}");

Sortie attendue :

Apres creation : 3 elements
Apres ajout Toulouse : 4 elements
Apres suppression Lyon : 3 elements
  "Paris"
  "Marseille"
  "Toulouse"

Pourquoi cet exercice est progressif :

Il fait afficher l’état après chaque étape, ce qui force l’étudiant à vérifier que chaque opération a l’effet attendu. C’est un pattern de test incremental – préférable à un test final “tout ou rien”.

Coût : ~5 minutes pour l’étudiant (3 étapes + 4 affichages).

// TODO etudiant : Creez une liste RDF ["Paris", "Lyon", "Marseille"] avec AssertList
// Ajoutez "Toulouse" avec AddToList, supprimez "Lyon" avec RemoveFromList
// Affichez la liste apres chaque operation avec GetListItems
Console.WriteLine("Exercice a completer");
Exercice a completer

Références savantes

  • RDF 1.1 Concepts and Abstract Syntax - Cyganiak, Wood, Lanthaler (eds.), Recommandation W3C, 25 février 2014. Modèle de données abstrait (triplets sujet-prédicat-objet, IRIs, blank nodes, littéraux) dont dérivent tous les formats manipulés dans ce notebook.
  • RDF 1.1 Turtle - Beckett, Berners-Lee, Prud’hommeaux, Recommandation W3C, 25 février 2014. Format de sérialisation compact et lisible, défaut moderne pour les graphes RDF.
  • RDF 1.1 NTriples - Recommandation W3C, 25 février 2014. Format ligne par ligne, idéal pour le piping UNIX et le debugging.
  • RDF 1.1 RDF/XML - Recommandation W3C, 10 février 2004. Format XML, le plus ancien et le plus répandu historiquement.
  • dotNetRDF Library - Documentation officielle, dotnetrdf.org. Implementation .NET de reference pour RDF/Semantic Web.
  • SPARQL 1.1 Query Language - Harris, Seaborne, Prud’hommeaux, Recommandation W3C, 21 mars
    1. Langage de requête pour RDF, équivalent de SQL pour les graphes.

Trois ouvrages de reference :

  • Programming the Semantic Web - Segaran, Evans, Taylor (O’Reilly, 2009). Introduction pratique avec exemples Python et .NET.
  • Semantic Web for the Working Ontologist - Allemang, Hendler (Morgan Kaufmann, 2011). Approche modèle-first, focus sur RDFS et OWL.
  • Foundations of Semantic Web Technologies - Hitzler, Krotzsch, Rudolph (CRC Press, 2010). Reference théorique approfondie.

Quatre outils complémentaires :

  • Apache Jena (Java) : alternative open-source à dotNetRDF, avec un écosystème plus large (Fuseki pour le serveur SPARQL, ARQ pour le query engine).
  • RDFLib (Python) : équivalente à dotNetRDF pour Python.
  • Redland (C) : bibliothèque C pour RDF, plus bas niveau.
  • Blazegraph : base de données RDF native, supporte les transactions et le scaling.

Résumé

Section Concepts clés APIs principales
1. Lecture Parsers, StringParser, Handlers TurtleParser, NTriplesParser, CountHandler
2. Écriture Writers, configuration NTriplesWriter, CompressingTurtleWriter, RdfXmlWriter
3. Fusion Merge, deduplication IGraph.Merge()
4. Sélection LINQ sur graphes GetTriplesWithSubject/Predicate/Object
5. Listes RDF AssertList, RetractList AssertList, RetractList, AddToList, RemoveFromList

Trois concepts clés à retenir :

  1. RDF est un modèle de triplets : tout est (sujet, predicat, objet). Les listes sont encodees avec rdf:first/rdf:rest. Les graphes sont des ensembles de triplets (pas des multisets).
  2. dotNetRDF est une bibliothèque mature : elle implémente la majorité de RDF 1.1 (Turtle, NTriples, RDF/XML, JSON-LD) avec une API .NET idiomatique (LINQ, IDisposable).
  3. LINQ est le langage de requête : pour les requêtes complexes, on chaîne Where(), Select(), GroupBy(), OrderBy() plutot que d’utiliser SPARQL.

Pour aller plus loin :

  • SW-4-SPARQL : requêtes SPARQL avec LeviathanQueryProcessor (moteur SPARQL integre).
  • SW-5-LinkedData : publication de graphes sur le web (Linked Data principles).
  • SW-11-KnowledgeGraphs : graphes de connaissances à grande échelle.
  • SemanticKernel : integration .NET de graphes RDF dans les pipelines LLM (cf. projet GenAI/RAG-et-Mémoire-Sémantique).

Quatre idées forces transversales :

  1. La sérialisation est réversible : tout graphe peut être sérialisé dans plusieurs formats et parse de nouveau. La sémantique est préservée.
  2. La sémantique d’ensemble évite les doublons : Merge() ne duplique pas les triplets identiques, et les noeuds blancs sont renommés pour éviter les collisions.
  3. Les listes chaînées sont verbeuses : 2N triplets pour N éléments. Pour les grandes listes, préférer rdf:Seq ou le stockage en mémoire.
  4. LINQ est puissant pour les requêtes simples : pour les requêtes multi-patterns (joins, filtres), il faut passer à SPARQL (cf. SW-4-SPARQL).

Trois références complémentaires :

  • SHACL : W3C Recommendation pour la validation de graphes RDF par rapport à un schéma.
  • OWL 2 : W3C Recommendation pour l’ontologie formelle (classes, propriétés, restrictions).
  • SKOS : W3C Recommendation pour les vocabulaires simples (thesaurus, taxonomies). ***

Navigation : << 2-RDFBasics | Index | 4-SPARQL >>

Retour au sommet