Can Two Teams Describe the Same Resource?
Distinguish resource identifiers from literals, read RDF statements and build SPARQL patterns without confusing RDF with the property-graph model.
13 sections · 4 questions · 1 coding challenge
The arrow keys move between sections too.
Web Resources Needed Descriptions That Could Travel
BOTIE
That helps our two maintenance teams state which resource a claim concerns. Agreement on identifiers and predicates still requires modelling work. Publishing a statement in RDF does not establish that it is correct.
Two Files Agree on a Name and Mean Different Things
One supplier calls a pump P7. Another uses P7 for a spare motor. Meanwhile, two teams describe the same supply under different display names. Botie cannot combine these records safely by comparing their text labels.
We need identifiers whose meaning survives the move between datasets, and a shared way to state a relationship. RDF expresses a statement using a subject, a predicate and an object. SPARQL asks which assignments to variables make patterns of those statements match.
Invented maintenance records. Python exercises and workshop components expose the mechanism locally. Source-linked Cypher or SPARQL examples require the stated external database setup.
Step 1 of 1
A predicate gives the arrow its meaning
The full resource identifiers distinguish entities even when the diagram uses short display names.
Nodes and properties (3)
- ex:P7 · Resource
- ex:P8 · Resource
- ex:S1 · Resource
- ex:P7 to ex:S1: ex:dependsOn
- ex:P8 to ex:S1: ex:dependsOn
Resolve resource dependencies from RDF records
The resource projection preserves RDF object categories, then predicate and target matching yield distinct subject resources rather than literal lookalikes.
Resolve resource dependencies from RDF records
The same identifier text occurs as a resource and as a literal.
BOTIE
Result for these records
- 0https://perceptronlab.example/P7
- 1https://perceptronlab.example/P8
Follow the operations
1. Expanded statement records
[ { "s": "https://perceptronlab.example/P7", "p": "https://perceptronlab.example/dependsOn", "o": "https://perceptronlab.example/S1", "kind": "iri" }, { "s": "https://perceptronlab.example/P8", "p": "https://perceptronlab.example/dependsOn", "o": "https://perceptronlab.example/S1", "kind": "iri" }, { "s": "https://perceptronlab.example/P7", "p": "https://perceptronlab.example/dependsOn", "o": "https://perceptronlab.example/S1", "kind": "iri" }, { "s": "https://perceptronlab.example/P9", "p": "https://perceptronlab.example/dependsOn", "o": "https://perceptronlab.example/S1", "kind": "literal" }, { "s": "https://perceptronlab.example/P7", "p": "https://perceptronlab.example/near", "o": "https://perceptronlab.example/S2", "kind": "iri" } ]2. Project resource triples as a graph
Nodes and properties (4)
- https://perceptronlab.example/P7 · Resource
- https://perceptronlab.example/S1 · Resource
- https://perceptronlab.example/P8 · Resource
- https://perceptronlab.example/S2 · Resource
- https://perceptronlab.example/P7 to https://perceptronlab.example/S1: https://perceptronlab.example/dependsOn
- https://perceptronlab.example/P8 to https://perceptronlab.example/S1: https://perceptronlab.example/dependsOn
- https://perceptronlab.example/P7 to https://perceptronlab.example/S2: https://perceptronlab.example/near
Inspect the node and relationship records
{ "nodes": [ { "id": "https://perceptronlab.example/P7", "kind": "Resource" }, { "id": "https://perceptronlab.example/S1", "kind": "Resource" }, { "id": "https://perceptronlab.example/P8", "kind": "Resource" }, { "id": "https://perceptronlab.example/S2", "kind": "Resource" } ], "edges": [ { "id": "[\"https://perceptronlab.example/P7\",\"https://perceptronlab.example/dependsOn\",\"https://perceptronlab.example/S1\"]", "source": "https://perceptronlab.example/P7", "target": "https://perceptronlab.example/S1", "type": "https://perceptronlab.example/dependsOn" }, { "id": "[\"https://perceptronlab.example/P8\",\"https://perceptronlab.example/dependsOn\",\"https://perceptronlab.example/S1\"]", "source": "https://perceptronlab.example/P8", "target": "https://perceptronlab.example/S1", "type": "https://perceptronlab.example/dependsOn" }, { "id": "[\"https://perceptronlab.example/P7\",\"https://perceptronlab.example/near\",\"https://perceptronlab.example/S2\"]", "source": "https://perceptronlab.example/P7", "target": "https://perceptronlab.example/S2", "type": "https://perceptronlab.example/near" } ] }3. Read graph relationships
[ { "id": "[\"https://perceptronlab.example/P7\",\"https://perceptronlab.example/dependsOn\",\"https://perceptronlab.example/S1\"]", "source": "https://perceptronlab.example/P7", "target": "https://perceptronlab.example/S1", "type": "https://perceptronlab.example/dependsOn" }, { "id": "[\"https://perceptronlab.example/P8\",\"https://perceptronlab.example/dependsOn\",\"https://perceptronlab.example/S1\"]", "source": "https://perceptronlab.example/P8", "target": "https://perceptronlab.example/S1", "type": "https://perceptronlab.example/dependsOn" }, { "id": "[\"https://perceptronlab.example/P7\",\"https://perceptronlab.example/near\",\"https://perceptronlab.example/S2\"]", "source": "https://perceptronlab.example/P7", "target": "https://perceptronlab.example/S2", "type": "https://perceptronlab.example/near" } ]4. Choose a relationship type
[ { "id": "[\"https://perceptronlab.example/P7\",\"https://perceptronlab.example/dependsOn\",\"https://perceptronlab.example/S1\"]", "source": "https://perceptronlab.example/P7", "target": "https://perceptronlab.example/S1", "type": "https://perceptronlab.example/dependsOn" }, { "id": "[\"https://perceptronlab.example/P8\",\"https://perceptronlab.example/dependsOn\",\"https://perceptronlab.example/S1\"]", "source": "https://perceptronlab.example/P8", "target": "https://perceptronlab.example/S1", "type": "https://perceptronlab.example/dependsOn" } ]5. Requested resource IRI
https://perceptronlab.example/S1
6. Choose an endpoint match
[ { "id": "[\"https://perceptronlab.example/P7\",\"https://perceptronlab.example/dependsOn\",\"https://perceptronlab.example/S1\"]", "source": "https://perceptronlab.example/P7", "target": "https://perceptronlab.example/S1", "type": "https://perceptronlab.example/dependsOn" }, { "id": "[\"https://perceptronlab.example/P8\",\"https://perceptronlab.example/dependsOn\",\"https://perceptronlab.example/S1\"]", "source": "https://perceptronlab.example/P8", "target": "https://perceptronlab.example/S1", "type": "https://perceptronlab.example/dependsOn" } ]7. Collect distinct identifiers
- 0https://perceptronlab.example/P7
- 1https://perceptronlab.example/P8
8. Result
- 0https://perceptronlab.example/P7
- 1https://perceptronlab.example/P8
These components operate on the supplied records in your browser. They do not run a graph database or interpret Cypher.
1. Read a statement before choosing a database
In the statement P7 dependsOn S1, the subject is P7, the predicate is dependsOn and the object is S1. RDF identifies predicates with IRIs too, so two independently invented properties with the same short name need not mean the same thing.
A resource IRI is an identifier, not a promise that fetching its URL will return a useful page. Choose and document identifiers that remain stable when a display name changes.
The full identifier, rather than the label drawn beside a node, controls matching.
2. An object can be a resource or a literal
A resource object can participate in more statements. A literal is a value, such as a name, a date or a number. A literal whose characters look like an IRI is still a literal. Datatype and language tags carry meaning that a plain string comparison can lose.
Our compact Python fixture uses kind to distinguish IRI objects from literals. It deliberately omits blank nodes and datatype processing. Blank nodes represent resources without a global IRI; their local labels are not durable cross-file business keys.
The workshop projects only resource-to-resource statements. It is not a general RDF parser.
Keep an identifier separate from its written text
The characters match, but the object categories differ.
objects=[{'kind':'iri','value':'https://perceptronlab.example/S1'},{'kind':'literal','value':'https://perceptronlab.example/S1'}]
print([o['kind'] for o in objects])['iri', 'literal']Selecting a resource relationship must check the kind as well as the characters.
3. Let a variable occupy the unknown position
In SPARQL, ?asset names a variable. A triple pattern with that variable as subject asks which resources have the stated predicate and object. PREFIX only abbreviates IRIs in the query text; SELECT chooses the variables returned.
The braces contain the graph pattern. Several patterns sharing ?asset must agree on the same resource binding. FILTER can constrain a bound value, while OPTIONAL adds bindings when a pattern exists without requiring it for every row.
The same variable-binding idea from Cypher appears in a different graph data model.
Run in your own environment
Find resources depending on S1
Run this against an RDF store or SPARQL engine containing the Turtle fixture below.
Load this complete Turtle into a disposable dataset: @prefix ex: <https://perceptronlab.example/> . ex:P7 ex:dependsOn ex:S1 . ex:P8 ex:dependsOn ex:S1 . ex:P9 ex:dependsOn "https://perceptronlab.example/S1" . ex:P7 ex:near ex:S2 .
The expected resources are ex:P7 and ex:P8. Bind external values using your chosen RDF client’s supported RDF-term/query-binding mechanism; do not concatenate unchecked query fragments.
PREFIX ex: <https://perceptronlab.example/>
SELECT DISTINCT ?asset
WHERE { ?asset ex:dependsOn ex:S1 . }
ORDER BY ?assetP9 has a literal object. Its text looks like the resource IRI, but it does not match the resource object ex:S1.
Official documentation. Documentation checked .
Check yourself
Questions on Parts 1 to 3
Question 1 of 2
Which subject matches the resource-object pattern ?asset ex:dependsOn ex:S1?
Use the information below.
The data contains ex:P7 ex:dependsOn ex:S1 and ex:P9 ex:dependsOn "https://perceptronlab.example/S1". The second object is a literal.4. Separate graph statements from query-result duplicates
An RDF graph is a set of triples, so repeating an identical triple does not add another graph statement. A query can still produce repeated projected bindings through several matching combinations. DISTINCT applies to the projected query result, not to an imagined duplicate edge identity.
In a property graph, two separately identified relationships can have the same endpoints and type. That difference matters when importing visit events. Do not mechanically convert each event into an identical RDF triple and expect its identity to survive.
Model an event as an identified resource when the event itself needs its own properties or evidence.
5. Missing information is not automatically false
If the dataset has no dependsOn statement for P9, we know that this dataset did not produce a match. We have not proved that P9 has no physical dependency. RDF’s open-world interpretation leaves room for information that has not been recorded.
Inference is also an explicit choice. A SPARQL store does not necessarily apply every ontology rule automatically. Know the configured entailment regime before interpreting a query that relies on inferred types or relationships.
State whether the report concerns recorded statements, inferred statements or a verified complete inventory.
6. Keep statement evidence when combining sources
A short RDF triple does not by itself say who asserted it or which source document supported it. Named graphs and explicit provenance models can retain source context, but their interpretation must be documented.
Our next lesson keeps claims separate from the sources that support them. That gives Botie somewhere to look when the two teams disagree instead of silently treating the latest import as the truth.
Interoperable syntax makes disagreement easier to represent; it does not resolve it.
Check yourself
Questions on Parts 1 to 6
Question 1 of 2
What preserves those event identities?
Use the information below.
An import repeats an identical RDF triple, while another import represents two distinct inspection events with the same asset and technician.Try it with Botie
Build with Botie
Write and run Python