#r "nuget: dotNetRDF, 3.2.1"- dotNetRDF, 3.2.1
Navigation : << 3-GraphOperations | Index | 5-LinkedData >>
A la fin de ce notebook, vous saurez : 1. Comprendre SPARQL comme le SQL du Web sémantique — ce notebook couvre la moitié interrogation du langage (SELECT et ses clauses) ; la moitié manipulation (SPARQL 1.1 Update : INSERT/DELETE) est traitée dans SW-4b-Python-SPARQL, section « SPARQL Update : la seconde moitié du langage » 2. Ecrire des requêtes SELECT avec variables et patterns de triplets 3. Utiliser FILTER et OPTIONAL pour affiner les résultats 4. Combiner des patterns avec UNION 5. Trier et paginer avec ORDER BY, LIMIT, OFFSET 6. Construire des requêtes programmatiquement avec le QueryBuilder de dotNetRDF
Notebook C# / .NET Interactive sur les requêtes SPARQL dans dotNetRDF.
Pourquoi ce notebook dans la série SemanticWeb : - C’est le 4e notebook technique C# de la série (après SW-1 Setup, SW-2 RDF Basics, SW-3 Graph Opérations). - Il introduit le langage de requête standard du Web sémantique (SPARQL 1.1, W3C 2013). - Cas Prong B applicable (sota-not-workaround) : on utilise le vrai moteur SPARQL de dotNetRDF (lib C# de référence), pas une reimplementation jouet.
Deux approches coexistent dans ce notebook, et c’est deliberé : - chaîne brute avec SparqlQueryParser — simple, syntaxe directe, seule voie pour LIMIT/OFFSET ; - QueryBuilder fluide (IQueryBuilder) — type-safe et composable.
Chaque section montre l’une, l’autre, ou les deux, et la section 6 les compare frontalement.
Prerequis : SW-3-GraphOperations (lecture/ecriture/fusion/sélection), notions de C#/.NET.
Importation des espaces de noms dotNetRDF pour SPARQL, QueryBuilder et la gestion des parsers.
La sortie verbatim est dotNetRDF 3.2.1 pret - espaces de noms SPARQL charges. : la bibliotheque est chargée et les espaces de noms spécifiques a SPARQL sont accessibles.
Les espaces de noms importes :
| Namespace | Ce qu’il apporte |
|---|---|
VDS.RDF |
Graph, Triple, INode, IUriNode, ILiteralNode, IBlankNode |
VDS.RDF.Parsing |
parsers Turtle, NTriples, RDF/XML |
VDS.RDF.Query |
SparqlQueryParser, SparqlResultSet, ISparqlQuery |
VDS.RDF.Query.Builder |
IQueryBuilder (builder fluide), QueryBuilder (classe statique de depart) |
VDS.RDF.Query.Datasets |
InMemoryDataset, pour les graphes locaux |
Note .NET Interactive : les using sont executes en début de cellule et persistent pour les cellules suivantes du notebook. Pas besoin de les repeter.
Ou s’executent les requêtes de ce notebook : via g.ExecuteQuery(sparql) as SparqlResultSet, sur le moteur SPARQL integre de dotNetRDF (Leviathan) et sur des graphes en memoire. Les endpoints HTTP distants sont le sujet de SW-5 (Linked Data).
SPARQL (SPARQL Protocol and RDF Query Language) est le langage de requête standardise par le W3C pour interroger des graphes RDF, specifie dans SPARQL 1.1 (Harris & Seaborne, SPARQL 1.1 Query Language, W3C Recommendation 2013). Il joue pour RDF le même rôle que SQL pour les bases relationnelles.
| SQL | SPARQL | Description |
|---|---|---|
SELECT col FROM table WHERE condition |
SELECT ?var WHERE { pattern } |
Extraire des données |
| Tables et colonnes | Graphes et triplets | Structure de données |
JOIN |
Patterns partageant des variables | Jointure |
WHERE condition |
FILTER(condition) |
Filtrage |
Trois composantes structurent le langage : - Pattern matching : on cherche les sous-graphes qui matchent un pattern de triplets (sujet-prédicat-objet, avec variables). - Filtres : FILTER applique des conditions sur les valeurs (numériques, regex, chaînes). - Formes de résultats : SELECT (table), CONSTRUCT (graphe), ASK (booléen), DESCRIBE (graphe). Ce notebook couvre SELECT.
Syntaxe de base :
PREFIX prefix: <URI>
SELECT ?variable1 ?variable2
WHERE {
?sujet prefix:predicat ?objet .
FILTER(?objet > 10)
}
Une requête SELECT retourne un ensemble de lignes (bindings) pour les variables demandees ; le pattern WHERE définit les contraintes sur les triplets.
Pattern minimal :
SELECT ?x WHERE { ?x ?p ?o . }
Cette requête retourne tous les sujets du graphe (avec doublons possibles).
Pourquoi commencer par SELECT : - C’est la forme la plus naturelle (analogie SQL directe). - Les résultats sont des SparqlResult (dictionnaire variable -> valeur), faciles a manipuler en C#. - C’est la base dont les autres formes (CONSTRUCT, ASK, DESCRIBE) sont des variantes.
// 2.1 Requete SELECT simple avec QueryBuilder
string x = "x";
var queryBuilder = QueryBuilder
.Select(new string[] { x })
.Where(
(triplePatternBuilder) =>
{
triplePatternBuilder
.Subject(x)
.PredicateUri(new Uri("http://www.w3.org/2001/vcard-rdf/3.0#FN"))
.Object("John Smith");
});
var query = queryBuilder.BuildQuery();
Console.WriteLine("=== Requete generee ===");
Console.WriteLine(query.ToString());=== Requete generee ===
SELECT ?x WHERE
{ ?x <http://www.w3.org/2001/vcard-rdf/3.0#FN> ?John Smith . }
La requête cherche tous les sujets ?x ayant un triplet de prédicat vcard:FN. C’est la forme la plus basique de SPARQL.
Sortie observée (verbatim) :
=== Requete generee ===
SELECT ?x WHERE
{ ?x <http://www.w3.org/2001/vcard-rdf/3.0#FN> ?John Smith . }
Correspondance code -> SPARQL genere :
| Composant | Code QueryBuilder | SPARQL genere |
|---|---|---|
| Variables retournees | .Select(new[] { "x" }) |
SELECT ?x |
| Pattern sujet | .Subject(x) |
?x (variable) |
| Pattern prédicat | .PredicateUri(uri) |
<uri> (URI fixe) |
| Pattern objet | .Object("John Smith") |
?John Smith (variable) |
Piege QueryBuilder :
.Object("John Smith")(string simple) est lu comme un nom de variable (?John Smith), pas comme un littéral. Pour filtrer sur la valeur littérale"John Smith", il faut un noeud type :NodeFactory.CreateLiteralNode("John Smith"). C’est visible dans la sortie ci-dessus — le?devantJohn Smithtrahit la variable.
Pourquoi le QueryBuilder plutot que la chaîne brute : - Type-safe : les erreurs de syntaxe sont detectees a la compilation. - Refactoring : renommer une variable se propage depuis l’IDE. - Composition : la requête se construit par étapes (build conditionnel).
Cas d’usage typique : trouver toutes les personnes portant un nom donne, tous les sujets d’un certain type.
// 2.2 PREFIX avec plusieurs patterns de triplets
var prefixes = new NamespaceMapper(true);
prefixes.AddNamespace("vcard", new Uri("http://www.w3.org/2001/vcard-rdf/3.0#"));
string y = "y";
var givenName = new SparqlVariable("givenName");
var qb = QueryBuilder
.Select(new SparqlVariable[] { givenName })
.Where(
(tp) =>
{
tp.Subject(y).PredicateUri("vcard:Family").Object("Smith");
tp.Subject(y).PredicateUri("vcard:Given").Object(givenName);
});
qb.Prefixes = prefixes;
Console.WriteLine("=== SELECT avec PREFIX ===");
Console.WriteLine(qb.BuildQuery().ToString());=== SELECT avec PREFIX ===
PREFIX vcard: <http://www.w3.org/2001/vcard-rdf/3.0#>
SELECT ?givenName WHERE
{
?y vcard:Family ?Smith .
?y vcard:Given ?givenName .
}
Le mot-cle PREFIX declare des abreviations d’URI — l’équivalent des imports en programmation :
| Sans prefix | Avec prefix |
|---|---|
<http://www.w3.org/2001/vcard-rdf/3.0#FN> |
vcard:FN |
Requête générée :
PREFIX vcard: <http://www.w3.org/2001/vcard-rdf/3.0#>
SELECT ?givenName
WHERE {
?y vcard:Family ?Smith .
?y vcard:Given ?givenName .
}
Decomposition : - PREFIX vcard: <...> declare le prefixe vcard: pour http://www.w3.org/2001/vcard-rdf/3.0#. - Deux patterns de triplets, joints par la variable partagee ?y (même sujet).
Pourquoi deux patterns — plusieurs patterns dans un même Where() forment une conjonction (AND) : ?y doit satisfaire les deux contraintes simultanement. - C’est l’équivalent d’un INNER JOIN en SQL. - Les variables partagees (ici ?y) jouent le rôle des clés de jointure. - Si le premier pattern matche 100 sujets et le second 50, le résultat en compte au plus 50 — ceux qui matchent les deux.
Résultat : pour chaque personne dont Family = "Smith", on retourne son Given name.
Les deux clauses de cette section repondent a deux besoins opposes :
Les trois cellules qui suivent les illustrent dans cet ordre : filtre numérique, filtre regex, puis OPTIONAL combine a un FILTER.
// 3.1 FILTER numerique
var prefixes = new NamespaceMapper(true);
prefixes.AddNamespace("info", new Uri("http://somewhere/peopleInfo#"));
string resource = "resource";
string age = "age";
var qb = QueryBuilder
.Select(new string[] { resource })
.Where(
(tp) =>
{
tp.Subject(resource).PredicateUri($"info:{age}").Object(age);
})
.Filter((b) => b.Variable(age) > 24);
qb.Prefixes = prefixes;
Console.WriteLine("=== FILTER numerique ===");
Console.WriteLine(qb.BuildQuery().ToString());=== FILTER numerique ===
PREFIX info: <http://somewhere/peopleInfo#>
SELECT ?resource WHERE
{
?resource info:age ?age .
FILTER(?age > 24 )
}
FILTER applique une condition sur les valeurs des variables, avec les opérateurs de comparaison standards (>, <, >=, <=, =, !=).
Sortie observée (verbatim) :
=== FILTER numerique ===
PREFIX info: <http://somewhere/peopleInfo#>
SELECT ?resource WHERE
{
?resource info:age ?age .
FILTER(?age > 24 )
}
Decomposition : - ?resource info:age ?age matche les triplets de prédicat info:age. - FILTER(?age > 24) ne garde que ceux dont ?age > 24.
Ce que fait le QueryBuilder ici : il traduit l’expression C# b.Variable(age) > 24 en syntaxe SPARQL equivalente. Noter que ?age a deux rôles dans la même requête — objet du pattern de triplet, et variable de filtrage.
Remarque sur la modelisation : le prédicat info:age (URI http://somewhere/peopleInfo#age) montre qu’une propriete RDF represente n’importe quelle relation ; ici l’age est un littéral numérique, ce qui rend la comparaison possible dans FILTER.
Cas d’usage : filtrer des produits par prix, des personnes par age, des événements par date.
// 3.2 FILTER Regex (expressions regulieres)
var prefixes = new NamespaceMapper(true);
prefixes.AddNamespace("vcard", new Uri("http://www.w3.org/2001/vcard-rdf/3.0#"));
var givenName = new SparqlVariable("givenName");
var qb = QueryBuilder
.Select(new SparqlVariable[] { givenName })
.Where(
(tp) =>
{
tp.Subject("y").PredicateUri("vcard:Given").Object(givenName);
})
.Filter((b) => b.Regex(b.Variable("givenName"), "sarah", "i"));
qb.Prefixes = prefixes;
Console.WriteLine("=== FILTER Regex ===");
Console.WriteLine(qb.BuildQuery().ToString());=== FILTER Regex ===
PREFIX vcard: <http://www.w3.org/2001/vcard-rdf/3.0#>
SELECT ?givenName WHERE
{
?y vcard:Given ?givenName .
FILTER(REGEX(?givenName,"sarah","i"))
}
FILTER supporte aussi les expressions régulières via REGEX(?var, pattern, flags), pour les filtres textuels.
Sortie observée (verbatim) :
=== FILTER Regex ===
PREFIX vcard: <http://www.w3.org/2001/vcard-rdf/3.0#>
SELECT ?givenName WHERE
{
?y vcard:Given ?givenName .
FILTER(REGEX(?givenName,"sarah","i"))
}
REGEX(?givenName, "sarah", "i") matche les chaînes contenant « sarah », insensible a la casse grace au flag "i".
Flags disponibles : "i" insensible a la casse · "s" single-line (. matche \n) · "m" multi-line (^/$ sur chaque ligne) · "x" extended (espaces et commentaires ignores).
Quand l’utiliser : - Patterns partiels : la valeur exacte est inconnue (tous les noms commencant par « A »). - Normalisation : on filtre après avoir normalise (lowercase, trim). - Validation : on vérifie un format (email, telephone).
Note de portee : SPARQL REGEX suit la syntaxe XQuery/XPath, pas POSIX. Pour des cas simples, preferer l’egalite ou les opérateurs de comparaison — un regex est plus couteux a evaluer.
Recapitulatif des trois formes de FILTER vues jusqu’ici :
| Type de filtre | Syntaxe SPARQL | Code QueryBuilder |
|---|---|---|
| Comparaison | FILTER(?age > 24) |
.Filter(b => b.Variable(age) > 24) |
| Regex | FILTER(REGEX(?name, "pattern", "i")) |
.Filter(b => b.Regex(...)) |
| Egalite | FILTER(?x = 10) |
.Filter(b => b.Variable(x) == 10) |
// 3.3 OPTIONAL avec FILTER
var prefixes = new NamespaceMapper(true);
prefixes.AddNamespace("info", new Uri("http://somewhere/peopleInfo#"));
prefixes.AddNamespace("vcard", new Uri("http://www.w3.org/2001/vcard-rdf/3.0#"));
string name = "name";
string age = "age";
string person = "person";
var qb = QueryBuilder
.Select(new string[] { name, age })
.Where(
(tp) =>
{
tp.Subject(person).PredicateUri("vcard:FN").Object(name);
})
.Optional(
(optionalBuilder) =>
{
optionalBuilder.Where(
(tp) =>
{
tp.Subject(person).PredicateUri($"info:{age}").Object(age);
});
optionalBuilder.Filter((b) => b.Variable(age) > 42);
});
qb.Prefixes = prefixes;
Console.WriteLine("=== OPTIONAL avec FILTER ===");
Console.WriteLine(qb.BuildQuery().ToString());=== OPTIONAL avec FILTER ===
PREFIX info: <http://somewhere/peopleInfo#>
PREFIX vcard: <http://www.w3.org/2001/vcard-rdf/3.0#>
SELECT ?name ?age WHERE
{
?person vcard:FN ?name .
OPTIONAL {
?person info:age ?age .
FILTER(?age > 42 )
}
}
OPTIONAL rend un pattern facultatif : si le pattern ne matche pas, le sujet reste dans le résultat et la variable optionnelle est laissee non liee (unbound).
Sortie observée (verbatim) :
=== OPTIONAL avec FILTER ===
PREFIX info: <http://somewhere/peopleInfo#>
PREFIX vcard: <http://www.w3.org/2001/vcard-rdf/3.0#>
SELECT ?name ?age WHERE
{
?person vcard:FN ?name .
OPTIONAL {
?person info:age ?age
La requête cherche les personnes ayant un nom (vcard:FN) et optionnellement un age (info:age). Une personne sans age apparait quand même, avec age = unbound.
| Sans OPTIONAL (WHERE strict) | Avec OPTIONAL |
|---|---|
| Personne sans age : exclue | Personne sans age : incluse (age non lie) |
C’est exactement la différence entre un INNER JOIN et un LEFT JOIN en SQL.
Implementation C# (QueryBuilder) :
var qb = QueryBuilder
.Select(new string[] { name, age })
.Where(
(tp) =>
{
tp.Subject(person).PredicateUri("vcard:FN").Object(name);
})
.Optional(
(optionalBuilder) =>
{
optionalBuilder.Where(
(tp) =>
{
tp.Subject(person).PredicateUri($"info:{age}").Object(age);
});
optionalBuilder.Filter((b) => b.Variable(age) > 42);
});
qb.Prefixes = prefixes;Le Filter place a l’interieur du bloc Optional restreint la valeur optionnelle sans exclure le sujet : toutes les personnes ayant un nom sont retournees, l’age n’apparait que s’il depasse 42.
Pourquoi OPTIONAL est critique en pratique : - Sources heterogenes : les données Linked Data viennent de sources multiples, chacune avec son schema. - Evolution de schema : un champ ajoute recemment n’existe pas dans les données anciennes. - Tolerance : une source partiellement incomplete ne fait pas perdre les autres données.
Pattern recommande : WHERE strict pour les identifiants (nom, type) · OPTIONAL pour les attributs facultatifs (age, email, telephone) · FILTER dans l’OPTIONAL pour restreindre la valeur optionnelle.
UNION combine des patterns alternatifs (OR logique) : un résultat est retourne s’il correspond a l’un ou l’autre des patterns.
UNION combine deux patterns : un résultat est retenu s’il matche l’un OU l’autre.
Sortie observée de code[7] (verbatim) : la requête UNION cherche les sujets ayant soit foaf:name soit vcard:FN.
Syntaxe :
SELECT ?name WHERE {
{ ?s foaf:name ?name . }
UNION
{ ?s vcard:FN ?name . }
}
Implementation C# (QueryBuilder) :
var qb = QueryBuilder
.Select(new string[] { name, age })
.Where(
(tp) =>
{
tp.Subject(person).PredicateUri("vcard:FN").Object(name);
})
.Optional(
(optionalBuilder) =>
{
optionalBuilder.Where(
(tp) =>
{
tp.Subject(person).PredicateUri($"info:{age}").Object(age);
});
optionalBuilder.Filter((b) => b.Variable(age) > 42);
});
qb.Prefixes = prefixes;Cas d’usage : - Données heterogenes : certaines personnes utilisent foaf, d’autres vcard. - Migration de schemas : on supporte l’ancien et le nouveau schema. - Equivalences sémantiques : owl:equivalentClass, owl:equivalentProperty.
Performance : - UNION sur de gros graphes peut etre lent (double evaluation des patterns). - Preferez OPTIONAL quand c’est possible. - Indexez les prédicats frequents (foaf:name, rdf:type).
// 4.1 UNION de deux patterns
var prefixes = new NamespaceMapper(true);
prefixes.AddNamespace("foaf", new Uri("http://xmlns.com/foaf/0.1/"));
prefixes.AddNamespace("vcard", new Uri("http://www.w3.org/2001/vcard-rdf/3.0#"));
string name = "name";
var qb = QueryBuilder.Select(new string[] { name });
qb.Union(
(unionBuilder) =>
{
unionBuilder.Where(
(tp) => { tp.Subject<IBlankNode>("abc").PredicateUri($"foaf:{name}").Object(name); });
},
(unionBuilder) =>
{
unionBuilder.Where(
(tp) => { tp.Subject<IBlankNode>("abc").PredicateUri("vcard:FN").Object(name); });
});
qb.Prefixes = prefixes;
Console.WriteLine("=== UNION ===");
Console.WriteLine(qb.BuildQuery().ToString());=== UNION ===
PREFIX foaf: <http://xmlns.com/foaf/0.1/>
PREFIX vcard: <http://www.w3.org/2001/vcard-rdf/3.0#>
SELECT ?name WHERE
{ { _:abc foaf:name ?name . }
UNION
{ _:abc vcard:FN ?name . } }
UNION combine les résultats de deux patterns : un sujet entre dans le résultat s’il matche l’un OU l’autre. Ici, les sujets ayant soit foaf:name, soit vcard:FN.
SELECT ?name
WHERE {
{ _:abc foaf:name ?name }
UNION
{ _:abc vcard:FN ?name }
}
UNION vs OPTIONAL — c’est la confusion la plus frequente, et les deux clauses ne se substituent pas :
| Cas | UNION | OPTIONAL |
|---|---|---|
Personne avec foaf:name seulement |
Incluse | Incluse |
Personne avec vcard:FN seulement |
Incluse | Depend du pattern principal |
| Personne avec les deux | 2 résultats | 1 résultat |
UNION : deux alternatives independantes (OR). OPTIONAL : un pattern principal, complété facultativement.
Performance : - UNION sur de grands graphes peut etre lent (double evaluation des deux branches). - Preferer OPTIONAL quand la sémantique le permet (evaluation simple). - Reserver UNION aux cas ou les patterns sont vraiment independants.
Note de portee : SPARQL 1.1 autorise la combinaison des deux (un OPTIONAL d’un UNION, par exemple). C’est expressif mais difficile a lire et a optimiser — a utiliser avec parcimonie.
Le tri et la pagination controlent l’ordre et la quantite de résultats retournes.
ORDER BY trie les résultats, LIMIT restreint le nombre, OFFSET décale le début.
Sortie observée de code[9] (verbatim) : la requête utilise ORDER BY DESC(?age) LIMIT 10 OFFSET 5. La cellule montre aussi comment acceder aux proprietes de la requête (Type, Limit, Offset).
Implementation C# :
Pagination keyset (alternative OFFSET) :
string sparql = @"
PREFIX foaf: <http://xmlns.com/foaf/0.1/>
SELECT ?name ?age
WHERE {
?person foaf:name ?name .
?person foaf:age ?age .
}
ORDER BY DESC(?age)
LIMIT 10
OFFSET 5
";
var sparqlParser = new SparqlQueryParser();
var parsedQuery = sparqlParser.ParseFromString(sparql);
Console.WriteLine($"Limit : {parsedQuery.Limit}, Offset : {parsedQuery.Offset}");Cas d’usage : - Affichage pagine : LIMIT/OFFSET pour les UI classiques. - Batch processing : keyset pour les traitements longs (meilleure performance). - Top N : ORDER BY DESC(?score) LIMIT 10.
// 5.1 ORDER BY
var prefixes = new NamespaceMapper(true);
prefixes.AddNamespace("foaf", new Uri("http://xmlns.com/foaf/0.1/"));
string name = "name";
var qb = QueryBuilder
.Select(new string[] { name })
.Where(
(tp) =>
{
tp.Subject("x").PredicateUri($"foaf:{name}").Object(name);
})
.OrderBy(name);
qb.Prefixes = prefixes;
Console.WriteLine("=== ORDER BY ===");
Console.WriteLine(qb.BuildQuery().ToString());=== ORDER BY ===
PREFIX foaf: <http://xmlns.com/foaf/0.1/>
SELECT ?name WHERE
{ ?x foaf:name ?name . }
ORDER BY ASC(?name)
ORDER BY trie les résultats par une ou plusieurs variables, en ordre croissant (ASC, défaut) ou décroissant (DESC).
Sortie observée (verbatim) :
=== ORDER BY ===
PREFIX foaf: <http://xmlns.com/foaf/0.1/>
SELECT ?name WHERE
{ ?x foaf:name ?name . }
ORDER BY ASC(?name)
Le QueryBuilder a traduit .OrderBy("name") en ORDER BY ASC(?name) : le tri est ascendant par défaut.
Variantes : - ORDER BY DESC(?name) : tri décroissant. - ORDER BY DESC(?age) ?name : tri multi-critères — age décroissant, puis nom croissant.
Pourquoi ORDER BY est en fin de requête : c’est une clause de post-traitement, appliquee après l’evaluation du WHERE. L’ordre de sortie est indépendant de l’ordre d’evaluation des patterns.
Limite du QueryBuilder, et ce qu’elle implique pour la suite : .OrderBy(var) existe, mais LIMIT et OFFSET ne sont pas exposes par l’API fluide. La cellule suivante bascule donc volontairement sur une chaîne SPARQL brute parsee par SparqlQueryParser.
| Approche | Code | Avantage |
|---|---|---|
| QueryBuilder | .OrderBy("name") |
Type safety, IntelliSense |
| Chaîne brute | ORDER BY DESC(?age) |
Syntaxe directe, acces a LIMIT/OFFSET |
Note : le tri est couteux sur de gros graphes. Sur des résultats déjà pagines, preferer un tri local (LINQ C#).
// 5.2 LIMIT et OFFSET en chaine brute
string sparql = @"
PREFIX foaf: <http://xmlns.com/foaf/0.1/>
SELECT ?name ?age
WHERE {
?person foaf:name ?name .
?person foaf:age ?age .
}
ORDER BY DESC(?age)
LIMIT 10
OFFSET 5
";
var sparqlParser = new SparqlQueryParser();
var parsedQuery = sparqlParser.ParseFromString(sparql);
Console.WriteLine("=== LIMIT + OFFSET ===");
Console.WriteLine(parsedQuery.ToString());
Console.WriteLine($"\nType de requete : {parsedQuery.QueryType}");
Console.WriteLine($"Limit : {parsedQuery.Limit}");
Console.WriteLine($"Offset : {parsedQuery.Offset}");=== LIMIT + OFFSET ===
PREFIX foaf: <http://xmlns.com/foaf/0.1/>
SELECT ?name ?age WHERE
{
?person foaf:age ?age .
?person foaf:name ?name .
}
ORDER BY DESC(?age) LIMIT 10 OFFSET 5
Type de requete : Select
Limit : 10
Offset : 5
LIMIT restreint le nombre de résultats, OFFSET décale le début. Combinés à ORDER BY, ils donnent la pagination.
Sortie observée (verbatim) :
=== LIMIT + OFFSET ===
PREFIX foaf: <http://xmlns.com/foaf/0.1/>
SELECT ?name ?age WHERE
{
?person foaf:age ?age .
?person foaf:name ?name .
}
ORDER BY DESC(?age) LIMIT 10 OFFSET 5
Type de requete : Select
Limit : 10
Offset : 5
Decomposition : ORDER BY DESC(?age) trie par age décroissant · LIMIT 10 plafonne a 10 résultats · OFFSET 5 saute les 5 premiers (on commence au 6e).
Les trois dernières lignes de la sortie ne viennent pas de la requête mais de son analyse statique : SparqlQueryParser valide la syntaxe et expose les proprietes de la requête parsee.
Recapitulatif des clauses de tri et de pagination :
| Clause | Effet |
|---|---|
ORDER BY ?var |
Tri ascendant (A-Z, 1-9) |
ORDER BY DESC(?var) |
Tri descendant |
LIMIT n |
Au plus n résultats |
OFFSET n |
Sauter les n premiers |
Cas d’usage : pagination web (LIMIT 20, OFFSET = (page-1)*20) · streaming (LIMIT 100 + itération) · echantillonnage.
Note de portee — deux pieges classiques : - OFFSET est O(N) : c’est un scan-and-skip. Sur de très gros graphes, preferer la keyset pagination (FILTER(?age < lastSeenAge)). - LIMIT sans ORDER BY donne un résultat non déterministe : l’ordre depend de l’evaluation du graphe.
Le QueryBuilder de dotNetRDF offre une API fluide pour construire des requêtes SPARQL dynamiquement, avec validation a la compilation.
// 6.1 Requete complexe avec QueryBuilder
var prefixes = new NamespaceMapper(true);
prefixes.AddNamespace("foaf", new Uri("http://xmlns.com/foaf/0.1/"));
prefixes.AddNamespace("info", new Uri("http://somewhere/peopleInfo#"));
string person = "person";
string name = "name";
string age = "age";
string email = "email";
var qb = QueryBuilder
.Select(new string[] { name, age, email })
.Where(
(tp) =>
{
tp.Subject(person).PredicateUri($"foaf:{name}").Object(name);
tp.Subject(person).PredicateUri($"info:{age}").Object(age);
})
.Optional(
(opt) =>
{
opt.Where(
(tp) =>
{
tp.Subject(person).PredicateUri("foaf:mbox").Object(email);
});
})
.Filter((b) => b.Variable(age) > 18)
.OrderBy(name);
qb.Prefixes = prefixes;
Console.WriteLine("=== Requete complexe ===");
Console.WriteLine(qb.BuildQuery().ToString());=== Requete complexe ===
PREFIX foaf: <http://xmlns.com/foaf/0.1/>
PREFIX info: <http://somewhere/peopleInfo#>
SELECT ?name ?age ?email WHERE
{
?person foaf:name ?name .
?person info:age ?age .
OPTIONAL { ?person foaf:mbox ?email . }
FILTER(?age > 18 )
}
ORDER BY ASC(?name)
La requête combine quatre clauses SPARQL en un seul appel fluide :
| Clause | Méthode | Effet sur les résultats |
|---|---|---|
WHERE (2 patterns) |
.Where(tp => ...) |
Conjonction : le nom ET l’age doivent exister |
OPTIONAL |
.Optional(opt => ...) |
Email retourne si present, sinon variable non liee |
FILTER |
.Filter(b => b.Variable(age) > 18) |
Exclut les moins de 18 ans |
ORDER BY |
.OrderBy(name) |
Tri alphabetique sur le nom |
Ordre de composition : Select -> Where -> Optional -> Filter -> OrderBy. Le QueryBuilder produit une syntaxe SPARQL valide quel que soit l’ordre des appels, mais suivre l’ordre logique (sélection, pattern, filtre, tri) ameliore nettement la lisibilite.
Ce que le QueryBuilder apporte, et ce qu’il coute :
| Pour | type-safe (erreurs a la compilation) · composable (build conditionnel en if/else) · refactoring propage par l’IDE |
| Contre | plus verbeux qu’une chaîne brute · certaines constructions avancees (Federation SERVICE, Named Graphs) ne sont pas exposees |
Recommandation : QueryBuilder pour les requêtes simples a moyennes et surtout dynamiques (parametrees a l’exécution) ; chaîne brute pour les requêtes fixes, connues a l’avance, ou trop complexes pour l’API fluide.
Cette requête est exécutée sur un graphe local dans la cellule suivante, pour en observer les résultats concrets.
// 6.2 Execution sur un graphe local
IGraph g = new Graph();
string ttlData = @"
@prefix foaf: <http://xmlns.com/foaf/0.1/> .
@prefix ex: <http://example.org/> .
ex:alice foaf:name ""Alice"" ;
foaf:age 30 ;
foaf:mbox <mailto:alice@example.org> .
ex:bob foaf:name ""Bob"" ;
foaf:age 25 .
ex:charlie foaf:name ""Charlie"" ;
foaf:age 35 ;
foaf:mbox <mailto:charlie@example.org> .
ex:diana foaf:name ""Diana"" ;
foaf:age 17 .
";
new TurtleParser().Load(g, new StringReader(ttlData));
Console.WriteLine($"Graphe de test charge : {g.Triples.Count} triplets");
string sparql = @"
PREFIX foaf: <http://xmlns.com/foaf/0.1/>
SELECT ?name ?age
WHERE {
?person foaf:name ?name .
?person foaf:age ?age .
FILTER(?age >= 18)
}
ORDER BY ?name
";
var results = g.ExecuteQuery(sparql) as SparqlResultSet;
Console.WriteLine($"\n{results.Count} resultats :");
foreach (var result in results)
{
Console.WriteLine($" {result["name"]} - age: {result["age"]}");
}Graphe de test charge : 10 triplets
3 resultats :
Alice^^http://www.w3.org/2001/XMLSchema#string - age: 30^^http://www.w3.org/2001/XMLSchema#integer
Bob^^http://www.w3.org/2001/XMLSchema#string - age: 25^^http://www.w3.org/2001/XMLSchema#integer
Charlie^^http://www.w3.org/2001/XMLSchema#string - age: 35^^http://www.w3.org/2001/XMLSchema#integer
Jusqu’ici les cellules affichaient la requête générée. Celle-ci l’execute : dotNetRDF embarque un moteur SPARQL complet capable de tourner directement sur un IGraph en memoire, sans serveur ni endpoint.
Sortie observée (verbatim) :
Graphe de test charge : 10 triplets
3 resultats :
Alice^^http://www.w3.org/2001/XMLSchema#string - age: 30^^http://www.w3.org/2001/XMLSchema#integer
Bob^^http://www.w3.org/2001/XMLSchema#string - age: 25^^http://www.w3.org/2001/XMLSchema#integer
Charlie^^http://www.w3.org/2001/XMLSchema#string - age: 35^^http://www.w3.org/2001/XMLSchema#integer
Les suffixes ^^http://www.w3.org/2001/XMLSchema#string sont les types de données XSD des litteraux : ToString() sur un INode rend la forme complète, type compris.
Les APIs en jeu :
| Méthode | Usage |
|---|---|
g.ExecuteQuery(sparql) |
Exécution sur un graphe local |
SparqlResultSet |
Résultat d’un SELECT (table de bindings) |
IGraph |
Résultat d’un CONSTRUCT/DESCRIBE (graphe) |
result["name"] |
Acces a une variable du binding |
Caractéristiques de l’exécution locale : rapide (aucun aller-retour réseau) · en memoire (le graphe entier doit tenir en RAM) · sans concurrence (ni cache, ni verrou).
Cas d’usage : tests unitaires (graphe de test + assertions sur les résultats) · traitement batch d’un fichier RDF · petits datasets (< 1M triplets).
Le moteur local supporte la majorite de SPARQL 1.1 :
SELECT,CONSTRUCT,ASK,DESCRIBE,GROUP BY,HAVING.
// 6.3 Requete OPTIONAL executee sur le graphe local
string sparqlOptional = @"
PREFIX foaf: <http://xmlns.com/foaf/0.1/>
SELECT ?name ?email
WHERE {
?person foaf:name ?name .
OPTIONAL { ?person foaf:mbox ?email . }
}
ORDER BY ?name
";
var results = g.ExecuteQuery(sparqlOptional) as SparqlResultSet;
Console.WriteLine($"{results.Count} resultats (avec OPTIONAL email) :");
foreach (var result in results)
{
string emailStr = result.HasBoundValue("email") ? result["email"].ToString() : "(non renseigne)";
Console.WriteLine($" {result["name"]} - email: {emailStr}");
}4 resultats (avec OPTIONAL email) :
Alice^^http://www.w3.org/2001/XMLSchema#string - email: mailto:alice@example.org
Bob^^http://www.w3.org/2001/XMLSchema#string - email: (non renseigne)
Charlie^^http://www.w3.org/2001/XMLSchema#string - email: mailto:charlie@example.org
Diana^^http://www.w3.org/2001/XMLSchema#string - email: (non renseigne)
La section 3 avait montre OPTIONAL sur une requête affichée ; ici on en observe l’effet sur des résultats reels, et on voit ce que le cote C# doit faire des variables non liees.
Sortie observée (verbatim) :
4 resultats (avec OPTIONAL email) :
Alice^^http://www.w3.org/2001/XMLSchema#string - email: mailto:alice@example.org
Bob^^http://www.w3.org/2001/XMLSchema#string - email: (non renseigne)
Charlie^^http://www.w3.org/2001/XMLSchema#string - email: mailto:charlie@example.org
Diana^^http://www.w3.org/2001/XMLSchema#string - email: (non renseigne)
Ce que l’OPTIONAL a change : sans lui (WHERE strict), seuls Alice et Charlie seraient retournes — Bob et Diana n’ont pas d’email. Les 4 personnes sont la, email non lie pour deux d’entre elles.
Cote C#, une variable non liee n’est pas une chaîne vide : y acceder directement leverait une exception. Le motif correct teste la liaison avant l’acces.
Méthode de SparqlResult |
Rôle |
|---|---|
result["varName"] |
accesseur de la valeur (INode) |
result.HasBoundValue("varName") |
la variable est presente et liee |
result["varName"].ToString() |
conversion explicite en string |
Pattern recommande : tester HasBoundValue avant tout acces a un champ optionnel · prevoir une valeur par défaut pour l’affichage · lever une exception seulement si la valeur est reellement obligatoire.
Selon l’usage en aval : affichage -> valeur par défaut lisible ((non renseigne)) · export CSV -> chaîne vide · validation -> exception.
Cette section contient 3 exercices progressifs : 1. Exercice 1 : SELECT avec FILTER (facile). 2. Exercice 2 : QueryBuilder avec OPTIONAL (moyenne). 3. Exercice 3 : Requête sur animals.ttl (moyenne).
Note pedagogique : les exercices utilisent le graphe local (10 triplets) de la cellule 6.2. Pour des exercices plus realistes, vous pouvez charger un fichier RDF (voir SW-3 pour les APIs de lecture).
Difficulte progressive : facile -> moyenne -> moyenne. Les exercices 2 et 3 combinent plusieurs concepts (OPTIONAL + QueryBuilder pour 2, lecture de fichier + SPARQL pour 3).
Écrivez une requête SPARQL qui sélectionne les noms et âges des personnes de plus de 20 ans a partir du graphe de test. Executez-la avec g.ExecuteQuery().
Console.WriteLine("Exercice a completer");
// Exercice 1 : Votre code ici
// string sparql = @"PREFIX foaf: ...
// SELECT ?name ?age WHERE { ... FILTER(?age > 20) } ORDER BY DESC(?age)";
// var results = g.ExecuteQuery(sparql) as SparqlResultSet;
// foreach (var r in results) Console.WriteLine(...);Exercice a completer
Objectif : utilisez le QueryBuilder pour ecrire une requête OPTIONAL qui retourne le nom et (optionnellement) l’email de toutes les personnes du graphe local, triée par nom.
Sortie attendue :
Alice - mailto:alice@example.org
Bob - (non renseigne)
Charlie - mailto:charlie@example.org
Diana - (non renseigne)
Indices : - Le prédicat foaf:name est obligatoire (WHERE strict). - Le prédicat foaf:mbox porte l’email (OPTIONAL). - Utilisez .Optional(...) du QueryBuilder, et HasBoundValue cote C# pour l’affichage.
Difficulte : moyenne. Application des sections 3 (OPTIONAL) et 6 (QueryBuilder).
Console.WriteLine("Exercice a completer");
// Exercice 2 : Votre code ici
// var prefixes = new NamespaceMapper(true);
// prefixes.AddNamespace("foaf", ...);
// var qb = QueryBuilder.Select(...).Where(...).Optional(...).OrderBy(...);
// qb.Prefixes = prefixes;
// Console.WriteLine(qb.BuildQuery().ToString());Exercice a completer
Chargez data/animals.ttl et écrivez une requête SPARQL qui retourne le nom et l’age de tous les animaux de type ex:Dog.
Console.WriteLine("Exercice a completer");
// Exercice 3 : Votre code ici
// IGraph animals = new Graph();
// new TurtleParser().Load(animals, "data/animals.ttl");
// string sparql = @"PREFIX ex: <http://example.org/animals#>
// SELECT ?name ?age WHERE {
// ?animal a ex:Dog . ?animal ex:name ?name . ?animal ex:age ?age .
// }";
// var results = animals.ExecuteQuery(sparql) as SparqlResultSet;Exercice a completer
Ce que chaque section a introduit :
| Section | Concepts clés | APIs dotNetRDF |
|---|---|---|
| 1. Introduction | Syntaxe SPARQL, PREFIX, SELECT, WHERE | SparqlQueryParser |
| 2. SELECT | Pattern matching, variables, conjonction | QueryBuilder.Select |
| 3. FILTER / OPTIONAL | Numérique, regex, pattern facultatif (LEFT JOIN) | .Filter, .Optional |
| 4. UNION | Combinaison OR de patterns | QueryBuilder.Union |
| 5. ORDER BY, LIMIT, OFFSET | Tri et pagination | .OrderBy, chaîne brute |
| 6. QueryBuilder | API fluide type-safe, exécution locale | IQueryBuilder, IGraph.ExecuteQuery |
Correspondance SPARQL <-> QueryBuilder :
| Clause SPARQL | Méthode QueryBuilder | Fonction |
|---|---|---|
SELECT ?var |
.Select(variables) |
Variables a retourner |
WHERE { pattern } |
.Where(triplePattern) |
Patterns de triplets |
FILTER(cond) |
.Filter(condition) |
Conditions de filtrage |
OPTIONAL { } |
.Optional(pattern) |
Patterns facultatifs |
UNION { } { } |
.Union(pattern1, pattern2) |
Alternatives (OR) |
ORDER BY ?var |
.OrderBy(var) |
Tri des résultats |
LIMIT n |
(chaîne brute) | Limiter le nombre de résultats |
OFFSET n |
(chaîne brute) | Sauter les premiers résultats |
Choisir son approche :
| Approche | Avantage | Cas d’usage |
|---|---|---|
| Chaîne brute | Simple, LIMIT/OFFSET direct | Requêtes fixes, connues a l’avance |
| QueryBuilder | Type safety, composition | Requêtes dynamiques, parametrees |
| SparqlQueryParser | Validation de syntaxe | Verification avant exécution |
SERVICE) : requêtes distribuees sur plusieurs endpoints.Dans SW-5-LinkedData, nous interrogerons des endpoints SPARQL distants comme DBpedia et Wikidata.
SERVICE pour requêtes federees.Navigation : << 3-GraphOperations | Index | 5-LinkedData >>