Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/jbarrasa/goingmeta/llms.txt

Use this file to discover all available pages before exploring further.

Session 4 of Going Meta (broadcast May 3, 2022) tackles a fundamental challenge in knowledge graphs: meaning is often implicit. A database might record that someone DIRECTED a movie, but the fact that this person is therefore a Director lives only in the developer’s head. This session shows how to lift that implicit knowledge into the graph itself — first by hand, then by building a lightweight ontology, and finally by wiring it to an APOC trigger so that inference happens automatically whenever new data arrives.

What You Will Learn

  • Why implicit semantics are a problem and how explicit ontologies solve it
  • Modelling a mini-ontology inside Neo4j using _Category and _Relationship nodes
  • Writing a generic Cypher “meta-query” that infers node types from relationship patterns
  • Automating inference with an APOC trigger that fires on every new relationship
  • Importing an OWL ontology into Neo4j with n10s and using it as the source of truth for inference
  • Using n10s.inference.nodesLabelled for on-the-fly, non-materialised inference

From Implicit to Explicit Semantics

The movie database shipped with Neo4j is a convenient starting point. A director is anyone who has a DIRECTED relationship to a Movie — but that definition lives only in the query:
MATCH (x)-[:DIRECTED]->(:Movie)
RETURN DISTINCT x.name AS director
We can materialise this with a label-setting script:
MATCH (x)-[:DIRECTED]->(:Movie)
SET x:Director
But this is brittle: every time the model changes, someone has to remember to update the script. The session asks: can we make the definition of “Director” part of the data itself?

Building the In-Graph Ontology

1

Define the Director–Movie relationship in the ontology

MERGE (from:_Category { name: "Director" })
MERGE (to:_Category   { name: "Movie" })
MERGE (from)<-[:_from]-(r:_Relationship { name: "DIRECTED" })-[:_to]->(to)
2

Add the Actor definition

MERGE (from:_Category { name: "Actor" })
MERGE (to:_Category   { name: "Movie" })
MERGE (from)<-[:_from]-(r:_Relationship { name: "ACTED_IN" })-[:_to]->(to)
3

Write the generic meta-query

This single query reads the ontology and surfaces inferred types for every node in the graph — regardless of how many relationship types are defined:
MATCH (from:_Category)<-[:_from]-(r:_Relationship)-[:_to]->(to:_Category)
MATCH (x)-[rel]->(y)
WHERE type(rel) = r.name
RETURN x, " is a " + from.name, y, " is a " + to.name
4

Materialise the inferred labels in bulk

MATCH (from:_Category)<-[:_from]-(r:_Relationship)-[:_to]->(to:_Category)
MATCH (x)-[rel]->(y)
WHERE type(rel) = r.name
CALL apoc.create.addLabels(x, [from.name]) YIELD node AS xs
CALL apoc.create.addLabels(y, [to.name])   YIELD node AS ys
RETURN count(xs) + count(ys) + " nodes updated"

Automating Inference with a Trigger

Instead of running the enrichment script manually, wire it to an APOC trigger so that inference is applied immediately when new relationships are created:
CALL apoc.trigger.add('microinferencer',
'
MATCH (from:_Category)<-[:_from]-(r:_Relationship)-[:_to]->(to:_Category)
MATCH (x)-[rel]->(y)
WHERE rel IN $createdRelationships AND type(rel) = r.name
CALL apoc.create.addLabels(x, [from.name]) YIELD node AS xs
CALL apoc.create.addLabels(y, [to.name])   YIELD node AS ys
RETURN count(xs) + count(ys) + " nodes updated"
', { phase: 'before' })
Triggers require apoc.trigger.enabled=true in your neo4j.conf configuration file. The trigger runs inside the same transaction as the write, ensuring the inference is either fully applied or fully rolled back.
Test the trigger by creating new nodes and relationships — the inferred labels appear immediately:
MERGE (alex:Person { name: "Alex Erdl" })
MERGE (jb:Person   { name: "Jesús Barrasa" })
MERGE (gm:Movie    { title: "Going Meta!" })
MERGE (alex)-[:DIRECTED]->(gm)<-[:ACTED_IN]-(jb)
Verify the shortest path between the two people in the enriched graph:
MATCH path = shortestPath(
  (:Person { name: "Alex Erdl" })-[*..2]-(:Person { name: "Jesús Barrasa" })
)
RETURN path

Using an OWL Ontology as the Source of Truth

If you already have an ontology in OWL, you can import it directly with n10s and use the same inference procedures.
1

Set up the graph config for OWL import

CREATE CONSTRAINT n10s_unique_uri FOR (r:Resource) REQUIRE r.uri IS UNIQUE;

CALL n10s.graphconfig.init({
  handleVocabUris: "IGNORE",
  classLabel:           "_Category",
  objectPropertyLabel:  "_Relationship",
  domainRel:            "_from",
  rangeRel:             "_to",
  force: true
});
2

Preview and import the OWL ontology inline

CALL n10s.onto.preview.inline('
@prefix owl:  <http://www.w3.org/2002/07/owl#> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
@prefix mov:  <http://myvocabularies.com/Movies#> .

mov:Actor    a owl:Class ; rdfs:label "Actor" .
mov:Director a owl:Class ; rdfs:label "Director" .
mov:Artist   a owl:Class ; rdfs:label "Artist" .
mov:Critic   a owl:Class ; rdfs:label "Critic" .
mov:Movie    a owl:Class ; rdfs:label "Movie" .

mov:Actor    rdfs:subClassOf mov:Artist .
mov:Director rdfs:subClassOf mov:Artist .

mov:DIRECTED a owl:ObjectProperty ;
  rdfs:domain mov:Director ;
  rdfs:range  mov:Movie ;
  rdfs:label  "WROTE" .

mov:ACTED_IN a owl:ObjectProperty ;
  rdfs:domain mov:Actor ;
  rdfs:range  mov:Movie ;
  rdfs:label  "ACTED_IN" .

mov:REVIEWED a owl:ObjectProperty ;
  rdfs:domain mov:Critic ;
  rdfs:range  mov:Movie ;
  rdfs:label  "REVIEWED" .
', 'Turtle')

On-the-Fly Inference with n10s

Rather than materialising inferred labels, you can query them at runtime using n10s.inference.nodesLabelled. This is useful when the ontology changes frequently or when you want to avoid storing derived data.
// Give me all Artists (includes Actors and Directors via subClassOf)
CALL n10s.inference.nodesLabelled("Artist")
// Narrow down: how many of the inferred Artists are explicitly labelled Actor?
CALL n10s.inference.nodesLabelled("Artist") YIELD node
WHERE node:Actor
RETURN count(node)
n10s.inference.nodesLabelled traverses the rdfs:subClassOf hierarchy stored in the graph, so it always reflects the current state of your ontology without requiring any re-materialisation step.

Resources

Watch the Recording

Full live-stream recording of Going Meta Session 4 on YouTube.

Session Code on GitHub

All Cypher scripts and ontology snippets from this session.

Build docs developers (and LLMs) love