SW-10-CSharp-RDFStar — Jumeau C# : annoter des triplets (réification) via dotNetRDF

Jumeau C# (.NET 9) de SW-10-Python-RDFStar. Le notebook Python utilise rdflib pour illustrer l’annotation de triplets (provenance, confiance, validité temporelle) via la réification classique RDF (4 triplets par assertion) et les graphes nommés (rdflib.Dataset). Ce jumeau invoque le vrai moteur .NET natif : dotNetRDF 3.4.1 (VDS.RDF.Graph, Triple, SparqlQueryParser, ExecuteQuery) — pas une réimplémentation jouet.

Source Python : SW-10-Python-RDFStar.ipynb — rdflib.Graph/Dataset, reify() helper, requêtes SPARQL sur rdf:Statement.

Objectifs d’apprentissage

  1. Comprendre la limitation fondamentale de RDF : un triplet est une affirmation atomique, on ne peut pas (en RDF 1.0) annoter directement un triplet.
  2. Maîtriser la réification classique : décomposer une assertion en 4 triplets (rdf:Statement + rdf:subject/predicate/object) pour pouvoir l’annoter.
  3. Construire un graphe de connaissances annoté : provenance, score de confiance, validité temporelle.
  4. Interroger ces annotations via SPARQL (SparqlQueryParser + ExecuteQuery), avec filtrage.
  5. Découvrir les graphes nommés comme alternative plus compacte à la réification.

Vérification mathématique (cibles de concordance)

  • Coût de la réification : annoter 1 fait = 7 triplets exactement (1 original + 4 réification + 2 annotations). Concordance avec rdflib.
  • Graphe de connaissances : 4 personnes (8 triplets type/name) + faits annotés.
  • Requête SPARQL filtrée : 2 relations à confiance ≥ 0,60 (Alice→Bob 0,95, Bob→Carol 0,60), 2 à confiance < 0,60 (Carol→Dave 0,30, Alice→Dave 0,50). Concordance avec le twin Python.

Posture du jumeau : mêmes concepts, moteur natif

La méthode de concordance fixée pour cette paire : chaque étape du notebook Python (reify() helper, graphe de connaissances, filtrage SPARQL, graphes nommés, sérialisation) possède ici son exécution dotNetRDF équivalente, avec les mêmes valeurs d’exemple (mêmes personnes, mêmes scores de confiance) — les compteurs de triplets et les tables de résultats doivent coïncider, aux différences d’API près. Quand un chiffre diverge entre les deux notebooks, l’écart est documenté dans le texte (il traduit presque toujours une différence de vocabulaire d’annotation, pas de mécanique). Cette discipline fait du couple un même cours exécutable dans deux écosystèmes, plutôt que deux cours parallèles.

1. Installation et espace de noms

dotNetRDF est le moteur .NET natif pour RDF/SPARQL. On charge le NuGet dotNetRDF 3.4.1 (le même que SW-9-CSharp-JSONLD). On définit les préfixes et URI de base comme le twin Python (ex:, foaf:, rdf:, xsd:). Côté espaces de noms, tout vit sous VDS.RDF.* : Graph/TripleStore pour les graphes, Parsing/Writing pour les sérialisations, Query pour SPARQL — miroir des modules rdflib.graph/rdflib.plugins.sparql du côté Python.

Le chargement #r "nuget: dotNetRDF, 3.4.1" installe le moteur depuis NuGet à la première exécution (mécanisme .NET Interactive), puis les using VDS.RDF.* ouvrent les quatre quadrants de la bibliothèque : VDS.RDF (le modèle : Graph, Triple, nœuds), VDS.RDF.Parsing (lecteurs Turtle/JSON-LD et SparqlQueryParser), VDS.RDF.Writing (sérialiseurs, dont CompressingTurtleWriter), VDS.RDF.Query (SparqlResultSet, ISparqlResult). La correspondance avec rdflib est terme à terme : Graph ↔︎ rdflib.Graph, Assert ↔︎ add, ExecuteQuery ↔︎ query — apprendre l’un, c’est savoir naviguer l’autre.

#r "nuget: dotNetRDF, 3.4.1"

using VDS.RDF;
using VDS.RDF.Parsing;
using VDS.RDF.Query;
using VDS.RDF.Writing;
using System;
using System.IO;
using System.Text;

Console.WriteLine("dotNetRDF charge (VDS.RDF) -- marathon #4956, parite avec SW-10-Python-RDFStar (rdflib)");
Installed Packages
  • dotNetRDF, 3.4.1
dotNetRDF charge (VDS.RDF) -- marathon #4956, parite avec SW-10-Python-RDFStar (rdflib)

2. Le problème : pourquoi annoter des triplets ?

RDF représente des faits sous forme de triplets (sujet, prédicat, objet). Mais un triplet est atomique : on ne peut pas lui attacher directement une provenance, un score de confiance ou une date. Pour dire « Alice affirme que Bob connaît Carol avec 85 % de confiance », il faut réifier le triplet — le décomposer en 4 triplets auxiliaires — puis annoter ce nœud-réification.

2.1 La réification classique : 4 triplets pour 1 assertion

La réification d’un triplet (s, p, o) crée un nœud stmt typé rdf:Statement relié au triplet original via rdf:subject, rdf:predicate, rdf:object. On peut alors annoter stmt (source, confiance, date) sans toucher au fait original.

Un point clé du vocabulaire : les quatre prédicats (rdf:type rdf:Statement, rdf:subject, rdf:predicate, rdf:object) sont des triplets ordinaires — le nœud stmt est un URI comme un autre, requêtable par SPARQL comme n’importe quelle ressource. La réification n’est pas une construction spéciale du moteur : c’est une convention de modélisation entièrement exprimée en RDF, ce qui explique à la fois sa portabilité totale (tout moteur RDF la comprend) et son coût (rien n’est gratuit puisque tout est explicite).

L’exemple filé du notebook

Tout le notebook s’appuie sur un même scénario : un graphe social où Alice, Bob, Carol et Dave sont reliés par foaf:knows, chaque relation étant rapportée par une source différente — LinkedIn (fiable), Twitter (moyenne), aucune source, ou une rumeur — avec un score de confiance en conséquence. La réification est ce qui permet au graphe de porter à la fois la relation (Alice connaît Bob) et son contexte épistémique (selon LinkedIn, à 95 %) au lieu de les confondre dans un triplet muet.

Une façon de sentir la limitation avant même le code : en RDF, un triplet (s, p, o) est insécable — il n’a pas d’identité propre, pas d’« intérieur » où ranger une métadonnée. Une propriété RDF s’attache à une ressource (le sujet), jamais à une affirmation. « La confiance de Bob knows Carol » n’est donc pas exprimable directement : il faut d’abord matérialiser l’affirmation en tant que ressource — c’est littéralement ce que fait le mot réification : transformer une chose (le triplet) en une chose d’un autre type (un nœud) pour pouvoir en parler. La cellule suivante fait cette matérialisation en sept triplets, et tout le reste du notebook en exploite les conséquences.

// Espace de noms et URI de base (identiques au twin Python)
const string NS = "http://example.org/";
var g = new Graph();
g.NamespaceMap.AddNamespace("ex", new Uri(NS));
g.NamespaceMap.AddNamespace("foaf", new Uri("http://xmlns.com/foaf/0.1/"));

// Helper : un URI node racourci
IUriNode U(string local) => g.CreateUriNode("ex:" + local);
IUriNode Pred(Uri u) => g.CreateUriNode(u);
IUriNode rdfType = g.CreateUriNode("rdf:type");
IUriNode rdfStmt = g.CreateUriNode("rdf:Statement");
IUriNode rdfSubject = g.CreateUriNode("rdf:subject");
IUriNode rdfPredicate = g.CreateUriNode("rdf:predicate");
IUriNode rdfObject = g.CreateUriNode("rdf:object");
IUriNode foafKnows = Pred(new Uri("http://xmlns.com/foaf/0.1/knows"));

// Reification classique : decomposer le triplet (Bob, knows, Carol)
g.Assert(new Triple(U("Bob"), foafKnows, U("Carol")));          // le fait original (1)

var stmt = U("statement1");
g.Assert(new Triple(stmt, rdfType, rdfStmt));                   // rdf:type rdf:Statement
g.Assert(new Triple(stmt, rdfSubject, U("Bob")));               // rdf:subject Bob
g.Assert(new Triple(stmt, rdfPredicate, foafKnows));            // rdf:predicate foaf:knows
g.Assert(new Triple(stmt, rdfObject, U("Carol")));              // rdf:object Carol   (4)

// Annotations : qui affirme ce fait, avec quel degre de confiance ?
g.Assert(new Triple(stmt, U("assertedBy"), U("Alice")));        // assertedBy Alice
g.Assert(new Triple(stmt, U("confidence"), g.CreateLiteralNode("0.85", new Uri("http://www.w3.org/2001/XMLSchema#double"))));  // (2)

Console.WriteLine($"Nombre total de triplets : {g.Triples.Count}");
Console.WriteLine("  - Fait original : 1 triplet");
Console.WriteLine("  - Reification : 4 triplets");
Console.WriteLine("  - Annotations : 2 triplets");
Console.WriteLine("  - Total : 7 triplets pour annoter 1 fait");
Nombre total de triplets : 7
  - Fait original : 1 triplet
  - Reification : 4 triplets
  - Annotations : 2 triplets
  - Total : 7 triplets pour annoter 1 fait

Interprétation : coût de la réification

Approche Triplets nécessaires Lisibilité Garantie d’existence du fait
Triplet nu 1 excellente oui (le triplet existe)
Réification + annotations 7 verbeuse non (le fait original peut ne pas exister)

C’est précisément ce coût (4 triplets auxiliaires + N annotations) que RDF-star (RDF 1.2) vise à éliminer avec la syntaxe << s p o >>. Mais la réification reste le mécanisme portable, indépendant du moteur. La sérialisation Turtle ci-dessous montre les 7 triplets. À l’échelle, l’arithmétique devient le vrai sujet : annoter 1 000 faits avec 2 annotations chacun coûte 7 000 triplets — et surtout, toute requête sur les faits doit désormais traverser la jointure rdf:Statement de la section 5. C’est ce coût en volume et en complexité de requête, plus que la verbosité, qui motive RDF-star : mêmes annotations, zéro triplet auxiliaire.

Lecture des mesures

La sortie affiche le compte exact : 7 triplets pour un fait annoté de deux manières — 1 fait, 4 auxiliaires, 2 annotations. Deux détails de la cellule méritent l’œil avant de passer au Turtle :

  • Le nœud-réification est ici un URI nommé (ex:statement1), construit par U("statement1"). Le twin Python laisse par défaut un blank node anonyme. Ce n’est pas un caprice d’API : un URI nommé reste adressable après sérialisation N-Triples (où les identifiants de blank nodes sont précaires) et référenceable depuis un autre graphe — au prix d’un nom à inventer. Le blank node économise le nommage mais borne la portée. Les deux notebooks démontrent la même mécanique avec ce choix différent, et la sérialisation ci-dessous le rend visible.
  • Le littéral de confiance est construit par g.CreateLiteralNode("0.85", new Uri(...#double)) : la forme lexicale "0.85" avec son datatype explicite. La forme doit utiliser le point décimal — d’où le helper Confidence() de la section 3 qui force InvariantCulture : un ToString() par défaut sur une machine en culture française produirait "0,85" et casserait silencieusement le littéral typé (plus personne ne saurait le comparer en SPARQL). Première rencontre, dans ce notebook, de la règle .NET « tout littéral qui traverse une frontière se formate en culture invariante ».

L’arithmétique à l’échelle annoncée ci-dessus se vérifie sur la ligne de compte : chaque annotation supplémentaire ne coûte que 1 triplet (c’est linéaire), mais les 4 auxiliaires sont dus une fois par fait annoté — c’est le facteur constant qui, multiplié par des millions de faits, remplit les stores et motive RDF-star.

// Serialisation Turtle (equivalent rdflib.serialize(format="turtle"))
var writer = new CompressingTurtleWriter();
var sw = new System.IO.StringWriter();
writer.Save(g, sw);
Console.WriteLine(sw.ToString());
@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 ex: <http://example.org/>.
@prefix foaf: <http://xmlns.com/foaf/0.1/>.

ex:Bob foaf:knows ex:Carol.
ex:statement1 ex:assertedBy ex:Alice;
              ex:confidence "0.85"^^xsd:double;
              rdf:object ex:Carol;
              rdf:predicate foaf:knows;
              rdf:subject ex:Bob;
              a rdf:Statement.

Lecture du résultat : le Turtle compressé rend la réification lisible

La sérialisation ouvre sur les directives @prefix (rdf:, rdfs:, xsd:, ex:, foaf:) puis compacte chaque triplet réifié sur une ligne ex:stmt1 rdf:type rdf:Statement ; rdf:subject ... ; rdf:predicate ... ; rdf:object ... — le point-virgule Turtle (prédicats multiples pour un même sujet) suit exactement la structure du nœud-réification. C’est toute la valeur du format : là où le graphe en mémoire est une collection plate de triplets, le Turtle compressé regroupe visuellement les 4 triplets auxiliaires en un bloc par assertion, et les annotations du même nœud à sa suite. Le même contenu en N-Triples (une ligne par triplet, URIs en clair) resterait correct mais illisible — c’est le choix fait ici par CompressingTurtleWriter, l’équivalent exact du serialize(format="turtle") de rdflib côté Python.

Notez au passage deux choix du CompressingTurtleWriter visibles dans la sortie : il émet un @prefix rdfs: que le code n’a jamais déclaré (le writer pré-déclare les préfixes usuels du modèle, y compris inutilisés ici), et il réordonne les prédicats du nœud-réification par ordre alphabétique (ex:assertedBy, ex:confidence, rdf:object, rdf:predicate, rdf:subject, puis a rdf:Statement) sans respecter l’ordre d’insertion — un graphe RDF est un ensemble de triplets, l’ordre n’a pas de sens, et le sérialiseur en dispose librement pour compacter. Conséquence pratique pour les tests : ne jamais comparer deux sérialisations Turtle chaîne à chaîne ; comparer les graphes re-parsés (ou leurs comptes), comme le fait l’aller-retour de la section 8.

La ligne ex:statement1 ex:assertedBy ex:Alice; montre enfin la raison d’être du point-virgule Turtle : tout ce qui suit sur le bloc partage le même sujet — le nœud-réification et ses six propriétés forment visuellement une fiche, alors qu’en mémoire ce sont sept triplets indépendants.

3. Fonction utilitaire Reify()

Pour réduire la verbosité, le twin Python définit une fonction reify(graph, s, p, o). Voici l’équivalent C# : elle crée le nœud-réification rdf:Statement + les 3 liens, sans répéter le fait original (à ajouter séparément). Retourne le nœud stmt pour annotation. Plutôt que d’écrire les 4 triplets auxiliaires à la main à chaque annotation, on factorise le geste dans un helper — c’est aussi l’occasion d’expliciter son contrat : ce qu’il ajoute au graphe, ce qu’il laisse à l’appelant, et pourquoi.

Deux différences de signature avec le twin Python valent d’être repérées avant d’exécuter. D’abord, le paramètre name : là où reify() Python accepte un stmt_uri=None (blank node par défaut), la version C# exige un nom — elle retourne un IUriNode construit par U(name). Ensuite, elle ne prend pas le graphe en paramètre : elle asserte directement dans le g capturé par la méthode locale (fermeture sur la variable de cellule). Deux ergonomies, un même contrat : quatre triplets auxiliaires ajoutés, nœud retourné à l’appelant pour annotation, fait original non touché.

Le helper Confidence(double v) juste en dessous porte la subtilité de culture vue en section 2 : v.ToString(CultureInfo.InvariantCulture) garantit la forme lexicale à point ("0.6"), seul format conforme pour xsd:double.

// Reify : cree les 4 triplets de reification et retourne le noeud statement.
// (Le fait original doit etre ajoute separement si on veut qu'il existe dans le graphe.)
IUriNode Reify(IUriNode s, IUriNode p, IUriNode o, string name)
{
    var stmtNode = U(name);
    g.Assert(new Triple(stmtNode, rdfType, rdfStmt));
    g.Assert(new Triple(stmtNode, rdfSubject, s));
    g.Assert(new Triple(stmtNode, rdfPredicate, p));
    g.Assert(new Triple(stmtNode, rdfObject, o));
    return stmtNode;
}

// Helper pour creer un LiteralNode de confiance typé xsd:double.
ILiteralNode Confidence(double v) =>
    g.CreateLiteralNode(v.ToString(System.Globalization.CultureInfo.InvariantCulture),
                         new Uri("http://www.w3.org/2001/XMLSchema#double"));

// Demo : un 2e fait reifie via le helper (Bob connait Dave, affirmé par Carol).
g.Assert(new Triple(U("Bob"), foafKnows, U("Dave")));              // fait original
var stmt2 = Reify(U("Bob"), foafKnows, U("Dave"), "statement2");
g.Assert(new Triple(stmt2, U("assertedBy"), U("Carol")));
g.Assert(new Triple(stmt2, U("confidence"), Confidence(0.60)));

Console.WriteLine($"Apres 2e fait reifie : graphe g = {g.Triples.Count} triplets au total");
Apres 2e fait reifie : graphe g = 14 triplets au total

Lecture du résultat : le contrat de Reify()

Après le second appel, le graphe g compte 14 triplets : les 7 du premier fait réifié (1 original + 4 de réification + 2 annotations) plus 7 pour le second. La fonction illustre un contrat important : elle retourne le nœud-réification (IUriNode) sans l’ajouter au graphe — c’est l’appelant qui décide d’Assert le fait original ou non. Cette séparation reflète une subtilité du modèle : la réification décrit une assertion, pas un fait — le triplet Alice connaît Carol peut être réifié (quelqu’un l’affirme) sans exister dans le graphe (le système ne l’endosse pas). C’est aussi ce qui rend le mécanisme général : n’importe quelle cellule peut annoter n’importe quel triplet, y compris un triplet qu’elle refuse d’affirmer elle-même.

La mesure confirme le contrat : le graphe passe de 7 à 14 triplets — le second fait réifié (Bob connaît Dave, affirmé par Carol à 0,60) ajoute exactement le même bloc 1 + 4 + 2. La linéarité est la bonne nouvelle (chaque annotation ne coûte qu’un triplet de plus) ; la constante de 4 auxiliaires par fait est la mauvaise — elle ne se négocie pas tant que le mécanisme est la réification.

Sur la distinction assertion/fait, un prolongement concret : imaginez un module de vérification qui reçoit des affirmations externes sans vouloir les endosser. Avec Reify(), il enregistre chaque affirmation (le bloc auxiliaire + annotations) et s’abstient du Assert du triplet original — le graphe des faits (ce que le système affirme) reste vierge de toute contamination, tandis que le graphe des mentions (ce que d’autres affirment) grossit. La requête de la section 5 liste les deux sans les distinguer — c’est au consommateur de choisir son motif : jointure sur les triplets nus pour raisonner, jointure sur rdf:Statement pour auditer.

4. Graphe de connaissances annoté

Construisons un graphe réaliste : 4 personnes (Alice, Bob, Carol, Dave) reliées par foaf:knows, chaque relation annotée d’un score de confiance et d’une source. Le fait 4 (Alice connaît Dave) est seulement affirmé par Bob mais n’existe pas comme triplet direct — c’est précisément l’intérêt de la réification : on peut enregistrer une affirmation sans l’ajouter au graphe des faits.

La construction de la cellule suit le plan du twin Python à l’identique — quatre personnes typées foaf:Person avec leurs noms complets, puis quatre relations dont trois assertées et une seulement mentionnée. Les scores sont calibrés pour que le filtrage de la section 6 produise une partition équilibrée (2 fiables / 2 incertaines) : 0,95 (LinkedIn), 0,60 (Twitter), 0,30 (Rumeur), et 0,50 pour la relation non sourcée affirmée par Bob.

Observons aussi le geste d’API : le graphe gk repart de zéro (new Graph()) avec ses propres helpers locaux (N, Conf, ReifyK) — en .NET Interactive, chaque cellule compile dans un espace partagé, et re-déclarer Reify provoquerait une collision de symboles ; le suffixe K (pour knowledge graph) neutralise le conflit. C’est une contrainte du notebook C#, pas du moteur — côté Python, chaque cellule redéfinit reify sans état résiduel.

Côté vocabulaire, le triplet d’annotations retenu (ex:confidence, ex:source, ex:assertedBy) n’est pas du RDF standard mais un choix de modélisation local — le standard fournit le mécanisme (le nœud rdf:Statement annotables), pas les prédicats d’annotation. Une industrie qui a besoin d’interopérer choisirait des prédicats publiés (le W3C PROV pour la provenance, par exemple) ; ici la brièveté l’emporte. La contrepartie à connaître : deux graphes annotés avec des vocabulaires différents ne se requêtent pas par la même clause — choisir ses prédicats d’annotation est un engagement d’API autant qu’un choix sémantique.

var gk = new Graph();
gk.NamespaceMap.AddNamespace("ex", new Uri(NS));
gk.NamespaceMap.AddNamespace("foaf", new Uri("http://xmlns.com/foaf/0.1/"));

// Helpers locaux (gk)
IUriNode N(string local) => gk.CreateUriNode("ex:" + local);
IUriNode foafName = gk.CreateUriNode(UriFactory.Create("http://xmlns.com/foaf/0.1/name"));
IUriNode rdfTypeK = gk.CreateUriNode("rdf:type");
IUriNode foafPerson = gk.CreateUriNode(UriFactory.Create("http://xmlns.com/foaf/0.1/Person"));
IUriNode knowsK = gk.CreateUriNode(UriFactory.Create("http://xmlns.com/foaf/0.1/knows"));
ILiteralNode Str(string s) => gk.CreateLiteralNode(s);
ILiteralNode Conf(double v) => gk.CreateLiteralNode(
    v.ToString(System.Globalization.CultureInfo.InvariantCulture),
    new Uri("http://www.w3.org/2001/XMLSchema#double"));

// Reify sur gk : retourne le noeud statement
IUriNode ReifyK(IUriNode s, IUriNode p, IUriNode o, string name)
{
    var st = N(name);
    gk.Assert(new Triple(st, rdfTypeK, gk.CreateUriNode("rdf:Statement")));
    gk.Assert(new Triple(st, gk.CreateUriNode("rdf:subject"), s));
    gk.Assert(new Triple(st, gk.CreateUriNode("rdf:predicate"), p));
    gk.Assert(new Triple(st, gk.CreateUriNode("rdf:object"), o));
    return st;
}

// === Personnes ===
foreach (var (name, uri) in new[] { ("Alice Dupont", N("Alice")), ("Bob Martin", N("Bob")),
                                     ("Carol Laurent", N("Carol")), ("Dave Petit", N("Dave")) })
{
    gk.Assert(new Triple(uri, rdfTypeK, foafPerson));
    gk.Assert(new Triple(uri, foafName, Str(name)));
}

// === Fait 1 : Alice connait Bob (haute confiance, LinkedIn) ===
gk.Assert(new Triple(N("Alice"), knowsK, N("Bob")));
var s1 = ReifyK(N("Alice"), knowsK, N("Bob"), "stmt1");
gk.Assert(new Triple(s1, N("confidence"), Conf(0.95)));
gk.Assert(new Triple(s1, N("source"), Str("LinkedIn")));

// === Fait 2 : Bob connait Carol (confiance moyenne, Twitter) ===
gk.Assert(new Triple(N("Bob"), knowsK, N("Carol")));
var s2 = ReifyK(N("Bob"), knowsK, N("Carol"), "stmt2");
gk.Assert(new Triple(s2, N("confidence"), Conf(0.60)));
gk.Assert(new Triple(s2, N("source"), Str("Twitter")));

// === Fait 3 : Carol connait Dave (basse confiance, rumeur) ===
gk.Assert(new Triple(N("Carol"), knowsK, N("Dave")));
var s3 = ReifyK(N("Carol"), knowsK, N("Dave"), "stmt3");
gk.Assert(new Triple(s3, N("confidence"), Conf(0.30)));
gk.Assert(new Triple(s3, N("source"), Str("Rumeur")));

// === Fait 4 : Alice connait Dave (NON asserte, seulement affirme par Bob) ===
var s4 = ReifyK(N("Alice"), knowsK, N("Dave"), "stmt4");
gk.Assert(new Triple(s4, N("assertedBy"), N("Bob")));
gk.Assert(new Triple(s4, N("confidence"), Conf(0.50)));

Console.WriteLine($"Graphe de connaissances : {gk.Triples.Count} triplets");
Graphe de connaissances : 35 triplets

Lecture du résultat : le graphe de connaissances assemblé

35 triplets composent maintenant le graphe gk : les faits foaf:knows entre les quatre personnes, leurs noms, et pour chaque relation un bloc de réification portant ex:confidence (le score) et ex:source (la provenance). C’est l’échelle où l’inspection à l’œil devient pénible — 35 triplets, 4 sources hétérogènes (LinkedIn, Twitter, aucune, Rumeur), des confiances de 0,30 à 0,95 — et où la question naturelle cesse d’être « qu’y a-t-il dans le graphe ? » pour devenir « que sait-on, et à quel degré croire chaque chose ? ». Les deux sections suivantes répondent à cette question dans la langue du moteur : SPARQL.

Décomposition des 35 triplets

Le compte se vérifie bloc par bloc dans le code : 8 triplets d’identité (4 personnes × rdf:type + foaf:name), fait 1 : 7 (1 assertion + 4 auxiliaires + 2 annotations), fait 2 : 7, fait 3 : 7, fait 4 : 6 (aucune assertion — 4 auxiliaires + 2 annotations). Total : 8 + 7 + 7 + 7 + 6 = 35, à l’identique du compteur affiché. Le twin Python mesure 37 sur le même scénario : ses deux premiers faits portent une annotation ex:since supplémentaire chacun — l’écart de 2 triplets traduit un vocabulaire d’annotation plus riche côté Python, pas une mécanique différente. C’est le type de divergence documentée qu’une paire jumelle doit rendre lisible plutôt que masquer.

La conséquence la plus importante reste structurelle : cherchez le triplet ex:Alice foaf:knows ex:Dave dans ce graphe — il n’existe pas. Seul le bloc stmt4 en parle. Un parcours des foaf:knows ne voit que 3 relations ; la quatrième n’existe que pour la jointure sur rdf:Statement. Assertion et mention ont des audiences distinctes dans le même store — le moteur d’inférence raisonne sur les assertions, l’auditeur sur les mentions, et personne ne confond les deux.

5. Interroger les annotations avec SPARQL

Pour extraire les relations annotées, on joint sur rdf:subject/rdf:predicate/rdf:object et on récupère les annotations. La requête ci-dessous liste toutes les relations connues avec leur confiance et source, triées par confiance décroissante.

Ce que la requête fait ne peut pas se faire sans réification : un triplet nu n’a pas de « confiance » ni de « source » à joindre — la jointure ci-dessous existe précisément parce que les annotations vivent sur le nœud rdf:Statement.

L’exécution passe par les trois gestes canoniques dotNetRDF : new SparqlQueryParser() puis ParseFromString(texte) produit un objet requête compilé, gk.ExecuteQuery(q) l’évalue contre le graphe et retourne un SparqlResultSet que l’on parcourt en foreach. Les deux helpers d’extraction (ConfVal, LitStr) existent pour un trait d’API à connaître : r[v].ToString() sur un littéral typé renvoie la forme avec suffixe (0.95^^...#double) — pour comparer ou afficher, il faut descendre à ((ILiteralNode)r[v]).Value (la forme lexicale nue) et parser soi-même. Le twin Python n’a pas cette marche : rdflib retourne directement des float pour les littéraux typés numériques. Économie d’un cast, coût d’un helper — le prix habituel de l’API typée .NET.

Une remarque d’ordre de grandeur pour situer la jointure : sur ce graphe de 35 triplets, la requête traverse 4 nœuds rdf:Statement et 8 patterns par ligne de résultat — invisible à cette échelle. Sur le graphe industriel de la section 2 (mille faits annotés, sept mille triplets), la même requête joint chaque annotation à son fait via les trois patterns rdf:subject/rdf:predicate/rdf:object : c’est le moment où le choix du moteur compte (dotNetRDF tient la charge sur des graphes de travail ; un store dédié comme Jena/Fuseki prend le relais au-delà), et où RDF-star, qui supprime la jointure en la remplaçant par un motif direct, change réellement la classe de coût.

// Requete 1 : toutes les relations annotées (joiture sur rdf:Statement)
string queryAll = @"
PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>
PREFIX ex:   <http://example.org/>
PREFIX foaf: <http://xmlns.com/foaf/0.1/>

SELECT ?person1 ?person2 ?confidence ?source
WHERE {
    ?stmt a rdf:Statement ;
          rdf:subject ?person1 ;
          rdf:predicate foaf:knows ;
          rdf:object ?person2 ;
          ex:confidence ?confidence .
    OPTIONAL { ?stmt ex:source ?source . }
}
ORDER BY DESC(?confidence)";

// Helper : extraire la valeur lexicale d'un LiteralNode (sans le suffixe ^^datatype).
// r[v].ToString() renvoie "0.95^^...XSD#double" -> on veut "0.95".
double ConfVal(ISparqlResult r, string v) =>
    double.Parse(((ILiteralNode)r[v]).Value, System.Globalization.CultureInfo.InvariantCulture);
// Helper : valeur lexicale d'un LiteralNode string (sans le suffixe ^^datatype).
string LitStr(ISparqlResult r, string v) => r.HasValue(v) && r[v] is ILiteralNode ln ? ln.Value : "-";

var parser = new SparqlQueryParser();
var q = parser.ParseFromString(queryAll);
var results = gk.ExecuteQuery(q) as SparqlResultSet;

Console.WriteLine("=== Relations annotées (triées par confiance) ===");
Console.WriteLine($"{"Personne 1",-12} {"Personne 2",-12} {"Confiance",10} {"Source",-12}");
Console.WriteLine(new string('-', 50));
foreach (var r in results)
{
    var p1 = r["person1"].ToString().Split('/').Last();
    var p2 = r["person2"].ToString().Split('/').Last();
    var conf = ConfVal(r, "confidence");
    var src = LitStr(r, "source");
    Console.WriteLine($"{p1,-12} {p2,-12} {conf,10:F2} {src,-12}");
}
=== Relations annotées (triées par confiance) ===
Personne 1   Personne 2    Confiance Source      
--------------------------------------------------
Alice        Bob                0,95 LinkedIn    
Bob          Carol              0,60 Twitter     
Alice        Dave               0,50 -           
Carol        Dave               0,30 Rumeur      

Lecture du résultat : la jointure rdf:Statement restitue les annotations

La requête produit la table des quatre relations annotées, triées par confiance décroissante : Alice→Bob à 0,95 (LinkedIn), Bob→Carol à 0,60 (Twitter), Alice→Dave à 0,50 (source « - »), Carol→Dave à 0,30 (Rumeur). Mécaniquement, chaque ligne est le produit d’une jointure : les quatre triplets rdf:type rdf:Statement identifient les nœuds-réifications, les patterns rdf:subject/rdf:predicate/rdf:object récupèrent la relation décrite, et les patterns ex:confidence/ex:source ramènent les annotations du même nœud — tout l’appareil de la section 2, requêté en une passe. Un détail mérite l’œil : la ligne Alice→Dave affiche « - » comme source. Ce n’est pas une donnée manquée par la requête — la relation a été réifiée sans annotation ex:source, et SPARQL, dont la sémantique est ouverte, ne fabrique rien : le OPTIONAL (ou l’absence de pattern obligatoire) laisse la variable non liée, rendue ici par le tiret. Une absence d’information reste une information explicite.

Un détail d’affichage français à ne pas confondre avec une donnée : la table montre « 0,95 » avec virgule décimale. La forme lexicale stockée dans le graphe est bien "0.95" à point (conforme xsd:double, grâce à InvariantCulture dans le helper) ; c’est le formatage console {conf,10:F2} qui applique la culture courante de la machine — sur un système configuré en français, le double s’affiche avec virgule. Deux couches bien distinctes : la forme lexicale RDF (invariante, échangée) et le rendu terminal (local, éphémère). Les twins doivent cette vigilance à tout littéral numérique qui traverse une frontière — stockage invariant, affichage local, et jamais l’inverse.

Sur le fond, la table confirme la concordance fixée en tête de notebook : mêmes quatre relations, mêmes scores, même ordre que le twin Python (0,95 → 0,60 → 0,50 → 0,30) — la jointure rdf:Statement restitue fidèlement l’appareil de la section 2, y compris l’absence non liée du OPTIONAL rendue par le tiret.

6. Filtrer par score de confiance

Cas d’usage clé : séparer les faits fiables (confiance ≥ 0,60) des faits incertains (< 0,60). On utilise une clause FILTER SPARQL.

Le seuil de 0,60 n’est pas une constante du standard : c’est un choix de politique de consommation, arbitré selon le coût d’une erreur dans chaque sens. Une application de recommandation tolérera 0,40 ; un dossier d’enquête exigera 0,90. SPARQL rend ce choix externe au graphe — les données portent des scores continus, la politique vit dans la requête, et changer de seuil ne touche aucune donnée.

Syntaxe C# à noter pour la requête : le littéral du FILTER est écrit ""0.60""^^xsd:double — le doublement des guillemets est l’échappement des verbatim strings C# (@"..."), pas une convention SPARQL. Côté requête elle-même, tout est standard : le FILTER compare le littéral typé du graphe au littéral typé de la requête, et l’ordre de sortie (DESC pour les fiables, ascendant pour les incertaines) met chaque moitié dans sa lecture naturelle — la plus solide en tête d’une part, la plus douteuse en tête d’autre part.

string queryHigh = @"
PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>
PREFIX ex:   <http://example.org/>
PREFIX foaf: <http://xmlns.com/foaf/0.1/>
PREFIX xsd:  <http://www.w3.org/2001/XMLSchema#>

SELECT ?person1 ?person2 ?confidence ?source
WHERE {
    ?stmt a rdf:Statement ;
          rdf:subject ?person1 ;
          rdf:predicate foaf:knows ;
          rdf:object ?person2 ;
          ex:confidence ?confidence .
    FILTER (?confidence >= ""0.60""^^xsd:double)
    OPTIONAL { ?stmt ex:source ?source . }
}
ORDER BY DESC(?confidence)";

string queryLow = @"
PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>
PREFIX ex:   <http://example.org/>
PREFIX foaf: <http://xmlns.com/foaf/0.1/>
PREFIX xsd:  <http://www.w3.org/2001/XMLSchema#>

SELECT ?person1 ?person2 ?confidence ?source
WHERE {
    ?stmt a rdf:Statement ;
          rdf:subject ?person1 ;
          rdf:predicate foaf:knows ;
          rdf:object ?person2 ;
          ex:confidence ?confidence .
    FILTER (?confidence < ""0.60""^^xsd:double)
    OPTIONAL { ?stmt ex:source ?source . }
}
ORDER BY ?confidence";

var rsHigh = (SparqlResultSet)gk.ExecuteQuery(new SparqlQueryParser().ParseFromString(queryHigh));
var rsLow  = (SparqlResultSet)gk.ExecuteQuery(new SparqlQueryParser().ParseFromString(queryLow));

Console.WriteLine($"=== Relations FIABLES (confiance >= 0.60) : {rsHigh.Count} ===");
foreach (var r in rsHigh)
{
    var p1 = r["person1"].ToString().Split('/').Last();
    var p2 = r["person2"].ToString().Split('/').Last();
    var src = LitStr(r, "source");
    var conf = ConfVal(r, "confidence");
    Console.WriteLine($"  {p1} connait {p2} (confiance={conf:F2}, source={src})");
}

Console.WriteLine();
Console.WriteLine($"=== Relations INCERTAINES (confiance < 0.60) : {rsLow.Count} ===");
foreach (var r in rsLow)
{
    var p1 = r["person1"].ToString().Split('/').Last();
    var p2 = r["person2"].ToString().Split('/').Last();
    var src = LitStr(r, "source");
    var conf = ConfVal(r, "confidence");
    Console.WriteLine($"  {p1} connait {p2} (confiance={conf:F2}, source={src})");
}
=== Relations FIABLES (confiance >= 0.60) : 2 ===
  Alice connait Bob (confiance=0,95, source=LinkedIn)
  Bob connait Carol (confiance=0,60, source=Twitter)

=== Relations INCERTAINES (confiance < 0.60) : 2 ===
  Carol connait Dave (confiance=0,30, source=Rumeur)
  Alice connait Dave (confiance=0,50, source=-)

Lecture du résultat : le seuil partitionne exactement

Le FILTER (?confidence >= 0.60) coupe le graphe en deux moitiés : 2 relations fiables (Alice→Bob 0,95 LinkedIn ; Bob→Carol 0,60 Twitter) et 2 incertaines (Alice→Dave 0,50 sans source ; Carol→Dave 0,30 Rumeur). La partition n’a rien de magique — 0,60 est un seuil métier arbitré ici pour l’exemple — mais sa lecture est instructive : elle recoupe presque exactement la frontière des sources. Ce qui passe le seuil vient de plateformes nominatives (LinkedIn, Twitter) ; ce qui échoue est soit sans source, soit une « Rumeur ». La confiance chiffrée et la provenance qualitative racontent la même histoire par deux canaux — c’est précisément pourquoi on a annoté les deux : un consommateur du graphe peut filtrer sur le score, auditer via la source, ou exiger leur accord.

La sortie chiffrée le partitionnement annoncé : 2 fiables (Alice→Bob 0,95 LinkedIn ; Bob→Carol 0,60 Twitter), 2 incertaines (Alice→Dave 0,50 sans source ; Carol→Dave 0,30 Rumeur). Le recoupement avec les sources est la vraie leçon de cette table : la frontière du seuil coïncide avec la frontière qualitative des provenances. Ce n’est pas un hasard de l’exemple — c’est la situation visée : quand la confiance est bien calibrée, filtrer sur le score ou auditer par la source conduit à la même partition, et l’écart entre les deux canaux signale les cas à examiner (une source nominative à faible score, ou une rumeur bien notée). Dans un pipeline réel, ce sont précisément ces écarts qu’on met en file d’attente de vérification humaine.

7. Alternative : les graphes nommés

Les graphes nommés (Named Graphs) offrent une autre approche pour grouper des triplets par contexte : plutôt que de réifier chaque triplet individuellement, on regroupe un ensemble de triplets sous une URI de graphe. On peut alors annoter le graphe tout entier (provenance, confiance globale). dotNetRDF expose cela via la classe VDS.RDF.ThreadSafeTripleStore + IGraph nommés.

La granularité est le trade-off central : réification = une annotation par triplet (fin, coûteux), graphe nommé = une annotation par source (grossier, économique). En pratique les deux cohabitent — un graphe nommé par source, contenant des triplets réifiés quand le score varie à l’intérieur d’une même source.

L’alternative change l’unité d’annotation : plus « un nœud par triplet à annoter » mais « un graphe par source qui parle ». Dans la cellule suivante, chaque plateforme (LinkedIn, Twitter) devient un graphe nommé contenant ses affirmations — l’URI du graphe porte la provenance, et une annotation posée sur cette URI (confiance de la source, date de collecte) vaut pour tout son contenu. Le store devient la structure porteuse (TripleStore), et l’identité d’un graphe passe par son nœud-nom — un point de migration dotNetRDF 3.x que la cellule documente en commentaire et que la lecture qui suit détaille.

var store = new TripleStore();

// dotNetRDF 3.x : un graphe nomme se construit via Graph(IRefNode) -- le noeud-nom
// (Name, IRefNode) definit l'identite du graphe dans le store. Le vieux pattern
// `new Graph { BaseUri = uri }` laisse Name == null et les graphes se confondent
// avec le graphe par defaut -> collision a l'ajout. On garde aussi un guard HasGraph
// (non deprecate, via IRefNode) pour rester idempotent sur re-execution dans l'IDE.
var nLinkedIn = new Graph().CreateUriNode(UriFactory.Create(NS + "graph/linkedin"));
var gLinkedIn = new Graph(nLinkedIn);
gLinkedIn.Assert(new Triple(
    gLinkedIn.CreateUriNode(UriFactory.Create(NS + "Alice")),
    gLinkedIn.CreateUriNode(UriFactory.Create("http://xmlns.com/foaf/0.1/knows")),
    gLinkedIn.CreateUriNode(UriFactory.Create(NS + "Bob"))));
if (!store.HasGraph(nLinkedIn)) store.Add(gLinkedIn);

var nTwitter = new Graph().CreateUriNode(UriFactory.Create(NS + "graph/twitter"));
var gTwitter = new Graph(nTwitter);
gTwitter.Assert(new Triple(
    gTwitter.CreateUriNode(UriFactory.Create(NS + "Bob")),
    gTwitter.CreateUriNode(UriFactory.Create("http://xmlns.com/foaf/0.1/knows")),
    gTwitter.CreateUriNode(UriFactory.Create(NS + "Carol"))));
if (!store.HasGraph(nTwitter)) store.Add(gTwitter);

// dotNetRDF 3.x : les requetes sur store passent par un ISparqlQueryProcessor.
// On liste ici directement les graphes nommes du store et leur contenu (plus pedagogique).
Console.WriteLine("=== Graphes nommés du store ===");
foreach (IGraph ng in store.Graphs)
{
    var gName = ng.Name is IUriNode un ? un.Uri.ToString().Split('/').Last() : "(default)";
    Console.WriteLine($"  graphe {gName} : {ng.Triples.Count} triplet(s)");
    foreach (var t in ng.Triples)
        Console.WriteLine($"    {t.Subject} {t.Predicate} {t.Object}");
}
=== Graphes nommés du store ===
  graphe linkedin : 1 triplet(s)
    http://example.org/Alice http://xmlns.com/foaf/0.1/knows http://example.org/Bob
  graphe twitter : 1 triplet(s)
    http://example.org/Bob http://xmlns.com/foaf/0.1/knows http://example.org/Carol

Lecture du résultat : deux niveaux de granularité pour annoter

Le store liste ses graphes nommés — linkedin et twitter, 1 triplet chacun — et leur contenu : la relation que chaque source affirme. Là où la réification annotait chaque triplet individuellement (4 triplets auxiliaires par assertion), le graphe nommé annote un ensemble entier d’un seul geste : l’URI du graphe (ex:linkedin) joue le rôle de provenance, et toute annotation posée sur cette URI (confiance globale, date de collecte, licence) vaut pour tous ses triplets. Note d’API visible dans le code : en dotNetRDF 3.x, un graphe nommé se construit par new Graph(IRefNode) — le nœud-nom définit l’identité — là où l’ancien pattern BaseUri laissait Name == null et un graphe implicitement « par défaut » ; c’est le piège de migration documenté dans le commentaire de la cellule. Le choix réification vs graphes nommés n’est donc pas esthétique : c’est un arbitrage entre granularité fine (chaque triplet porte son propre score) et granularité de source (un score par provenance) — le graphe de connaissances de la section 4 utilise la première, ce store la seconde.

Lecture des mesures et de l’API

Le store liste 2 graphes nommés, 1 triplet chacun : linkedin porte Alice knows Bob, twitter porte Bob knows Carol. Comparez la facture : ces deux relations, réifiées comme en section 4, coûteraient 2 × 4 auxiliaires ; ici elles ne coûtent rien de plus que leur existence — c’est l’économie du mécanisme, et sa contrepartie (les deux relations du même graphe ne peuvent plus porter des confiances différentes sans re-scinder le graphe).

Sur l’API, le commentaire de la cellule mérite son poids : en dotNetRDF 3.x, new Graph(nLinkedIn) (constructeur par nœud) est la seule façon de donner au graphe une identité correcte dans le store. L’ancien pattern new Graph { BaseUri = uri } laisse Name == null — à l’ajout au store, le graphe se confond avec le graphe par défaut, et le second ajout écrase ou fusionne le premier : un bug silencieux typique des migrations 2.x → 3.x. Le guard if (!store.HasGraph(n)) complète la robustesse : la cellule devient idempotent en re-exécution dans l’IDE (un Add répété d’un même nom lèverait autrement une exception de graphe déjà présent).

8. Sérialisation et interopérabilité

dotNetRDF supporte plusieurs formats de sérialisation (Turtle, N-Triples, RDF/XML, JSON-LD). Le Turtle (compressé) est le plus lisible pour la réification.

Le test d’interopérabilité le plus simple est l’aller-retour : sérialiser côté C# avec dotNetRDF, relire côté Python avec rdflib.graph().parse(format="turtle") — le graphe relit doit être isomorphe au graphe sérialisé. C’est le contrat de la paire SW-10 : le même contenu, prouvé depuis les deux écosystèmes.

La cellule suivante sérialise le graphe de connaissances complet (35 triplets, réifications comprises) en Turtle compressé et affiche les compteurs de volume. La question que ce test pose n’est pas « est-ce joli ? » mais « est-ce échangeable ? » : le texte produit doit être consommable tel quel par rdflib côté Python, par Jena, par tout parseur Turtle conforme — la sérialisation n’est qu’un encodage du graphe abstrait, et la preuve d’interopérabilité se joue à la relecture.

// Serialisation du graphe de connaissances en Turtle compressé
var w = new CompressingTurtleWriter();
var sw2 = new System.IO.StringWriter();
w.Save(gk, sw2);
var turtle = sw2.ToString();
Console.WriteLine($"Serialisation Turtle : {turtle.Length} caracteres, {gk.Triples.Count} triplets");
Console.WriteLine("--- (extrait) ---");
Console.WriteLine(turtle.Substring(0, Math.Min(500, turtle.Length)));
Serialisation Turtle : 1370 caracteres, 35 triplets
--- (extrait) ---
@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 ex: <http://example.org/>.
@prefix foaf: <http://xmlns.com/foaf/0.1/>.

ex:Alice a foaf:Person;
         foaf:knows ex:Bob;
         foaf:name "Alice Dupont".
ex:Bob a foaf:Person;
       foaf:knows ex:Carol;
       foaf:name "Bob Martin".
ex:Carol a foaf:Person;
         foaf:knows ex:Dave;
         foaf:name "

Lecture du résultat : 1370 caractères pour 35 triplets

La sérialisation du graphe complet tient en 1370 caractères pour 35 triplets — environ 39 caractères par triplet, contre plus de 100 en N-Triples où chaque URI est répétée en clair. Le gain vient de la compression : préfixes déclarés une fois, prédicats multiples factorisés par le point-virgule, objets partagés par la virgule. C’est aussi ce qui fait du Turtle le format d’interopérabilité pratique : le fichier produit ici est parsable tel quel par rdflib (le twin Python), par Jena, ou par tout moteur conforme — la sérialisation est un encodage du même graphe abstrait, pas une donnée propriétaire. L’aller-retour sérialiser/relire sans perte est l’invariant qui rend la chaîne C# → disque → Python possible.

Lecture des mesures

Les compteurs donnent la densité exacte : 1370 caractères pour 35 triplets, soit ~39 caractères par triplet compressé — contre plus de 100 en N-Triples non compressé (URIs en clair, une ligne par triplet). Le facteur ~2,5 tient à trois compressions visibles dans l’extrait : les préfixes déclarés une fois pour toutes (ex: pour 20 caractères d’URI économisés par occurrence), le point-virgule qui factorise le sujet commun, et le regroupement des objets d’un même prédicat.

L’extrait s’interrompt au milieu du nom de Carol — il ne montre que les 500 premiers caractères (coupure Substring(0, 500) du code), pas une sérialisation tronquée : les 870 caractères restants contiennent les noms manquants et les quatre blocs rdf:Statement. Détail de lecture : la sérialisation n’affiche aucun ex:Alice foaf:knows ex:Dave — la relation mentionnée de la section 4 est invisible hors de son bloc de réification, comme prévu.

Exercices

Les exercices ci-dessous sont à compléter (convention C.1 : stubs exécutables sans erreur).

Exercice 1 — Ajouter une annotation de date de validité

Étendez le graphe de connaissances : pour chaque fait annoté, ajoutez un triplet ex:validSince avec une date typée xsd:date. Modifiez ensuite la requête SPARQL pour afficher cette colonne.

Méthode conseillée : les trois exercices réutilisent les objets déjà construits (gk, stmt1..stmt4, store) — aucune reconstruction n’est nécessaire, seulement des Assert additionnels et des requêtes. En cas de blocage, le réflexe est toujours le même : retourner à la section dont l’exercice transpose le geste (sections 4, 5 et 7 respectivement), relire la requête modèle, puis n’en changer que la clause demandée.

// Exercice 1 : annotation temporelle
// TODO : pour chaque stmt (stmt1..stmt4), ajouter gk.Assert(new Triple(stmt, N("validSince"),
//        gk.CreateLiteralNode("2020-01-01", xsd:date))).
// Indice : utilisez gk.CreateLiteralNode(dateStr, new Uri("http://www.w3.org/2001/XMLSchema#date")).
// Etape 1 : choisir une date de validité par fait. Etape 2 : modifier queryAll pour ajouter ?since.
Console.WriteLine("Exercice 1 a completer : annotation de date de validite sur les faits.");
Exercice 1 a completer : annotation de date de validite sur les faits.

Exercice 2 — Requête de transitivité inférée

Alice connaît Bob (0,95), Bob connaît Carol (0,60). Écrivez une requête SPARQL qui calcule les « connaissances transitives » (chemins de longueur 2) et combine les confiances par produit.

Piège classique de l’exercice : la jointure double doit porter sur deux nœuds-réifications distincts (?stmtA pour a→b, ?stmtB pour b→c), sinon la requête ne matche rien. Et le produit des confiances se fait dans le SELECT ((?cA * ?cB) AS ?confChemin), pas dans le WHERE — SPARQL n’évalue d’expressions arithmétiques que sur les résultats. Vérification attendue : Alice→Bob→Carol donne 0,95 × 0,60 = 0,57.

// Exercice 2 : transitivité des connaissances
// TODO : requete SPARQL avec deux jointures ?a foaf:knows ?b . ?b foaf:knows ?c .
// Indice : la confiance d'un chemin a->b->c = confiance(a->b) * confiance(b->c).
// Etape 1 : ecrire la jointure sur les stmt reifies. Etape 2 : multiplier les confiances dans le SELECT.
Console.WriteLine("Exercice 2 a completer : requete de connaissances transitives (longueur 2).");
Exercice 2 a completer : requete de connaissances transitives (longueur 2).

Exercice 3 — Graphes nommés multi-sources

Construisez un 3e graphe nommé (par ex. graph/intranet) avec un fait Bob→Dave, puis écrivez une requête qui fusionne les 3 graphes et identifie les faits présents dans plusieurs sources.

L’indice de la cellule contient les deux clés : GRAPH ?g { ?s foaf:knows ?o } ouvre tous les graphes nommés du store en une seule requête, et le GROUP BY ?s ?o HAVING(COUNT(*) > 1) isole les faits corroborés par plusieurs sources. Avec les données présentes (linkedin : Alice→Bob ; twitter : Bob→Carol), il faudra que votre graphe intranet affirme l’un des deux faits existants pour que la corroboration produise une ligne — sinon la requête retourne vide, ce qui sera aussi une réponse à interpréter.

// Exercice 3 : fusion multi-sources via graphes nommés
// TODO : ajouter store.Add(gIntranet) avec un fait, puis requete UNION/GRAPH combinant les sources.
// Indice : une requete SELECT ?g ?s ?o WHERE { GRAPH ?g { ?s foaf:knows ?o } } liste tous les faits
// avec leur graphe d'origine ; un GROUP BY ?s ?o HAVING(COUNT(*)>1) trouve les faits multi-sources.
Console.WriteLine("Exercice 3 a completer : 3e graphe nommé + detection des faits multi-sources.");
Exercice 3 a completer : 3e graphe nommé + detection des faits multi-sources.

Conclusion — synthèse

Mécanisme Coût (triplets) Lisibilité Portabilité
Triplet nu 1 excellente totale
Réification classique 4 + N annotations verbeuse totale (RDF 1.0)
RDF-star (<< >>) 1 + N excellente récente (RDF 1.2)
Graphes nommés 1 + annotation du graphe bonne totale (SPARQL 1.1)

Leçon clé : la réification classique est le mécanisme portable pour annoter des triplets (provenance, confiance, temporalité) — au prix d’une verbosité de 4 triplets auxiliaires par fait. RDF-star (RDF 1.2) vise à éliminer ce coût avec une syntaxe dédiée << s p o >>, mais reste un standard jeune. Les graphes nommés offrent un compromis intermédiaire : grouper des triplets par contexte et annoter le groupe. dotNetRDF gère les trois (Graph, SparqlQueryParser, TripleStore) avec la même API VDS.RDF que ce notebook utilise.


Navigation : << SW-9 JSONLD | Index SemanticWeb | Jumeau Python : SW-10-Python-RDFStar

Marathon #4956 — parité .NET ⇄ Python. Prong A : vrai moteur dotNetRDF 3.4.1 natif invoqué.

Quelle approche choisir ?

  • Triplet nu quand la donnée est certaine et sans contexte à porter — l’essentiel d’un graphe de faits.
  • Réification quand chaque assertion porte son propre degré de croyance ou sa propre provenance — fusion de sources hétérogènes, graphe de connaissances auditable.
  • Graphe nommé quand la provenance est homogène par lots — ingestion multi-sources où chaque source parle pour tout ce qu’elle apporte.
  • RDF-star quand le moteur le supporte : la même expressivité que la réification à la syntaxe près, au prix d’une adoption encore partielle (RDF 1.2).

La compétence visée n’est pas de connaître les quatre par cœur, mais de choisir la granularité d’annotation adaptée à la question « qui affirme quoi, et à quel point le croire ? ».

Ce qu’il faut retenir côté C

Les gestes du notebook se traduisent un-à-un vers rdflib (Graph ↔︎ rdflib.Graph, Assert ↔︎ add, CompressingTurtleWriter ↔︎ serialize(format="turtle"), TripleStore + graphe nommé ↔︎ ConjunctiveGraph/Dataset) : la compétence transférable est le modèle RDF — réification, nœuds, sérialisation, SPARQL — pas l’API. La différence notable est culturelle : dotNetRDF expose l’identité d’un graphe nommé par son nœud (Graph(IRefNode)), quand rdflib la porte par URI — même mathématique, ergonomie propre à chaque écosystème.

Fil des mesures du jumeau

Le parcours chiffré du notebook condense le tableau ci-dessus : 7 triplets pour le premier fait annoté (section 2), 14 après le second (section 3), 35 pour le graphe de connaissances complet (section 4) — la réification facture partout ses 4 auxiliaires par fait, et la sérialisation finale (1370 caractères, section 8) en montre le volume. Les graphes nommés (section 7) n’ont facturé que 2 nœuds-noms pour 2 relations — l’économie mesurée du mécanisme alternatif. Toutes ces valeurs concordent avec le twin Python aux vocabulaires d’annotation près (37 vs 35 : deux annotations since de plus côté Python), et c’est la discipline de la paire : mêmes concepts, mêmes scénarios, mesures en regard.

Ce qui doit rester du côté C# spécifiquement : les trois marches d’API franchies ici — nœuds-réifications URI nommés (adressables) plutôt que blank nodes, littéraux typés en culture invariante (sinon le xsd:double casse silencieusement en culture française), identité des graphes nommés par nœud-nom Graph(IRefNode) (le piège de migration 3.x). Trois détails qui ne touchent pas au modèle RDF — et c’est le point : le modèle, lui, est exactement le même des deux côtés de la paire.

Retour au sommet