Navigation

Tuesday, February 15, 2011

Cooking with Sesame: parsing and writing RDF with Rio

The Sesame Cookbook has moved to my new site: http://rivuli-development.com/
The Sesame framework includes a set of parsers and writers called Rio. Rio (a rather imaginative acronym for "RDF I/O") is a toolkit that can be used independently from the rest of Sesame. In this recipe, we will take a look at various ways to use Rio to parse from or write to an RDF document. I will show how to do a simple parse and collect the results, how to count the number of triples in a file, how to convert a file from one syntax format to another, and how to dynamically create a parser for the correct syntax format.

If you use Sesame as a triplestore (via the Repository API), then  typically you will not need to use the parsers directly: you simply supply the document (either via a URL, or as a File, InputStream or Reader object) to the RepositoryConnection and the parsing is all handled internally. However, sometimes you may want to parse an RDF document without immediately storing it in a triplestore. For those cases, you can use Rio directly.

 The Rio parsers all work with a set of Listener interfaces that they report results to: ParseErrorListener, ParseLocationListener, and RDFHandler. Of these three, RDFHandler is the most useful one: this is the listener that receives parsed RDF triples. So we will concentrate on this interface here.

The RDFHandler interface is quite simple, it contains just five methods: startRDF, handleNamespace, handleComment, handleStatement, and endRDF. Rio also provides a number of default implementations of RDFHandler, such as RDFInserter, which immediately adds any received RDF triples to its supplied RepositoryConnection, and StatementCollector, which stores all received RDF triples in a Java Collection. Depending on what you want to do with parsed statements, you can either reuse one of the existing RDFHandlers, or, if you have a specific task in mind, you can simply write your own implementation of RDFHandler. Here, I will show you some simple examples of things you can do with RDFHandlers.

Collecting all parsed triples in a List

As a simple example of how to use Rio, we parse an RDF document and collect all the parsed statements in a Java List object. For this, we need the following ingredients:
  • an RDF file;
  • a RDFParser object;
  • a RDFHandler object.
For the RDF file, let's say we have a Turtle file, available at http://example.org/example.ttl:


java.net.URL documentUrl 
               = new URL("http://example.org/example.ttl");
InputStream inputStream = documentUrl.openStream();

We now have an open InputStream to our RDF file. Now we need a RDFParser object that reads this InputStream and creates RDF statements out of it. Since we are reading a Turtle file, we create a TurtleParser object:


RDFParser rdfParser = new TurtleParser();

(note: all Rio classes and interfaces are in package org.openrdf.rio  or one of its subpackages)

We also need an RDFHandler which can receive RDF statements from the parser. Since we just want to create a Java List of Statements for now, we'll just use Rio's StatementCollector:

java.util.ArrayList myList = new ArrayList();
StatementCollector collector = new StatementCollector(myList);
rdfParser.setRDFHandler(collector);

Finally, we need to set the parser to work:

try {
   rdfParser.parse(inputStream, documentURL.toString());
catch (IOException e) {
  // handle IO problems (e.g. the file could not be read)
}
catch (RDFParseException e) {
  // handle unrecoverable parse error
}
catch (RDFHandlerException e) {
  // handle a problem encountered by the RDFHandler
}

After the parse() method has executed (and provided no exception has occurred), the list myList will be filled by the StatementCollector. As an aside: you do not have to provide the StatementCollector with a list in advance, you can also use an empty constructor and then just get the collection, using StatementCollector.getStatements() .

Using your own RDFHandler: counting statements

As a simple example of writing your own RDFHandler, suppose you want to simply count the number of triples in the RDF file. You could of course use the above code for this, adding all triples to a List, and then just checking the size of the List. However, this will get you into trouble when you are parsing very large RDF files: you might run out of memory. And in any case: creating and storing all these Statement objects just to be able to count them seems a bit of a waste. So instead, we will create our own RDFHandler, which just counts the parsed RDF statements and then immediately throws them away.

To create your own RDFHandler implementation, you can of course just create a class that implements the RDFHandler interface, but a useful shortcut is to instead create a subclass of RDFHandlerBase. This is a base class that provides dummy implementations of all interface methods. The advantage is that you only have to override the methods in which you need to do something. Since what we want to do is just count statements, we only need to override the handleStatement method. Additionaly, we of course need a way to get back the total number of statements found by our counter.


class StatementCounter extends RDFHandlerBase {

  private int countedStatements = 0;

  @Override
  public void handleStatement(Statement st) { 
     countedStatements++;
  }

 public int getCountedStatements() {
   return countedStatements;
 }
}

Once we have this, our custom RDFHandler class, we can supply that to the parser instead of the StatementCollector, and we're done.

Converting RDF serialization formats


A useful trick with Rio is to pipeline parsers and writers. Since all Rio RDFWriters are in fact RDFHandler implementations, you can directly supply them to a parser, thus creating a very simple syntax convertor.

Say, you have a file in Turtle format, and you want to convert it to RDF/XML. Simply create a TurtleParser as shown above, and provide it with a RDFXMLWriter, which is an RDFHandler implementation that writes the received RDF statements to an outputstream in RDF/XML format.

Creating the right parser for the right format

In the examples sofar, we have created a parser by simply using the constructor of the specific format's parser class. In other words: our program code assumes that the input file is a Turtle file, so we just create a new TurtleParser object. However, you may not always know in advance what exact format the RDF file is in. What then? Fortunately, Rio has a couple of useful features to help you.

The Rio class is a factory class which can create a RDFParser object given a specific RDFFormat. RDFFormat is a set of constants defining the available serialization formats. It also has a couple of utility methods for guessing the correct format, given either a filename or a MIME-type. For example, to get back the RDF format for our Turtle file, we could do the following:

RDFFormat format = RDFFormat.forFileName(documentURL.toString());

This will guess, based on the extension of the file (.ttl) that the file is a Turtle file and return the correct format. We can then use that with the Rio factory class to create the correct parser dynamically:

RDFParser rdfParser = Rio.createParser(format);
 
As you can see, we still have the same result: we have created an RDFParser object which we can use to parse our file, but now we have not made the explicit assumption that the input file is in Turtle format: if we would later use the same code with a different file (say, a .owl file - which is in RDF/XML format), it would still work.

Summary

I have tried to show a couple of useful ways to employ Rio in practice. It is a versatile set of streaming RDF parsers and writers that can be easily used in your own programs, and it can be used separately from the rest of the Sesame framework. Of course, more could be said about it (for example, how to configure its error handling, datatype verification, and so on), but that's for next time and/or the comments. Enjoy! And any feedback on these recipes is of course much appreciated.

Saturday, February 05, 2011

Implementing SPARQL 1.1 Query - first findings

I am currently in the middle of implementing SPARQL 1.1 Query Language into Sesame 2 (code can be found in Sesame's subversion repository, branch 2.4). The current working draft specifies a number of new features for SPARQL, and I will briefly make some points about the features I have implemented thus far, noting problems I encountered or where the current working draft was unclear to me.

1. Expressions in SELECT

This new feature was fairly straightforward to implement, mainly as Sesame already had support for it in its query algebra. I only needed to adapt the parser.

2. Negation

In section 8 two additional operators are introduced, both of which can be used to express negation. They are (NOT) EXISTS, and MINUS. Implementation of the EXISTS function again was quite straightforward, Sesame already having algebraic support for it.

The definition of MINUS in SPARQL gave me some headaches, however. In Sesame's native query language SeRQL, the MINUS operator is a set operator operating on collections of triples - that is, the result of {A} MINUS {B} is the set of all triples matching A, minus all triples matching B. In SPARQL, however, MINUS is defined in terms of  compatible solutions. This means that Sesame's own algebra operator for MINUS can not simply be reused for SPARQL. However, it also seems that SPARQL's definition of MINUS makes it, for all practical purposes, exactly equivalent to using a NOT EXISTS filter. To see why this is, we have to take a look at the definitions of both operators.

In section 8.3 , the difference between NOT EXISTS and MINUS is explained, with a number of examples. This explanation shows that when the right-hand side pattern shares no variables with the left-hand pattern, the outcome is different. However, what is also apparent from this explanation that when a MINUS operator is used and no shared variables exist between the two patterns, the MINUS operator effectively does nothing.

This also follows if we look at  the definition of MINUS in the SPARQL algebra and the definition of compatible solutions in section 17.3: by definition any two solutions µ and µ' which share no variables v are compatible. So the outcome of any such query would be exactly the same as if the MINUS were not there. This leaves us with two scenarios:
  1. the two patterns share a variable, in this case the MINUS can be replaced with a NOT EXISTS;
  2. the two patterns do not share a variable, in this case the MINUS can be ignored.
All in all it seems to me that MINUS as currently defined does not add additional expressivity to the language and is only a syntactic variant. If that is intended, that might be useful to clarify in the working draft.

3. Subqueries

Sesame already having basic support for this in the algebra, again this was rather simple to add, as it only required me to tweak the SPARQL parser. I will probably need to test it further though.

4. Aggregates

Fortunately for me, it turns out that basic support for aggregate functions had been added to Sesame's algebra earlier, courtesy of David Huynh. It required a bit of tweaking to be compatible with SPARQL's definitions, but the framework was already there, ready for me to extend.

There are a number of things unclear in the working draft however, regarding the expected behaviour of aggregate functions.

The first problem has to do with datatypes. Most examples take it as given that all input to, say, a SUM operator will be numeric values. It is not clearly stated what the expected behaviour is if a particular variable binding turns out to be non-numeric value. As a case in point, SUM is formally defined in terms of the XPath function op:numeric-add. This function explicitly states that it operates only on specific numeric types. No mention is made however, of expected behaviour when one operand is not a numeric type (section 16.3 of the SPARQL WD does mention that a type error results for incompatible operands, but it is not clear if this also applies to aggregate functions). Moreover, it is not clearly stated how a type error in an aggregate function should influence the result. I can see a number of possible scenarios, when a type error occurs during evaluation of an aggregate function:
  1. the entire query fails with an error;
  2. the incompatible operand value is ignored and evaluation continues;
  3. the aggregate operator fails silently, returning 0.

From a usability perspective, I would probably have a preference for option 2, although I note that in other mathematical operators (+, -, *, etc.) Sesame's interpretation currently is that an incompatible operand results in a failed query.

Another problem with the definition of aggregates, or more in general with aggregates in combination with several other features, is that it is not always clearly defined how they should interact (disclaimer: perhaps it is properly defined in section 10.2, but I'm having a hard time following the definitions there). For example, what happens when we apply an ORDER BY on a graph pattern that already has a GROUP BY and an aggregate function? For example, take the following data set:

:org1 :affiliates :auth1, :auth2 .
:auth1 :name "John" .
:auth2 :name "Paul" .
:org2 :affiliates :auth3 .
:auth3 :name "Ringo" .

And the following query:

SELECT (GROUP_CONCAT(?name) AS ?names)
WHERE {
  ?org :affiliates ?auth .
  ?auth :name ?name.
}
GROUP BY ?org
ORDER BY ASC(str(?name))

My intuitive understanding would be that the result of this query would be:

?names     
"Ringo"
"John Paul"

That is: the ordering is applied to the intermediate result of the grouping, thus supplying the aggregate operator (in this case, GROUP_CONCAT) with an ordered sequence (which makes sure that we get a concatenated string "John Paul" rather than "Paul John"). But it is not completely clear to me from the working draft if the ORDER BY clause should be applied to a grouping in this fashion.

These are my findings thus far. I have not yet started on property paths or federated query. In the mean time, I would welcome any feedback on my notes, including feedback that tells me I should have read section so-and-so and it's all clear as glass if I had just taken the time to study it properly :)

Also, this: in the course of this work I have written several DAWG-Manifest style unit tests to check conformance as I saw it. They can be found in Sesame's SVN repository, and I'd be happy to let them be reused.


Friday, January 21, 2011

Back to working on Sesame

Ontotext, the Bulgarian Semantic Web company of OWLIM fame, have given me the opportunity to return to working on Sesame as a developer. Specifically, I have been contracted to help implement SPARQL 1.1 Query and Update into the Sesame framework.

Work has already started and I'm happy to report that various features have already been implemented and tested - the fact that Sesame's query algebra is based on the SeRQL language helps a lot here, as various SPARQL 1.1 features were already available in SeRQL. This includes the use of expressions in the SELECT, as well as negation features and subqueries. More challenging will be the inclusion of aggregates and property paths. More soon.