Queries

Given a SOML schema, the Semantic Objects generate a powerful query language in the form of GraphQL Input Objects. These objects can be used as Field Arguments to filter, order, and limit field queries:

<field> (ID: [ID!], limit: PositiveInteger, offset: PositiveInteger,
         orderBy: <fieldType>_OrderBy, where: <fieldType>_Where_Multi):
           [<FieldType>]!

Such arguments are generated for:

  • Each SOML object type, as a root query that allows you to find one or more objects of that type.
  • Each multi-valued field of every object.

More precisely, the generated arguments depend slightly on the type of field (scalar or object):

<scalarField> (limit: PositiveInteger, offset: PositiveInteger,
               orderBy:         _OrderBy, where: <scalarType>_Where_Multi):
                 [<scalarType>]!
<objectField> (limit: PositiveInteger, offset: PositiveInteger, ID: [ID!],
               orderBy: <Object>_OrderBy, where: <ObjectType>_Where_Multi):
                 [<ObjectType>]!
  • All scalar fields share the same enum _OrderBy.
  • Object fields can be filtered by ID.

Example Schema

Assume the following simple SOML schema:

Film:
  name: rdfs:label
  props:
    director: {max: inf}
    episodeId: {range: integer}
    desc:
      kind: literal
      label: Description
      range: stringOrLangString
      rdfProp: voc:desc
    planet: {descr: "Planets which appear in the Film", max: inf, range: Planet}

Planet:
  name: rdfs:label
  props:
    desc:
      kind: literal
      label: Description
      range: stringOrLangString
      rdfProp: voc:desc
    climate:
    diameter: {label: "Diameter in Km", range: integer}
    film: {descr: "Films which the planet appeared in", max: inf, range: Film}

The following GraphQL schema is generated:

# business objects

type Film implements Nameable & Object {
  id: ID
  type(ID: [ID!], limit: PositiveInteger, offset: PositiveInteger, orderBy: _OrderBy, where: ID_Where_Multi): [ID]!
  name: String
  episodeId: Integer
  desc(value: String_Where, lang: String): Literal
  director(limit: PositiveInteger, offset: PositiveInteger, orderBy: _OrderBy, where: String_Where_Multi, lang: String): [String]!
  planet(ID: [ID!], limit: PositiveInteger, offset: PositiveInteger, orderBy: Planet_OrderBy, where: Planet_Where_Multi, lang: String): [Planet]!
}

type Planet implements Nameable & Object {
  id: ID
  type(ID: [ID!], limit: PositiveInteger, offset: PositiveInteger, orderBy: _OrderBy, where: ID_Where_Multi): [ID]!
  name: String
  desc(value: String_Where, lang: String): Literal
  climate: String
  diameter: Integer
  film(ID: [ID!], limit: PositiveInteger, offset: PositiveInteger, orderBy: Film_OrderBy, where: Film_Where_Multi, lang: String): [Film]!
}

# root queries

type Query {
  object(ID: [ID!], limit: PositiveInteger, offset: PositiveInteger, orderBy: Object_OrderBy, where: Object_Where_Multi, lang: String): [Object!]
  film  (ID: [ID!], limit: PositiveInteger, offset: PositiveInteger, orderBy: Film_OrderBy,   where: Film_Where_Multi, lang: String):   [Film!]
  planet(ID: [ID!], limit: PositiveInteger, offset: PositiveInteger, orderBy: Planet_OrderBy, where: Planet_Where_Multi, lang: String): [Planet!]
}

The generated input objects for order (<fieldType>_OrderBy) and filter (<fieldType>_Where_Multi) are described in further sections.

Limit and Offset

The limit and offset arguments allow you to take a slice of the total results for a root query or multi-value field and implement pagination. (There is no way to find the total number of results without getting the last one.)

Performance Considerations

Important performance considerations to keep in mind:

  • limit and where are optional, so you can request all objects of a certain type, all values of a multi-value field, or even all object in the system.
  • This is potentially very expensive, so you should always provide where filters and/or limits.
  • orderBy on a large result set is always expensive (even with limit) because GraphDB needs to find all results and then sort them in memory.
  • Deeply nested GraphQL queries can also be very expensive, even if you provide reasonable limits on every level. You should ensure that the number of results per level does not grow in a geometric progression.
  • The Semantic Objects include some Denial of Service safeguards that refuse overly complex queries (over 15 levels), limit the number of results (total 100k triples), and timeout queries that are too slow.

OrderBy

Ordering features (what you can order by):

  • You can use any single-value field to order its parent (multi-valued) object.
  • You can order by several fields by putting them into a dictionary. The dictionary order determines the majority of ordering. (Note: this slightly contravenes the GraphQL spec that uses ordered dicts for busness Objects but not for input dicts: “Input object should be […] an unordered map supplied by a variable. […] The result of coercion is an unordered map.”)
  • You can order by a chain of nested fields, as soon as each one is single-valued.
  • You can order a scalar array by its values.

The following example illustrates all these points (assuming altName is a string multi-valued field):

query companiesByCountryCodeThenName {
  company(orderBy: {country: {gn_countryCode: ASC}, name: ASC}) {
    id name
    country {
      id name
      altName(orderBy: ASC)
      gn_countryCode
    }
  }
}

OrderBy Inputs

A specifies ordering by using order field specifications, which are nested Input Objects. To implement the features described above, we generate the following ordering arguments (Input Object types):

  • All scalars are ordered the same way, so they share enum _OrderBy that specifies the ordering direction (ASC/DESC).

  • Every single-value field of an object gets an ordering argument corresponding to its type.

    • Only single-value fields are orderable, as we cannot be sure which value to use for a multi-value field.

    • All objects are orderable because they have at least one orderable field: id.

    • We use _typename with one underscore because two underscores are reserved for GraphQL Introspection:

      The input field must not have a name which begins with the characters “__” (two underscores)”.

  • name gets the same _OrderBy. The field used as name (e.g., for Film and Planet below that is rdfs:label) does not get such an argument because it would be redundant.

  • The built-in types Interface Object and Literal also get respective ordering Input types.

"Ordering of scalars"
enum _OrderBy {ASC DESC}

"Order fields of Object"
input Object_OrderBy {
  id:         _OrderBy
  _typename:  _OrderBy
}

"Order fields of Literal"
input Literal_OrderBy {
  value:      _OrderBy
  type:       _OrderBy
  lang:       _OrderBy
}

"Order fields of Film"
input Film_OrderBy {
  id:         _OrderBy
  _typename:  _OrderBy
  name:       _OrderBy
  episodeId:  _OrderBy
}

"Order fields of Planet"
input Planet_OrderBy {
  id:         _OrderBy
  _typename:  _OrderBy
  name:       _OrderBy
  climate:    _OrderBy
  diameter:   _OrderBy
}

You could get all objects in the system ordered by _typename then id with a query like this. (But that would be a very bad idea, see Performance Considerations.)

query AllObjects(orderBy: {_typename: ASC, id: DESC}) {
  id
  __typename
}

Scalar Ordering

Here are some details about how scalars are ordered. These details come from the GraphDB SPARQL query processor and you can check them with a query like the following (try also to change the direction to DESC()):

prefix xsd: <http://www.w3.org/2001/XMLSchema#>
prefix my: <http://example.org/>
base <http://example.org/>
select * {
  values ?x {
    "0001-01-01T00:00:00"^^xsd:dateTime "0001-01-01"^^xsd:date
    "z" "1" "2"
    "z"@en "z"@en-GB "z"@fr "1"@en "1"@en-GB "1"@fr
    undef
    002 2 002.000 2.0 "002.000"^^xsd:double "2.0"^^xsd:double
    001 1 001.000 1.0 "001.000"^^xsd:double "1.0"^^xsd:double
    "1"^^my:foo "1"^^my:bar "2"^^my:baz
    <foo> <http://example.org/bar> my:baz <mailto:foo@example.org> <geo:42.68,23.21> <urn:uuid:1234> <urn:isbn:4567>
  }
} order by ASC(?x)

Values are grouped by kind. The ordering of these groups is as follows in the ASC (ascending) direction:

  • Null (undef): when the ordering field is null for some objects. Check the Null Handling section below for more information.

  • IRIs, ordered alphabetically (prefixed and relative IRIs are expanded), e.g.,:

    geo:42.68,23.21 http://example.org/bar http://example.org/baz http://example.org/foo mailto:foo@example.org urn:isbn:4567 urn:uuid:1234
    
  • Numeric values ordered numerically: see Lexical vs Value Space for details. (Note: the Turtle/SPARQL shortcuts 002 and 002.000 mean "002"^^xsd:integer and "002.000"^^xsd:decimal, respectively):

    "001"^^xsd:integer "1"^^xsd:integer "001.000"^^xsd:decimal "1.0"^^xsd:decimal "001.000"^^xsd:double "1.0"^^xsd:double
    "002"^^xsd:integer "2"^^xsd:integer "002.000"^^xsd:decimal "2.0"^^xsd:decimal "002.000"^^xsd:double "2.0"^^xsd:double
    
  • Dates ordered chronologically:

    "000001-01-01"^^xsd:date "0001-01-01"^^xsd:date "0001-01-02"^^xsd:date
    
  • dateTimes ordered chronologically (please note these are not comparable to dates):

    "0001-01-01T00:00:00"^^xsd:dateTime "000001-01-01T00:00:00"^^xsd:dateTime
    
  • Datatyped literals (other than numbers, dates, and dateTimes), ordered first by datatype then by value:

    "1"^^my:bar "2"^^my:baz "1"^^my:foo
    
  • langStrings are ordered only by value and ignore the language. Note the order is not guaranteed if multiple triples have the same value in multiple languages:

    "1"@en "1"@en-GB "1"@fr "z"@en "z"@en-GB "z"@fr
    

    or:

    "1"@fr "1"@en "1"@en-GB "z"@en-GB "z"@fr "z"@en
    
  • Plain strings:

    "1"^^xsd:string "2"^^xsd:string "z"^^xsd:string
    

The order is stable, i.e., equal values of the same kind are emitted in the same order as encountered.

If you use DESC (descending), the order is reversed, so for example nulls come last. See Null Handling for more information. However, stability is preserved, which means that ASC and DESC are not complete inverses of each other.

Other SPARQL implementations may use different ordering between the groups. GitHub issue sparql-12#88 may lead to standardization of ordering in SPARQL 1.2.

Null Handling

While in SPARQL undefined values (undef) are placed in the results based on the ordering directions, the Semantic Objects always emit them at the end. This ensures that the relevant results are always retrieved first, helping to build UI components that would never have an empty first result page.

Note

The Semantic Objects alway emits the NULL values at the end, regardless of the sorting direction.

ID Filtering

To find objects or subobjects by IRI, you can use the argument ID. It is a convenient shortcut, as ID: ... means the same as id: {IN: ...}. It takes single or multiple IRIs as argument, represented as strings. (You can omit the brackets around a singleton array value.)

Some examples (for brevity, we will use values like "foo" "bar" that are not really valid IRIs):

  • Object by fixed IRI(s)

    {object (ID:"foo") {id name}}
    {object (ID:["foo","bar"]) {id name}}
    
  • Object by IRI and then accessing its subobject by IRI(s):

    {object(ID:"foo") {id name
      subobject(ID:"bar") {id name}}}
    {object(ID:"foo") {id name
      subobject(ID:["bar","baz"]) {id name}}}
    
  • Object by fixed IRI(s) of a sub-object. Please note that this query is different from the previous one: it finds objects that have a sub-object with a given IRI.

    {object (where: {subobject: {ID:"foo"}})         {id name}}
    {object (where: {subobject: {ID:["foo","bar"]}}) {id name}}
    
  • Object by fixed external IRI(s). These are scalar fields of type iri, so you need to use the comparison operators described in the following sections.

    {object (where: {websiteUrl: {IN:"foo"}})         {id name}}
    {object (where: {websiteUrl: {IN:["foo","bar"]}}) {id name}}
    

Where Filtering

For top-level queries and multi-valued fields, you can use the where argument that expresses a search/filter condition in a structured way (an Input Object similar to a JSON dictionary), using field names (including nested fields), comparison operators (e.g., LT) and Boolean operators (e.g., OR). This takes care of 80% of the simple cases (comparing a field to a constant, checking for existence, and combinations thereof), and offers query writing assistance and validation.

Our where syntax is inspired by Hasura, a GraphQL implementation over PostgreSQL. See Hasura’s query manual and query API reference for more details.

Each where field represents a filter clause.

  • Each clause must be satisfied for the filter to match (implicit AND).
  • It is enough to find one instance of matching fields or nested fields (implicit EXISTS).

We first illustrate each comparison operator and logical connective with an example, then describe the Where Input objects (formal grammar), and finally give more extensive examples.

For our demonstration purposes here, we give many examples in one or two lines in order to fit in more; normally you would place each field and closing bracket on a separate line (this is what GraphiQL’s Prettify button does).

Comparison Operators

Clauses can reference an object field followed by a comparison operator or nested field access.

  • Comparison operators come in pairs:

    • EQ:NEQ: equality/inequality.

    • IN:NIN: inside/outside of an array of values (put them in square brackets). Brackets around a single value are optional, so all the queries below do the same. However, for clarity you should use IN for multiple values, and EQ for single values:

      {object (where: {field: {EQ:3}})   {name}}
      {object (where: {field: {IN:[3]}}) {name}}
      {object (where: {field: {IN:3}})   {name}}
      
    • LT:GTE and LTE:GT: comparison of numbers, strings or dates (less-than, greater-than-or-equal, less-than-or-equal, greater-than)

    • RE:NRE and IRE:NIRE: regular expression match or mismatch (N) for strings or IRIs. The I variants make case-insensitive comparisons, using Unicode alphabet collation.

      Warning

      Regex comparison is slow, so you should only use it on small result sets.

  • Please note that SPARQL logic is 3-valued: true, false and undef (in case the input field is missing), so it is possible for both of these to be unsatisfied:

    {object (where: {field: {EQ:3}})  {id name}}  # looks for a value equal to 3
    {object (where: {field: {NEQ:3}}) {id name}}  # looks for a value different from 3
    
  • To access nested fields, nest them in further input objects:

    query BulgarianCompanies {
      company (where: {country: {gn_countryCode: {EQ: "BG"}}}) {
        id name
        country {id name gn_countryCode}
      }
    }
    

The following table shows which comparisons are applicable to which scalar types:

Datatypes Comparisons
iri EQ NEQ IN NIN RE NRE IRE NIRE
string EQ NEQ IN NIN LT GT LTE GTE RE NRE IRE NIRE
unsignedByte unsignedShort unsignedLong unsignedInt integer positiveInteger nonPositiveInteger negativeInteger nonNegativeInteger positiveFloat nonPositiveFloat negativeFloat nonNegativeFloat double decimal EQ NEQ IN NIN LT GT LTE GTE
date time dateTime year yearMonth EQ NEQ IN NIN LT GT LTE GTE
boolean EQ NEQ IN NIN LT GT LTE GTE

To compare literals and union datatypes (literal, langString, stringOrLangString, dateOrYearOrMonth) you need to access their value subfield. You can then use all comparison operators.

Note

When filtering by dateTime, the following formats will be accepted:

(where: {dateTimeRegular: {GT: "2016-06-23T09:07:20"}})
(where: {dateTimeRegular: {GT: "2016-06-23T09:07:20Z"}})

The following would not be accepted:

(where:dateTimeRegular:{GT: "2016-06-23T09:07:20.000"}})

Logical Connectives

You can use the following logical connectives:

  • AND (conjunction) is implicit between clauses in the same input object

    • To compare two fields of the same object, just put them together in the same dict. E.g., search for people by birth date and name (Note: commas are optional in GraphQL):

      {person (where: {birthDate: {EQ:"2010-01-01"}, name: {IRE:"smith"}}) {name}}
      
    • To apply two comparisons to one field (e.g., a range check), just put them together in the same dict. E.g., search for people born between 2010 and 2015 (exclusive). Note: this compares birthDate directly, so it assumes a declaration like birthDate: {range: date} (and not a literal):

      {person (where: {birthDate: {GTE:"2010-01-01", LT:"2015-01-01"}}) {name}}
      
    • AND is an explicit conjunction. You will need to use it in two cases:

      1. To check two different instances of the same field. For example, look for people who have siblings born on two different dates:

        {person (where: {AND: [{sibling: {birthDate: {EQ:"2010-01-01"}}},
                               {sibling: {birthDate: {EQ:"2015-01-01"}}}]}) {
          name
          sibling {name birthDate}}}
        
        • You cannot write this query using IN because that would look for one sibling born on any of the dates:

          {person (where: {sibling: {birthDate: {IN: ["2010-01-01","2015-01-01"}}}]) {
            name
            sibling {name birthDate}}}
          
        • However, if the sibling field being checked has multiple values or can match several times, such a check could still match a single node. The example query below will match a person with a single sibling named “John-Jane”:

          {person (where: {AND: [{sibling: {name: {IRE: "john"}}},
                                 {sibling: {name: {IRE: "jane"}}}]}) {
             name
             sibling {name}}}
          
      2. To make two comparisons on one field using the same operator. (Note: this is only needed for the string operators RE IRE NRE NIRE.)

        • Imagine you need to check for two words, “cheese” and “ham”, in any order. If you use AND at the outer level, there is no guarantee the same “title” value will be used:

          {article: (where: {AND: [{title: {IRE: "cheese"}},
                                   {title: {IRE: "ham"}}]}) {
            title}}
          
        • So it is better to make the conjunction at the operator level:

          {article: (where: {title: {AND: [{IRE: "cheese"},
                                           {IRE: "ham"}]}}) {
            title}}
          
        • You could also do it with a single more expensive regex:

          {article: (where: {title: {IRE: "cheese.*ham|ham.*cheese"}}) {
            title}}
          
  • OR takes an array of clauses and applies a disjunction.

    • For example, look for people named “john” (first name) or “smith” (last name):

      {person (where: {OR: [{firstName: {IRE: "john"}},
                            {lastName:  {IRE: "smith"}}]}) {
        firstName lastName}}
      
    • If you need to check for one of several values in the same field, it is easier to use RE with | (for text comparison) or IN (for fixed values). For example, look for “smith” or “jones” in last name:

      {person (where: {lastName: {IRE: "smith|jones"}}) {
        lastName}}
      
    • Look for a person born on one of two dates:

      {person (where: {birthDate: {IN: ["2010-01-01","2015-01-01"]}}) {
        name birthDate}}}
      
    • You can use OR at the operator level if you need a disjunction of two comparisons on the same field. This query looks for articles with rating (a decimal number) outside the range 1..3:

      {article (where: {rating: {OR: [{LT:1} {GT:3}]}}) {
        title rating}}
      
  • NOT negates a clause. Most of the time you will not need it because you can use negation at the operator level. But you may need it for some complicated tests.

  • EXISTS is implicit when checking fields and sub-fields (one value is enough to satisfy the condition).

  • ALL is applicable to multi-valued fields and checks that all values satisfy the condition. For example, find people all of whose siblings are young babies (as of 2019):

    {person (where: {ALL: {sibling: {birthDate: {GTE: "2019-01-01"}}}}) {
      name
      sibling {name birthDate}}}
    
  • Note that a person without any siblings will also be returned by the above query (a vacuous truth), so you may want to add an existence check:

    {person (where: {ALL: {sibling: {birthDate: {GTE: "2019-01-01"}}}
                     sibling: {birthDate: {GTE: "2019-01-01"}}}) {
      name
      sibling {name birthDate}}}
    
  • For such cases you can use the ALL_EXISTS shortcut. It is applicable to multi-valued fields and checks both ALL and EXISTS. The example query below finds people who have young baby siblings, and only such siblings:

    {person (where: {ALL_EXISTS: {sibling: {birthDate: {GTE: "2019-01-01"}}}}) {
      name
      sibling {name birthDate}}}
    

Please note that ALL is expensive since it involves a SPARQL condition FILTER NOT EXISTS. We have implemented query optimizations that use the GraphDB cardinality statistics (similar to what is printed in the Explain Plan) to convert such clause to DISTINCT or MINUS in appropriate cases.

ALL_EXISTS is even more expensive since it combines a FILTER NOT EXISTS with FILTER EXISTS

Where Inputs

The definition of the where “formal grammar” is in the form of Input Objects added to the generated GraphQL schema. By using these definitions, your query development environment (GraphiQL, GraphQL Playground or similar) should provide efficient assistance for writing where queries.

Here is an example from Ontotext’s Star Wars API demo service:

../_images/graphiql-swapi-where.png

The GraphiQL UI guides you with autocompletion to easily enter a query like this one:

{
  starship(where: {cargoCapacity: {LT:100}}) {
    id
    name
    cargoCapacity
  }
}

For every GraphQL object type (including Literal) we generate two input objects.

  • <Type>_Where is used in contexts where the type appears in a single-value field.
  • <Type>_Where_Multi is used in contexts where the type appears in a mutli-value field. The difference is that this one has the connectives ALL and ALL_EXISTS.
  • You can use logical connectives AND OR NOT to make compound comparisons using the same inputs. (The first two take an array of comparisons.)
  • Both inputs enumerate all object fields, both scalar and object.
  • The prop mapped to the name characteristic (if any) is skipped since you can filter by field name.
"<Type> comparisons, single-value fields"
input <Type>_Where {
  "Check by id"                       ID:           [ID!]
  "Conjunction of <Type> comparisons" AND:          [<Type>_Where!]
  "Disjunction of <Type> comparisons" OR:           [<Type>_Where!]
  "Negation of <Type> comparison"     NOT:          <Type>_Where
  # for each single-value field (scalar except "name", or object):
  "<single_field> comparisons"        single_field: <single_field_Type>_Where
  # for each multi-value field (scalar, or object):
  "<multi_field> comparisons"         multi_field:  <multi_field_Type>_Where_Multi
}

"<Type> comparisons, multi-value fields"
input <Type>_Where_Multi {
  "Check by id"                       ID:           [ID!]
  "Conjunction of <Type> comparisons" AND:          [<Type>_Where_Multi!]
  "Disjunction of <Type> comparisons" OR:           [<Type>_Where_Multi!]
  "Negation of <Type> comparison"     NOT:          <Type>_Where_Multi
  "ForAll for <Type>"                 ALL:          <Type>_Where_Multi
  "ForAll and Exists for <Type>"      ALL_EXISTS:   <Type>_Where_Multi
  # for each single-value field (scalar except "name", or object):
  "<single_field> comparisons"        single_field: <single_field_Type>_Where
  # for each multi-value field (scalar or object):
  "<multi_field> comparisons"         multi_field:  <multi_field_Type>_Where_Multi
}

For every scalar type we also generate two input objects.

  • Similarly to the above, one is for single-value fields, the other for multi-value fields.
  • They also add the appropriate logical connectives.
  • NOT is excluded at this level because you can use negation at the operator level (e.g., NIN is the negation of IN).
  • They enumerate the appropriate comparison operators for that scalar. For example, Int and Date have the following comparisons:
"Int comparisons, single-value fields"
input Int_Where {
  "Equal to Int"                      EQ:         Int
  "Not equal to Int"                  NEQ:        Int
  "In list of Int"                    IN:         [Int!]
  "Not in list of Int"                NIN:        [Int!]
  "Less than Int"                     LT:         Int
  "Less or equal to Int"              LTE:        Int
  "Greater than Int"                  GT:         Int
  "Greater or equal to Int"           GTE:        Int
  "Conjunction of Int comparisons"    AND:        [Int_Where!]
  "Disjunction of Int comparisons"    OR:         [Int_Where!]
}

"Int comparisons, multi-value fields"
input Int_Where_Multi {
  "Equal to Int"                      EQ:         Int
  "Not equal to Int"                  NEQ:        Int
  "In list of Int"                    IN:         [Int!]
  "Not in list of Int"                NIN:        [Int!]
  "Less than Int"                     LT:         Int
  "Less or equal to Int"              LTE:        Int
  "Greater than Int"                  GT:         Int
  "Greater or equal to Int"           GTE:        Int
  "Conjunction of Int comparisons"    AND:        [Int_Where_Multi!]
  "Disjunction of Int comparisons"    OR:         [Int_Where_Multi!]
  "ForAll for Int"                    ALL:        Int_Where_Multi
  "All and Exists for Int"            ALL_EXISTS: Int_Where_Multi
}
  • Finally, String and ID have the additional comparison operators RE IRE NRE NIRE:
"String comparisons, single-value fields"
input String_Where {
  "Equal to String"                   EQ:         String
  "Not equal to String"               NEQ:        String
  "In list of String"                 IN:         [String!]
  "Not in list of String"             NIN:        [String!]
  "Less than String"                  LT:         String
  "Less or equal to String"           LTE:        String
  "Greater than String"               GT:         String
  "Greater or equal to String"        GTE:        String
  "Regex match"                       RE:         String
  "Case-insensitive regex match"      IRE:        String
  "Regex mismatch"                    NRE:        String
  "Case-insensitive regex mismatch"   NIRE:       String
  "Conjunction of String comparisons" AND:        [String_Where!]
  "Disjunction of String comparisons" OR:         [String_Where!]
}

"String comparisons, single-value fields"
input String_Where_Multi {
  "Equal to String"                   EQ:         String
  "Not equal to String"               NEQ:        String
  "In list of String"                 IN:         [String!]
  "Not in list of String"             NIN:        [String!]
  "Less than String"                  LT:         String
  "Less or equal to String"           LTE:        String
  "Greater than String"               GT:         String
  "Greater or equal to String"        GTE:        String
  "Regex match"                       RE:         String
  "Case-insensitive regex match"      IRE:        String
  "Regex mismatch"                    NRE:        String
  "Case-insensitive regex mismatch"   NIRE:       String
  "Conjunction of String comparisons" AND:        [String_Where_Multi!]
  "Disjunction of String comparisons" OR:         [String_Where_Multi!]
  "ForAll for String"                 ALL:        String_Where_Multi
  "All and Exists for String"         ALL_EXISTS: String_Where_Multi
}

Where Examples: Persons and Articles

Now that you are familiar with the grammar of where queries, let’s look at more complex examples. We start with some examples about articles and persons.

  • persons with some article with rating>=4, ordered by name
{person (where: {article: {rating: {GTE:4}}} orderBy: {name:ASC}) {
  name}}
  • same, but also return those articles, ordered by title
{person (where: {article: {rating: {GTE:4}}} orderBy: {name:ASC}) {
  id name
  article (where: {rating: {GTE:4}} orderBy: {title:ASC}) {
    id title}}}
  • person by name, and his articles
{person (where: {name: {EQ: "A.U.Thor"}}) {
  id
  article {id name}}}
  • persons named “smith” (case insensitive)
{person (where: {name: {IRE:"smith"}}) {
  id name}}
  • articles whose titles start with Pizza (case-sensitive)
{article (where: {title: {RE:"^Pizza"}}) {
  id title}}
  • persons named “smith” who wrote any article with “pizza” in the title (case-insensitive)
{person (where: {name: {IRE:"smith"}
                 article: {title: {IRE:"pizza"}}}) {
  name
  article {title}}}

Where Examples: Star Wars

Below are some examples that you can try on Ontotext’s Star Wars API demo service. See the blog post A New Hope: The Rise of the Knowledge Graph (Navigating through the Star Wars universe with knowledge graphs, SPARQL and GraphQL) for an explanation of the data model.

You can compare these to similar queries at the GraphCool SWAPI.

  • find ships with cargo capacity between 110 and 150 tons:
{starship (where: {cargoCapacity: {GTE:110, LTE:150}}) {
  name cargoCapacity}}
  • find ships that have only tall pilots (over 180cm), have at least one such pilot, and return all their pilots:
{starship (where: {ALL_EXISTS: {pilot: {height: {GT:180}}}}) {
  name
  pilot {name height}}}
  • we can express this in a more verbose manner through double negation (for ALL) and repeating the clause (for EXISTS). In the above case the Platform optimizes in the second clause: it looks for EXISTS pilot but does not expand the condition further ({height: {GT:180}}). This optimization is not possible if you write out the second clause in full.
{starship (where: {NOT: {pilot: {NOT: {height: {GT:180}}}}
                   pilot: {height: {GT:180}}}) {
  name
  pilot {name height}}}
  • find ships that have pilots that also drive a vehicle whose name contains “a”, and return all those ships, pilots and vehicles. Unfortunately we need to repeat the same sub-condition in the ship, pilot and vehicle filters (unlike field fragments, there are no “filter fragments” to let us reuse filters):
{starship (where: {pilot: {vehicle: {name: {IRE: "a"}}}}) {name
  pilot (where: {vehicle: {name: {IRE: "a"}}}) {name
    vehicle (where: {name: {IRE: "a"}}) {name}}}}
  • find ships that do not have tall pilots driving a vehicle whose name contains “a”, and return the ships, pilots and vehicles.
{starship (where: {NOT: {pilot: {height: {GT:180}
                                 vehicle: {name: {IRE: "a"}}}}})
  {name
   pilot {name height
     vehicle {name}}}}

Note that the last query returns:

  • starships without any pilot (e.g., “Calamari Cruiser”)
  • pilots that do not drive a vehicle (e.g., “Darth Vader” of “TIE Advanced x1”)
  • pilots without any height (e.g., “Arvel Crynyd” of “A-wing”)
  • tall pilots that drive a vehicle but it has no “a” in the name (e.g., “Obi-Wan Kenobi” who drives “Tribubble bongo”)

Where Examples: Companies

Query examples about companies:

  • Bulgarian companies with any funding transaction. The empty dict funding: {} just checks for existence
{country (ID:"http://sws.geonames.org/732800/") {
  company (where: {funding: {}}) {
    id name}}}
  • companies with a funding transaction of over $1M, and their three largest fundings
{company (where: {funding: {amount: {GT:1.0}}}) {
  id name
  funding (where: {amount: {GT:1.0}, orderBy: {amount:DESC}, limit:3}) {
    date amount}}}
  • companies founded between 2015 and 2019 (exclusive)
{company (limit: 100, where: {foundedOn: {GTE:"2015-01-01", LT:"2019-01-01"}}) {
  id name foundedOn}}
  • companies located in cities within a geo bounding box (between Pazardzhik and Veliko Tarnovo in Bulgaria)
{company (where: {city: {wgs_lat:  {GTE:42.384992, LTE:43.009443},
                         wgs_long: {GTE:24.353453, LTE:25.613992}}}) {
  id name
  city {name}}}
  • companies of two given Bulgarian legal types, limited to 100
{company (limit: 100,
  where: {cg_type: {skos_notation: {IN: ["BG:EAD", "BG:AD"]}}}) {
  id name
  type {name skos_notation}}}

The following queries find funding transactions targeting a Bulgarian company (in our model each transaction has only one target company):

  • starting from the country
{country (ID: "http://sws.geonames.org/732800/") {
  company (where: {funding: {}}) {
    name
    funding {amount date}}}}
  • starting from the company
{company (where: {funding: {}
                  country: {ID: "http://sws.geonames.org/732800/"}}) {
  name
  funding {amount date}}}
  • starting from the funding
{transaction (where: {company: {country: {ID: "http://sws.geonames.org/732800/"}}}) {
  amount date
  company {name}}}

Scalar Comparisons

Scalars are compared according to their value (and not their lexical string), using the same logic illustrated in Scalar Ordering. For example, "2"^^xsd:integer = "002.000"^^xsd:double.

To ensure this, we implement EQ using SPARQL IN (...) rather than a direct value pattern. IN or = is a bit slower than a direct value pattern, but the GraphDB literal index makes it fast enough.

We also normalize scalar values on output, so "002.000"^^xsd:double stored in GraphDB will be returned as "2"^^xsd:double. While numerical values are comparable across datatypes, the normalization never changes the datatype.

Language Preference for langString Properties

The lang argument can be used to pass the preferred language(s) to be fetched when selecting langString or stringOrLangString properties. The argument is applied to each generated query and on each complex property selection. This allows changing the default languages at each query level if needed.

The following example illustrates this:

query films {
    film(lang: "en,fr") {
      id
      name
      desc {
         value lang
      }
      planet(lang: "ALL") {
        id
        name
        desc {
           value lang
        }
      }
    }
}

For this query, we have defined that we want to retrieve the only English or French language value from the multi-language properties, and at planet property level we want all values for the multi-valued sub-properties.

For more details on the lang argument format and how to use it, check the Filtering Literal Values tutorial.