Foundation
Semantic Web for the Working Ontologist
Allemang, Hendler & Gandon. The conceptual foundation for this whole curriculum. Unusually honest about where RDF hurts. Read Chapters 1–3 alongside this module.
The conceptual comparison between RDF and labeled property graphs, followed by a working query lab on real Naruto data. Start the Fuseki server first — the understanding comes from running queries, not from reading about them.
Anyone arriving at RDF from Neo4j, or at Neo4j from RDF, asks the same question. Most public answers are not neutral. Vendor pieces make property graphs feel practical and RDF feel ceremonial. Semantic-web defenders make RDF feel principled and property graphs feel parochial. Both framings contain truth and both hide where real system design happens.
The honest answer is that RDF and labeled property graphs model many of the same facts but start from different primitives. Once you choose the primitives, consequences cascade: syntax, query shape, metadata, reasoning, reuse, integration, tooling, and team skill. This page tries to make that choice hard in the right way.
The Resume Graph Explorer runs on Neo4j. Its ESCO skill links are RDF. Understanding where those two stacks meet — and where they diverge — is the concrete version of Module 1's 30-second test.
RDF starts with the triple: subject, predicate, object. Subjects and predicates are IRIs. Objects can be IRIs, literals, or blank nodes. Classes, vocabularies, entailment, and ontology design sit on top of that single move — there is nothing else.
A labeled property graph starts with nodes and relationships. Nodes carry labels and properties; relationships carry types and properties. The model feels closer to how many developers already think: this person worked at this company, and the relationship itself has a title and start date.
Both models can say "Alice knows Bob" with equal ease. The divergence starts when you want to say something about the relationship itself — like "they have known each other since 2020."
@prefix foaf: <http://xmlns.com/foaf/0.1/> . :Alice a foaf:Person . :Bob a foaf:Person . :Alice foaf:knows :Bob . # "since 2020" needs: event node, # RDF-star, or classical reification.
CREATE
(alice:Person {name: 'Alice'}),
(bob:Person {name: 'Bob'}),
(alice)-[:KNOWS {since: 2020}]->(bob)
That "since 2020" is not a footnote — it is the first hint of the whole trade-off. LPG puts it on the edge and moves on. RDF asks whether this relationship is itself a thing worth naming. Annoying sometimes; clarifying also sometimes.
Quoted triples (RDF-star) reduce some historical friction around statement-level metadata. You will meet RDF-star in Module 3, alongside the other reification approaches. For now, the event-class pattern teaches the underlying modeling question better: when does a relationship deserve to become a first-class thing?
One person, two jobs, one degree, three skills linked to ESCO-style skill concepts. Small enough to fit on screen; structured enough to surface every modeling choice. The ESCO IRIs are illustrative placeholders.
CREATE
(p:Person {
name: "Alex Rivera",
email: "alex@example.com" }),
(a:Company {name: "Riverbend Analytics"}),
(b:Company {name: "Northwind Health"}),
(s:School {name: "Midstate University"}),
(p)-[:EMPLOYED_BY {
role: "Junior Data Analyst",
startDate: date("2020-01-15"),
endDate: date("2022-06-30")
}]->(a),
(p)-[:EMPLOYED_BY {
role: "Data Scientist",
startDate: date("2022-07-01")
}]->(b),
(p)-[:EDUCATED_AT {
degree: "BS Statistics",
endDate: date("2019-05-15")
}]->(s),
(py:Skill {name:"Python", escoId:"ccd0..."}),
(sq:Skill {name:"SQL", escoId:"29cd..."}),
(ml:Skill {name:"Machine learning",
escoId:"4d4d..."}),
(p)-[:HAS_SKILL {level:"advanced"}]->(py),
(p)-[:HAS_SKILL {level:"advanced"}]->(sq),
(p)-[:HAS_SKILL {level:"intermediate"}]->(ml)
@prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> . @prefix xsd: <http://www.w3.org/2001/XMLSchema#> . @prefix foaf: <http://xmlns.com/foaf/0.1/> . @prefix schema: <https://schema.org/> . @prefix skos: <http://www.w3.org/2004/02/skos/core#> . @prefix sensemaking: <https://sensemaking-ai.com/ns/> . :alex a foaf:Person ; foaf:name "Alex Rivera" ; sensemaking:hasEmployment :emp1, :emp2 ; sensemaking:hasEducation :edu1 ; sensemaking:hasSkill :s_py, :s_sql, :s_ml . :emp1 a sensemaking:Employment ; sensemaking:employer :riverbend ; sensemaking:role "Junior Data Analyst" ; sensemaking:startDate "2020-01-15"^^xsd:date ; sensemaking:endDate "2022-06-30"^^xsd:date . :emp2 a sensemaking:Employment ; sensemaking:employer :northwind ; sensemaking:role "Data Scientist" ; sensemaking:startDate "2022-07-01"^^xsd:date . :s_py a skos:Concept ; skos:prefLabel "Python" ; skos:exactMatch <http://data.europa.eu/esco/skill/ccd0> . :s_sql a skos:Concept ; skos:prefLabel "SQL" ; skos:exactMatch <http://data.europa.eu/esco/skill/29cd> . :s_ml a skos:Concept ; skos:prefLabel "Machine learning" ; skos:exactMatch <http://data.europa.eu/esco/skill/4d4d> .
Three things to notice. First, employment dates in Cypher go on the edge. In Turtle, employment becomes an event node with dates as properties. More characters, more nesting — but once employment is a named thing, it can carry salary ranges, confidence scores, supervisors, and provenance later. RDF makes you pay earlier so reuse has somewhere to attach.
Second, the skills. Cypher stores the ESCO ID as a string. Turtle types each skill as skos:Concept and adds skos:exactMatch to its ESCO IRI. The difference looks cosmetic. The ESCO section below shows it is not.
Third, line count. Turtle is longer. That is the URI tax arriving on the page — not a failure of concision. The next section shows what that tax buys.
MATCH (p:Person {name: "Alex Rivera"}) -[r:EMPLOYED_BY]->(c:Company) RETURN c.name, r.role, r.startDate ORDER BY r.startDate
SELECT ?orgName ?role ?start WHERE { :alex sensemaking:hasEmployment ?emp . ?emp sensemaking:employer ?org ; sensemaking:role ?role ; sensemaking:startDate ?start . ?org schema:name ?orgName . } ORDER BY ?start
The skills query is where the stacks separate. In Cypher, the ESCO ID is a string property. The database has no idea what that string points to. To follow the ESCO concept hierarchy you would need to import ESCO into your database as a separate node set, maintain it as ESCO updates, and join on the string ID.
In SPARQL, ESCO is published as RDF. Each skill's skos:exactMatch link points to a real skos:Concept in ESCO's graph. Following skos:broader into the occupational hierarchy is a normal triple-pattern — no import, no glue code, no joins on strings.
With RDF, skos:exactMatch connects your skill node to ESCO where ESCO lives. With LPG, ESCO has to be imported as a parallel node set or accessed outside the database via a string join.
// Works only if ESCO is in Neo4j already. MATCH (p:Person {name:"Alex Rivera"}) -[:HAS_SKILL]->(s:Skill) OPTIONAL MATCH (s)-[:EXACT_MATCH] ->(e:ESCOConcept)-[:BROADER] ->(b:ESCOConcept) RETURN s.name, e.iri, b.label
SELECT ?skillLabel ?broaderLabel WHERE { :alex sensemaking:hasSkill ?skill . ?skill skos:prefLabel ?skillLabel ; skos:exactMatch ?escoSkill . OPTIONAL { ?escoSkill skos:broader/skos:prefLabel ?broaderLabel . } }
The SPARQL version is not shorter. The unlock is that the broader-category data exists, is queryable, and did not have to be imported. SKOS is already RDF. ESCO is published using RDF, OWL, and SKOS. The query follows predicates the external vocabulary already understands — not "integrating" in the glue-code sense, just following the graph.
RDF is more verbose because every important term carries a globally identifiable IRI. Files have more prefixes. Diffs are longer. Beginners feel as if the syntax is asking them to write the whole internet before breakfast.
For an isolated application — one team, one database, no integration partners — the URI tax is pure overhead. There is no interop story to unlock. LPG is the humane choice, and the team ships faster.
For a graph that participates in a broader ecosystem — ESCO, FIBO, schema.org, PROV-O, Wikidata — the tax becomes infrastructure. The cost arrives early. The value compounds: every external vocabulary you can read is a query you do not have to engineer separately.
RDF does not win because it is cleaner to type. RDF wins when the future integration surface matters more than the immediate typing experience. The inverse is also true — which is why "always use RDF" is as wrong as "always use Neo4j."
The starter kit's data/example.ttl is ~200 lines and follows the curriculum's standard conventions. Before running the query lab, read the structure — not line by line, but section by section. Three things to notice before the queries begin.
# Class declaration — reuse before defining
sensemaking:Ninja a owl:Class ;
rdfs:subClassOf schema:Person ;
rdfs:label "Ninja" ;
rdfs:comment "A character trained in
jutsu and affiliated
with a hidden village." .
sensemaking:Ninja is declared as a subclass of schema:Person rather than a standalone class. A reasoner can infer every Ninja is also a Person without that triple being explicitly stated. This is not Module 3 magic — it is a design habit that starts with the first file.
# Symmetric property declaration
sensemaking:rivalOf
a owl:ObjectProperty,
owl:SymmetricProperty ;
rdfs:domain sensemaking:Ninja ;
rdfs:range sensemaking:Ninja .
owl:SymmetricProperty means a reasoner could infer the reverse: if Naruto rivals Sasuke, then Sasuke rivals Naruto — without that second triple being in the file. Challenge 4 below asks what a plain SPARQL query sees vs. what an OWL reasoner would add.
# SKOS for jutsu — controlled vocabulary,
# not a class hierarchy
sensemaking:Sharingan a skos:Concept ;
skos:prefLabel "Sharingan"@en ,
"写輪眼"@ja .
skos:Concept rather than OWL classes. That choice shows up in q03 and q04 where skos:prefLabel drives the language filter. It also demonstrates why "Sharingan" and "Byakugan" can coexist in a controlled vocabulary without needing class hierarchy machinery.
Read data/example.ttl in a text editor alongside this page. The inline comments explain every non-obvious choice. You should be able to predict what each query returns before running it. If you cannot, re-read the relevant section of the file first — prediction is where the learning happens.
Each query below demonstrates one core SPARQL pattern. Copy the query, paste it into the Fuseki SPARQL editor (linked per card), and run it. Then work through the "Think about this" prompts before moving on — the comprehension comes from prediction and modification, not from reading results.
PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> PREFIX schema: <https://schema.org/> PREFIX sensemaking: <https://sensemaking-ai.com/ns/example#> SELECT ?ninja ?name WHERE { ?ninja a sensemaking:Ninja ; schema:name ?name . } ORDER BY ?name
PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#> PREFIX schema: <https://schema.org/> PREFIX sensemaking: <https://sensemaking-ai.com/ns/example#> SELECT ?teamName ?villageName (COUNT(?ninja) AS ?memberCount) WHERE { ?team a sensemaking:Team ; schema:name ?teamName ; schema:memberOf ?village . ?village schema:name ?villageName . FILTER (LANG(?villageName) = "en") ?ninja sensemaking:memberOfTeam ?team . } GROUP BY ?teamName ?villageName ORDER BY DESC(?memberCount) ?teamName
PREFIX sensemaking: <https://sensemaking-ai.com/ns/example#> ASK WHERE { ?someone sensemaking:hasJutsu sensemaking:Sharingan . }
PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#> PREFIX schema: <https://schema.org/> PREFIX skos: <http://www.w3.org/2004/02/skos/core#> PREFIX sensemaking: <https://sensemaking-ai.com/ns/example#> SELECT ?name ?jutsuLabel WHERE { ?ninja a sensemaking:Ninja ; schema:name ?name ; sensemaking:memberOfVillage ?village . ?village rdfs:label ?villageLabel . FILTER (CONTAINS(LCASE(?villageLabel), "leaf")) OPTIONAL { ?ninja sensemaking:hasJutsu ?jutsu . ?jutsu skos:prefLabel ?jutsuLabel . FILTER (LANG(?jutsuLabel) = "en") } } ORDER BY ?name
PREFIX schema: <https://schema.org/> PREFIX sensemaking: <https://sensemaking-ai.com/ns/example#> CONSTRUCT { ?a sensemaking:colleagueOf ?b . } WHERE { ?a sensemaking:memberOfTeam ?team . ?b sensemaking:memberOfTeam ?team . FILTER (STR(?a) < STR(?b)) }
Each challenge asks you to write a query not in the starter kit. Start from the closest existing query and modify from there. Expected results are given inside the "expand" panel — write the query before opening it.
Start from q01. Add a pattern that excludes ninja who have at least one sensemaking:memberOfTeam triple. The SPARQL keyword is FILTER NOT EXISTS { ... }.
Expected: 2 rows — Gaara (affiliated with Suna, no team) and Kurenai Yuhi (listed as sensei, but no team membership in the data).
SELECT ?name WHERE { ?ninja a sensemaking:Ninja ; schema:name ?name . FILTER NOT EXISTS { ?ninja sensemaking:memberOfTeam ?team . } } ORDER BY ?name
Write a GROUP BY query that returns each ninja's name and a count of their jutsu. Only include ninja who have at least one jutsu. Start from q02's aggregation pattern.
Expected: 5 rows — Kakashi (2), then Naruto, Sasuke, Hinata, Gaara (1 each). Rock Lee, Sakura, and Kurenai have no jutsu and should not appear.
PREFIX schema: <https://schema.org/> PREFIX sensemaking: <https://sensemaking-ai.com/ns/example#> SELECT ?name (COUNT(?jutsu) AS ?jutsuCount) WHERE { ?ninja a sensemaking:Ninja ; schema:name ?name ; sensemaking:hasJutsu ?jutsu . } GROUP BY ?name ORDER BY DESC(?jutsuCount) ?name
Write a SELECT returning every sensei–student pair using sensemaking:senseiOf. Then extend it: use HAVING to return only sensei who teach more than one student in the dataset.
All pairs: Kakashi → Naruto, Kakashi → Sasuke, Kakashi → Sakura, Kurenai → Hinata.
Multi-student sensei only: Kakashi (3 students). Kurenai has one and should not appear.
# All pairs PREFIX schema: <https://schema.org/> PREFIX sensemaking: <https://sensemaking-ai.com/ns/example#> SELECT ?senseiName ?studentName WHERE { ?sensei sensemaking:senseiOf ?student ; schema:name ?senseiName . ?student schema:name ?studentName . } ORDER BY ?senseiName # Multi-student sensei only — add aggregation SELECT ?senseiName (COUNT(?student) AS ?n) WHERE { ?sensei sensemaking:senseiOf ?student ; schema:name ?senseiName . } GROUP BY ?senseiName HAVING (COUNT(?student) > 1)
Write a SELECT returning all rivalry pairs using sensemaking:rivalOf. Check example.ttl — is the rivalry stated in both directions, or only one? rivalOf is declared owl:SymmetricProperty. Does a plain SPARQL query over the raw Turtle pick up inferred triples, or only a reasoner?
Stated in the file: Naruto rivals Sasuke and Gaara rivals Rock Lee — asserted in one direction only.
The reasoning wrinkle: a plain SPARQL query against example.ttl returns only triples that are explicitly present. owl:SymmetricProperty is a declaration an OWL reasoner would use to infer the reverse pair — but Fuseki's default dataset does not apply OWL entailment. Run the query, count the rows, then think about what a fully-reasoned dataset would add.
PREFIX schema: <https://schema.org/> PREFIX sensemaking: <https://sensemaking-ai.com/ns/example#> SELECT ?aName ?bName WHERE { ?a sensemaking:rivalOf ?b ; schema:name ?aName . ?b schema:name ?bName . } ORDER BY ?aName
This is the open-world assumption and the reasoning layer showing up together. SPARQL queries what is present; OWL entailment expands what is present. Module 3 covers how to enable the latter.
This section is the answer to Module 1's 30-second test: explain to a working engineer, without slogans, why someone would choose RDF over a labeled property graph or vice versa. These are the heuristics that come up in an actual design review.
| Pressure | Usually favors | Why |
|---|---|---|
| Path traversal inside one product | LPG | Traversal ergonomics and edge properties earn their keep; the URI tax has no upside. |
| Vocabulary reuse across systems | RDF | Global IRIs and shared vocabularies are the structural point. SKOS, ESCO, PROV-O. |
| Formal reasoning | RDF | RDF/RDFS/OWL have formal semantics; LPG semantics are application-defined. |
| Provenance-heavy assertions | Depends | LPG edge properties are easy; RDF reification is more portable but more expensive. Module 3 covers four options. |
| Fast team ramp-up | The stack the team already knows | A correct LPG model beats a confused RDF model. The reverse is also true. Team fit is technical fit. |
Before moving to Module 2: explain to a working engineer why someone would choose RDF over a labeled property graph — without the words "semantic," "ontology," or "standards." If the answer is concrete and uses specific examples, the foundations have landed. If it feels vague, redo Exercise 1.3 before moving on. The comparison work is where the conceptual difference clicks; rushing past it makes Module 2's modeling work harder.
Not equal-purpose links — read them for different kinds of evidence.
Other materials for Submodule 1.1: synthesis document (static reading companion with annotated diagrams and side-by-side queries) · Greek mythology domain variant · Module 1 cheatsheet (coming soon)
Allemang, Hendler & Gandon. The conceptual foundation for this whole curriculum. Unusually honest about where RDF hurts. Read Chapters 1–3 alongside this module.
Hogan et al. Places RDF and property graphs in the same landscape without declaring a winner. The paper to send when debates get too confident.
DuCharme. The clearest single-source reference for SPARQL syntax and patterns — the curriculum's canonical reference. Chapters 1–2 belong in this module.
The largest public SPARQL endpoint. Fastest way to feel SPARQL on real, large data — modify the example queries in the helper UI rather than starting from scratch.
Author of Learning SPARQL. Ongoing posts on SPARQL patterns and the LLM + knowledge graph intersection. Worth bookmarking for the rest of the curriculum.
The full arc for Weeks 1–3: exercises, primary project, pain points, deliverables, and the 30-second test. This workbook covers Submodule 1.1; the README covers the rest.