Gravsearch: Virtual Graph Search

Basic Concept

Gravsearch is intended to offer the advantages of SPARQL endpoints (particularly the ability to perform queries using complex search criteria) while avoiding their drawbacks in terms of performance and security (see The Enduring Myth of the SPARQL Endpoint). It also has the benefit of enabling clients to work with a simpler RDF data model than the one Knora actually uses to store data in the triplestore, and makes it possible to provide better error-checking.

Rather than being processed directly by the triplestore, a Gravsearch query is interpreted by Knora, which enforces certain restrictions on the query, and implements paging and permission checking. The API server generates SPARQL based on the Gravsearch query submitted, queries the triplestore, filters the results according to the user’s permissions, and returns each page of query results as a Knora API response. Thus, Gravsearch is a hybrid between a RESTful API and a SPARQL endpoint.

A Gravsearch query conforms to a subset of the syntax of a SPARQL CONSTRUCT query, with some additional restrictions and functionality. In particular, the variable representing the top-level (or ‘main’) resource that will appear in each search result must be identified, statements must be included to specify the types of the entities being queried, OFFSET is used to control paging, and ORDER BY is used to sort the results.

It is certainly possible to write Gravsearch queries by hand, but we expect that in general, they will be automatically generated by client software, e.g. by a client user interface.

Submitting Gravsearch Queries

The recommended way to submit a Gravsearch query is via HTTP POST:

HTTP POST to http://host/v2/searchextended

This works like query via POST directly in the SPARQL 1.1 Protocol: the query is sent unencoded as the HTTP request message body, in the UTF-8 charset.

It is also possible to submit a Gravsearch query using HTTP GET. The entire query must be URL-encoded and included as the last element of the URL path:

HTTP GET to http://host/v2/searchextended/QUERY

The response to a Gravsearch query is an RDF graph, which can be requested in various formats (see Responses Describing Resources).

To request the number of results rather than the results themselves, you can do a count query:

HTTP POST to http://host/v2/searchextended/count

The response to a count query request is an object with one predicate, http://schema.org/numberOfItems, with an integer value.

Gravsearch and API Schemas

A Gravsearch query can be written in either of the two Knora API v2 schemas. The simple schema is easier to work with, and is sufficient if you don’t need to query anything below the level of a Knora value. If your query needs to refer to list nodes or standoff markup, you must use the complex schema. Each query must use a single schema, with one exception (see Date Comparisons).

Gravsearch query results can be requested in the simple or complex schema; see API Schema.

Using the Simple Schema

To write a query in the simple schema, use the knora-api ontology in the simple schema, and use the simple schema for any other Knora ontologies the query refers to, e.g.:

PREFIX knora-api: <http://api.knora.org/ontology/knora-api/simple/v2#>
PREFIX incunabula: <http://0.0.0.0:3333/ontology/0803/incunabula/simple/v2#>

In the simple schema, Knora values are represented as literals, which can be used FILTER expressions (see Filtering on Values in the Simple Schema).

Using the Complex Schema

To write a query in the complex schema, use the knora-api ontology in the complex schema, and use the complex schema for any other Knora ontologies the query refers to, e.g.:

PREFIX knora-api: <http://api.knora.org/ontology/knora-api/v2#>
PREFIX incunabula: <http://0.0.0.0:3333/ontology/0803/simple/v2#>

In the complex schema, Knora values are represented as objects belonging to subclasses of knora-api:Value, e.g. knora-api:TextValue, and have predicates of their own, which can be used in FILTER expressions (see Filtering on Values in the Complex Schema).

Main and Dependent Resources

The main resource is the top-level resource in a search result. Other resources that are in some way connected to the main resource are referred to as dependent resources. If the client asks for a resource A relating to a resource B, then all matches for A will be presented as main resources and those for B as dependent resources. The main resource must be represented by a variable, marked with knora-api:isMainResource, as explained under CONSTRUCT Clause.

Virtual incoming Links

Depending on the ontology design, a resource A points to B or vice versa. For example, a page A is part of a book B using the property incunabula:partOf. If A is marked as the main resource, then B is nested as a dependent resource in its link value incunabula:partOfValue. But in case B is marked as the main resource, B does not have a link value pointing to A because in fact B is pointed to by A. Instead, B has a virtual property knora-api:hasIncomingLink containing A’s link value:

"knora-api:hasIncomingLink" : {
    "@id" : "http://rdfh.ch/A/values/xy",
    "@type" : "knora-api:LinkValue",
    "knora-api:linkValueHasSource" : {
      "@id" : "http://rdfh.ch/A",
      "@type" : "incunabula:page",
      "incunabula:partOfValue" : {
        "@id" : "http://rdfh.ch/A/values/xy",
        "@type" : "knora-api:LinkValue",
        "knora-api:linkValueHasTargetIri" : {
          "@id" : "http://rdfh.ch/B"
        }
      }
    }
  },

Note that the virtually inserted link value inverts the relation by using knora-api:linkValueHasSource. The source of the link is A and its target B is only represented by an Iri (knora-api:linkValueHasTargetIri) since B is the main resource.

Graph Patterns and Result Graphs

The WHERE clause of a Gravsearch query specifies a graph pattern. Each query result will match this graph pattern, and will have the form of a graph whose starting point is a main resource. The query’s graph pattern, and hence each query result graph, can span zero more levels of relations between resources. For example, a query could request regions in images on pages of books written by a certain author, articles by authors who were students of a particular professor, or authors of texts that refer to events that took place within a certain date range.

Permission Checking

Each matching resource is returned only if the client has permission to see all the values of that resource that matched the criteria given in the WHERE clause. If a matching resource contains a value that was mentioned in the WHERE clause, but the client does not have permission to see that value, the resource is not returned in the results, and is instead replaced by a proxy resource called knora-api:ForbiddenResource.

Inference

Gravsearch queries are understood to imply RDFS reasoning. Depending on the triplestore being used, this may be implemented using the triplestore’s own reasoner or by query expansion in Knora.

This means that if a statement pattern specifies a property, the pattern will also match subproperties of that property, and if a statement specifies that a subject has a particular rdf:type, the statement will also match subjects belonging to subclasses of that type.

Gravsearch Syntax

Every Gravsearch query is a valid SPARQL 1.1 CONSTRUCT query. However, Gravsearch only supports a subset of the elements that can be used in a SPARQL Construct query, and a Gravsearch CONSTRUCT Clause has to indicate which variable is to be used for the main resource in each search result.

Supported SPARQL Syntax

The current version of Gravsearch accepts CONSTRUCT queries whose WHERE clauses use the following patterns, with the specified restrictions:

  • OPTIONAL: cannot be nested in a UNION.
  • UNION: cannot be nested in a UNION.
  • FILTER: may contain a complex expression using the Boolean operators AND and OR, as well as comparison operators. The left argument of a comparison operator must be a query variable. A Knora ontology entity IRI used in a FILTER must be a property IRI.
  • FILTER NOT EXISTS
  • MINUS
  • OFFSET: the OFFSET is needed for paging. It does not actually refer to the number of triples to be returned, but to the requested page of results. The default value is 0, which refers to the first page of results. The number of results per page is defined in app/v2 in application.conf.
  • ORDER BY: In SPARQL, the result of a CONSTRUCT query is an unordered set of triples. However, a Gravsearch query returns an ordered list of resources, which can be ordered by the values of specified properties. If the query is written in the complex schema, items below the level of Knora values may not be used in ORDER BY.
  • BIND: The value assigned must be a Knora resource IRI.

Resources, Properties, and Values

Resources can be represented either by an IRI or by a variable, except for the main resource, which must be represented by a variable.

It is possible to do a Gravsearch query in which the IRI of the main resource is already known, e.g. to request specific information about that resource and perhaps about linked resources. In this case, the IRI of the main resource must be assigned to a variable using BIND.

Properties can be represented by an IRI or a query variable. If a property is represented by a query variable, it can be restricted to certain property IRIs using a FILTER.

A Knora value (i.e. a value attached to a knora-api:Resource) must be represented as a query variable.

Filtering on Values

Filtering on Values in the Simple Schema

In the simple schema, a variable representing a Knora value can be used directly in a FILTER expression. For example:

?book incunabula:title ?title .
FILTER(?title = "Zeitglöcklein des Lebens und Leidens Christi")

Here the type of ?title is xsd:string.

The following Knora value types can be compared with literals in FILTER expressions in the simple schema:

  • Text values (xsd:string)
  • Uri values (xsd:anyURI)
  • Integer values (xsd:integer)
  • Decimal values (xsd:decimal)
  • Boolean values (xsd:boolean)
  • Date values (knora-api:Date)

A Knora value may not be represented as the literal object of a predicate; for example, this is not allowed:

?book incunabula:title "Zeitglöcklein des Lebens und Leidens Christi" .

Filtering on Values in the Complex Schema

In the complex schema, variables representing Knora values are not literals. You must add something to the query (generally a statement) to get a literal from a Knora value. For example:

?book incunabula:title ?title .
?title knora-api:valueAsString "Zeitglöcklein des Lebens und Leidens Christi" .

Here the type of ?title is knora-api:TextValue. Note that no FILTER is needed in this example. But if you want to use a different comparison operator, you need a FILTER:

?page incunabula:seqnum ?seqnum .
?seqnum knora-api:intValueAsInt ?seqnumInt .
FILTER(?seqnumInt <= 10)

To match a date value in the complex schema, you must use the knora-api:toSimpleDate function in a FILTER (see Date Comparisons). The predicates of knora-api:DateValue (knora-api:dateValueHasStartYear, etc.) are not available in Gravsearch.

Date Comparisons

In the simple schema, you can compare a date value directly with a knora-api:Date in a FILTER:

?book incunabula:pubdate ?pubdate .
FILTER(?pubdate < "JULIAN:1497"^^knora-api:Date)

In the complex schema, you must use the function knora-api:toSimpleDate, passing it the variable representing the date value. The date literal used in the comparison must still be a knora-api:Date in the simple schema. This is the only case in which you can use both schemas in a single query:

PREFIX incunabula: <http://0.0.0.0:3333/ontology/0803/incunabula/v2#>
PREFIX knora-api: <http://api.knora.org/ontology/knora-api/v2#>
PREFIX knora-api-simple: <http://api.knora.org/ontology/knora-api/simple/v2#>

CONSTRUCT {
    ?book knora-api:isMainResource true .
    ?book incunabula:pubdate ?pubdate .
} WHERE {
    ?book a incunabula:book .
    ?book incunabula:pubdate ?pubdate .
    FILTER(knora-api:toSimpleDate(?pubdate) < "JULIAN:1497"^^knora-api-simple:Date)
} ORDER BY ?pubdate

You can also use knora-api:toSimpleDate with to search for date tags in standoff text markup (see Matching Standoff Dates).

Searching for Matching Words

The function knora-api:match searches for matching words anywhere in a text value, and is implemented using a full-text search index if available. The first argument must be a variable of type xsd:string, and the second argument is a string containing the words to be matched, separated by spaces. The words to be matched are separated by spaces in a string literal. For example, to search for titles that contain the words ‘Zeitglöcklein’ and ‘Lebens’ in the simple schema:

FILTER knora-api:match(?title, "Zeitglöcklein Lebens")

In the complex schema:

?title knora-api:valueAsString ?titleStr .
FILTER knora-api:match(?titleStr, "Zeitglöcklein Lebens")

If knora-api:match is used in a FILTER, it must be the only expression in the FILTER.

Filtering Text by Language

To filter a text value by language in the simple schema, use the SPARQL lang function on the text value, e.g.:

FILTER(lang(?text) = "fr")

In the complex schema, the lang function is not supported. Use the text value’s knora-api:textValueHasLanguage predicate instead:

?text knora-api:textValueHasLanguage "fr" .

Regular Expressions

The SPARQL regex function is supported. In the simple schema, you can use it directly on the text value, e.g.

?book incunabula:title ?title .
FILTER regex(?title, "Zeit", "i")

In the complex schema, use it on the object of the text value’s knora-api:valueAsString predicate:

?book incunabula:title ?title .
?title knora-api:valueAsString ?titleStr .
FILTER regex(?titleStr, "Zeit", "i")

Searching for Text Markup

To refer to standoff markup in text values, you must write your query in the complex schema.

A knora-api:TextValue can have the property knora-api:textValueHasStandoff, whose objects are the standoff markup tags in the text. You can match the tags you’re interested in using rdf:type or other properties of each tag.

Matching Text in a Standoff Tag

The function knora-api:matchInStandoff searches for standoff tags containing certain terms. The implementation is optimised using the full-text search index if available. The function takes three arguments:

  1. A variable representing the string literal value of a text value.
  2. A variable representing a standoff tag.
  3. A string literal containing space-separated search terms.

This function can only be used as the top-level expression in a FILTER. For example:

PREFIX knora-api: <http://api.knora.org/ontology/knora-api/v2#>
PREFIX standoff: <http://api.knora.org/ontology/standoff/v2#>
PREFIX beol: <http://0.0.0.0:3333/ontology/0801/beol/v2#>

CONSTRUCT {
    ?letter knora-api:isMainResource true .
    ?letter beol:hasText ?text .
} WHERE {
    ?letter a beol:letter .
    ?letter beol:hasText ?text .
    ?text knora-api:valueAsString ?textStr .
    ?text knora-api:textValueHasStandoff ?standoffParagraphTag .
    ?standoffParagraphTag a standoff:StandoffParagraphTag .
    FILTER knora-api:matchInStandoff(?textStr, ?standoffParagraphTag, "Grund Richtigkeit")
}

Here we are looking for letters containing the words “Grund” and “Richtigkeit” within a single paragraph.

Matching Standoff Links

If you are only interested in specifying that a resource has some text value containing a standoff link to another resource, the most efficient way is to use the property knora-api:hasStandoffLinkTo, whose subjects and objects are resources. This property is automatically maintained by Knora. For example:

PREFIX knora-api: <http://api.knora.org/ontology/knora-api/v2#>
PREFIX beol: <http://0.0.0.0:3333/ontology/0801/beol/v2#>

CONSTRUCT {
    ?letter knora-api:isMainResource true .
    ?letter beol:hasText ?text .
} WHERE {
    ?letter a beol:letter .
    ?letter beol:hasText ?text .
    ?letter knora-api:hasStandoffLinkTo ?person .
    ?person a beol:person .
    ?person beol:hasIAFIdentifier ?iafIdentifier .
    ?iafIdentifier knora-api:valueAsString "(VIAF)271899510" .
}

Here we are looking for letters containing a link to the historian Claude Jordan, who is identified by his Integrated Authority File identifier, (VIAF)271899510.

However, if you need to specify the context in which the link tag occurs, you must use the function knora-api:standoffLink. It takes three arguments:

  1. A variable or IRI representing the resource that is the source of the link.
  2. A variable representing the standoff link tag.
  3. A variable or IRI representing the resource that is the target of the link.

This function can only be used as the top-level expression in a FILTER. For example:

PREFIX knora-api: <http://api.knora.org/ontology/knora-api/v2#>
PREFIX standoff: <http://api.knora.org/ontology/standoff/v2#>
PREFIX beol: <http://0.0.0.0:3333/ontology/0801/beol/v2#>

CONSTRUCT {
    ?letter knora-api:isMainResource true .
    ?letter beol:hasText ?text .
} WHERE {
    ?letter a beol:letter .
    ?letter beol:hasText ?text .
    ?text knora-api:textValueHasStandoff ?standoffLinkTag .
    ?standoffLinkTag a knora-api:StandoffLinkTag .
    FILTER knora-api:standoffLink(?letter, ?standoffLinkTag, ?person)
    ?person a beol:person .
    ?person beol:hasIAFIdentifier ?iafIdentifier .
    ?iafIdentifier knora-api:valueAsString "(VIAF)271899510" .
    ?standoffLinkTag knora-api:standoffTagHasStartParent ?standoffItalicTag .
    ?standoffItalicTag a standoff:StandoffItalicTag .
}

This has the same effect as the previous example, except that because we are matching the link tag itself, we can specify that its immediate parent is a StandoffItalicTag.

If you actually want to get the target of the link (in this example, ?person) in the search results, you need to add a statement like ?letter knora-api:hasStandoffLinkTo ?person . to the WHERE clause and to the CONSTRUCT clause:

PREFIX knora-api: <http://api.knora.org/ontology/knora-api/v2#>
PREFIX standoff: <http://api.knora.org/ontology/standoff/v2#>
PREFIX beol: <http://0.0.0.0:3333/ontology/0801/beol/v2#>

CONSTRUCT {
    ?letter knora-api:isMainResource true .
    ?letter beol:hasText ?text .
    ?letter knora-api:hasStandoffLinkTo ?person .
} WHERE {
    ?letter a beol:letter .
    ?letter beol:hasText ?text .
    ?text knora-api:textValueHasStandoff ?standoffLinkTag .
    ?standoffLinkTag a knora-api:StandoffLinkTag .
    FILTER knora-api:standoffLink(?letter, ?standoffLinkTag, ?person)
    ?person a beol:person .
    ?person beol:hasIAFIdentifier ?iafIdentifier .
    ?iafIdentifier knora-api:valueAsString "(VIAF)271899510" .
    ?standoffLinkTag knora-api:standoffTagHasStartParent ?standoffItalicTag .
    ?standoffItalicTag a standoff:StandoffItalicTag .
    ?letter knora-api:hasStandoffLinkTo ?person .
}

Matching Standoff Dates

You can use the knora-api:toSimpleDate function (see Date Comparisons) to match dates in standoff date tags, i.e. instances of knora-api:StandoffDateTag or of one of its subclasses. For example, here we are looking for a text containing an anything:StandoffEventTag (which is a project-specific subclass of knora-api:StandoffDateTag) representing an event that occurred sometime during the month of December 2016:

PREFIX knora-api: <http://api.knora.org/ontology/knora-api/v2#>
PREFIX anything: <http://0.0.0.0:3333/ontology/0001/anything/v2#>
PREFIX knora-api-simple: <http://api.knora.org/ontology/knora-api/simple/v2#>

CONSTRUCT {
    ?thing knora-api:isMainResource true .
    ?thing anything:hasText ?text .
} WHERE {
    ?thing a anything:Thing .
    ?thing anything:hasText ?text .
    ?text knora-api:textValueHasStandoff ?standoffEventTag .
    ?standoffEventTag a anything:StandoffEventTag .
    FILTER(knora-api:toSimpleDate(?standoffEventTag) = "GREGORIAN:2016-12 CE"^^knora-api-simple:Date)
}

Matching Ancestor Tags

Suppose we want to search for a standoff date in a paragraph, but we know that the paragraph tag might not be the immediate parent of the date tag. For example, the date tag might be in an italics tag, which is in a paragraph tag. In that case, we can use the inferred property knora-api:standoffTagHasStartAncestor. We can modify the previous example to do this:

PREFIX knora-api: <http://api.knora.org/ontology/knora-api/v2#>
PREFIX standoff: <http://api.knora.org/ontology/standoff/v2#>
PREFIX anything: <http://0.0.0.0:3333/ontology/0001/anything/v2#>
PREFIX knora-api-simple: <http://api.knora.org/ontology/knora-api/simple/v2#>

CONSTRUCT {
    ?thing knora-api:isMainResource true .
    ?thing anything:hasText ?text .
} WHERE {
    ?thing a anything:Thing .
    ?thing anything:hasText ?text .
    ?text knora-api:textValueHasStandoff ?standoffDateTag .
    ?standoffDateTag a knora-api:StandoffDateTag .
    FILTER(knora-api:toSimpleDate(?standoffDateTag) = "GREGORIAN:2016-12-24 CE"^^knora-api-simple:Date)
    ?standoffDateTag knora-api:standoffTagHasStartAncestor ?standoffParagraphTag .
    ?standoffParagraphTag a standoff:StandoffParagraphTag .
}

CONSTRUCT Clause

In the CONSTRUCT clause of a Gravsearch query, the variable representing the main resource must be indicated with knora-api:isMainResource true. Exactly one variable representing a resource must be marked in this way.

Any other statements in the CONSTRUCT clause must also be present in the WHERE clause. If a variable representing a resource or value is used in the WHERE clause but not in the CONSTRUCT clause, the matching resources or values will not be included in the results.

If the query is written in the complex schema, all variables in the CONSTRUCT clause must refer to Knora resources, Knora values, or properties. Data below the level of Knora values may not be mentioned in the CONSTRUCT clause.

Predicates from the rdf, rdfs, and owl ontologies may not be used in the CONSTRUCT clause. The rdfs:label of each matching resource is always returned, so there is no need to mention it in the query.

Gravsearch by Example

In this section, we provide some sample queries of different complexity to illustrate the usage of Gravsearch.

Getting All the Components of a Compound Resource

In order to get all the components of a compound resource, the following Gravsearch query can be sent to the API.

In this case, the compound resource is an incunabula:book identified by the IRI http://rdfh.ch/c5058f3a and the components are of type incunabula:page (test data for the Incunabula project). Since inference is assumed, we can use knora-api:StillImageRepresentation (incunabula:page is one of its subclasses). This makes the query more generic and allows for reuse (for instance, a client would like to query different types of compound resources defined in different ontologies).

ORDER BY is used to sort the components by their sequence number.

OFFSET is set to 0 to get the first page of results.

PREFIX knora-api: <http://api.knora.org/ontology/knora-api/simple/v2#>

CONSTRUCT {
   ?component knora-api:isMainResource true . # marking of the component searched for as the main resource, required
   ?component knora-api:seqnum ?seqnum . # return the sequence number in the response
   ?component knora-api:hasStillImageFileValue ?file . # return the StillImageFile in the response
} WHERE {
   ?component a knora-api:StillImageRepresentation . # restriction of the type of component
   ?component knora-api:isPartOf <http://rdfh.ch/c5058f3a> . # component relates to a compound resource via this property
   ?component knora-api:seqnum ?seqnum . # component must have a sequence number
   ?component knora-api:hasStillImageFileValue ?file . # component must have a StillImageFile
}
ORDER BY ASC(?seqnum) # order by sequence number, ascending
OFFSET 0 # get first page of results

The incunabula:book with the IRI http://rdfh.ch/c5058f3a has 402 pages. (This result can be obtained by doing a count query; see Submitting Gravsearch Queries.) However, with OFFSET 0, only the first page of results is returned. The same query can be sent again with OFFSET 1 to get the next page of results, and so forth. When a page of results is not full (see settings in app/v2 in application.conf) or is empty, no more results are available.

By design, it is not possible for the client to get more than one page of results at a time; this is intended to prevent performance problems that would be caused by huge responses. A client that wants to download all the results of a query must request each page sequentially.

Let’s assume the client is not interested in all of the book’s pages, but just in first ten of them. In that case, the sequence number can be restricted using a FILTER that is added to the query’s WHERE clause:

FILTER (?seqnum <= 10)

The first page starts with sequence number 1, so with this FILTER only the first ten pages are returned.

This query would be exactly the same in the complex schema, except for the expansion of the knora-api prefix:

PREFIX knora-api: <http://api.knora.org/ontology/knora-api/v2#>

Traversing Multiple Links

Here we are looking for regions of pages that are part of books that have a particular title. In the simple schema:

PREFIX incunabula: <http://0.0.0.0:3333/ontology/0803/incunabula/simple/v2#>
PREFIX knora-api: <http://api.knora.org/ontology/knora-api/simple/v2#>

CONSTRUCT {
  ?region knora-api:isMainResource true ;
    knora-api:isRegionOf ?page .

  ?page incunabula:partOf ?book .

  ?book incunabula:title ?title .
} WHERE {
  ?region a knora-api:Region ;
    knora-api:isRegionOf ?page .

  ?page a incunabula:page ;
    incunabula:partOf ?book .

  ?book incunabula:title ?title .

  FILTER(?title = "Zeitglöcklein des Lebens und Leidens Christi")
}

In the complex schema:

PREFIX incunabula: <http://0.0.0.0:3333/ontology/0803/incunabula/v2#>
PREFIX knora-api: <http://api.knora.org/ontology/knora-api/v2#>

CONSTRUCT {
  ?region knora-api:isMainResource true ;
    knora-api:isRegionOf ?page .

  ?page incunabula:partOf ?book .

  ?book incunabula:title ?title .
} WHERE {
  ?region a knora-api:Region ;
    knora-api:isRegionOf ?page .

  ?page a incunabula:page ;
    incunabula:partOf ?book .

  ?book incunabula:title ?title .

  ?title knora-api:valueAsString "Zeitglöcklein des Lebens und Leidens Christi" .
}

If we remove the line ?book incunabula:title ?title . from the CONSTRUCT clause, so that the CONSTRUCT clause no longer mentions ?title, the response will contain the same matching resources, but the titles of those resources will not be included in the response.

Requesting a Graph Starting with a Known Resource

Here the IRI of the main resource is already known, and we want specific information about it, as well as about related resources. In this case, the IRI of the main resource must be assigned to a variable using BIND:

PREFIX beol: <http://0.0.0.0:3333/ontology/0801/beol/simple/v2#>
PREFIX knora-api: <http://api.knora.org/ontology/knora-api/simple/v2#>

CONSTRUCT {
  ?letter knora-api:isMainResource true ;
    beol:creationDate ?date ;
    ?linkingProp1 ?person1 .

  ?person1 beol:hasFamilyName ?familyName .
} WHERE {
  BIND(<http://rdfh.ch/0801/_B3lQa6tSymIq7_7SowBsA> AS ?letter)

  ?letter a beol:letter ;
    beol:creationDate ?date ;
    ?linkingProp1 ?person1 .

  FILTER(?linkingProp1 = beol:hasAuthor || ?linkingProp1 = beol:hasRecipient)

  ?person1 beol:hasFamilyName ?familyName .
} ORDER BY ?date

This query would be the same in the complex schema, except for the prefix expansions:

PREFIX beol: <http://0.0.0.0:3333/ontology/0801/beol/v2#>
PREFIX knora-api: <http://api.knora.org/ontology/knora-api/v2#>

Searching for a List Value Referring to a Particular List Node

This is possible only in the complex schema:

PREFIX knora-api: <http://api.knora.org/ontology/knora-api/v2#>
PREFIX anything: <http://0.0.0.0:3333/ontology/0001/anything/v2#>

CONSTRUCT {
    ?thing knora-api:isMainResource true .
    ?thing anything:hasListItem ?listItem .
} WHERE {
    ?thing anything:hasListItem ?listItem .
    ?listItem knora-api:listValueAsListNode <http://rdfh.ch/lists/0001/treeList02> .
}

Type Inference

Gravsearch needs to be able to determine the types of the entities that query variables and IRIs refer to in the WHERE clause. In most cases, it can infer these from context and from the ontologies used. In particular, it needs to know:

  • The type of the subject and object of each statement.
  • The type that is expected as the object of each predicate.

Type Annotations

When one or more types cannot be inferred, Gravsearch will return an error message indicating the entities for which it could not determine types. The missing information must then be given by adding type annotations to the query. This can always done by adding statements with the predicate rdf:type. The subject must be a resource or value, and the object must either be knora-api:Resource (if the subject is a resource) or the subject’s specific type (if it is a value).

For example, consider this query that uses a non-Knora property:

PREFIX incunabula: <http://0.0.0.0:3333/ontology/0803/incunabula/simple/v2#>
PREFIX knora-api: <http://api.knora.org/ontology/knora-api/simple/v2#>
PREFIX dcterms: <http://purl.org/dc/terms/>

CONSTRUCT {
    ?book knora-api:isMainResource true ;
        dcterms:title ?title .

} WHERE {
    ?book dcterms:title ?title .
}

This produces the error message:

The types of one or more entities could not be determined:
  ?book, <http://purl.org/dc/terms/title>, ?title

To solve this problem, it is enough to specify the types of ?book and ?title; the type of the expected object of dcterms:title can then be inferred from the type of ?title.

PREFIX incunabula: <http://0.0.0.0:3333/ontology/0803/incunabula/simple/v2#>
PREFIX knora-api: <http://api.knora.org/ontology/knora-api/simple/v2#>
PREFIX dcterms: <http://purl.org/dc/terms/>

CONSTRUCT {
    ?book knora-api:isMainResource true ;
        dcterms:title ?title .

} WHERE {

    ?book rdf:type incunabula:book ;
        dcterms:title ?title .

    ?title rdf:type xsd:string .

}

It would also be possible to annotate the property itself, using the predicate knora-api:objectType; then the type of ?title would be inferred:

PREFIX incunabula: <http://0.0.0.0:3333/ontology/0803/incunabula/simple/v2#>
PREFIX knora-api: <http://api.knora.org/ontology/knora-api/simple/v2#>
PREFIX dcterms: <http://purl.org/dc/terms/>

CONSTRUCT {
    ?book knora-api:isMainResource true ;
        dcterms:title ?title .

} WHERE {

    ?book rdf:type incunabula:book ;
        dcterms:title ?title .

    dcterms:title knora-api:objectType xsd:string .

}

Note that it only makes sense to use dcterms:title in the simple schema, because its object is supposed to be a literal.

Here is another example, using a non-Knora class:

PREFIX knora-api: <http://api.knora.org/ontology/knora-api/simple/v2#>
PREFIX foaf: <http://xmlns.com/foaf/0.1/>

CONSTRUCT {
    ?person knora-api:isMainResource true .
} WHERE {
    ?person a foaf:Person .
    ?person foaf:familyName ?familyName .
    FILTER(?familyName = "Meier")
}

This produces the error message:

Types could not be determined for one or more entities: ?person

The solution is to specify that ?person is a knora-api:Resource:

PREFIX knora-api: <http://api.knora.org/ontology/knora-api/simple/v2#>
PREFIX foaf: <http://xmlns.com/foaf/0.1/>

CONSTRUCT {
    ?person knora-api:isMainResource true .
} WHERE {
    ?person a foaf:Person .
    ?person a knora-api:Resource .
    ?person foaf:familyName ?familyName .
    FILTER(?familyName = "Meier")
}

Inconsistent Types

Gravsearch will also reject a query if an entity is used with inconsistent types. For example:

PREFIX incunabula: <http://0.0.0.0:3333/ontology/0803/incunabula/simple/v2#>
PREFIX knora-api: <http://api.knora.org/ontology/knora-api/simple/v2#>

CONSTRUCT {
    ?book knora-api:isMainResource true ;
        incunabula:pubdate ?pubdate .
} WHERE {
    ?book a incunabula:book ;
        incunabula:pubdate ?pubdate .

  FILTER(?pubdate = "JULIAN:1497-03-01") .
}

This returns the error message:

One or more entities have inconsistent types:

<http://0.0.0.0:3333/ontology/0803/incunabula/simple/v2#pubdate>
  knora-api:objectType <http://api.knora.org/ontology/knora-api/simple/v2#Date> ;
  knora-api:objectType <http://www.w3.org/2001/XMLSchema#string> .

?pubdate rdf:type <http://api.knora.org/ontology/knora-api/simple/v2#Date> ;
  rdf:type <http://www.w3.org/2001/XMLSchema#string> .

This is because the incunabula ontology says that the object of incunabula:pubdate must be a knora-api:Date, but the FILTER expression compares ?pubdate with an xsd:string. The solution is to specify the type of the literal in the FILTER:

PREFIX incunabula: <http://0.0.0.0:3333/ontology/0803/incunabula/simple/v2#>
PREFIX knora-api: <http://api.knora.org/ontology/knora-api/simple/v2#>

CONSTRUCT {
    ?book knora-api:isMainResource true ;
        incunabula:pubdate ?pubdate .
} WHERE {
    ?book a incunabula:book ;
        incunabula:pubdate ?pubdate .

  FILTER(?pubdate = "JULIAN:1497-03-01"^^knora-api:Date) .
}