Skip to main content

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 URI http://xmlns.com/foaf/0.1/name which can also be written with the shorthand notation foaf: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 with npm 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 with foaf.knows
  • replace hasName with foaf.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 and xsd. 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 modules lincd-rdf , lincd-rdfs, lincd-shacl and lincd-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 like lin://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>
&nbsp;has friend&nbsp;
<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>
);
};