Working with SHACL and Elixir

Applying SPARQL.ex to RDF shapes

Tony Hammond
15 min readOct 12, 2018
“brown and black wall decor” by Callum Wale on Unsplash

Following up on my last post about querying RDF using the Elixir SPARQL.ex and SPARQL.Client.ex libraries from Marcel Otto, I wanted to focus here on a simple use case with SHACL and an RDF shape used to provide a template description for the result graph. That is, instead of using a specific SPARQL query we will generate SPARQL queries from the RDF shape that defines the valid result graphs. And in doing so we will also bump up against some limitations with the current SPARQL.ex implementation and show how we might work around those.

NOTE (2018-11-13): See the two comments below by Marcel Otto (2018–11-02, 2018–11–13) which update on new support added to the SPARQL.ex package for CONSTRUCT, UNION and BIND. This makes most of the workarounds developed in this article unnecessary, although it may still be interesting to see how these features could be alternatively supported.

1. Review querying scenarios

The diagram below shows how a SPARQL query can be run against either a local in-memory graph or a remote graph accessed through a service, i.e. a SPARQL endpoint. (And note that we don’t care how a remote datastore is actually organized, as long as it appears to be an RDF graph store.)

For local queries we can make use of the SPARQL.ex library, whereas for remote queries we use the SPARQL.Client.ex library. To create a new graph from a query we would typically use a SPARQL construct query, instead of a SPARQL select query which just picks out separate RDF terms.

Now one thing I didn’t really discuss in my last post was any restrictions placed on querying with these Elixir libraries. According to the documentation there should be no limitations on querying by SPARQL.Client.ex, whereas SPARQL.ex is still a work in progress. (For the actual list of features currently supported see the full listing on the GitHub project.)

One shortcoming with the current implementation of SPARQL.ex is that only select queries are supported – not construct queries. This means that we would have to use a select query and then transform the resulting table to a graph, just as if we had used a construct query. The diagram below shows how an RDF shape graph can be used in building a SPARQL query to be run against either a local in-memory graph or a remote graph accessed through a service (SPARQL endpoint).

So, let’s see how to use a local RDF shape graph to build a SPARQL query for local and for remote querying.

2. Create a ‘TestSHACL’ project

As usual, let’s create a new project TestSHACL with Mix (and note this time we use the module option to pass the desired project name TestSHACL instead of the default casing TestShacl):

% mix new test_shacl --module TestSHACL

We’ll then declare a dependency on SPARQL.Client.ex in the mix.exs file. And we’ll also use the hackney HTTP client in Erlang as recommended.

defp deps do
[
{:sparql_client, "~> 0.2.1"},
{:hackney, "~> 1.6"}
]

And we add this line to the config.exs file:

config :tesla, :adapter, Tesla.Adapter.Hackney

We then use Mix to add in the dependency:

% mix deps.get

Let’s also clear out the boilerplate in lib/test_shacl.ex and add in a @moduledoc annotation. And we’ll also add a module attribute @priv_dir to locate our project artefacts directory priv/.

defmodule TestSHACL do
@moduledoc """
Top-level module used in "Working with SHACL and Elixir" post.
"""

@priv_dir "#{:code.priv_dir(:test_shacl)}
end

We’ll create a lib/test_shacl/ directory for the client module we’re going to add in the file client.ex.

% mkdir -p lib/test_shacl/

And we’ll add in a wrapper for our TestSHACL.Client module.

defmodule TestSHACL.Client do
@moduledoc """
This module provides test functions for the SPARQL.Client module.
"""
@priv_dir "#{:code.priv_dir(:test_shacl)}end

We’ll also create the priv/ directory tree.

% mkdir -p priv/data/
% mkdir -p priv/shapes/queries/

And for testing let’s also copy over the query convenience functions we defined in the last post for the TestQuery and TestQuery.Client modules. We’ll copy query/0, query/1 and query/2 to the TestSHACL module, and rquery/0, rquery/1 and rquery/2 to the TestSHACL.Client module.

And also to simplify naming in IEx we’ll add a .iex.exs configuration file.

% cat .iex.exs
import TestSHACL
import TestSHACL.Client

See here for the project TestSHACL code.

3. Define a query for an RDF shape

Now let’s reuse the same RDF description we have used in the last two posts.

@prefix bibo: <http://purl.org/ontology/bibo/> .
@prefix dc: <http://purl.org/dc/elements/1.1/> .
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
<urn:isbn:978-1-68050-252-7> a bibo:Book ;
dc:creator <https://twitter.com/bgmarx> ;
dc:creator <https://twitter.com/josevalim> ;
dc:creator <https://twitter.com/redrapids> ;
dc:date "2018-03-14"^^xsd:date ;
dc:format "Paper" ;
dc:publisher <https://pragprog.com/> ;
dc:title "Adopting Elixir"@en .

As before, we can define a simple data/0 function to retrieve this RDF data from the file 978–1–68050–252–7.ttl which we’ll add to priv/data/.

@data_dir @priv_dir <> "/data/"
@data_file "978-1-68050-252-7.ttl"
def data() do
RDF.Turtle.read_file!(@data_dir <> @data_file)
end

And let’s try that.

iex(7)> data |> RDF.Turtle.write_string! |> IO.puts
<urn:isbn:978-1-68050-252-7>
a <http://purl.org/ontology/bibo/Book> ;
<http://purl.org/dc/elements/1.1/creator> <https://twitter.com/bgmarx>, <https://twitter.com/josevalim>, <https://twitter.com/redrapids> ;
<http://purl.org/dc/elements/1.1/date> "2018-03-14"^^<http://www.w3.org/2001/XMLSchema#date> ;
<http://purl.org/dc/elements/1.1/format> "Paper" ;
<http://purl.org/dc/elements/1.1/publisher> <https://pragprog.com/> ;
<http://purl.org/dc/elements/1.1/title> "Adopting Elixir"@en .
:ok

Looks good. No prefix forms here but it is a valid RDF description. So now to defining an RDF shape for this graph.

The new W3C standard for RDF introduced last year, the Shapes Constraint Langauge (SHACL), has been a very significant development for RDF as it specifies a “language for describing and validating RDF graphs”. While most attention is usually focused on the validation part, the really unique value proposition of SHACL is its formalization of RDF graph descriptions. Because, of course, before validating a graph one needs to be able to define it properly. And the fact that RDF shapes in SHACL are themselves modelled as RDF means that these graph descriptions can be queried over in turn as native data constructs. This is powerful. This is very much the “code is data” paradigm.

SHACL defines two basic types of shapes:

  • shapes about a focus node, called node shapes
  • shapes about the values of a particular property or path for the focus node, called property shapes

Our use case here is based on a node shape. The set of focus nodes for a node shape may be identified using target declarations via the sh:targetClass property. In this use case we have but one focus node which is identified with the sh:targetClass of bibo:Book.

Now we can define a basic RDF shape for our book description as below. And for the purposes of this tutorial let’s expressly comment out a couple properties: dc:format and dc:publisher. This leaves just dc:creator, dc:date and dc:title. Let’s assume for whatever reason that we wish to limit our book descriptions to just title, author and date. (This example is admittedly more than a little contrived but it will serve our purposes here in defining a proper subgraph.)

% cat priv/shapes/book_shape.ttl
@prefix bibo: <http://purl.org/ontology/bibo/> .
@prefix dc: <http://purl.org/dc/elements/1.1/> .
@prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
@prefix sh: <http://www.w3.org/ns/shacl#> .
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
@prefix shapes: <http://example.org/shapes/> .# shape - Bookshapes:Book
a sh:NodeShape ;
sh:targetClass bibo:Book ;
rdfs:label "SHACL shape for the bibo:Book model" ;
sh:closed true ;
sh:property [ sh:path dc:creator ] ;
sh:property [ sh:path dc:date ] ;
# sh:property [ sh:path dc:format ] ;
# sh:property [ sh:path dc:publisher ] ;
sh:property [ sh:path dc:title ] ;
.

We can define a simple shape/0 function to retrieve the shape from the file book_shape.ttl which we’ll add to priv/shapes/.

@shapes_dir @priv_dir <> "/shapes/"
@shape_file "book_shape.ttl"
def shape() do
RDF.Turtle.read_file!(@shapes_dir <> @shape_file)
end

And again let’s try that.

iex(11)> shape |> RDF.Turtle.write_string! |> IO.puts
<http://example.org/shapes/Book>
a <http://www.w3.org/ns/shacl#NodeShape> ;
<http://www.w3.org/2000/01/rdf-schema#label> "SHACL shape for the bibo:Book model" ;
<http://www.w3.org/ns/shacl#closed> true ;
<http://www.w3.org/ns/shacl#property> [
<http://www.w3.org/ns/shacl#path> <http://purl.org/dc/elements/1.1/creator>
], [
<http://www.w3.org/ns/shacl#path> <http://purl.org/dc/elements/1.1/title>
] ;
<http://www.w3.org/ns/shacl#targetClass>
<http://purl.org/ontology/bibo/Book> .
:ok

So, we’re going to define a query for this shape in the file book_shape_query.rq which we’ll add to priv/shapes/queries/.

prefix bibo: <http://purl.org/ontology/bibo/>
prefix dc: <http://purl.org/dc/elements/1.1/>
prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>
prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#>
prefix sh: <http://www.w3.org/ns/shacl#>
prefix xsd: <http://www.w3.org/2001/XMLSchema#>
prefix shapes: <http://example.org/shapes/>construct {
?s ?p ?o
}
where {
{
select distinct ?p
where {
?shape sh:targetClass bibo:Book .
?shape sh:property [ sh:path ?p ] .
}
}
?s a bibo:Book .
?s ?p ?o .
}

The query pattern here is to use an inner query (a select query) against an RDF shape to get a list of allowed properties for a given class (identified by the sh:targetClass) and to use that to drive a construct query which matches instances of that class and decorates them with the allowed properties. Basically we’re retrieving RDF descriptions for things which are restricted to the property list specified in the RDF shape. In our case here we’re looking for instances of a book class (specifically a bibo:Book).

We’ll also define a convenience function shape_query/0 to read this query.

@shapes_queries_dir @priv_dir <> "/shapes/queries/"
@shape_query_file "book_shape_query.rq"
def shape_query() do
File.read!(@shapes_queries_dir <> @shape_query_file)
end

Now, we can verify this query by running against a local triplestore. Here I’m using GraphDB Free edition (version 8.5.0) from Ontotext with a new repo shape-test which just includes the data and the shape graphs. (In a production system we would typically use named graphs to organize the data but here it is not necessary.)

It works!

We have just those properties we defined in the RDF shape, and not those we commented.

4. Query in-memory RDF models

So let’s try now to emulate this with SPARQL.ex, notwithstanding any known limitations.

First let’s replicate the repo shape_test by using the RDF.Graph.add/2 function to merge the two graphs data and shape:

iex(20)> shape_test = RDF.Graph.add(data, shape)
#RDF.Graph{name: nil
~B<b0>
~I<http://www.w3.org/ns/shacl#path>
~I<http://purl.org/dc/elements/1.1/creator>
~B<b1>
~I<http://www.w3.org/ns/shacl#path>
~I<http://purl.org/dc/elements/1.1/date>
~B<b2>
~I<http://www.w3.org/ns/shacl#path>
~I<http://purl.org/dc/elements/1.1/title>
~I<http://example.org/shapes/Book>
~I<http://www.w3.org/1999/02/22-rdf-syntax-ns#type>
~I<http://www.w3.org/ns/shacl#NodeShape>
~I<http://www.w3.org/2000/01/rdf-schema#label>
~L"SHACL shape for the bibo:Book model"
~I<http://www.w3.org/ns/shacl#closed>
%RDF.Literal{value: true, datatype: ~I<http://www.w3.org/2001/XMLSchema#boolean>}
~I<http://www.w3.org/ns/shacl#property>
~B<b0>
~B<b1>
~B<b2>
~I<http://www.w3.org/ns/shacl#targetClass>
~I<http://purl.org/ontology/bibo/Book>
~I<urn:isbn:978-1-68050-252-7>
~I<http://purl.org/dc/elements/1.1/creator>
~I<https://twitter.com/bgmarx>
~I<https://twitter.com/josevalim>
~I<https://twitter.com/redrapids>
~I<http://purl.org/dc/elements/1.1/date>
%RDF.Literal{value: ~D[2018-03-14], datatype: ~I<http://www.w3.org/2001/XMLSchema#date>}
~I<http://purl.org/dc/elements/1.1/format>
~L"Paper"
~I<http://purl.org/dc/elements/1.1/publisher>
~I<https://pragprog.com/>
~I<http://purl.org/dc/elements/1.1/title>
~L"Adopting Elixir"en
~I<http://www.w3.org/1999/02/22-rdf-syntax-ns#type>
~I<http://purl.org/ontology/bibo/Book>}

And note that this RDF.Graph serialization also shows blank nodes marked with the ~B sigil, alongside the IRIs marked with the ~I sigil, and literals marked with the the ~L sigil.

Now if we try and run our query against this we get this:

iex(21)> SPARQL.execute_query(shape_test, shape_query)
%SPARQL.Query.Result{
results: [
%{
"o" => ~I<https://twitter.com/bgmarx>,
"p" => ~I<http://purl.org/dc/elements/1.1/creator>,
"s" => ~I<urn:isbn:978-1-68050-252-7>
},
%{
"o" => ~I<https://twitter.com/josevalim>,
"p" => ~I<http://purl.org/dc/elements/1.1/creator>,
"s" => ~I<urn:isbn:978-1-68050-252-7>
},
%{
"o" => ~I<https://twitter.com/redrapids>,
"p" => ~I<http://purl.org/dc/elements/1.1/creator>,
"s" => ~I<urn:isbn:978-1-68050-252-7>
},
%{
"o" => %RDF.Literal{value: ~D[2018-03-14], datatype: ~I<http://www.w3.org/2001/XMLSchema#date>},
"p" => ~I<http://purl.org/dc/elements/1.1/date>,
"s" => ~I<urn:isbn:978-1-68050-252-7>
},
%{
"o" => ~L"Paper",
"p" => ~I<http://purl.org/dc/elements/1.1/format>,
"s" => ~I<urn:isbn:978-1-68050-252-7>
},
%{
"o" => ~I<https://pragprog.com/>,
"p" => ~I<http://purl.org/dc/elements/1.1/publisher>,
"s" => ~I<urn:isbn:978-1-68050-252-7>
},
%{
"o" => ~L"Adopting Elixir"en,
"p" => ~I<http://purl.org/dc/elements/1.1/title>,
"s" => ~I<urn:isbn:978-1-68050-252-7>
},
%{
"o" => ~I<http://purl.org/ontology/bibo/Book>,
"p" => ~I<http://www.w3.org/1999/02/22-rdf-syntax-ns#type>,
"s" => ~I<urn:isbn:978-1-68050-252-7>
}
],
variables: ["s", "p", "o"]
}

Well, this is a little surprising. We’re getting a SPARQL.Query.Result struct which we’d expect from a select query – not from a construct query. Also we’re getting all of the properties from our data graph returned, so it looks as though our inner query using the shape graph has been ignored.

And if we check the documentation for SPARQL.ex we see that indeed a whole bunch of things are still on the to-do list, among them construct and inner queries. (Which is fair enough – it takes time to implement a complete spec and SPARQL is one of the larger suites of specifications from the W3C.)

4a. Build a query for an RDF shape
So, let’s try and reimagine our approach. We’ll ditch the inner query and build the shape_query using a function instead.

For this we’ll make use of a simpler book_shape_query_helper.rq query and we’ll aim to just make a select of the properties ?p for a given subject ?s.

Now we would like to set the sh:targetClass to a given class (bibo:Book) via the bound variable ?s as a way of generalizing, but since SPARQL.ex does not support the bind keyword we will make do in this example by setting sh:targetClass to the unbound variable ?s and also to the given class.

@prefix bibo: <http://purl.org/ontology/bibo/> .
@prefix dc: <http://purl.org/dc/elements/1.1/> .
@prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
@prefix sh: <http://www.w3.org/ns/shacl#> .
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
@prefix shapes: <http://example.org/shapes/> .select distinct ?s ?p
where {
# bind (bibo:Book as ?s)
?shape sh:targetClass bibo:Book .
?shape sh:targetClass ?s .
?shape sh:property [ sh:path ?p ] .
}

And again we define a convenience function shape_query_helper/0 to read this query.

@shapes_queries_dir @priv_dir <> "/shapes/queries/"
@shape_query_helper_file "book_shape_query_helper.rq"
def shape_query_helper() do
File.read!(@shapes_queries_dir <> @shape_query_helper_file)
end

So we can now provide a very basic query builder query_from_shape/2 which applies a shape_query to the shape.

def query_from_shape(shape, shape_query) do  qh = "select ?s ?p ?o\nwhere {\n"
qt = "}\n"
result = SPARQL.execute_query(shape, shape_query) # add the subject type
s = result |> SPARQL.Query.Result.get(:s) |> List.first
q = qh <> " ?s a <#{s}> .\n"
# add the properties
q = q <> List.to_string(
result
|> SPARQL.Query.Result.get(:p)
|> Enum.map(&(" ?s <#{&1}> ?o .\n ?s ?p ?o .\n"))
)
q <> qt
end

Now we would like to set the property in the triple pattern to be the variable ?p bound to the actual property, but since SPARQL.ex does not support the bind keyword we will make do by using two triple patterns – one with the actual property and one with the variable ?p as the property. (And I should point out that this is an unashamed hack and will only work for this tutorial since we are running the query against a known dataset with just one RDF description.)

In our case the shape_query argument applied will be the query from shape_query_helper/0. And running this we get:

iex(7)> query_from_shape(shape, shape_query_helper) |> IO.puts
select ?s ?p ?o
where {
?s a <http://purl.org/ontology/bibo/Book> .
?s <http://purl.org/dc/elements/1.1/creator> ?o .
?s ?p ?o .
?s <http://purl.org/dc/elements/1.1/date> ?o .
?s ?p ?o .
?s <http://purl.org/dc/elements/1.1/title> ?o .
?s ?p ?o .
}
:ok

But this is not going to work. It would work for a single property only but for multiple properties can only match if this was a union over separate graph patterns – and SPARQL.ex does not currently support union. We’re going to need something more radical.

What we’ll need to do is generate instead a list of queries – one query for each property. So let’s try that.

def queries_from_shape(shape, shape_query) do  qh = "select ?s ?p ?o\nwhere {\n"
qt = "}\n"
result = SPARQL.execute_query(shape, shape_query) # get the subject
s = result |> SPARQL.Query.Result.get(:s) |> List.first
# get the properties
(result |> SPARQL.Query.Result.get(:p))
|> Enum.map(
&(qh
<> " # bind (<#{&1}> as ?p)\n"
<> " ?s a <#{s}> .\n"
<> " ?s <#{&1}> ?o .\n"
<> " ?s ?p ?o .\n"
<> qt
)
)
end

How does this now look? (And we’ll pipe this through Enum.map/2 with String.duplicate/2 function to add in a line separator.)

iex(8)> queries_from_shape(shape, shape_query_helper) \
...(8)> |> Enum.map(&(String.duplicate("#",60) <> "\n" <> &1)) \
...(8)> |> IO.puts
############################################################
select ?s ?p ?o
where {
# bind (<http://purl.org/dc/elements/1.1/creator> as ?p)
?s a <http://purl.org/ontology/bibo/Book> .
?s <http://purl.org/dc/elements/1.1/creator> ?o .
?s ?p ?o .
}
############################################################
select ?s ?p ?o
where {
# bind (<http://purl.org/dc/elements/1.1/date> as ?p)
?s a <http://purl.org/ontology/bibo/Book> .
?s <http://purl.org/dc/elements/1.1/date> ?o .
?s ?p ?o .
}
############################################################
select ?s ?p ?o
where {
# bind (<http://purl.org/dc/elements/1.1/title> as ?p)
?s a <http://purl.org/ontology/bibo/Book> .
?s <http://purl.org/dc/elements/1.1/title> ?o .
?s ?p ?o .
}
:ok

Well, that looks better.

4b. Transform result sets to RDF graph
One thing we haven’t really discussed yet is how to transform from the SPARQL.Query.Result struct resulting from a select query to an RDF.Graph which would result from a construct query.

The to_graph/4 function shown here is cribbed from SPARQL.Query.Result.get/1 and extends the function signature from one variable to three variables for an RDF triple. It just selects for three named fields (the variables named as arguments) from each map in the list and wraps those up as a regular tuple (the RDF triple) with the RDF.triple/3 function. The resulting list of triples is then returned as an RDF.Graph using the RDF.graph/1 function.

def to_graph(%SPARQL.Query.Result{results: results, variables:
variables}, variable1, variable2, variable3) do
if variable1 in variables
and variable2 in variables
and variable3 in variables
do
triples =
Enum.map results,
fn r ->
RDF.triple(r[variable1], r[variable2], r[variable3])
end
RDF.graph(triples)
end
end

And for convenience we also support passing the variables as atoms instead of strings.

def to_graph(result, variable1, variable2, variable3)
when is_atom(variable1) and is_atom(variable2) and is_atom(variable3),
do: to_graph(result, to_string(variable1), to_string(variable2), to_string(variable3))

So let’s try with our default query (running against the default RDF description) which simply makes a select over all our ?s, ?p, and ?o variables.

iex(16)> query |> to_graph(:s, :p, :o)
#RDF.Graph{name: nil
~I<urn:isbn:978-1-68050-252-7>
~I<http://purl.org/dc/elements/1.1/creator>
~I<https://twitter.com/bgmarx>
~I<https://twitter.com/josevalim>
~I<https://twitter.com/redrapids>
~I<http://purl.org/dc/elements/1.1/date>
%RDF.Literal{value: ~D[2018-03-14], datatype: ~I<http://www.w3.org/2001/XMLSchema#date>}
~I<http://purl.org/dc/elements/1.1/format>
~L"Paper"
~I<http://purl.org/dc/elements/1.1/publisher>
~I<https://pragprog.com/>
~I<http://purl.org/dc/elements/1.1/title>
~L"Adopting Elixir"en
~I<http://www.w3.org/1999/02/22-rdf-syntax-ns#type>
~I<http://purl.org/ontology/bibo/Book>}

And let’s try this now on our queries_from_shape/2 function:

iex(17)> queries_from_shape(shape, shape_query_helper) \
...(17)> |> Enum.map(&query/1) \
...(17)> |> Enum.map(&(to_graph(&1, :s, :p, :o)))
[
#RDF.Graph{name: nil
~I<urn:isbn:978-1-68050-252-7>
~I<http://purl.org/dc/elements/1.1/creator>
~I<https://twitter.com/bgmarx>
~I<https://twitter.com/josevalim>
~I<https://twitter.com/redrapids>},
#RDF.Graph{name: nil
~I<urn:isbn:978-1-68050-252-7>
~I<http://purl.org/dc/elements/1.1/date>
%RDF.Literal{value: ~D[2018-03-14], datatype: ~I<http://www.w3.org/2001/XMLSchema#date>}},
#RDF.Graph{name: nil
~I<urn:isbn:978-1-68050-252-7>
~I<http://purl.org/dc/elements/1.1/title>
~L"Adopting Elixir"en}
]

Just one thing left – to merge those graphs. And using the IEx helper v() to recall the last value from the IEx history. We use the RDF.Graph.add/2 function within List.foldl/3 to aggregate the graphs via an accumulator supplied by RDF.Graph.new/0.

iex(18)> v() |> List.foldl(RDF.Graph.new, fn g1, g2 -> RDF.Graph.add(g1, g2) end)
#RDF.Graph{name: nil
~I<urn:isbn:978-1-68050-252-7>
~I<http://purl.org/dc/elements/1.1/creator>
~I<https://twitter.com/bgmarx>
~I<https://twitter.com/josevalim>
~I<https://twitter.com/redrapids>
~I<http://purl.org/dc/elements/1.1/date>
%RDF.Literal{value: ~D[2018-03-14], datatype: ~I<http://www.w3.org/2001/XMLSchema#date>}
~I<http://purl.org/dc/elements/1.1/title>
~L"Adopting Elixir"en}

Or, more meaningfully as a Turtle string, and again using the v() helper.

iex(19)> v() |> RDF.Turtle.write_string! |> IO.puts
<urn:isbn:978-1-68050-252-7>
<http://purl.org/dc/elements/1.1/creator> <https://twitter.com/bgmarx>, <https://twitter.com/josevalim>, <https://twitter.com/redrapids> ;
<http://purl.org/dc/elements/1.1/date> "2018-03-14"^^<http://www.w3.org/2001/XMLSchema#date> ;
<http://purl.org/dc/elements/1.1/title> "Adopting Elixir"@en .
:ok

So, that graph is retrieved from the RDF data using our RDF shape to filter out properties (or rather, to select for only those properties we actually want).

5. Query remote RDF models

So, in order to reuse the same data and shape graph let’s use the same GraphDB repository we loaded earlier and query remotely against that. The endpoint for GraphDB will therefore be:

@service "http://localhost:7200/repositories/shape-test"

We add that to the TestSHACL.Client module along with an accessor function.

def get_service, do: @service

And using the rquery/2 convenience function we defined in the previous post (which we earlier copied to TestSHACL.Client), and the get_service accessor function we can now query this GraphDB repository.

Oh, an error.

iex(1)> rquery(shape_query, get_service)
{:error,
%Tesla.Env{
__client__: %Tesla.Client{
fun: nil,
post: [],
pre: [
{Tesla.Middleware.Headers, :call,
[
[
{"Accept",
"text/turtle, application/n-triples, application/n-quads, application/ld+json, */*;p=0.1"},
{"Content-Type", "application/x-www-form-urlencoded"}
]
]},
{Tesla.Middleware.FollowRedirects, :call, [[max_redirects: 5]]}
]
},
__module__: SPARQL.Client,
body: "MALFORMED QUERY: Multiple prefix declarations for prefix 'rdf'",
headers: [
{"vary", "Accept-Encoding"},
{"cache-control", "no-store"},
{"content-type", "text/plain;charset=UTF-8"},
{"content-language", "en-GB"},
{"content-length", "62"},
{"date", "Thu, 11 Oct 2018 13:15:09 GMT"},
{"connection", "close"},
{"server", "GraphDB-Free/8.5.0 RDF4J/2.2.4"}
],
method: :post,
opts: [],
query: [],
status: 400,
url: "http://localhost:7200/repositories/shape-test"
}}

Turns out that GraphDB is complaining about the rdf prefix declaration. So, we comment that out of the query, and retry. Same thing for rdfs and for xsd prefix declarations. So finally, we get this:

iex(4)> rquery(shape_query, get_service)
{:ok, #RDF.Graph{name: nil
~I<urn:isbn:978-1-68050-252-7>
~I<http://purl.org/dc/elements/1.1/creator>
~I<https://twitter.com/bgmarx>
~I<https://twitter.com/josevalim>
~I<https://twitter.com/redrapids>
~I<http://purl.org/dc/elements/1.1/date>
%RDF.Literal{value: ~D[2018-03-14], datatype: ~I<http://www.w3.org/2001/XMLSchema#date>}
~I<http://purl.org/dc/elements/1.1/title>
~L"Adopting Elixir"en}}

So, let’s capture that in the variable graph by matching on that last tuple returned using the v() helper.

iex(5)> {:ok, graph} = v()
{:ok, #RDF.Graph{name: nil
~I<urn:isbn:978-1-68050-252-7>
~I<http://purl.org/dc/elements/1.1/creator>
~I<https://twitter.com/bgmarx>
~I<https://twitter.com/josevalim>
~I<https://twitter.com/redrapids>
~I<http://purl.org/dc/elements/1.1/date>
%RDF.Literal{value: ~D[2018-03-14], datatype: ~I<http://www.w3.org/2001/XMLSchema#date>}
~I<http://purl.org/dc/elements/1.1/title>
~L"Adopting Elixir"en}}

And using the RDF convenience string writer for Turtle we get this.

iex(6)> graph |> RDF.Turtle.write_string! |> IO.puts
<urn:isbn:978-1-68050-252-7>
<http://purl.org/dc/elements/1.1/creator> <https://twitter.com/bgmarx>, <https://twitter.com/josevalim>, <https://twitter.com/redrapids> ;
<http://purl.org/dc/elements/1.1/date> "2018-03-14"^^<http://www.w3.org/2001/XMLSchema#date> ;
<http://purl.org/dc/elements/1.1/title> "Adopting Elixir"@en .
:ok

Done!

We again have our RDF description with only those properties that were explicitly defined in the RDF shape.

Summary

I’ve shown here in this post how the SPARQL.ex and SPARQL.Client.ex packages can be used together with RDF shapes for querying RDF datastores.

Specifically we’ve used SPARQL.ex to build up dynamically a set of SPARQL queries by querying an RDF shape and have used these to query a local (in-memory) RDF model. We have shown how the result sets can be transformed into an RDF graph.

We have also used SPARQL.Client.ex to query against an RDF shape using a stored query. The RDF datastore in this case was hosted on a local machine with Ontotext GraphDB but was queried remotely via its SPARQL endpoint.

While the local querying shows up some current limitations in SPARQL.ex we are also able to find some workarounds, e.g. transforming result sets into RDF graphs.

Again, the real reasons for an interest in Elixir for semantic applications is not so much in basic query mechanics but in its exemplary support for fault tolerant and distributed computing which I hope to address in a subsequent post.

See here for the project TestSHACL code.

This is the third in a series of posts. See my previous post ‘Querying RDF with Elixir’.

You can also follow me on Twitter as @tonyhammond.

--

--

Tony Hammond

Data architect | author ‘Exploring Graphs with Elixir’ (Nov 2022)