Sensemaking AI · Sensemaking Semantic Web
Submodule 1.1 · Foundations

RDF, Turtle, and SPARQL

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.

Module 1 · Weeks 1–3 Requires Fuseki + starter kit ~90 min hands-on RDF · LPG · Turtle · SPARQL
Dataset Naruto Greek mythology
Checking for Fuseki at localhost:3030…

Which one should I be using?

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.

Same fact, different primitives.

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.

Turtle
@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.
Cypher
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.

RDF 1.2 note

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?

A small career graph, twice.

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.

Cypher create statements
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)
Turtle equivalent
@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.

Three queries, three answers

Cypher — all jobs, chronological
MATCH (p:Person {name: "Alex Rivera"})
      -[r:EMPLOYED_BY]->(c:Company)
RETURN c.name, r.role, r.startDate
ORDER BY r.startDate
Ergonomic edge: LPG
SPARQL — all jobs, chronological
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 ESCO interop unlock

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.

Cypher — skills with ESCO categories
// 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
SPARQL — skills with ESCO categories
SELECT ?skillLabel ?broaderLabel WHERE {
  :alex sensemaking:hasSkill ?skill .
  ?skill skos:prefLabel ?skillLabel ;
         skos:exactMatch ?escoSkill .
  OPTIONAL {
    ?escoSkill skos:broader/skos:prefLabel
               ?broaderLabel .
  }
}
Interop edge: RDF

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.

The verbosity is real.

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.

Mental shift

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

Eight ninja, five patterns.

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." .
Reuse before defining. 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 .
Property annotations carry meaning. Declaring 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 .
Concepts, not classes. Jutsu are typed as 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.

Open the file before running queries

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.

Five queries, five patterns.

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.

q01
Who are all the ninja in the dataset?
Pattern: SELECT · two triple patterns · ORDER BY
Open Fuseki ↗
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
q02
How many ninja are on each team, and which village?
Pattern: GROUP BY · COUNT aggregation · multilingual literal trap
Open Fuseki ↗
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
q03
Does anyone in the dataset know the Sharingan?
Pattern: ASK · existence check · open-world assumption
Open Fuseki ↗
PREFIX sensemaking: <https://sensemaking-ai.com/ns/example#>

ASK
WHERE {
  ?someone sensemaking:hasJutsu sensemaking:Sharingan .
}
q04
Which Konoha ninja have jutsu listed, and which don't?
Pattern: FILTER on bound variable · OPTIONAL left-join · row multiplication
Open Fuseki ↗
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
q05
If two ninja share a team, emit a new "colleague" triple for each pair.
Pattern: CONSTRUCT · inference materialization · symmetric deduplication
Open Fuseki ↗
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))
}

Four things to build.

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.

C1 Find all ninja who are not on any team

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 result and one approach

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
C2 Count jutsu per ninja, most to fewest

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 result and one approach

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
C3 List all sensei–student relationships; then filter to multi-student sensei only

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.

Expected result and one approach

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)
C4 Find rivals — then reason about what symmetry means in plain SPARQL

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?

Expected result and the reasoning note

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.

Heuristics, not slogans.

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.

Choose LPG when

  • The system is path-heavy and contained in one product.
  • Relationship metadata is central and local — dates on edges, weights on edges.
  • Your team already ships comfortably in Neo4j or similar.
  • No external vocabulary needs to be queried as first-class data.

Choose RDF when

  • The data is vocabulary-driven or integration-heavy.
  • External graphs — ESCO, FIBO, Wikidata, schema.org — already speak your terms.
  • Reasoning, equivalence, or subsumption matter to the application.
  • Long-term portability matters more than short-term terseness.
PressureUsually favorsWhy
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.

The 30-second test

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.

Six good next reads.

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)

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.

Balanced frame

Knowledge Graphs, section 3

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.

Query reference

Learning SPARQL

DuCharme. The clearest single-source reference for SPARQL syntax and patterns — the curriculum's canonical reference. Chapters 1–2 belong in this module.

Hands-on endpoint

Wikidata Query Service

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.

Practitioner blog

Bob DuCharme's blog

Author of Learning SPARQL. Ongoing posts on SPARQL patterns and the LLM + knowledge graph intersection. Worth bookmarking for the rest of the curriculum.

Curriculum context

Module 1 README

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.