← Suite du cours #4

De l'Ontologie à l'Application

Protégé · Ontologie · Fuseki · SPARQL · Frontend
Construisez une application e-commerce complète
du modèle conceptuel à la page web.

LIEN AVEC LE COURS #4

La théorie rencontre la pratique

Pendant quatre cours, on a exploré le web sémantique en profondeur : des triplets RDF à Turtle, de Prolog à OWL, de SPARQL à SHACL. On a construit des parseurs, des moteurs d'inférence, des serveurs SPARQL.

Ce cinquième cours est différent. On va prendre tous ces outils et les assembler pour construire une vraie application : un catalogue e-commerce complet, de l'ontologie à la page web.

Protégé ──▶ .ttl ──▶ Fuseki ──▶ SPARQL ──▶ HTML/JS
« Un outil sans application pratique, c'est comme une thèse sans soutenance : ça existe, mais personne ne sait à quoi ça sert. » — Un professeur qui n'a pas tort

On va utiliser Protégé pour modéliser l'ontologie (la partie qu'on faisait à la main dans les cours précédents), Fuseki comme serveur SPARQL (notre mini_fuseki.py en version professionnelle), et une page web toute simple en HTML/JavaScript pour le rendu final.

À la fin de ce cours, vous aurez une boutique en ligne fonctionnelle qui utilise le web sémantique de bout en bout. Pas un jouet pédagogique. Une vraie application, avec des vrais produits, des vraies catégories, et un vrai moteur de recherche sémantique.

CHAPITRE 1 VISION

Le projet : une boutique en ligne sémantique

Architecture générale

CoucheTechnologieRôle
ModélisationProtégé Créer l'ontologie (classes, propriétés, instances)
SérialisationTurtle Exporter l'ontologie au format .ttl
StockageApache Jena Fuseki Serveur SPARQL avec données persistantes
RequêtageSPARQL Interroger le catalogue (produits, catégories, prix…)
FrontendHTML + JavaScript Interface utilisateur de la boutique
📦 Le cas d'usage : Une boutique de produits high-tech et culturels : smartphones, livres, casques audio, jeux vidéo. Assez de variété pour montrer la puissance du modèle, pas trop pour noyer le lecteur. Le catalogue contient une vingtaine de produits répartis en 5 catégories, avec marques, avis clients et stock. De quoi faire du SPARQL sérieux.

Pourquoi cette stack ?

On aurait pu utiliser un SGBD relationnel (PostgreSQL, MySQL) et faire du SQL. Mais le web sémantique apporte des avantages décisifs pour ce genre d'application :

🔗 Interopérabilité

Les données sont directement exploitables par d'autres applications. Vous pouvez lier vos produits à schema.org, Wikidata, DBpedia. Essayez ça avec MySQL.

🧩 Flexibilité du modèle

Ajouter une nouvelle propriété ou une nouvelle classe ne casse pas le schéma. Un produit peut avoir des attributs que d'autres n'ont pas. En SQL, c'est des migrations. En RDF, c'est un triplet de plus.

🔍 Recherche sémantique

Avec SPARQL, on peut faire des requêtes transverses qui traversent les catégories : « tous les produits de moins de 100€ avec une note supérieure à 4 et un stock disponible ». En SQL, c'est 4 tables. En RDF, c'est 4 triple patterns.

🌐 Web-ready

Fuseki parle HTTP et retourne du JSON. Votre frontend peut l'interroger directement sans backend. C'est une API REST sans framework.

Prérequis

Pour suivre ce cours, vous aurez besoin de :

CHAPITRE 2 OUTIL

Protégé : le couteau suisse des ontologies

« Protégé est à l'ingénieur ontologique ce que Photoshop est au graphiste : l'outil qu'on adore détester, mais sans lequel on ne peut pas travailler. » — Un utilisateur de Protégé depuis 2004 (il a des cicatrices)

Qu'est-ce que Protégé ?

Protégé est un éditeur d'ontologies gratuit et open-source développé par l'Université de Stanford (le même endroit qui a inventé Google, mais en moins riche). C'est l'outil de référence pour créer et manipuler des ontologies OWL et RDF.

Protégé fait trois choses essentiellement :

  1. Modéliser — créer des classes, des propriétés, des axiomes, des restrictions, le tout avec une interface graphique.
  2. Instancier — ajouter des individus (instances) de vos classes, avec leurs propriétés.
  3. Raisonner — lancer un raisonneur OWL (HermiT, Pellet, ELK) pour classifier, vérifier la cohérence, inférer.

Installation

Bash
# 1. Aller sur https://protege.stanford.edu/products.php
#    Télécharger la version "Protégé Desktop" (fichier .zip/.tar.gz)

# 2. Décompresser
tar xzf Protege-*-linux.tar.gz
# ou dézipper le .zip selon votre OS

# 3. Lancer
cd Protege-*/
./run.sh
# Sur Mac : ouvrir Protege.app
# Sur Windows : double-clic sur run.bat
🐍 Note Python : Il existe aussi une bibliothèque Python appelée Protégé Python API, mais on va utiliser l'interface graphique. C'est plus visuel, plus intuitif, et ça permet de voir ce qu'on fait. On est dans un cours, pas dans un terminal. (Même si le terminal, c'est la vie. Mais les ontologies, c'est mieux avec des clics.)

Premier lancement — Interface

Quand vous lancez Protégé pour la première fois, vous voyez un écran avec plusieurs onglets (tabs) et vues (views). Voici les plus importants :

OngletUtilité
EntitiesVue centralisée : classes, propriétés, individus
ClassesHiérarchie des classes, équivalences, restrictions
Object PropertiesPropriétés reliant des individus (ex: aPourCategorie)
Data PropertiesPropriétés reliant des littéraux (ex: aPrix)
IndividualsListe et édition des instances
OWL VizVisualisation graphique de l'ontologie (super utile pour les présentations)
DL QueryRequêtage en logique descriptive — la version SPARQL des pauvres

Créer une nouvelle ontologie

  1. Lancer Protégé
  2. File → New ou cliquer sur l'icône « Nouveau »
  3. Dans le champ Ontology IRI, entrer http://example.org/vente
  4. Cliquer OK. Votre ontologie est créée.

L'IRI (Internationalized Resource Identifier) est l'identifiant unique de votre ontologie. C'est ce qui permettra de la référencer depuis d'autres ontologies. Choisissez-le bien, car le changer après, c'est comme renommer un projet en cours de route : possible, mais douloureux.

🏛️ Point culture : Protégé existe depuis 1987. Oui, 1987. C'est plus vieux que Python (1991), que Java (1995), et que le web sémantique lui-même (1998). À l'origine, c'était un système expert médical pour traiter les maladies du foie. Aujourd'hui, c'est l'outil standard pour créer des ontologies dans le monde entier. Preuve que même les projets médicaux peuvent avoir une seconde carrière dans l'informatique.

🔌 Plugins

Protégé a des centaines de plugins : raisonneurs (HermiT, Pellet, ELK), visualiseurs (OWL Viz, OntoGraf), import/export (Turtle, RDF/XML, JSON-LD, CSV). Window → Plugins.

💡 Astuce

Utilisez OWL Viz pour visualiser votre ontologie en temps réel. C'est parfait pour les diapos de présentation. Ça impressionne toujours les gens qui ne connaissent pas RDF.

CHAPITRE 3 ONTOLOGIE

Modélisation du domaine de vente

Assez de théorie. Créons notre ontologie de vente dans Protégé. On va modéliser un catalogue e-commerce avec des produits, des catégories, des marques, des clients, des commandes et des avis.

3.1 — Les classes

On commence par créer la hiérarchie des classes. Dans Protégé, allez dans l'onglet Classes. Vous voyez déjà owl:Thing (la classe racine, équivalent de en OWL). Toutes nos classes seront des sous-classes de owl:Thing.

Turtle (ce qu'on va obtenir)
# Hiérarchie des classes
:Produit       rdfs:subClassOf  owl:Thing .
:Categorie     rdfs:subClassOf  owl:Thing .
:Marque        rdfs:subClassOf  owl:Thing .
:Client        rdfs:subClassOf  owl:Thing .
:Commande      rdfs:subClassOf  owl:Thing .
:Avis          rdfs:subClassOf  owl:Thing .

# Sous-classes de Produit
:Smartphone    rdfs:subClassOf  :Produit .
:Livre         rdfs:subClassOf  :Produit .
:CasqueAudio   rdfs:subClassOf  :Produit .
:JeuVideo      rdfs:subClassOf  :Produit .
:Accessoire    rdfs:subClassOf  :Produit .

Dans Protégé, pour chaque classe :

  1. Clic droit sur owl:ThingAdd subclass
  2. Entrer le nom (ex: Produit)
  3. Répéter pour Categorie, Marque, Client, Commande, Avis
  4. Pour les sous-classes de Produit, clic droit sur ProduitAdd subclassSmartphone, etc.

3.2 — Les propriétés

Maintenant, les propriétés. On distingue deux types :

Object Properties

PropriétéDomaineRangeDescription
aPourCategorieProduitCategorieCatégorie d'un produit
estFabriqueParProduitMarqueMarque du produit
aPourAvisProduitAvisAvis sur le produit
estAcheteParCommandeClientClient qui a passé la commande
contientProduitCommandeProduitProduits dans la commande

Dans Protégé : onglet Object PropertiesAdd. Remplir le nom, puis dans l'onglet Description, définir Domains (intersection) et Ranges (intersection).

Data Properties

PropriétéDomaineTypeDescription
nomProduit, Marque, Client, Categoriexsd:stringNom de l'entité
prixProduitxsd:decimalPrix en euros
descriptionProduitxsd:stringDescription textuelle
stockProduitxsd:integerQuantité en stock
noteAvisxsd:integerNote sur 5
commentaireAvisxsd:stringTexte de l'avis
dateCommandeCommandexsd:dateTimeDate de la commande
emailClientxsd:stringAdresse email
urlImageProduitxsd:stringURL de l'image
⚠️ Domaine vs. Range : En OWL, déclarer domaine et range n'est pas une contrainte comme en SQL (où la base vous empêche d'insérer). C'est un axiome qui permet de l'inférence : si prix a pour domaine Produit, alors toute ressource qui a un prix sera inférée comme étant un Produit. C'est la logique au service des données, pas l'inverse.

3.3 — Les instances

Maintenant, ajoutons des individus (instances). Dans l'onglet Individuals, créez :

Catégories

Turtle
:smartphones  a  :Categorie ;  :nom  "Smartphones"@fr .
:livres       a  :Categorie ;  :nom  "Livres"@fr .
:casques      a  :Categorie ;  :nom  "Casques Audio"@fr .
:jeux         a  :Categorie ;  :nom  "Jeux Vidéo"@fr .
:accessoires  a  :Categorie ;  :nom  "Accessoires"@fr .

Marques

Turtle
:apple     a  :Marque ;  :nom  "Apple"@fr .
:samsung   a  :Marque ;  :nom  "Samsung"@fr .
:sony      a  :Marque ;  :nom  "Sony"@fr .
:gallimard a  :Marque ;  :nom  "Gallimard"@fr .
:nintendo  a  :Marque ;  :nom  "Nintendo"@fr .

Produits

Turtle
:iphone15  a  :Smartphone ;
    :nom  "iPhone 15 Pro"@fr ;
    :prix  1299.00 ;
    :stock  42 ;
    :description  "Le dernier smartphone Apple avec puce A17 Pro"@fr ;
    :urlImage  "https://picsum.photos/seed/iphone15/400/400" ;
    :aPourCategorie  :smartphones ;
    :estFabriquePar  :apple .

:galaxyS24  a  :Smartphone ;
    :nom  "Galaxy S24 Ultra"@fr ;
    :prix  1149.00 ;
    :stock  38 ;
    :description  "Le flagship Samsung avec IA intégrée"@fr ;
    :urlImage  "https://picsum.photos/seed/galaxys24/400/400" ;
    :aPourCategorie  :smartphones ;
    :estFabriquePar  :samsung .

:lepetitprince  a  :Livre ;
    :nom  "Le Petit Prince"@fr ;
    :prix  7.50 ;
    :stock  150 ;
    :description  "L'essentiel est invisible pour les yeux"@fr ;
    :urlImage  "https://picsum.photos/seed/petitprince/400/400" ;
    :aPourCategorie  :livres ;
    :estFabriquePar  :gallimard .

:zelda  a  :JeuVideo ;
    :nom  "The Legend of Zelda: Tears of the Kingdom"@fr ;
    :prix  69.99 ;
    :stock  25 ;
    :description  "L'aventure épique dans un monde ouvert"@fr ;
    :urlImage  "https://picsum.photos/seed/zelda/400/400" ;
    :aPourCategorie  :jeux ;
    :estFabriquePar  :nintendo .

:wh1000xm5  a  :CasqueAudio ;
    :nom  "WH-1000XM5"@fr ;
    :prix  399.00 ;
    :stock  17 ;
    :description  "Casque sans fil à réduction de bruit active"@fr ;
    :urlImage  "https://picsum.photos/seed/wh1000xm5/400/400" ;
    :aPourCategorie  :casques ;
    :estFabriquePar  :sony .

Avis clients

Turtle
:avis1  a  :Avis ;
    :note  5 ;
    :commentaire  "Excellent produit, je recommande !"@fr .

:avis2  a  :Avis ;
    :note  4 ;
    :commentaire  "Très bon, mais un peu cher"@fr .

:avis3  a  :Avis ;
    :note  5 ;
    :commentaire  "Indispensable pour les fans de la saga"@fr .

# Lier les avis aux produits
:iphone15  :aPourAvis  :avis1 .
:wh1000xm5  :aPourAvis  :avis2 .
:zelda  :aPourAvis  :avis3 .
✨ Astuce Protégé : Pour créer un individu dans Protégé, allez dans l'onglet Individuals, sélectionnez la classe dans la hiérarchie à gauche, puis cliquez sur le bouton Individuals → Add (ou tapez Ctrl+I). Ensuite, dans le panneau Property assertions, cliquez sur le + à côté de chaque type de propriété pour ajouter des valeurs. C'est un peu long pour 20 produits, mais c'est visuel et ça évite les erreurs.

🧪 Le raisonneur HermiT

Une fois votre ontologie créée, allez dans Reasoner → HermiT et lancez-le. HermiT va vérifier la cohérence de votre ontologie et classifier les classes. Si quelque chose est incohérent, il le signalera. C'est le moment de vérité.

📤 Exporter en Turtle

File → Export Inferred Ontology As... → choisir Turtle comme format. Protégé vous demandera où sauvegarder le fichier .ttl. C'est ce fichier qu'on va charger dans Fuseki.

CHAPITRE 4 DÉPLOIEMENT

Exporter l'ontologie et la charger dans Fuseki

Notre ontologie est prête dans Protégé. Il est temps de la sortir du laboratoire et de la mettre dans un serveur SPARQL qui pourra être interrogé par notre future application web.

4.1 — Exporter depuis Protégé

  1. Dans Protégé : File → Export Inferred Ontology As...
  2. Choisir le format : Turtle (extension .ttl)
  3. Nommer le fichier : vente.ttl
  4. Cliquer OK

Vous obtenez un fichier Turtle. Ouvrez-le, il doit ressembler à ce qu'on a écrit dans le chapitre précédent, mais avec les préfixes et les annotations OWL en plus. Protégé ajoute automatiquement les déclarations rdfs:label, rdfs:comment et les métadonnées d'ontologie.

4.2 — Installer et lancer Fuseki

Apache Jena Fuseki est le serveur SPARQL de référence. C'est une version professionnelle de notre mini_fuseki.py du cours #3. Il gère les transactions, les inférences, la persistence, et il a une interface web intégrée.

Bash
# 1. Télécharger Fuseki (dernière version)
wget https://dlcdn.apache.org/jena/binaries/apache-jena-fuseki-5.4.0.tar.gz

# 2. Décompresser
tar xzf apache-jena-fuseki-*.tar.gz
cd apache-jena-fuseki-*/

# 3. Lancer Fuseki (mode mémoire, avec mise à jour)
./fuseki-server --update --mem /ds

# Variante : mode fichier (données persistantes)
./fuseki-server --update --loc=data /ds
# Les données sont sauvegardées dans le dossier data/

Fuseki démarre sur http://localhost:3030. Ouvrez cette URL dans votre navigateur. Vous voyez l'interface d'administration de Fuseki.

📜 Origine du nom : Fuseki est un mot japonais (ふせき) qui signifie « pierre flottante » — une référence aux pierres de go, parce que Jena (la bibliothèque sous-jacente) a été nommée d'après la ville de Jena en Allemagne, où se trouve l'Institut Fraunhofer qui a contribué au projet. Le « go » est un jeu de stratégie japonais. Donc Fuseki, c'est « la pierre de go flottante ». Le web sémantique aime les métaphores tirées par les cheveux.

4.3 — Charger l'ontologie

Deux façons de charger votre fichier Turtle dans Fuseki :

Bash
# Méthode 1 : via l'interface web (recommandée pour commencer)
#   1. Aller sur http://localhost:3030
#   2. Cliquer sur "dataset /ds"
#   3. Aller dans l'onglet "Upload"
#   4. Sélectionner vente.ttl
#   5. Cliquer "Upload"
#   → Les triplets sont chargés

# Méthode 2 : en ligne de commande (pour les scripts)
curl -X POST \
  -H "Content-Type: text/turtle" \
  --data-binary '@vente.ttl' \
  'http://localhost:3030/ds/data'

# Méthode 3 : au démarrage (mode fichier)
# Copier vente.ttl dans data/ avant de lancer Fuseki
cp vente.ttl apache-jena-fuseki-*/data/
./fuseki-server --update --loc=data /ds

4.4 — Vérifier le chargement

Une fois l'ontologie chargée, on peut vérifier que tout est bien présent. Dans l'interface Fuseki, allez dans l'onglet Query :

SPARQL
-- Combien de triplets avons-nous ?
SELECT (COUNT(*) AS ?total) WHERE { ?s ?p ?o }

-- Lister tous les produits
SELECT ?produit ?nom ?prix WHERE {
  ?produit  a  :Produit .
  ?produit  :nom  ?nom .
  ?produit  :prix  ?prix .
}

Si ces requêtes retournent des résultats, félicitations ! Vous avez une ontologie e-commerce opérationnelle accessible via SPARQL sur HTTP. Votre backend sémantique est prêt.

🌐 Interface web Fuseki

Fuseki fournit une interface web complète sur http://localhost:3030. Vous pouvez y exécuter des requêtes SPARQL, uploader des données, gérer les datasets. C'est le phpMyAdmin du web sémantique, en mieux.

🔌 API REST

Le vrai pouvoir de Fuseki, c'est son API HTTP. POST /ds/query pour les SELECT, POST /ds/update pour les INSERT/DELETE, POST /ds/data pour uploader. Notre frontend va utiliser l'API query.

⚡ Inférence

Fuseki peut activer un raisonneur OWL au niveau du dataset. Comme ça, il déduit automatiquement les relations inférées (sous-classes, domaines, ranges, etc.). Activable dans fuseki-server --config=config.ttl.

CHAPITRE 5 REQUÊTES

SPARQL en action : interroger le catalogue

Maintenant que nos données sont dans Fuseki, on peut les interroger avec SPARQL. Ce chapitre est une boîte à outils de requêtes prêtes à l'emploi pour notre boutique.

Pour chaque requête, ouvrez l'onglet Query de Fuseki (ou utilisez curl) et exécutez-la.

5.1 — Requêtes de base

SPARQL
-- Tous les produits (nom, prix, stock)
PREFIX : <http://example.org/vente#>
SELECT ?nom ?prix ?stock WHERE {
  ?p  a  :Produit .
  ?p  :nom  ?nom .
  ?p  :prix  ?prix .
  ?p  :stock  ?stock .
}
ORDER BY DESC(?prix)

-- Résultat :
-- "iPhone 15 Pro"   1299.00  42
-- "Galaxy S24 Ultra"  1149.00  38
-- "WH-1000XM5"      399.00   17
-- "Zelda TOTK"       69.99   25
-- "Le Petit Prince"   7.50   150

5.2 — Filtres

SPARQL
-- Produits entre 50 et 500 €, en stock
PREFIX : <http://example.org/vente#>
SELECT ?nom ?prix ?stock WHERE {
  ?p  a  :Produit .
  ?p  :nom  ?nom .
  ?p  :prix  ?prix .
  ?p  :stock  ?stock .
  FILTER(?prix >= 50 && ?prix <= 500)
  FILTER(?stock > 0)
}
ORDER BY ?prix

-- Résultat : "WH-1000XM5" 399.00 17
--           "Zelda TOTK"  69.99 25
SPARQL
-- Produits d'une catégorie spécifique
PREFIX : <http://example.org/vente#>
SELECT ?nom ?prix ?marque WHERE {
  ?p  a  :Produit .
  ?p  :nom  ?nom .
  ?p  :prix  ?prix .
  ?p  :aPourCategorie  ?cat .
  ?p  :estFabriquePar  ?m .
  ?m  :nom  ?marque .
  ?cat  :nom  "Smartphones"@fr .
}
ORDER BY ?prix

5.3 — Recherche textuelle

SPARQL
-- Recherche par mot-clé (dans le nom ou la description)
PREFIX : <http://example.org/vente#>
SELECT ?nom ?prix WHERE {
  ?p  a  :Produit .
  ?p  :nom  ?nom .
  ?p  :prix  ?prix .
  ?p  :description  ?desc .
  FILTER(
    CONTAINS(LCASE(?nom), "zelda") ||
    CONTAINS(LCASE(?desc), "zelda")
  )
}

5.4 — Agrégegations

SPARQL
-- Note moyenne par catégorie
PREFIX : <http://example.org/vente#>
SELECT ?cat (AVG(?note) AS ?moyenne) (COUNT(?avis) AS ?nbAvis) WHERE {
  ?p  a  :Produit .
  ?p  :aPourCategorie  ?c .
  ?c  :nom  ?cat .
  ?p  :aPourAvis  ?a .
  ?a  :note  ?note .
}
GROUP BY ?cat
ORDER BY DESC(?moyenne)
SPARQL
-- Statistiques : nombre de produits par catégorie
PREFIX : <http://example.org/vente#>
SELECT ?cat (COUNT(?p) AS ?nbProduits) WHERE {
  ?p  a  :Produit .
  ?p  :aPourCategorie  ?c .
  ?c  :nom  ?cat .
}
GROUP BY ?cat
ORDER BY DESC(?nbProduits)

5.5 — Requêtes avec OPTIONAL

SPARQL
-- Tous les produits avec leur note moyenne (si notés)
PREFIX : <http://example.org/vente#>
SELECT ?nom ?prix (ROUND(AVG(?note)) AS ?moyenne) WHERE {
  ?p  a  :Produit .
  ?p  :nom  ?nom .
  ?p  :prix  ?prix .
  OPTIONAL {
    ?p  :aPourAvis  ?a .
    ?a  :note  ?note .
  }
}
GROUP BY ?nom ?prix
ORDER BY ?nom

5.6 — Requête CONSTRUCT (créer un graphe)

SPARQL
-- Créer un sous-graphe avec uniquement les produits en promotion
-- (prix < 100€, considérés comme "petits prix")
PREFIX : <http://example.org/vente#>
CONSTRUCT {
  ?p  rdf:type  :PetitPrix .
  ?p  :nom  ?nom .
  ?p  :prix  ?prix .
} WHERE {
  ?p  a  :Produit .
  ?p  :nom  ?nom .
  ?p  :prix  ?prix .
  FILTER(?prix < 100)
}
🧠 Pourquoi SPARQL plutôt qu'une API REST classique ? Imaginez que votre patron vous demande : « Je veux la liste des produits de la catégorie Smartphones, triés par note décroissante, avec un prix inférieur à 1000€, dont le stock est positif, et qui ont au moins 2 avis. »

En REST classique : /api/produits?categorie=smartphones&prix_max=1000&stock_min=1&tri=note&ordre=desc — vous implémentez un endpoint spécifique pour chaque combinaison.

En SPARQL : vous écrivez une requête de 8 lignes. C'est tout. Pas de code backend à modifier. Pas de nouvelle API à déployer. La puissance de SPARQL, c'est que toutes les requêtes sont possibles sans changer une ligne de code serveur.

🔤 PREFIX

Dans Fuseki, vous pouvez définir les préfixes dans la configuration du dataset. Sinon, vous devez les répéter dans chaque requête. Ou utilisez le raccourci PREFIX : <http://example.org/vente#>

📤 Export JSON

Fuseki retourne les résultats en JSON par défaut. Ajoutez Accept: application/sparql-results+json dans vos headers HTTP. Notre frontend va utiliser ça.

CHAPITRE 6 APPLICATION

Frontend : la boutique qui parle SPARQL

On a l'ontologie, on a Fuseki, on a SPARQL. Maintenant, on va connecter le tout à une page web qui servira de vitrine commerciale.

Architecture frontend

Pas de framework. Pas de React, pas de Vue, pas d'Angular. Juste du HTML, du CSS, et du JavaScript pur. Pourquoi ?

Le principe : notre page envoie des requêtes SPARQL à Fuseki via fetch(), reçoit du JSON, et affiche les résultats dans le DOM.

6.1 — Structure HTML

HTML
<!DOCTYPE html>
<html lang="fr">
<head>
  <meta charset="UTF-8">
  <title>Ma Boutique Sémantique</title>
  <style>
    * { box-sizing: border-box; margin: 0; padding: 0; }
    body { font-family: 'Inter', sans-serif;
           background: #0c0c0c; color: #e0e0e0;
           line-height: 1.6; }
    header { background: #1a1a2e; padding: 1rem 2rem;
             display: flex; justify-content: space-between;
             align-items: center; }
    header h1 { color: #f43f5e; font-size: 1.3rem; }
    .search { padding: .6rem 1rem; width: 300px;
              border-radius: 8px; border: 1px solid #333;
              background: #1a1a1a; color: #fff; }
    .container { max-width: 1200px; margin: 2rem auto;
                 padding: 0 1.5rem; }
    .filters { display: flex; gap: .5rem; margin-bottom: 2rem;
               flex-wrap: wrap; }
    .filters button { padding: .5rem 1rem; border-radius: 20px;
                       border: 1px solid #333; background: #1a1a1a;
                       color: #ccc; cursor: pointer; }
    .filters button.active { background: #f43f5e; color: #fff;
                              border-color: #f43f5e; }
    .products { display: grid; grid-template-columns:
                repeat(auto-fill, minmax(260px, 1fr)); gap: 1.2rem; }
    .product { background: #1a1a2e; border-radius: 12px;
               padding: 1.2rem; border: 1px solid #2a2a44; }
    .product img { width: 100%; height: 200px; object-fit: cover;
                   border-radius: 8px; margin-bottom: .8rem; }
    .product h3 { color: #fff; font-size: 1rem; margin-bottom: .3rem; }
    .product .price { color: #34d399; font-weight: 700;
                       font-size: 1.2rem; }
    .product .meta { color: #888; font-size: .8rem;
                     margin-top: .4rem; }
    .product .stock { display: inline-block; padding: .15rem .5rem;
                       border-radius: 10px; font-size: .75rem;
                       margin-top: .5rem; }
    .stock.oui { background: #065f46; color: #34d399; }
    .stock.non { background: #5f0a1a; color: #f87171; }
    .avis { font-size: .8rem; color: #fbbf24; margin-top: .4rem; }
    .loading { text-align: center; padding: 3rem; color: #888; }
    .error { text-align: center; padding: 2rem; color: #f87171; }
  </style>
</head>
<body>
  <header>
    <h1>🛍️ Boutique Sémantique</h1>
    <div>
      <input id="search" class="search"
             placeholder="Rechercher un produit..."
             oninput="rechercher()">
    </div>
  </header>

  <div class="container">
    <div id="filtres" class="filters"></div>
    <div id="products" class="products">
      <div class="loading">⏳ Chargement...</div>
    </div>
  </div>

  <script>
    /* — Le JavaScript arrive ci-dessous — */
  </script>
</body>
</html>

6.2 — Le JavaScript : interroger Fuseki

Le cœur de l'application, c'est la fonction sparql() qui envoie une requête SPARQL à Fuseki et retourne les résultats en JSON :

JavaScript
// URL du serveur Fuseki
const FUSEKI = 'http://localhost:3030/ds';

// Préfixes SPARQL (pour alléger les requêtes)
const PREFIXES = `
  PREFIX : <http://example.org/vente#>
  PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>
  PREFIX xsd: <http://www.w3.org/2001/XMLSchema#>
`;

// Envoie une requête SPARQL et retourne les bindings
async function sparql(query) {
  const url = `${FUSEKI}/query`;
  const body = `query=${encodeURIComponent(PREFIXES + query)}`;

  const res = await fetch(url, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/x-www-form-urlencoded',
      'Accept': 'application/sparql-results+json'
    },
    body: body
  });

  if (!res.ok) {
    throw new Error(`Erreur SPARQL: ${res.status}`);
  }

  const data = await res.json();
  return data.results.bindings;
}

6.3 — Afficher les produits

JavaScript
// Charge tous les produits (ou filtre par catégorie + recherche)
async function chargerProduits(categorie = null, recherche = '') {
  const div = document.getElementById('products');
  div.innerHTML = '<div class="loading">⏳ Chargement...</div>';

  try {
    // Construction de la requête SPARQL
    let filtreCategorie = '';
    let filtreRecherche = '';

    if (categorie) {
      filtreCategorie =
        `?cat :nom "${categorie}"@fr .
         ?p :aPourCategorie ?cat .`;
    }

    if (recherche) {
      filtreRecherche =
        `FILTER(CONTAINS(LCASE(?nom), "${recherche.toLowerCase()}") ||
                CONTAINS(LCASE(?desc), "${recherche.toLowerCase()}"))`;
    }

    const query = `
      SELECT ?nom ?prix ?stock ?image ?marque
             (GROUP_CONCAT(DISTINCT ?catNom; SEPARATOR=", ") AS ?categories)
             (AVG(?n) AS ?noteMoy) WHERE {
        ?p  a  :Produit .
        ?p  :nom  ?nom .
        ?p  :prix  ?prix .
        ?p  :stock  ?stock .
        ?p  :urlImage  ?image .
        ?p  :estFabriquePar  ?m .
        ?m  :nom  ?marque .
        ${filtreCategorie}
        OPTIONAL { ?p  :aPourCategorie  ?cat .
                   ?cat  :nom  ?catNom . }
        OPTIONAL { ?p  :aPourAvis  ?a .
                   ?a  :note  ?n . }
        ${filtreRecherche}
      }
      GROUP BY ?nom ?prix ?stock ?image ?marque
      ORDER BY ?nom
    `;

    const produits = await sparql(query);
    afficherProduits(produits);

  } catch (err) {
    div.innerHTML =
      `<div class="error">❌ Erreur : ${err.message}</div>`;
  }
}

// Affiche les produits dans la grille
function afficherProduits(produits) {
  const div = document.getElementById('products');

  if (produits.length === 0) {
    div.innerHTML = '<div class="loading">Aucun produit trouvé.</div>';
    return;
  }

  div.innerHTML = produits.map(p => {
    const stock = parseInt(p.stock.value);
    const note = p.noteMoy ? Math.round(p.noteMoy.value) : '—';
    const etoiles = '★'.repeat(note) + '☆'.repeat(5 - note);

    return `
      <div class="product">
        <img src="${p.image.value}" alt="${p.nom.value}"
             loading="lazy" onerror="this.src='https://picsum.photos/400/400?random'">
        <h3>${p.nom.value}</h3>
        <div class="price">${p.prix.value} €</div>
        <div class="meta">${p.marque.value}</div>
        <div class="avis">${etoiles}</div>
        <span class="stock ${stock > 0 ? 'oui' : 'non'}">
          ${stock > 0 ? '✓ En stock (' + stock + ')' : '✗ Rupture'}
        </span>
      </div>
    `;
  }).join('');
}

6.4 — Filtres et recherche

JavaScript
// Charge les catégories pour les boutons de filtre
async function chargerCategories() {
  const query = `
    SELECT ?nom (COUNT(?p) AS ?nb) WHERE {
      ?c  a  :Categorie .
      ?c  :nom  ?nom .
      ?p  :aPourCategorie  ?c .
    }
    GROUP BY ?nom
    ORDER BY ?nom
  `;

  const cats = await sparql(query);
  const div = document.getElementById('filtres');

  div.innerHTML = `
    <button onclick="filtrer('')" class="active" id="btn_all">
      🏷️ Tous
    </button>
    ${cats.map(c => `
      <button onclick="filtrer('${c.nom.value}')"
              id="btn_${c.nom.value}">
        ${c.nom.value} (${c.nb.value})
      </button>
    `).join('')}
  `;
}

// Variable globale pour la catégorie active
let categorieActive = '';

// Filtre par catégorie
function filtrer(cat) {
  categorieActive = cat;

  // Met à jour l'état actif des boutons
  document.querySelectorAll('#filtres button').forEach(b =>
    b.classList.remove('active'));
  const btnId = cat ? `btn_${cat}` : 'btn_all';
  const btn = document.getElementById(btnId);
  if (btn) btn.classList.add('active');

  // Recharge les produits avec le filtre
  const recherche = document.getElementById('search').value;
  chargerProduits(cat, recherche);
}

// Recherche textuelle
function rechercher() {
  const recherche = document.getElementById('search').value;
  chargerProduits(categorieActive, recherche);
}

// Initialisation
chargerCategories();
chargerProduits();
🌐 CORS : le problème classique Si votre page est ouverte depuis file:// ou un serveur différent de Fuseki, le navigateur bloquera les requêtes pour des raisons de sécurité. Solution : soit lancer Fuseki avec CORS activé (via un fichier de configuration config.ttl), soit servir la page depuis le même serveur (ex: avec Python : python3 -m http.server 8080 dans le dossier). Notre mini_fuseki.py du cours #3 incluait déjà les headers CORS. Pour Apache Fuseki, vous pouvez ajouter un proxy inverse (Nginx) ou utiliser l'extension --cors si disponible.

6.5 — Le fichier complet

Le fichier complet de la boutique est disponible dans le dossier exemples/boutique.html. Il contient tout le code ci-dessus assemblé, avec un design soigné et des fonctionnalités supplémentaires comme l'affichage des catégories et la gestion des erreurs. Ouvrez-le dans votre navigateur après avoir lancé Fuseki, et vous aurez une boutique en ligne fonctionnelle basée sur le web sémantique.

🛒 Aller plus loin : le panier

Ajoutez un panier d'achat avec une requête CONSTRUCT qui crée un graphe représentant la commande. Utilisez POST /ds/update pour insérer la commande dans Fuseki. Le web sémantique gère aussi l'écriture.

🏷️ Recommandations

« Les clients qui ont acheté ce produit ont aussi aimé… » Une simple requête SPARQL avec jointure : deux produits commandés par les mêmes clients. Pas besoin d'algorithme de recommandation.

⚡ Pagination

Ajoutez LIMIT 10 OFFSET 0 dans vos requêtes et des boutons « Page suivante ». Gérez l'offset avec une variable JavaScript. Pas de backend à modifier.

📊 Tableau de bord

Une page d'admin avec des stats : produits les plus vendus, catégories les plus populaires, notes moyennes, stock faible. Tout avec des requêtes SPARQL d'agrégation.

CHAPITRE 7 PRODUCTION

Déploiement : mettre l'application en production

Notre application marche en local. Maintenant, on veut la mettre en ligne. Voici comment faire avec un VPS (ou un Raspberry Pi, comme dans les déploiements précédents).

7.1 — Architecture de production

┌─────────────┐     ┌──────────────┐     ┌─────────────┐
│  Navigateur │────▶│   Nginx      │────▶│   Fuseki    │
│  (frontend) │     │  (proxy)     │     │  (SPARQL)   │
└─────────────┘     └──────────────┘     └─────────────┘
                         │
                         ▼
                    ┌──────────────┐
                    │  boutique.html│
                    │ (fichier stat.)│
                    └──────────────┘
  

7.2 — Configuration Nginx

Nginx
# /etc/nginx/sites-available/boutique
server {
    listen 80;
    server_name boutique.example.com;

    # Fichiers statiques (la page HTML)
    root /var/www/boutique;
    index boutique.html;

    # Proxy inverse vers Fuseki
    location /sparql/ {
        proxy_pass http://127.0.0.1:3030/ds/;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;

        # Nécessaire pour les requêtes POST SPARQL
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    }

    # Interface Fuseki (protégée par mot de passe)
    location /fuseki/ {
        proxy_pass http://127.0.0.1:3030/;
        auth_basic "Accès restreint";
        auth_basic_user_file /etc/nginx/.fuseki_passwd;
    }
}

Avec cette configuration, le frontend utilise l'URL :

JavaScript
// Au lieu de http://localhost:3030/ds
const FUSEKI = '/sparql';
// Nginx fait la translation vers Fuseki

7.3 — Fuseki en service systemd

Bash
# /etc/systemd/system/fuseki.service
[Unit]
Description=Apache Jena Fuseki SPARQL Server
After=network.target

[Service]
Type=simple
User=pi
ExecStart=/home/pi/fuseki/fuseki-server \
    --update \
    --loc=/home/pi/fuseki/data \
    /ds
Restart=on-failure
RestartSec=5

[Install]
WantedBy=multi-user.target

7.4 — Script de déploiement complet

Bash
#!/usr/bin/env bash
# deploy-boutique.sh — Déploie la boutique sémantique
set -euo pipefail

HOST="${1:-pi@192.168.0.40}"

echo ":: 1. Envoi de l'ontologie..."
scp vente.ttl ${HOST}:/home/pi/fuseki/data/

echo ":: 2. Envoi de la page frontend..."
scp boutique.html ${HOST}:/var/www/boutique/

echo ":: 3. Rechargement de Fuseki..."
ssh ${HOST} "sudo systemctl restart fuseki"

echo ":: 4. Rechargement de Nginx..."
ssh ${HOST} "sudo systemctl reload nginx"

echo "✅ Boutique déployée sur http://boutique.example.com"
🔒 Note de sécurité : En production, ne laissez jamais votre endpoint SPARQL en accès libre sans authentification. N'importe qui pourrait faire un DROP ALL et supprimer votre catalogue. Utilisez toujours : La liberté du web sémantique s'arrête là où commence la sécurité.
ÉPILOGUE

Et après ? Votre boutique sémantique est en ligne

Ce cours vous a montré comment aller de l'ontologie à l'application en utilisant Protégé, Fuseki, SPARQL et une page web. Vous avez construit un catalogue e-commerce fonctionnel basé sur le web sémantique.

Ce que vous avez appris

  1. Protégé — créer une ontologie, définir classes, propriétés et instances
  2. Modélisation — structurer un domaine e-commerce en OWL
  3. Fuseki — déployer un serveur SPARQL, charger des données, l'administrer
  4. SPARQL avancé — filtres, agrégations, recherche textuelle, OPTIONAL
  5. Frontend SPARQL — connecter une page web à un endpoint SPARQL sans backend
  6. Déploiement — mettre l'application en production avec Nginx et systemd

Directions pour la suite

🛒 Panier et commandes

Étendez l'ontologie avec des classes Commande et Facture. Utilisez les requêtes SPARQL UPDATE pour créer des commandes depuis le frontend. Gérez les statuts de commande.

🔐 Authentification

Ajoutez un système de connexion. Liez les clients à leurs commandes via l'ontologie. Utilisez JSON Web Tokens (JWT) pour sécuriser l'API SPARQL en écriture.

📱 Application mobile

Le même endpoint SPARQL peut alimenter une app mobile (React Native, Flutter, ou même une PWA). Le backend sémantique ne change pas.

🔗 Liaison avec Wikidata

Au lieu de créer vos propres marques, liez-les à Wikidata : :apple owl:sameAs <http://www.wikidata.org/entity/Q312>. Vous héritez de toutes les connaissances de Wikidata.

🏷️ schema.org

Ajoutez des annotations schema.org à vos produits. Google les utilise pour les rich snippets dans les résultats de recherche. Votre boutique apparaît avec prix, stock, avis directement dans Google.

🤖 Assistant IA

Connectez un LLM (GPT, Claude) à votre endpoint SPARQL. « Quelle est la note moyenne des smartphones Samsung ? » → le LLM génère la requête SPARQL → le résultat est renvoyé en langage naturel. Le saint Graal.

« Une application sans ontologie, c'est un corps sans squelette. Ça tient debout, mais ça ne peut pas grandir. » — Une architecte de systèmes sémantiques (qui a arrêté les métaphores après celle-ci)
🎓 Bilan des cinq cours :
Cours #1 : Du texte aux vecteurs — TF-IDF, embeddings
Cours #2 : Des triplets aux graphes — Triplets, Prolog, Turtle
Cours #3 : Des faits aux concepts — OWL, raisonnement, SPARQL
Cours #4 : Du graphe aux motifs — Turtle avancé, chemins, SHACL
Cours #5 : De l'ontologie à l'application — Protégé, Fuseki, Frontend

Vous avez maintenant tout le pipeline du web sémantique entre les mains : de la modélisation conceptuelle dans Protégé jusqu'à l'interface utilisateur dans le navigateur. Vous pouvez construire des applications qui comprennent les données au lieu de simplement les stocker.
Protégé → .ttl → Fuseki → SPARQL → Web