SW-4-SPARQL

Navigation : << 3-GraphOperations | Index | 5-LinkedData >>

SPARQL : Interroger les graphes RDF

Duree estimee : 45 minutes

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

Prerequis

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.

#r "nuget: dotNetRDF, 3.2.1"
Installed Packages
  • dotNetRDF, 3.2.1

Importation des espaces de noms dotNetRDF pour SPARQL, QueryBuilder et la gestion des parsers.

using VDS.RDF;
using VDS.RDF.Parsing;
using VDS.RDF.Writing;
using VDS.RDF.Query;
using VDS.RDF.Query.Builder;
using System;
using System.IO;
using System.Linq;

Console.WriteLine("dotNetRDF 3.2.1 pret - espaces de noms SPARQL charges.");
dotNetRDF 3.2.1 pret - espaces de noms SPARQL charges.

Lecture de l’environnement dotNetRDF pour SPARQL

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).


1. Introduction a SPARQL

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)
}

2. SELECT : Requêtes de base

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 . }

Interprétation : SELECT simple avec QueryBuilder

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 ? devant John Smith trahit 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 . 
}

Interprétation : PREFIX et conjonction de patterns

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.


3. FILTER et OPTIONAL

Les deux clauses de cette section repondent a deux besoins opposes :

  • FILTER restreint les résultats selon une condition sur les valeurs — numérique, textuelle (regex) ou d’egalite.
  • OPTIONAL elargit les résultats en rendant un pattern facultatif : le sujet reste retourne même quand le pattern optionnel ne matche pas.

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 ) 
}

Interprétation : FILTER numérique

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")) 
}

Interprétation : FILTER Regex

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 ) 
  }
}

Interprétation : OPTIONAL avec FILTER

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.


4. UNION

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 . } }

Interprétation : UNION, et sa différence avec OPTIONAL

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.


5. ORDER BY, LIMIT, OFFSET

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# :

var qb = QueryBuilder
    .Select(new string[] { name })
    .Where(
        (tp) =>
        {
            tp.Subject("x").PredicateUri($"foaf:{name}").Object(name);
        })
    .OrderBy(name);
qb.Prefixes = prefixes;

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) 

Interprétation : ORDER BY

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

Interprétation : LIMIT et OFFSET (pagination)

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.

var sparqlParser = new SparqlQueryParser();
var parsedQuery = sparqlParser.ParseFromString(sparql);

Console.WriteLine($"Type de requete : {parsedQuery.QueryType}");
Console.WriteLine($"Limit           : {parsedQuery.Limit}");
Console.WriteLine($"Offset          : {parsedQuery.Offset}");

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.


6. QueryBuilder : Construction programmatique

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) 

Interprétation : requête complexe avec le QueryBuilder

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

Interprétation : exécution sur un graphe local

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.

IGraph g = new Graph();
new TurtleParser().Load(g, new StringReader(ttlData));

var results = g.ExecuteQuery(sparql) as SparqlResultSet;
foreach (var result in results)
{
    Console.WriteLine($"  {result["name"]} - age: {result["age"]}");
}

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)

Interprétation : OPTIONAL sur graphe local et gestion C# des valeurs non liees

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.

foreach (var result in results)
{
    string emailStr = result.HasBoundValue("email")
        ? result["email"].ToString()
        : "(non renseigne)";
    Console.WriteLine($"  {result["name"]} - email: {emailStr}");
}
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.


7. Exercices pratiques

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).

Exercice 1 : SELECT avec FILTER

É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

Exercice 2 : QueryBuilder avec OPTIONAL

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

Exercice 3 : Requête sur animals.ttl

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

Resume

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

Pour aller plus loin

  • SPARQL Federation (SERVICE) : requêtes distribuees sur plusieurs endpoints.
  • SPARQL Update : insertion/suppression de triplets — traite dans SW-4b-Python-SPARQL.
  • Property paths : navigation dans le graphe (sequences, alternatives, repetitions).
  • Named Graphs : requêtes sur des sous-graphes nommes.

Prochaine étape

Dans SW-5-LinkedData, nous interrogerons des endpoints SPARQL distants comme DBpedia et Wikidata.

Références

  • SPARQL 1.1 Query Language – Harris & Seaborne, W3C Recommendation (2013). w3.org/TR/sparql11-query
  • SPARQL 1.1 Overview – W3C SPARQL Working Group, W3C Recommendation (2013). Vue d’ensemble de la famille de specs SPARQL 1.1.
  • SPARQL 1.1 Federated Query – Prud’hommeaux & Buil-Aranda, W3C Recommendation (2013). Clause SERVICE pour requêtes federees.
  • SPARQL 1.1 Protocol – Feigenbaum, Williams, et al. (1re ed. Clark & Torres), W3C Recommendation (2013). Protocole d’acces aux endpoints SPARQL.

Navigation : << 3-GraphOperations | Index | 5-LinkedData >>

Retour au sommet