OpenLink Logo

Applying Semantic Reasoning to Neo4j using Virtuoso

Leveraging semantic technologies like RDFS and OWL to enrich Neo4j's property graph data can significantly amplify insights and reasoning capabilities. In this article, we demonstrate how Virtuoso's commercial edition, combined with Neo4j's powerful graph database, can provide robust semantic reasoning capabilities to your graph data.

Prerequisites

  • Virtuoso Commercial Edition (Free 30-day Trial)
  • Neo4j Database Instance (Aura or Self-Hosted) with the Query API enabled
  • Installation and configuration of the neo4j_bridge library (repo)

Installation & Setup

This guide covers how to set up a bridge between your Virtuoso and Neo4j instances using SQL and SPARQL-within-SQL (SPASQL) statements.

In this demo, we will be using the iSQL HTTP interface provided with Virtuoso out of box at the following locations:

  • http://localhost:8890/conductor/isql_main.vspx
  • https://localhost/conductor/isql_main.vspx
  • http://{CNAME}:port/conductor/isql_main.vspx
  • https://{CNAME}:{port}/conductor/isql_main.vspx

Creating A Procedure View for Cypher Results Sets

Create a procedure view using Virtuoso to bridge data from Neo4j:

SQL
CREATE PROCEDURE VIEW demo.neo4j.pg2rdf_https 
AS neo4j_bridge(query)(entity1_id VARCHAR, entity1_type VARCHAR, entity1_key VARCHAR, entity1_value VARCHAR, relationship VARCHAR, entity2_id VARCHAR, entity_2_type VARCHAR, entity2_key VARCHAR, entity2_value VARCHAR);

CREATE PROCEDURE VIEW demo.neo4j.relationship_attrs 
AS neo4j_bridge (query)(relationship_id VARCHAR,relationship_type VARCHAR, entity1_id VARCHAR, entity2_id VARCHAR, relationship_property_key VARCHAR, relationship_property_value VARCHAR,relationship_property_value_datatype VARCHAR);

Note: A procedure view is a Virtuoso feature that allows a stored procedure result set to be used in place of a table. (More info)

Physical SQL Table Generation

In this demonstration, we will materialize the procedure views into a physical table for use with reasoning and inference

SQL
CREATE TABLE demo.neo4j.pg2rdf_physical
AS
(
    SELECT * 
    FROM demo.neo4j.pg2rdf_https
    WHERE query = 'MATCH (a)-[r]->(b) WITH a, b, r, properties(a) AS entity1_props, properties(b) AS entity2_props, labels(a) AS entity1_labels, labels(b) AS entity2_labels UNWIND entity1_labels AS entity1_label UNWIND entity2_labels AS entity2_label UNWIND keys(entity1_props) AS key1 UNWIND keys(entity2_props) AS key2 WITH elementId(a) AS entity1_id, toString(entity1_label) AS entity1_type, toString(apoc.text.camelCase(key1)) AS entity1_key, toString(entity1_props[key1]) AS entity1_value, toString(apoc.text.camelCase(type(r))) AS relationship, elementId(b) AS entity2_id, toString(entity2_label) AS entity2_type, toString(apoc.text.camelCase(key2)) AS entity2_key, toString(entity2_props[key2]) AS entity2_value RETURN entity1_id, entity1_type, entity1_key, entity1_value, relationship, entity2_id, entity2_type, entity2_key, entity2_value'
)
WITH DATA;

CREATE TABLE demo.neo4j.relationship_attrs_physical
AS
(
    SELECT * 
    FROM demo.neo4j.relationship_attrs
    WHERE query = 'MATCH (a)-[r]->(b) UNWIND keys(r) AS raw_key WITH elementId(r) AS relationship_id, type(r) AS raw_type, elementId(a) AS entity1_id, elementId(b) AS entity2_id, raw_key, r[raw_key] AS raw_value, split(toLower(raw_key), ''_'') AS key_parts, split(toLower(type(r)), ''_'') AS type_parts WITH relationship_id, entity1_id, entity2_id, raw_value, CASE WHEN raw_value IS NULL THEN [] WHEN raw_value IN [true, false] THEN [raw_value] WHEN raw_value =~ ''.*'' THEN [raw_value] ELSE raw_value END AS value_list, key_parts[0] + reduce(s = '''', part IN key_parts[1..] | s + toUpper(substring(part, 0, 1)) + substring(part, 1)) AS relationship_property_key, type_parts[0] + reduce(s = '''', part IN type_parts[1..] | s + toUpper(substring(part, 0, 1)) + substring(part, 1)) AS relationship_type UNWIND value_list AS value WITH relationship_id, relationship_type, entity1_id, entity2_id, relationship_property_key, toString(value) AS relationship_property_value, CASE WHEN value IS NULL THEN ''null'' WHEN value IN [true, false] THEN ''boolean'' WHEN toString(value) =~ ''^-?\\\\d+$'' THEN ''integer'' WHEN toString(value) =~ ''^-?\\\\d+\\\\.\\\\d+$'' THEN ''float'' ELSE ''string'' END AS relationship_property_value_datatype RETURN relationship_id, relationship_type, entity1_id, entity2_id, relationship_property_key, relationship_property_value, relationship_property_value_datatype'
)
WITH DATA;

RDF View Creation via R2RML

Deploy the following R2RML mapping script into Virtuoso:

SPARQL
-- Install R2RML Script
SPARQL DROP QUAD MAP <urn:neo4j:r2ml:mapping>;
SPARQL CLEAR GRAPH <urn:neo4j:r2ml:mapping>;

SPARQL
prefix rr: <http://www.w3.org/ns/r2rml#>
prefix neo4j: <http://demo.openlinksw.com/schemas/neo4j-demo#>
prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>
prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#>
prefix owl: <http://www.w3.org/2002/07/owl#>

INSERT INTO GRAPH <urn:neo4j:r2ml:mapping>
{
    <#TriplesMapNeo4jEntity1> a rr:TriplesMap; rr:logicalTable [ rr:sqlQuery """SELECT DISTINCT entity1_id, entity1_type, entity1_key, entity1_value FROM demo.neo4j.pg2rdf_physical"""]; 
    rr:subjectMap [ rr:termType rr:IRI  ; rr:template "http://demo.openlinksw.com/neo4j-demo/{entity1_id}#this"; rr:class owl:Thing; rr:graph  ];
    rr:predicateObjectMap [ rr:predicateMap [ rr:constant rdf:type ] ; rr:objectMap [ rr:termType rr:IRI; rr:template "http://demo.openlinksw.com/schemas/neo4j-demo#{entity1_type}" ]; ] ;
    rr:predicateObjectMap [ rr:predicateMap [ rr:termType rr:IRI; rr:template "http://demo.openlinksw.com/schemas/neo4j-demo#{entity1_key}" ] ; rr:objectMap [ rr:column "entity1_value" ]; ] .

    <#TriplesMapNeo4jEntity2> a rr:TriplesMap; rr:logicalTable [ rr:sqlQuery """SELECT DISTINCT entity2_id, entity_2_type, entity2_key, entity2_value FROM demo.neo4j.pg2rdf_physical"""]; 
    rr:subjectMap [ rr:termType rr:IRI  ; rr:template "http://demo.openlinksw.com/neo4j-demo/{entity2_id}#this"; rr:class owl:Thing; rr:graph  ];
    rr:predicateObjectMap [ rr:predicateMap [ rr:constant rdf:type ] ; rr:objectMap [ rr:termType rr:IRI; rr:template "http://demo.openlinksw.com/schemas/neo4j-demo#{entity_2_type}" ]; ] ;
    rr:predicateObjectMap [ rr:predicateMap [ rr:termType rr:IRI; rr:template "http://demo.openlinksw.com/schemas/neo4j-demo#{entity2_key}" ] ; rr:objectMap [ rr:column "entity2_value" ]; ] .

    <#TriplesMapNeo4jRelationships> a rr:TriplesMap; rr:logicalTable [ rr:sqlQuery """SELECT DISTINCT entity1_id, relationship, entity2_id FROM demo.neo4j.pg2rdf_physical"""]; 
    rr:subjectMap [ rr:termType rr:IRI  ; rr:template "http://demo.openlinksw.com/neo4j-demo/{entity1_id}#this"; rr:graph  ];
    rr:predicateObjectMap [ rr:predicateMap [ rr:termType rr:IRI; rr:template "http://demo.openlinksw.com/schemas/neo4j-demo#{relationship}" ] ; rr:objectMap [ rr:termType rr:IRI  ; rr:template "http://demo.openlinksw.com/neo4j-demo/{entity2_id}#this" ]; ] .

    <#TriplesMapNeo4jRelationshipsMeta> a rr:TriplesMap; rr:logicalTable [ rr:sqlQuery """SELECT DISTINCT entity1_id, entity2_id, relationship_id, MD5(CONCAT(entity1_id,entity2_id,relationship_property_key)) as relationship_hash, relationship_property_key, relationship_property_value FROM demo.neo4j.relationship_attrs_physical"""]; 
    rr:subjectMap [ rr:termType rr:IRI  ; rr:template "http://demo.openlinksw.com/neo4j-demo/{relationship_hash}#this"; rr:class neo4j:PropertyAnnotation; rr:graph  ];
    rr:predicateObjectMap [ rr:predicateMap [ rr:constant rdfs:label ] ; rr:objectMap [ rr:column "relationship_property_key" ]; ];
    rr:predicateObjectMap [ rr:predicateMap [ rr:constant neo4j:sourceNode ] ; rr:objectMap [ rr:termType rr:IRI  ; rr:template "http://demo.openlinksw.com/neo4j-demo/{entity1_id}#this" ]; ] ;
    rr:predicateObjectMap [ rr:predicateMap [ rr:constant neo4j:nodeRelationship ] ; rr:objectMap [ rr:termType rr:IRI  ; rr:template "http://demo.openlinksw.com/schemas/neo4j-demo#{relationship_property_key}" ]; ] ;
    rr:predicateObjectMap [ rr:predicateMap [ rr:constant neo4j:targetNode ] ; rr:objectMap [ rr:termType rr:IRI  ; rr:template "http://demo.openlinksw.com/neo4j-demo/{entity2_id}#this" ]; ] ;
    rr:predicateObjectMap [ rr:predicateMap [ rr:constant neo4j:targetValue ] ; rr:objectMap [ rr:column "relationship_property_value" ]; ] .
};

EXEC ('SPARQL ' || DB.DBA.R2RML_MAKE_QM_FROM_G ('urn:neo4j:r2ml:mapping','urn:neo4j:r2ml:map'));

Verify RDF View Creation

Confirm successful RDF creation with a test SPARQL query:

SPARQL
PREFIX neo4j: <http://demo.openlinksw.com/schemas/neo4j-demo#>
SELECT DISTINCT ?person ?name ?born
FROM <http://demo.openlinksw.com/neo4j/lpg2rdf#>
WHERE {
   ?person a neo4j:Person;
   neo4j:name ?name;
   neo4j:born ?born.
}

Materializing RDF Data

Materialize the Virtual RDF views for reasoning and inference application

SPARQL
SPARQL CLEAR GRAPH <urn:lpg2rdf:data>;
SPARQL
INSERT INTO GRAPH <urn:lpg2rdf:data> { ?s ?p ?o }
WHERE {
    GRAPH <http://demo.openlinksw.com/neo4j/lpg2rdf#>{
        ?s ?p ?o FILTER(?p != rdfs:subPropertyOf).
    }
};

Adding Semantic Reasoning via Inference Rules

Set up inference rules using OWL and RDFS terms to enable powerful reasoning:

SPARQL
SPARQL
CLEAR GRAPH <urn:inference:lpg2rdf>;

SPARQL 
PREFIX neo4j: <http://demo.openlinksw.com/schemas/neo4j-demo#>
prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#>
prefix owl: <http://www.w3.org/2002/07/owl#>
prefix dbr: <http://dbpedia.org/resource/>

INSERT INTO GRAPH <urn:inference:lpg2rdf>{

# Properties
# RDFS
 neo4j:name rdfs:subPropertyOf rdfs:label.
 neo4j:name rdfs:subPropertyOf schema:name.
 neo4j:title rdfs:subPropertyOf rdfs:label.
 neo4j:title rdfs:subPropertyOf schema:title.
 neo4j:tagline rdfs:subPropertyOf schema:alternativeHeadline.
 neo4j:released rdfs:subPropertyOf schema:datePublished.
 neo4j:born rdfs:subPropertyOf schema:birthDate.
 neo4j:follows rdfs:subPropertyOf schema:follows.

# OWL
 schema:actor owl:inverseOf neo4j:actedIn.
 schema:director owl:inverseOf neo4j:directed.
 schema:producer owl:inverseOf schema:produced.
 schema:author   owl:inverseOf neo4j:wrote.

# Classes
# OWL
neo4j:Person owl:equivalentClass schema:Person.
neo4j:Movie  owl:equivalentClass schema:Movie.
};

Create a Rules Set

SQL
RDFS_RULE_SET('urn:inference:lpg2rdf:rule','urn:inference:lpg2rdf');

Querying with RDFS and OWL Reasoning enabled

From here, we can query the transformed Labelled property graph using our loaded inference rules.

Test Query Without Reasoning and Inference (Expected Empty Result Set)

SQL
-- Test 0: Who acted as Neo in The Matrix? (No reasoning and Inference)
SPARQL
PREFIX neo4j: <http://demo.openlinksw.com/schemas/neo4j-demo#>
SELECT DISTINCT *

FROM <urn:lpg2rdf:data>
WHERE
{
  ?movie schema:title ?movieTitle;
   schema:actor ?actor.
  ?actor schema:name ?actorName.

 ?movieAnnotation a neo4j:PropertyAnnotation;
 neo4j:sourceNode ?actor;
 neo4j:nodeRelationship ?relationship;
 neo4j:targetNode ?movie;
 neo4j:targetValue 'Neo'.
}

Test Query With Reasoning and Inference Applied

SQL
-- Test 1: Who acted as Neo  in The Matrix? (w/ RDFS and OWL Reasoning and Inference)

SPARQL
DEFINE input:inference 'urn:inference:lpg2rdf:rule'
PREFIX neo4j: <http://demo.openlinksw.com/schemas/neo4j-demo#>
SELECT DISTINCT *

FROM <urn:lpg2rdf:data>
WHERE
{
  ?movie schema:title ?movieTitle;
   schema:actor ?actor.
  ?actor schema:name ?actorName.

 ?movieAnnotation a neo4j:PropertyAnnotation;
 neo4j:sourceNode ?actor;
 neo4j:nodeRelationship ?relationship;
 neo4j:targetNode ?movie;
 neo4j:targetValue 'Neo'.
}

Additional Examples

MCP Server Compatability

Through use of the OpenLink AI Layer, Virtuoso users can run SPARQL queries with reasoning and inference enabled.

Claude Desktop Example

Conclusion

Integrating Neo4j's property graphs with Virtuoso's semantic reasoning capabilities via RDFS and OWL enables richer insights and advanced querying capabilities. Follow the outlined steps to elevate your graph analytics and unlock deeper semantic value from your existing data.