Mapping graph models

Mappings can be used for applying transformations to the RDF as it’s imported into Neo4j or to transform the vocabulary used in a Neo4j graph as it’s exported through the different RDF export methods described in Exporting RDF data.

The following are two examples of situations where mappings can be useful:

  • We have a graph in Neo4j that we want to publish as JSON-LD through a REST api, but we want to map the elements in our graph (labels, property names, relationship names) to a public vocabulary so our API 'speaks' that public vocabulary and is therefore easily consumable by applications that 'speak' the same vocabulary.

  • We want to import a public taxonomy specified using the SKOS model but we want to persist it in Neo4j using our own schema terms. An example of this is that we want skos:narrower relationships in the taxonomy to be called :CHILD_CATEGORY in our Neo4j graph.

Both types of transformations can be described using mappings and neosemantics will action on them both on import and on export as described.

Mappings can be used to rename terms between two vocabularies (the neo4j graph schema and a public RDF schema). They are essentially pairs of equivalent terms, one from each vocabulary. More complex correspondences between elements of different types (for instance, mapping a property to a node+property) are currently not possible.

Public Vocabularies/Ontologies

A public graph model is also called an Ontology (or a schema, or a vocabulary). We will not go into the details of the subtle differences between each of them in this manual. All we need to know is that a graph model typically defines a set of types (categories), their internal structure (properties), and how they relate to each other (relationships). Some popular examples are schema.org, FIBO or the FOAF vocabulary.

Public vocabularies like the ones mentioned, typically uniquely identify the terms with a name and a namespace. So roughly speaking, a namespace identifies a vocabulary (or a part of it like in the case of FIBO where different modules are split in different namespaces).

To create a mapping with n10s we need to do two things: first, create a namespace prefix definition as describe in section Defining custom prefixes for namespaces, and then define individual mappings from elements in the Neo4j schema to elements in the public schema.

As mentioned in the intro, mappings are 1:1 and the same mapping definition mechanism is used both for importing and exporting RDF. Here’s how to do it:

Defining mappings

First we’ll create a namespace prefix definition for the vocabulary (SKOS in this case). This is described in section Defining custom prefixes for namespaces.

CALL n10s.nsprefixes.add("skos","http://www.w3.org/2004/02/skos/core#");

We can create as many namespace prefix definitions as needed. Also, they can be deleted using the n10s.nsprefixes.remove method passing as single parameter the prefix of the namespace prefix definition we want deleted.

Once we have created a namespace prefix definition for a public vocabulary/schema, we can create actual mappings for individual elements in our graph to elements in the public schemas and vice-versa. We use the n10s.mapping.add procedure for this. This method takes two parameters:

  • The full URI of the public vocabulary element we want to map. A prefix for the element’s namespace must have been defined as described above via n10s.nsprefixes.add

  • The name of the element in the Neo4j graph (a property name, a label or a relationship type).

The following example shows how to define a map from a relationship called :CHILD_CATEGORY in a Neo4j graph to the skos:narrower relationship in the SKOS model.

CALL n10s.mapping.add("http://www.w3.org/2004/02/skos/core#narrower", "CHILD_CATEGORY");

We can list existing mappings using n10s.mapping.listMappings and filter the list with an optional search string parameter to return only mappings where either the graph element name or the schema element name match the search string.

call n10s.mapping.list();
Table 1. Results
schemaNs schemaPrefix schemaElement elemName

"http://www.w3.org/2004/02/skos/core#"

"skos"

"narrower"

"CHILD_CATEGORY"

It is also possible to remove individual mappings with n10s.mapping.drop passing as single parameter the name of the graph model element on which the mapping is defined.

call n10s.mapping.drop("CHILD_CATEGORY");

Mappings for export

Let’s look in detail at the case where we want to publish a graph in Neo4j but we want to map it to our organisation’s canonical model, our Enterprise Ontology or any public vocabulary. For this example we’re going to use the Northwind database in Neo4j :play northwind-graph and the public schema.org vocabulary.

Here’s the script that defines the reference to the schema.org public vocabulary and a few individual mappings for elements in the Northwind database in Neo4j.

//set parameter uri ->   :param uri: "http://schema.org/"

CALL n10s.nsprefixes.add("sch",$uri);
CALL n10s.mapping.add($uri + "Order","Order");
CALL n10s.mapping.add($uri + "orderNumber","orderID");
CALL n10s.mapping.add($uri + "orderDate","orderDate");

CALL n10s.mapping.add($uri + "orderedItem","ORDERS");

CALL n10s.mapping.add($uri + "Product","Product");
CALL n10s.mapping.add($uri + "productID","productID");
CALL n10s.mapping.add($uri + "name","productName");

CALL n10s.mapping.add($uri + "category","PART_OF");

CALL n10s.mapping.add($uri + "name","categoryName");

After running the previous script, we can check that the mappings have been correctly defined with

call n10s.mapping.list();

That should return:

Table 2. Results
schemaNs schemaPrefix schemaElement elemName

"http://schema.org/"

"sch"

"Order"

"Order"

"http://schema.org/"

"sch"

"orderNumber"

"orderID"

"http://schema.org/"

"sch"

"orderDate"

"orderDate"

"http://schema.org/"

"sch"

"orderedItem"

"ORDERS"

"http://schema.org/"

"sch"

"Product"

"Product"

"http://schema.org/"

"sch"

"productID"

"productID"

"http://schema.org/"

"sch"

"category"

"PART_OF"

"http://schema.org/"

"sch"

"name"

"categoryName"

Now we can see these mappings in action by running any of the RDF generating methods described in Exporting RDF data (/describe, /describe/find or /cypher). Let’s use the /cypher method to serialise as RDF an order given its orderID.

:POST /rdf/cypher
{ "cypher" : "MATCH path = (n:Order { orderID : '10785'})-[:ORDERS]->()-[:PART_OF]->(:Category { categoryName : 'Beverages'}) RETURN path " , "format": "RDF/XML" , "mappedElemsOnly" : true }

The Cypher query uses the elements in the Neo4j graph but the generated RDF uses schema.org vocabulary elements. The mapping we just defined is bridging the two. Note that the mapping is completely dynamic which means that any change to the mapping definition will be applied to any subsequent request.

Elements for which no mapping has been defined will use the default Neo4j schema but we can specify that only mapped elements are to be exported by setting the mappedElemsOnly parameter to true in the request.

Here’s the output generated by the previous request:

<?xml version="1.0" encoding="UTF-8"?>
<rdf:RDF
	xmlns:neovoc="neo4j://com.neo4j/voc#"
	xmlns:neoind="neo4j://com.neo4j/indiv#"
	xmlns:sch="http://schema.org/"
	xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">

<rdf:Description rdf:about="neo4j://com.neo4j/indiv#786">
	<rdf:type rdf:resource="http://schema.org/Order"/>
	<sch:orderNumber>10785</sch:orderNumber>
	<sch:orderDate>1997-12-18 00:00:00.000</sch:orderDate>
</rdf:Description>

<rdf:Description rdf:about="neo4j://com.neo4j/indiv#74">
	<rdf:type rdf:resource="http://schema.org/Product"/>
	<sch:productID>75</sch:productID>
	<neovoc:productName>Rhönbräu Klosterbier</neovoc:productName>
</rdf:Description>

<rdf:Description rdf:about="neo4j://com.neo4j/indiv#80">
	<sch:name>Beverages</sch:name>
</rdf:Description>

<rdf:Description rdf:about="neo4j://com.neo4j/indiv#786">
	<sch:orderedItem rdf:resource="neo4j://com.neo4j/indiv#74"/>
</rdf:Description>

<rdf:Description rdf:about="neo4j://com.neo4j/indiv#74">
	<sch:category rdf:resource="neo4j://com.neo4j/indiv#80"/>
</rdf:Description>

</rdf:RDF>

There’s another example of use of mappings for export in this blog post.

Mappings for import

In this section we’ll see how to use mappings to apply changes to an RDF dataset on ingestion using the RDF import procedures described in Importing RDF Data.

Let’s say we are importing into Neo4j the the Open PermID dataset from Thomson Reuters. Here is a small fragment of the 'Person' file:

@prefix vcard: <http://www.w3.org/2006/vcard/ns#> .
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
@prefix permid: <https://permid.org/> .

permid:1-34419230351
  a vcard:Person ;
  vcard:given-name "Keith"^^xsd:string .

permid:1-34419198943
  vcard:family-name "Peltz"^^xsd:string ;
  vcard:given-name "Maxwell"^^xsd:string ;
  vcard:additional-name "S"^^xsd:string ;
  a vcard:Person .

permid:1-34418273443
  vcard:family-name "Benner"^^xsd:string ;
  vcard:given-name "Thomas"^^xsd:string ;
  a vcard:Person ;
  vcard:friend-of <https://permid.org/1-34419230351> .

As part of the import process, we want to drop the namespaces (as described in Importing RDF Data, this can be done using the handleVocabUris: "IGNORE" configuration) BUT in this case, we also want to create more neo4j-friendly names for properties. We want to get rid of the dashes in property names like given-name or additional-name and use 'camelCase' notation instead. The way to tell Neosemantics to do that is by defining a model mapping and setting the handleVocabUris parameter on import to 'MAP'.

We’ll start by defining a mapping like the one we defined for exporting RDF. Note that the properties we want to map are all in the same vcard vocabulary: http://www.w3.org/2006/vcard/ns#. The following script should do the job:

WITH
[{ neoSchemaElem : "givenName", publicSchemaElem:	"given-name" },
{ neoSchemaElem : "familyName", publicSchemaElem: "family-name" },
{ neoSchemaElem : "additionalName", publicSchemaElem: "additional-name" },
{ neoSchemaElem : "FRIEND_OF", publicSchemaElem: "friend-of" }] AS mappings,
"http://www.w3.org/2006/vcard/ns#" AS vcardUri

CALL n10s.nsprefixes.add("vcard",vcardUri) YIELD namespace
UNWIND mappings as m
CALL n10s.mapping.add(vcardUri + m.publicSchemaElem,m.neoSchemaElem) YIELD schemaElement
RETURN count(schemaElement) AS mappingsDefined;

Just like we did in the previous section, we define a namespace prefix for the vocabulary with n10s.nsprefixes.add and then we add individual mappings for elements in the vocabulary with n10s.mapping.add. If there were multiple vocabularies to map, we would just need repeat the process for each of them.

Now we can check that the mappings are correctly defined by running:

CALL n10s.mapping.list();
Table 3. Results
schemaNs schemaPrefix schemaElement elemName

"http://www.w3.org/2006/vcard/ns#"

"vcard"

"given-name"

"givenName"

"http://www.w3.org/2006/vcard/ns#"

"vcard"

"family-name"

"familyName"

"http://www.w3.org/2006/vcard/ns#"

"vcard"

"additional-name"

"additionalName"

"http://www.w3.org/2006/vcard/ns#"

"vcard"

"friend-of"

"FRIEND_OF"

Important to note that when using the option handleVocabUris: "MAP" in our Graph Config, all non-mapped vocabulary elements in any RDF that we import will get the default treatment they get when the 'IGNORE' option is selected.

Once the mappings are defined and the Graph Config set to handleVocabUris: 'MAP', we can run the import process as described in Importing RDF Data as follows:

CALL n10s.graphconfig.init({handleVocabUris: 'MAP'});

CALL n10s.rdf.import.fetch("https://github.com/neo4j-labs/neosemantics/raw/3.5/docs/rdf/permid-person-fragment.ttl","Turtle");

After data load, we will be able to query the imported graph with a much more friendly cypher:

MATCH (n:Person)
RETURN n.uri AS uri, n.familyName as familyName
LIMIT 10;
Table 4. Results
uri familyName

"https://permid.org/1-34419230351"

null

"https://permid.org/1-34418273443"

"Benner"

"https://permid.org/1-34419198943"

"Peltz"

The combination of a mapping definition plus the use of the handleVocabUris: 'MAP' configuration can be applied not only to the n10s.rdf.import.* procedures but also to the preview ones n10s.rdf.preview.*.