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.
        }
    }
}

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.

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.

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)

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.