graphQL

GraphQL + OWL: Types & Fields

Introduction

In the first part (GraphQL + OWL: An “Ontologized” GraphQL Interface), we introduced and discussed a design for a GraphQL schema that provides a query interface (e.g., types, fields) compliant with an arbitrary set of ontologies.  
In the second part (GraphQL + OWL: Namespaces), we started coding the software module that realizes the above. Specifically, we discussed and illustrated how to deal with namespaces.

This third part deals with types and properties, which are the building blocks of ontology and GraphQL schema

At the end of the implementation, it must be possible to

Types, but also fields

Types are defined in the ontology as classes: once reading the input ontologies and creating a merged ontology, we need to iterate the available classes. Here’s an example:     

				
					<rdfs:Class rdf:about="http://xmlns.com/foaf/0.1/Person" rdfs:label="Person" rdfs:comment="A person." vs:term_status="stable">
    <rdf:type rdf:resource="http://www.w3.org/2002/07/owl#Class" />
    <owl:equivalentClass rdf:resource="http://schema.org/Person" />
    <owl:equivalentClass rdf:resource="http://www.w3.org/2000/10/swap/pim/contact#Person" />
    <rdfs:subClassOf><owl:Class rdf:about="http://xmlns.com/wordnet/1.6/Person"/></rdfs:subClassOf> -->
    <rdfs:subClassOf><owl:Class rdf:about="http://xmlns.com/foaf/0.1/Agent"/></rdfs:subClassOf>
    <rdfs:subClassOf><owl:Class rdf:about="http://xmlns.com/wordnet/1.6/Agent"/></rdfs:subClassOf> -->
    <rdfs:subClassOf><owl:Class rdf:about="http://www.w3.org/2003/01/geo/wgs84_pos#SpatialThing" rdfs:label="Spatial Thing"/></rdfs:subClassOf>
    <rdfs:isDefinedBy rdf:resource="http://xmlns.com/foaf/0.1/"/>
    <owl:disjointWith rdf:resource="http://xmlns.com/foaf/0.1/Organization"/>
    <owl:disjointWith rdf:resource="http://xmlns.com/foaf/0.1/Project"/>
  </rdfs:Class>
				
			

It sounds obvious that the system should have a corresponding type in the GraphQL schema for each class defined in the ontology.

You may have noticed that in the declaration above, no properties or attributes are attached to that class, only metadata (e.g., subclasses, equivalent classes, provenance, disjunctions). The schema validator would complain if we create and register a GraphQL type when the ontology parser meets that definition: in GraphQL a registered type must have at least one field

All entities in our domain must be reachable by using their identifier, which is a URI.  Consequently, all GraphQL types in the schema have at least a “uri” property that returns the URI of the corresponding instance. 

				
					type Person {
    uri: ID!
}
				
			

Minimalist, but that is a valid type definition in GraphQL

Done? No, no: we still need to answer the following questions: 

  • How can we retrieve an entity instance by URI?
  • How can we search and get a set of entity instances? 
  • What about fields
  • How do we deal with name clashes? That is, classes having the same name defined in different ontologies? The snippet above is the declaration of the (foaf) Person; note it also mention other two classes named Person

Get by URI

The top-level Query type should define a field allowing retrieving an entity belonging to a given type by its URI

We cannot put the field directly into the Query type because, if you remember from the first part (GraphQL + OWL: An “Ontologized” GraphQL Interface), top-level fields under the Query type are namespaces.

				
					type Query {
    dc: dc
    foaf: foaf
    ...
}
				
			

And that perfectly fits with the design: the Person type above belongs to the foaf namespace. As a consequence of that, the person retrieval field will be set at that level, as follows:

				
					type Query {
    dc: dc
    foaf: foaf
    ...
}

type foaf {
    # Retrieves a Person instance by its Uniform Resource Locator (URI)  
    person(uri:String!): Person
}
				
			

At query-time, a request would look like this:

				
					query {
    foaf {
        person(uri: "https://mydomain.org/people/91283") {
            uri
        }
    }
}
				
			

Search, Request And Response Types

In the previous paragraph, we retrieved an entity instance by its URI; the focus here is instead the definition of a field that enables searching over a given entity type.

Since that field provides a search service, its signature must include sorting, paginating, and filtering capabilities. Please note we do not consider at this time any full-text search feature.

Following the same namespace logic described above, here’s a first attempt:

				
					type Query {
    dc: dc
    foaf: foaf
    ...
}

type foaf {
    # Retrieves a Person instance by its Uniform Resource Locator (URI)  
    person(uri:String!): Person
    
    # Retrieves a list of people according to the input sort, pagination and 
    # filtering criteria 
    people(sort:String, offset:Int, rows:Int, filters: [String]): [Person]
}
				
			

Returning only an array of Person instances is reductive because, in the response, we wouldn’t have any metadata like the start offset we are dealing with or the total number of occurrences found.

In addition, being a search service, one possible (future) option could be to add cool search features like faceting, spellchecking, more like this, and so on.

This is to say a Person array, as result, is not enough; we need to define a response type in our schema that wraps the entities list and the additional metadata we want to be part of the result. Here’s the type we are looking for:

				
					type PersonCollection {
    # The list of resources in this response.
    resources: [Person]
    
    # The start offset of results in this response.
    offset: Int
    
    # The total number of matches.
    totalMatches: Int    
    
    # other metadata in the future can be defined below as additional fields
}
				
			

As a consequence of that, the field signature above becomes the following:

				
					...
type foaf {
    # Retrieves a Person instance by its Uniform Resource Locator (URI)  
    person(uri:String!): Person
    
    # Retrieves a list of people according to the input sort, pagination and 
    # filtering criteria 
    people(sort:String, offset:Int, rows:Int, filters: [String]): [PersonCollection]
}
...
				
			

At query-time:

				
					...
query {
    foaf {
        people(offset: 10, rows: 10, sort: "name desc") {
            resources {
                uri
            }
            totalMatches
            offset
        }
    }
}
...
				
			

Fields, Properties, (Still) Namespaces

So far, we defined the entities and their retrieval methods. However, our entities are a bit poor; they contain just one field, the URI. We need to populate the registered type with the properties that could be associated with them.

And here comes the challenge: yes, because

any RDF property can be applied to any RDF resource.

That conflicts with having a schema where types and fields are statically definedThe sentence above is clear: you can add any property to any type. GraphQL is schema-driven, so there is no way: we need to add that infinite set of properties to all types. How do we deal with that? Here’s our proposal.

Let’s constrain the domain, first: we said the system should be initialized with a predefined (and therefore finite) number of ontologies. Hence, we know in advance the number of types and properties we deal with. 

For example, foaf:Person has a property foaf:name (that, as the name suggests, represents the name of a person), and potentially it could also have a dc:title (quite strange, but from an RDF perspective, absolutely correct). At first sight, one option could be the following:

				
					type Person {
    name: String
    title: String
}
				
			

However, that option doesn’t scale. We are not talking about two properties: in the example, the two ontologies have a lot of properties, and in addition, we could potentially initialize the system with an arbitrary number of ontologies; ten, for example.

If we follow the same namespace semantic applied above, the properties assigned to a given type could be organized and grouped by namespaceThat requires creating (sub) types in our schema, one for each namespace/type pair:

				
					type FoafProperties {
    name: String
}

type DcProperties {
    title: String
}
				
			

With those additional types, the Person type definition becomes the following:

				
					type Person {
    foaf: FoadProperties
    dc: DcProperties
}
				
			

The query shape becomes more concrete:

				
					query {
    foaf {
        person(uri: "https://mydomain.org/people/91283") {
            dc {
                title
                ... other Dublin Core properties
            }
            
            foaf { 
                name
                ... other FOAF properties
            }
            
            ... other "namespaced" properties 
        }
    }
}
				
			

We can still push things ahead to improve the readability and usability: among all properties, there’s a group that belongs to the same namespace of the owning type. In that case, the namespace level is redundant and could be removed.

In the example, the Person type belongs to the foaf namespace, and “name” also belongs to the foaf. We should be able to request those fields directly without entering the owning namespace. Something like this:   

				
					query {
    foaf {
        person(uri: "https://mydomain.org/people/91283") {
            // name and other foad properties don't need the "namespace" level
            name
            ... other FOAF properties
    
            dc {
                title
                ... other Dublin Core properties
            }
            
            ... other "namespaced" properties 
        }
    }
}
				
			

In the reference implementation, we left both options so the user can arbitrarily request the properties that belong to the same namespace directly or nest them under the namespace level:

				
					query {
    foaf {
        person(uri: "https://mydomain.org/people/91283") {
            // implicit namespace option
            name
            ... other FOAF properties
    
            // explicit namespace option
            foaf { 
                name
                ... other FOAF properties
            }
            
            ... other "namespaced" properties 
        }
    }
}
				
			

Next Challenges

Types and properties management is an endless topic; what is described above is just the first step because within an ontology, there could be (see the example at the beginning of this article) a lot of metadata that drives the types and properties’ shape and behavior.

Here is a non-exhaustive list of things we are going to consider in the next articles:

  • inheritance
  • polymorphism
  • semantic relationships (e.g., symmetric, inverse)
 

Let's Try It!

The article is not just theoretical; a reference implementation in GitHub follows the content. The repository is not public; if you want to look, just let us know, and we will gladly give you access. 

Having said that, after cloning the repository, you can start GraphOWL in two ways.

Option #1: Docker

The project includes all what you need to create a working Docker image. You must have Docker installed on your machine, of course. Here are the step   

				
					> cd $PRJ_HOME
> mvn clean install

...
[INFO] 
[INFO] --- exec:3.1.0:exec (docker-build) @ graphowl-engine-api ---
#0 building with "desktop-linux" instance using docker driver

#1 [internal] load .dockerignore
#1 transferring context: 2B done

...
#8 naming to com.spaziocodice/graphowl-engine-api:1.0.0-SNAPSHOT done
#8 DONE 0.1s

[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time:  01:02 min

> cd src/test/resources
> docker-compose up

 
   .   ____          _            __ _ _
  /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
 ( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
  \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
   '  |____| .__|_| |_|_| |_\__, | / / / /
  =========|_|==============|___/=/_/_/_/
  :: Spring Boot ::               (v2.4.13)
2023-10-15 06:27:39.466  Starting App v1.0.0-SNAPSHOT using Java 17.0.2 ...

...

2023-10-15 06:28:11.542  <GRAPHOWL-00031> : Type dcterms:SpatialScheme registered.
2023-10-15 06:28:11.542  <GRAPHOWL-00031> : Type dcterms:TypeScheme registered.
2023-10-15 06:28:11.828  Exposing 2 endpoint(s) beneath base path '/actuator'
2023-10-15 06:28:11.843  Tomcat started on port(s): 8080 (http) with context path ''
2023-10-15 06:28:11.848  Started App in 32.564 seconds (JVM running for 32.763)
				
			

Option #2: IDE

If you load the repository in an IDE like IntelliJ, starting the module is just a matter of running the main class:

We would love to hear questions, doubts, and feedback about this blog post!
Feel free to contact us or leave a message in the comment box below.

Share this post

Leave a Reply