Protégé · Ontologie · Fuseki · SPARQL · Frontend
Construisez une application e-commerce complète
du modèle conceptuel à la page web.
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.
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.
| Couche | Technologie | Rôle |
|---|---|---|
| Modélisation | Protégé | Créer l'ontologie (classes, propriétés, instances) |
| Sérialisation | Turtle | Exporter l'ontologie au format .ttl |
| Stockage | Apache Jena Fuseki | Serveur SPARQL avec données persistantes |
| Requêtage | SPARQL | Interroger le catalogue (produits, catégories, prix…) |
| Frontend | HTML + JavaScript | Interface utilisateur de la boutique |
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 :
Les données sont directement exploitables par d'autres applications. Vous pouvez lier vos produits à schema.org, Wikidata, DBpedia. Essayez ça avec MySQL.
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.
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.
Fuseki parle HTTP et retourne du JSON. Votre frontend peut l'interroger directement sans backend. C'est une API REST sans framework.
Pour suivre ce cours, vous aurez besoin de :
https://protege.stanford.eduhttps://jena.apache.org/download/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 :
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
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 :
| Onglet | Utilité |
|---|---|
| Entities | Vue centralisée : classes, propriétés, individus |
| Classes | Hiérarchie des classes, équivalences, restrictions |
| Object Properties | Propriétés reliant des individus (ex: aPourCategorie) |
| Data Properties | Propriétés reliant des littéraux (ex: aPrix) |
| Individuals | Liste et édition des instances |
| OWL Viz | Visualisation graphique de l'ontologie (super utile pour les présentations) |
| DL Query | Requêtage en logique descriptive — la version SPARQL des pauvres |
File → New ou cliquer sur l'icône « Nouveau »http://example.org/venteL'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.
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.
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.
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.
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 :
owl:Thing → Add subclassProduit)Categorie, Marque, Client,
Commande, AvisProduit
→ Add subclass → Smartphone, etc.Maintenant, les propriétés. On distingue deux types :
| Propriété | Domaine | Range | Description |
|---|---|---|---|
aPourCategorie | Produit | Categorie | Catégorie d'un produit |
estFabriquePar | Produit | Marque | Marque du produit |
aPourAvis | Produit | Avis | Avis sur le produit |
estAchetePar | Commande | Client | Client qui a passé la commande |
contientProduit | Commande | Produit | Produits dans la commande |
Dans Protégé : onglet Object Properties → Add. Remplir le nom, puis dans l'onglet Description, définir Domains (intersection) et Ranges (intersection).
| Propriété | Domaine | Type | Description |
|---|---|---|---|
nom | Produit, Marque, Client, Categorie | xsd:string | Nom de l'entité |
prix | Produit | xsd:decimal | Prix en euros |
description | Produit | xsd:string | Description textuelle |
stock | Produit | xsd:integer | Quantité en stock |
note | Avis | xsd:integer | Note sur 5 |
commentaire | Avis | xsd:string | Texte de l'avis |
dateCommande | Commande | xsd:dateTime | Date de la commande |
email | Client | xsd:string | Adresse email |
urlImage | Produit | xsd:string | URL de l'image |
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.
Maintenant, ajoutons des individus (instances). Dans l'onglet Individuals, créez :
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 .
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 .
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 .
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 .
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é.
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.
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.
File → Export Inferred Ontology As....ttl)vente.ttl
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.
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.
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
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.
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.
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.
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.
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.
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
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
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") ) }
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)
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
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) }
/api/produits?categorie=smartphones&prix_max=1000&stock_min=1&tri=note&ordre=desc
— vous implémentez un endpoint spécifique pour chaque combinaison.
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#>
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.
On a l'ontologie, on a Fuseki, on a SPARQL. Maintenant, on va connecter le tout à une page web qui servira de vitrine commerciale.
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.
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>
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; }
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(''); }
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();
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.
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.
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.
« 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.
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.
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.
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).
┌─────────────┐ ┌──────────────┐ ┌─────────────┐
│ Navigateur │────▶│ Nginx │────▶│ Fuseki │
│ (frontend) │ │ (proxy) │ │ (SPARQL) │
└─────────────┘ └──────────────┘ └─────────────┘
│
▼
┌──────────────┐
│ boutique.html│
│ (fichier stat.)│
└──────────────┘
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
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
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"
DROP ALL et supprimer
votre catalogue. Utilisez toujours :
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.
É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.
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.
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.
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.
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.
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.