SPARQL Templates

The Semantic Object Modeling Language (SOML) used by the Ontotext Semantic Objects describes a fixed ontology model. Once this model is accepted by the Semantic Objects, it is converted to several dependent static models:

  • GraphQL schema model
  • SHACL validation model, if enabled
  • SPARQL query model based on the input GraphQL query

The generated SPARQL queries are dynamic as well, but their behavior is fixed at the moment of the model deployment. To eliminate this restriction, we need a way to augment the model at query time.

SPARQL Templates in Model Properties

Defining a property in SOML is done as follows:

objects:
  Location:
    props:
      rdfs:label: {}
      label: {rdfProp: "rdfs:label"}

When this property is requested via a GraphQL query, the SPARQL transpiler will generate the following triple pattern:

# for 'rdfs:label'
?location rdfs:label ?rdfs_label.
# for 'label'
?location rdfs:label ?label.

Both patterns will fetch the same data.

The simple SPARQL template has the form of the patterns above. They should be written as follows:

objects:
  Location:
    props:
      label: {rdfProp: "?_subject rdfs:label ?_value."}

In the above SRARQL template:

  • the ?_subject is a special variable that will be replaced with the current subject binding.
  • the ?_value is a special variable that will be replaced with a proper output variable name that matches the currently selected property.

Consider the following addition to the SOML schema above:

objects:
  Person:
    props:
      birthPlace: {range: Location}

And the GraphQL query:

query {
  person {
    id
    birthPlace { label }
  }
}

Selecting the label property should result in the following SPARQL to be added to the query:

SELECT
    ?person
    ?person_label
    ?person_so_type
    ?person_birthPlace
    ?person_birthPlace_label
WHERE {
    ?person rdf:type ?person_rdf_type__0 .
    FILTER (?person_rdf_type__0 = voc:Person)
    BIND ('Person' as ?person_so_type) .
    OPTIONAL {
        ?person rdfs:label ?person_label.
    }
    OPTIONAL {
        ?person voc:birthPlace ?person_birthPlace .
        OPTIONAL {
            ?person_birthPlace rdfs:label ?person_birthPlace_label.
        }
    }
}

Global SPARQL Template Variables

Note

The following functionality is available in Semantic Objects 4.1.0 and later.

Global variables are extension to the current variables support. They allow exporting multiple results from a single sparql template to the GraphQL response or to other sparql templates in the same query.

In addition to that, the new functionality allows using variables that evade the variable escaping. The values of these variables could be exposed directly into properties or relations with the same name, or in other properties that share the same SPARQL context. Here is the list of possible definitions of global variables:

Previous behavior   Current behavior  
?_subject Defines the subject for the current template. It allows drill down the triple store or restricting the current subject. ?_subject Same as before.
?_subject.property Represents a property or relation that is at the same level as the current property.
?_subject.relation.property Represents a relation that is at the same level as the current property and it will expose the result in a sub property in the related object. The relation depth does not have a limit for example ?_subject.relation.relation.relation.property is valid but it must be resolvable in the model.
?_property Represents a short form of ?_subject.property variable.
?_relation.property Represents a short form of ?_subject.relation.property variable.
?_value Defines the only real output of a SPARQL template. ?_value Same as before.
?_value.property Represents a property or relation that is a sub property under the current relation result. This means that the ?_value bindings should be IRIs.
?_value.relation.property Represents a relation with a nested property under the current relation. This means that the ?_value and ?_value.relation bindings are IRIs.
  Any other variable is escaped to prevent collisions with other templates or double querying the same property with GraphQL alias. ?__varName Represents a global variable that would not be visible to properties in the model and is only visible to other SPARQL fragments at the SPARQL context or lower. No property will be created for such variable.

Note

Properties and relations that have prefixes or dash (-) in their names will be replaced with underscore. For example the property rdfs:label must be referenced as ?_rdfs_label!

To allow the global variable to export its result, the model must include a property that matches the property or relation part of the variable. For example, if you want the value of the property ?_totalHits to be visible in the GraphQL results, that property requires the current SOML type to have a property defined as totalHits: {range: int}. If such a property does not exist, one will be created in the corresponding type and will have range: string and cardinality of one. If no such definition is present, then the Semantic Objects will behave as if the totalHits: {range: string} definition is present. The automatic property creation works for immediate properties (i.e. the example above).

As for relations, the property generation is going to activate only if the relation already exists. For example, the relation variable ?_facets.facetValue could create the facetValue property in the Facets type only if the current type has {range: Facets, max: inf} relation definition facets. Property generation does not work for nested relations in other relations such as ?_relationToA.relationToB.label – in that case, the label in object B will not be created.

Note

Automatically created properties are always single valued and of type String. If the expected result is multi-valued or if it isn’t String or IRI, then the explicit property must be defined.

The example below uses the following data and demonstrates the usage and effects of using global variables:

@prefix : <http://example.com/resource/> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
@prefix skos: <http://www.w3.org/2004/02/skos/core#> .

:MeryAnn a :Person ;
    rdfs:label "Mery" ;
    skos:altLabel  "Ann" .

 :Barry a :Person ;
    rdfs:label "Barry".

And we have the following SOML schema

specialPrefixes:
  vocab_prefix:  voc
  vocab_iri:     http://example.com/resource/
  base_iri:      http://example.com/resource/

objects:
  Person:
    props:
      firstName:
      fullName:
        rdfProp: |
          OPTIONAL {?_subject rdfs:label ?_firstName.}
          OPTIONAL {?_subject skos:altLabel ?_lastName.}
          BIND(" " as ?__separator).
          BIND(
            CONCAT(
              IF(BOUND(?_firstName), ?_firstName, ""),
              ?__separator,
              IF(BOUND(?_lastName), ?_lastName, "")
            ) as ?_value
          ).

In the example schema above, the SPARQL template for the fullName property defines two global variables – ?_firstName and ?_lastName. Those variables will be exposed through the corresponding properties firstName and lastName if present in the GraphQL query. The variable ?__separator is global variable that cannot be returned in a GraphQL query even if such property already exists in the model.

Note

Note that lastName is not present in schema but can be queried with the query below.

The following GraphQL query:

query {
    person {
        firstName
        lastName
        fullName
    }
}

will produce the following SPARQL query:

BASE <http://example.com/resource/>
PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>
PREFIX voc: <http://example.com/resource/>
PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
PREFIX skos: <http://www.w3.org/2004/02/skos/core#>

SELECT
    ?person ?person_firstName ?person_fullName ?person_lastName ?person_so_type
WHERE {
    ?person rdf:type voc:Person .
    BIND ('Person' as ?person_so_type) .
    OPTIONAL {
        OPTIONAL { ?person rdfs:label ?person_firstName. }
        OPTIONAL { ?person skos:altLabel ?person_lastName. }
        BIND(
            CONCAT(
                IF(BOUND(?person_firstName), ?person_firstName, ""),
                " ",
                IF(BOUND(?person_lastName), ?person_lastName, "")
            ) as ?person_fullName
        ).
    }
}

While removing the fullName property selection from the query will produce the following SPARQL query:

BASE <http://example.com/resource/>
PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>
PREFIX voc: <http://example.com/resource/>

SELECT
    ?person ?person_firstName ?person_fullName ?person_lastName ?person_so_type
WHERE {
    ?person rdf:type voc:Person .
    BIND ('Person' as ?person_so_type) .
    OPTIONAL { ?person voc:firstName ?person_firstName. }
    OPTIONAL { ?person voc:lastName ?person_lastName. }
}

The example above makes it obvious that when the property fullName is omitted from the GraphQL query, its template does not affect the SPARQL query in any way. It also means that if the exposed properties do not point to real data in the database, nothing will be returned. This behavior could be corrected by setting proper values to the rdfProp characteristic to both properties as such:

firstName: {rdfProp: rdfs:label}
fullName: {rdfProp: skos:altLabel}

A more complex examples of global variables can be found in the Query GraphDB Connectors with Facets example below.

Template Validation

Each SPARQL template will be tested for valid SPARQL syntax when placed inside a SELECT query. If the template contains arguments, they will be replaced with random values based on the defined types.

Multiple properties in the same type can have SPARQL fragments that use the same variables without interference between the fragments. This is achieved by adding unique suffix to all variables in the same fragment.

objects:
  Person:
    props:
      firstName:
        rdfProp: |
          ?_subject voc:firstName ?name.
          BIND(str(?name) as ?_value).
      lastName:
        rdfProp: |
          ?_subject voc:lastName ?name.
          BIND(str(?name) as ?_value).

Any prefixes mentioned in the template must be defined in the prefixes section of the SOML schema. A template cannot define or use any prefix or base IRI that is not previously defined.

Relative IRIs are supported only if the specialPrefixes.base_iri is populated in the SOML schema.

Each schema has access to the following predefined prefixes:

prefixes:
  # internal system prefixes
  so:    http://www.ontotext.com/semantic-object/
  affected: http://www.ontotext.com/semantic-object/affected
  res:   http://www.ontotext.com/semantic-object/result/
  ec:    http://www.ontotext.com/connectors/entity-change#
  ecinst: http://www.ontotext.com/connectors/entity-change/instance#
  # common prefixes
  dct:   http://purl.org/dc/terms/
  gn:    http://www.geonames.org/ontology#
  owl:   http://www.w3.org/2002/07/owl#
  puml:  http://plantuml.com/ontology#
  rdf:   http://www.w3.org/1999/02/22-rdf-syntax-ns#
  rdfs:  http://www.w3.org/2000/01/rdf-schema#
  skos:  http://www.w3.org/2004/02/skos/core#
  void:  http://rdfs.org/ns/void#
  wgs84: http://www.w3.org/2003/01/geo/wgs84_pos#
  xsd:   http://www.w3.org/2001/XMLSchema#
  sh:    http://www.w3.org/ns/shacl#
  dash:  http://datashapes.org/dash#
  rsx:   http://rdf4j.org/shacl-extensions#

specialPrefixes:
  base_iri:     http://example.org/resource/
  ontology_iri: http://example.org/vocabulary
  vocab_iri:    http://example.org/vocabulary/
  vocab_prefix: voc
  shape_iri:    http://example.org/shape/
  shape_prefix: vocsh

Note

A prefix defined as elastic-index: http://www.ontotext.com/connectors/elasticsearch/instance# should be used as elastic_index:my_index.

Update: as of version 3.8.4 of Semantic Objects this is no longer required.

Template Restrictions

The following operations are not possible for the SPARQL template properties:

  • Mutation

    • Properties defined with SPARQL templates will be excluded from Mutation inputs. This also includes SHACL validation.
    • Types with typeProp referring to a property with a SPARQL template would be entirely excluded from the mutation.
  • It is not possible to index the properties via the Elasticsearch connectors and to use them in the Search Service as this is a limitation in the GraphDB connectors API.

    • Properties defined with SPARQL templates will not be eligible for indexing via GraphDB connectors.
    • Types with typeProp referring to a property with a SPARQL template would be entirely excluded from indexing and no connector/index will be created for them.
  • Is not advisable to define properties as inverseAlias of properties that:

    • Use filters
    • Use some form of subselects
  • It is not possible to filter or order a parent entity by property that uses subselects.

Template Arguments

SPARQL templates can have arguments that are passed from the GraphQL query. The template processing is done by Mustache.java.

The template engine allows defining variables that are dynamically replaced based on the arguments provided by the user. Any found variable will appear as arguments to the respective property in the GraphQL model.

The implementation supports:

  • Simple literal variables in the form of {{variable_name}}.
  • Block variables defined as {{#variable_name}}body{{/variable_name}}. They are generated as lists with a name matching the variable name. The block body will be outputted as many times as the number of elements in the input collection. If the collection is empty, nothing will be outputted.
  • A negated block variable will output its contents when the variable is an empty list: {{^variable_name}}body{{/variable_name}}.
  • Nested variables in the form of: - {{parent.child}} where the argument will have the form of {parent: {child: "value"}}. - {{#variable_name}} {{inner_variable}} {{/variable_name}} where the input structure would look like {variable_name: [{inner_variable: "value"}]}.
  • Change the delimiter with {{=newOpenDel newCloseDel=}}. An example of how to change the delimiters to <% and %>: {{=<% %>=}}.

All of the above is described in detail in the Mustache.js manual.

The Ontotext Semantic Objects also support the following extensions:

  • Literal list joining: {{#join}}variable_name{{/join}}. This will concatenate literal values with a comma-delimiter (,).
  • Literal list joining with custom delimiter: {{#join delimiter='---'}}variable_name{{/join delimiter='---'}}. This will concatenate a list of literal values using the delimiter ---.

Note

It is not allowed to use template arguments for properties used as typeProp or name in a SOML type. This restriction comes from the GraphQL inheritance limitations that sub-types cannot change the field arguments. Also, __typename selection is a system property for the GraphQL server and cannot be modified.

Note

Properties with arguments are excluded from filter input objects and orderBy selections in the GraphQL schema. This means that it is not allowed to filter or sort by properties using template arguments in GraphQL queries.

For example, the following type definition is invalid as name and typeProp point to a property with an argument. Removing the argument definition would make it valid.

objects:
  Person:
    typeProp: customType
    name: customType
    props:
      customType:
        min: 1
        rdfProp: |
          ?_subject rdf:type ?_value.
          {{sparql:sparql}}

Template Argument Types

By default, all single-value template variables are defined as GraphQL type String! and the iterable variables as [String!]!.

A template can override the generated variable type by using the following format:

{{variable_name:type}}

Where the type is one of the supported SOML scalar types: iri, string, integer, int, long, short, byte, boolean, double, decimal, float, unsignedLong, unsignedInt, unsignedShort, unsignedByte, positiveInteger, nonPositiveInteger, negativeInteger, nonNegativeInteger, negativeFloat, nonNegativeFloat, positiveFloat, nonPositiveFloat, year, yearMonth, date, time, dateTime.

Note

langString and stringOrLangString are currently not supported, but will be in future versions.

In addition to the default types, the sparql type is added to allow direct SPARQL injection. The values of such parameters are not escaped and allow SPARQL code to be passed from the GraphQL query.

All value types except for sparql are converted to RDF literal values and escaped to match the defined types.

Consider the following SOML fragment defining three templates with various arguments:

objects:
  Person:
    name: rdfs:label
    props:
      desc:
        max: inf
        range: stringOrLangString
        rdfProp: |
          ?_subject voc:desc ?_value.
          {{sparql:sparql}}

      hairColor:
        max: inf
        rdfProp: |
          ?_subject voc:hairColor ?_value.
          filter(?_value in ({{#join}}values{{/join}}))

      height:
        range: int
        rdfProp: |
          ?_subject voc:height ?_value.
          filter({{from:int}} < ?_value && ?_value < {{to:int}})

This will produce the following GraphQL fragment:

type Person {
 desc(args: Person_desc_Template_Input): [Literal]
 hairColor(args: Person_hairColor_Template_Input!): [String]
 height(args: Person_height_Template_Input!): Int
 # other properties
}

input Person_desc_Template_Input {
 sparql: String
}

input Person_hairColor_Template_Input {
 values: [String!]!
}

input Person_height_Template_Input {
 from: Int!
 to: Int!
}

Notice that the args argument of the desc property and its filter parameter are not required. This is because arguments of type sparql are expected to not break the query if not passed, thus they are generated as nullable. If not present, all other arguments can make the query invalid, so they are generated as non-nullable.

The following query can be used for the above schema:

query {
  person {
    id
    desc(args: {sparql: "FILTER (langMatches(lang(?_value), '*'))."})
    # desc(lang: "ALL:~") # same as as the filter above
        {value lang}
    hairColor(args: {values: ["brown", "blond"]})
    height(args: {from: 150, to: 185})
  }
}

It will return all Person entities and requested properties that match the given restrictions.

The generated SPARQL query will have the following fragments:

?person voc:desc ?person_desc.
FILTER (langMatches(lang(?_value), '*')).

?person voc:hairColor ?person_hairColor.
filter(?person_hairColor in ("brown", "blond"))

?person voc:height ?person_height.
filter("150"^^xsd:decimal < ?person_height && ?person_height < "185"^^xsd:decimal)

Template Argument Default Value

From Semantic Objects 4.1, the Sparql Template arguments support simple default values. Adding a default value to a template argument will make the argument optional. If such an argument is not present in the query, the default value will be used instead.

The template argument syntax is updated as follows: {{argument_name:argument_type:default_value}}. For the default type of string the argument looks like this: {{argument_name::default_value}}.

Here are some example argument definition:

height:
    rdfProp: |
      ?_subject voc:height ?_value.
      filter({{from:decimal:1,2}} < ?_value && ?_value < {{to:decimal:2,0}})

The provided default value should be compatible with the defined type and will produce an error during schema validation. For example, the argument definition {{from:decimal:true}} will produce the following error: ERROR: Property 'height' of 'Droid' defines rdfProp with invalid SPARQL fragment: The default value 'true' is not compatible with type 'decimal'.

Warning

The default value cannot include dot . characters. If you need to include a dot character, you need to use ## instead. For fractional number types, a comma , could also be used. For example, {{query::a.b}} should be written as {{query::a##b}}, while {{from:decimal:1.2}} could be written either as {{from:decimal:1,2}} or {{from:decimal:1##2}}.

Examples

Create New Values

The SPARQL templates can be used to create new computed values based on the data in the repository using SPARQL functions. Here is an example fragment that creates a new property from two separate predicate values:

objects:
  Person:
    props:
      firstName:
      lastName:
      fullName:
        rdfProp: |
          OPTIONAL {?_subject voc:firstName ?firstName.}
          OPTIONAL {?_subject voc:lastName ?lastName.}
          BIND(
            CONCAT(
              IF(BOUND(?firstName), ?firstName, ""),
              " ",
              IF(BOUND(?lastName), ?lastName, "")
            ) as ?_value
          ).

Select Value in Preference Order

Multiple values can be selected and returned in a single value.

objects:
  Vehicle:
    props:
      desc:
        max: inf
        rdfProp: |
          {
            ?_subject rdfs:label ?_value.
          } UNION {
            ?_subject skos:prefLabel ?_value.
          } UNION {
            ?_subject skos:altLabel ?_value.
          }

Load Data via SPARQL Federation

The following example loads the property value from a different repository using Internal SPARQL Federation.

objects:
  Book:
    props:
      label: {rdfProp: "rdfs:label"}

  Author:
    props:
      authorName:
        rdfProp: |
          SERVICE <repository:authors> {
            ?_subject rdfs:label ?_value.
          }

Query GraphDB Connectors

The following example filters the parent type based on an input query after querying an already existing Lucene GraphDB Connector.

id: /soml/connectors-lucene

prefixes:
  luc:        http://www.ontotext.com/connectors/lucene#
  luc-index:  http://www.ontotext.com/connectors/lucene/instance#
  wine:          http://www.ontotext.com/example/wine#
specialPrefixes:
  vocab_prefix:  wine
  vocab_iri:     http://www.ontotext.com/example/wine#

objects:
  Wine:
    props:
      label: {rdfProp: "rdfs:label"}
      grape: {max: inf, rdfProp: "wine:madeFromGrape", range: Grape}
      sugar: {rdfProp: "wine:hasSugar"}
      year: {range: int, rdfProp: "wine:hasYear"}
      searchScore: {rdfProp: "luc:score", range: decimal, restrictive: true}
      search:
        restrictive: true
        rdfProp: |
          BIND('''{{query:sparql}}''' as ?_value).
          ?search a luc_index:my_index ;
             luc:query '''{{query:sparql}}''' ;
             luc:entities ?_subject.

  Grape:
    name: rdfs:label

Note

A prefix defined as luc-index:  http://www.ontotext.com/connectors/lucene/instance# should be used as luc_index:wines.

In the type definition above, an important part of model is the characteristic restrictive: true as it instructs the SPARQL transpiler that the given property must restrict its subject and should not generate an OPTIONAL block around the property.

And here is an example query to fetch all wines from the year 2012 using the Lucene index.

{
  wine {
    id
    year
    sugar
    grape {name}
    search(args: {query: "year:2012"})
  }
}

The same approach can be applied for other connectors such as the Elasticsearch GraphDB connector.

Query GraphDB Connectors with Facets

The following functionality is available in Semantic Objects 4.1.0 and later.

The following example filters the parent type based on an input query after querying an already existing Elasticsearch GraphDB Connector.

id: /soml/connectors-elastic

prefixes:
  elastic:       http://www.ontotext.com/connectors/elasticsearch#
  elastic-index: http://www.ontotext.com/connectors/elasticsearch/instance#
  wine:          http://www.ontotext.com/example/wine#
specialPrefixes:
  vocab_prefix:  wine
  vocab_iri:     http://www.ontotext.com/example/wine#

objects:
  Wine:
    props:
      hasSugar:
      hasYear: {range: int}
      madeFromGrape: {range: Grape, max: inf}

  Grape:
    name: rdfs:label

  WineSearch:
    access: read-only
    typeProp: none
    props:
      results: {range: Wine, max: inf, rdfProp: "?__search elastic:entities ?_value"}
      totalHits: {range: int, rdfProp: "?__search elastic:totalHits ?_value"}
      facets: {range: Facets, max: inf, rdfProp: "?__search elastic:facets ?_value"}
      search:
        restrictive: true
        rdfProp: |
           BIND(<urn:search> as ?_subject).
           BIND('''{{query:sparql}}''' as ?_value).
           ?__search a elastic_index:my_index ;
                  elastic:facetFields {{facetFields}} ;
                  elastic:query ?_value .

  Facets:
    access: read-only
    props:
      facetName: {rdfProp: elastic:facetName, restrictive: true}
      facetValue: {rdfProp: elastic:facetValue, restrictive: true}
      facetCount: {range: int, rdfProp: elastic:facetCount, restrictive: true}

Note

A prefix defined as elastic-index: http://www.ontotext.com/connectors/elasticsearch/instance# should be used as elastic_index:my_index.

In the definition above there are made several important changes in order for the query to work properly. Here is the list of the important parts:

  • New type has been defined to hold different search configurations: WineSearch
  • New type Facets has been introduced to represent the facets information
  • New characteristic access: read-only has been used that instructs the GraphQL schema generator not to generate mutations for the types WineSearch and Facets.
  • The type WineSearch defines a special type property typeProp: none that will cause the type matching to be ignored.
  • The search property has the SPARQL function BIND(<urn:search> as ?_subject). to ensure single final result. This is required as querying the facets and results at the same time will cause cartesian product as 2 different searches are perform.
  • The search template defines a global variable ?_search. Global variables are variables that begin with underscore and are not the special ?_subject and ?_value variables.
  • The results, totalHits and facets properties use the global variable ?_search in order to resolve the corresponding information from the performed query.
  • The search property is defined as restrictive: true in order for the global variable to be visible to the rest of the properties at the same level as it instructs the SPARQL transpiler that the given property must restrict its subject and should not be put in an OPTIONAL OR UNION blocks.

And here is an example query and the result of it that fetches all wines from the year 2012 using the Elasticsearch index and calculate the facets for the sugar and year properties for the marched results.

query {
    wineSearch {
        search(args: {query: "year:2012", facetFields: "sugar,year"})
        results {
          id
          hasSugar
          hasYear
        }
        totalHits
        facets {
          facetName
          facetValue
          facetCount
      }
    }
}
{
    "data": {
        "wineSearch": [
            {
                "search": "year:2012",
                "results": [
                    {
                        "id": "http://www.ontotext.com/example/wine#Franvino",
                        "hasSugar": "dry",
                        "hasYear": 2012
                    },
                    {
                        "id": "http://www.ontotext.com/example/wine#Noirette",
                        "hasSugar": "medium",
                        "hasYear": 2012
                    },
                    {
                        "id": "http://www.ontotext.com/example/wine#Blanquito",
                        "hasSugar": "dry",
                        "hasYear": 2012
                    }
                ],
                "totalHits": 3,
                "facets": [
                    {
                        "facetName": "year",
                        "facetValue": "2012",
                        "facetCount": 3
                    },
                    {
                        "facetName": "sugar",
                        "facetValue": "dry",
                        "facetCount": 2
                    },
                    {
                        "facetName": "sugar",
                        "facetValue": "medium",
                        "facetCount": 1
                    }
                ]
            }
        ]
    }
}