Introducing ontologies
So far you've created the properties like hasFriend
and hasName
your self. However, Linked Data is all about linking data across different environments, thus virtually creating one global graph database.
To achieve that we need to have reusable schematics. That is, the types of things that exist and the properties things can have need to be reused across different datasets as much as possible.
Classes (defining a type of thing) and Properties are therefore often bundled in reusable 'Ontologies'. You can think of them as vocabularies for specific application domains.
Official definition: Ontologies are used to classify the terms that can be used in a particular application, characterize possible relationships, and define possible constraints on using those terms.
For example, one popular ontology is called FOAF, which stands for friend-of-a-friend. Which can be used to describe people and their connections.
Amongst other things, it defines foaf:Person
, foaf:knows
and foaf:name
.
Every property and class defined by an ontology has a URI that starts with the URI of that ontology, plus their own name. For example
foaf:name
has the full URIhttp://xmlns.com/foaf/0.1/name
which can also be written with the shorthand notationfoaf:name
:
foaf:
is called the prefix which represents the URI of the FOAF ontology:http://xmlns.com/foaf/0.1/
foaf:name
is a prefixed URI, representing the full URI:http://xmlns.com/foaf/0.1/name
.
So let's use this FOAF ontology so that our application aligns with other FOAF based Linked Data applications so that we could seamlessly use and connect each other`s data
Importing an ontology
LINCD bundles functionality into modules. Modules can export one or more ontologies.
A module named lincd-foaf
already exists, which exposes the foaf
ontology.
Add this module to your app
yarn add lincd-foaf
or
npm install lincd-foaf -S
You can execute this command in a new terminal, or temporarily stop the dev server with
ctrl + c
/command + c
, so you can install this module and then restart the dev server again withnpm start
/yarn start
afterwards
Next, import the foaf
ontology from the lincd-foaf
module with
import {foaf} from 'lincd-foaf/lib/ontologies/foaf';
foaf
is an object that gives access to all properties and classes defined by FOAF as NamedNode
.
For example, you can now use foaf.name
which points to a NamedNode
that already has the correct URI: http://xmlns.com/foaf/0.1/name
.
So let's first remove the declaration of hasFriend
and hasName
in the top of Home.tsx
.
Then replace the following variables with existing properties from the foaf
ontology anywear in Home.tsx:
- replace
hasFriend
withfoaf.knows
- replace
hasName
withfoaf.name
When done, you should not find any references to hasFriend
or hasName
anymore in your code.
Congrats, you're now using existing Linked Data properties!
Classes
When the W3C standardized the Semantic Web it released the rdf
ontology and RDF specifications, and later the rdfs
ontology.
Some of the resources described in these ontologies are now the most used terms in the entire global linked data graph, since they are fundamental building blocks to build graphs.
Two such building blocks are rdf:type
and rdfs:Class
.
In short: rdf:type
is used as a property to state what sort of a thing this node is, and the value of that property is itself always a rdf:Class
.
Let's see what that means in practice. We are currently just creating a new NamedNode
every time a new name is submitted. Let's add a first property straight away to indicate that this node represents a person.
First import the rdf ontology, which is bundled in lincd
:
import {rdf} from 'lincd/lib/ontologies/rdf';
Then go into the handler function that creates a new person and just after you create the node with NamedNode.create()
, add:
newPerson.set(rdf.type, foaf.Person);
This states that this new NamedNode is in fact a Person.
Note: LINCD.js exports several core ontologies for ease of access:
rdf
,rdfs
,shacl
andxsd
. It only exports a few classes and properties for each of those ontologies however. For the full versions of each of these ontologies, refer to the moduleslincd-rdf
,lincd-rdfs
,lincd-shacl
andlincd-xsd
instead.
The value of rdf:type
should always be a class. That is, it itself should have the property rdf:type
rdfs:Class
.
In order words, our graph will look something like this:
<lin://tmp/23> rdf:type foaf:Person.
foaf:Person rdf:type rdfs:Class
Note: this is Turtle notation again, used here to describe two edges in the graph.
Note: using
NamedNode.create()
creates a temporary URI likelin://tmp/23
. Full URI's are written between<
>
in TURTLE, whilst prefixed URI's are just written as is.
Let's see if that's true.
The properties of the node foaf:Person
are defined in the foaf
ontology itself. So first we want to load the data of foaf
into memory. This doesn't happen by default, so that you can keep your app lightweight if you just want to access the right URI's of the properties or classes of an ontology, but you don't need to know their properties.
In this case we want to double check that foaf:Person
is indeed a rdfs:Class
, so let's change the import from foaf to include loadData
:
import {foaf, loadData} from 'lincd-foaf/lib/ontologies/foaf';
This allows us to load the data of the ontology. Go ahead and execute it right after all the imports:
loadData();
Now when creating a new person, log the types of the new person, and the types of those types at the end of the createNewPerson
function:
//log the types of a new person, should be foaf:Person (http://xmlns.com/foaf/0.1/Person)
console.log(newPerson.getAll(rdf.type).toString());
//log the types of the types of this new person, if the data of the ontology was properly loaded, this should contain rdfs:Class (http://www.w3.org/2000/01/rdf-schema#Class)
console.log(newPerson.getAll(rdf.type).getAll(rdf.type).toString());
You should see foaf:Person
in the first log, and if the data of the ontology was indeed loaded, you should see rdfs:Class
as well as owl:Class
as the types of foaf:Person
. (we'll talk about owl
later)
Conclusion
That was a lot of text and little coding!
But we've now made sure our nodes use the same types and properties as many other nodes in the global graph by reusing an existing ontology.
And by making sure our nodes have the type 'Person', we can more easily retrieve all persons in our graph database if we were to store our graph somewhere later on.
Though included in the code below, you can remove the console.log()
statements before continuing to the next step.
Example code for this step:
import {useState} from 'react';
import {NamedNode} from 'lincd/lib/models';
import {rdf} from 'lincd/lib/ontologies/rdf';
import {foaf, loadData} from 'lincd-foaf/lib/ontologies/foaf';
loadData();
export default function Home() {
let [newName, setNewName] = useState<string>('');
let [persons, setPersons] = useState<NamedNode[]>([]);
let [person1, setPerson1] = useState<NamedNode>(null);
let [person2, setPerson2] = useState<NamedNode>(null);
let [bool, setBool] = useState(false);
let forceUpdate = () => setBool(!bool);
let createNewPerson = function (e) {
//create a new named node for the new person
let newPerson = NamedNode.create();
newPerson.set(rdf.type, foaf.Person);
//create a new edge from this new person to its own name, using the foaf.name property as the connecting edge
//to store a string in the graph, we use a Literal node.
newPerson.setValue(foaf.name, newName);
//add the person to the array and update the state
setPersons(persons.concat(newPerson));
//log the types of a new person, should be foaf:Person http://xmlns.com/foaf/0.1/Person)
console.log(newPerson.getAll(rdf.type).toString());
//log the types of the types of this new person, if the data of the ontology was properly loaded, this should contain rdfs:Class (http://www.w3.org/2000/01/rdf-schema#Class)
console.log(newPerson.getAll(rdf.type).getAll(rdf.type).toString());
};
let onSubmitConnection = function (e) {
if (person1 && person2) {
person1.set(foaf.knows, person2);
forceUpdate();
}
};
return (
<div>
<h1>Persons</h1>
<form>
<input value={newName} onChange={(e) => setNewName(e.currentTarget.value)} type="text" placeholder="name" />
<input type="button" value="Add new person" onClick={createNewPerson} />
</form>
<hr />
<ul>
{persons.map((person) => {
return (
<li key={person.uri}>
{person.getValue(foaf.name)} has {person.getAll(foaf.knows).size} friends:{' '}
{person
.getAll(foaf.knows)
.map((friend: NamedNode) => friend.getValue(foaf.name))
.join(', ')}{' '}
</li>
);
})}
</ul>
{persons.length > 1 && (
<form>
<SelectPerson persons={persons} setPerson={setPerson1} value={person1}></SelectPerson>
has friend
<SelectPerson persons={persons} setPerson={setPerson2} value={person2}></SelectPerson>
<input type="button" value="Make connection" onClick={onSubmitConnection} />
</form>
)}
</div>
);
}
const SelectPerson = function ({persons, setPerson, value}) {
return (
<select value={value?.uri} onChange={(v) => setPerson(NamedNode.getOrCreate(v.currentTarget.value))}>
<option value={null}>---</option>
{persons.map((person) => (
<option value={person.uri} key={person.uri}>
{person.getValue(foaf.name)}
</option>
))}
</select>
);
};