Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(api-v2): Make inference optional in Gravsearch #1696

Merged
merged 10 commits into from Sep 1, 2020
10 changes: 10 additions & 0 deletions docs/03-apis/api-v2/query-language.md
Expand Up @@ -212,6 +212,16 @@ 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.

If you know that reasoning will not return any additional results for
your query, you can disable it by adding this line to the `WHERE` clause:

```sparql
knora-api:GravsearchOptions knora-api:useInference false .
```

If Knora is implementing reasoning by query expansion, disabling it can
improve the performance of some queries.

## Gravsearch Syntax

Every Gravsearch query is a valid SPARQL 1.1
Expand Down
8 changes: 3 additions & 5 deletions docs/05-internals/design/api-v2/gravsearch.md
Expand Up @@ -315,17 +315,15 @@ When the triplestore-specific version of the query is generated:
for text searches.

- If Knora is not using the triplestore's inference (e.g. with Fuseki),
`SparqlTransformer.expandStatementForNoInference` removes `<http://www.knora.org/explicit>`, and expands unmarked
`SparqlTransformer.transformStatementInWhereForNoInference` removes `<http://www.knora.org/explicit>`, and expands unmarked
statements using `rdfs:subClassOf*` and `rdfs:subPropertyOf*`.

Gravsearch also provides some virtual properties, which take advantage of forward-chaining inference
as an optimisation if the triplestore provides it. For example, the virtual property
`knora-api:standoffTagHasStartAncestor` is equivalent to `knora-base:standoffTagHasStartParent*`, but
with GraphDB it is implemented using a custom inference rule (in `KnoraRules.pie`) and is therefore more
efficient. If Knora is not using the triplestore's inference,

`SparqlTransformer.transformStatementInWhereForNoInference` replaces `knora-api:standoffTagHasStartAncestor`
with `knora-base:standoffTagHasStartParent*`.
efficient. If Knora is not using the triplestore's inference, `SparqlTransformer.transformStatementInWhereForNoInference`
replaces `knora-api:standoffTagHasStartAncestor` with `knora-base:standoffTagHasStartParent*`.

# Optimisation of generated SPARQL

Expand Down
2 changes: 1 addition & 1 deletion webapi/scripts/fuseki-dump-repository.sh
Expand Up @@ -50,7 +50,7 @@ if [[ -z "${PASSWORD}" ]]; then
fi

if [[ -z "${HOST}" ]]; then
HOST="localhost:8080"
HOST="localhost:3030"
fi

curl -sS -X GET -H "Accept: application/trig" -u "${USERNAME}:${PASSWORD}" "http://${HOST}/${REPOSITORY}" > "${FILE}"
2 changes: 1 addition & 1 deletion webapi/scripts/fuseki-upload-repository.sh
Expand Up @@ -50,7 +50,7 @@ if [[ -z "${PASSWORD}" ]]; then
fi

if [[ -z "${HOST}" ]]; then
HOST="localhost:8080"
HOST="localhost:3030"
fi

curl -sS -X POST -H "Content-Type: application/trig" --data-binary "@${FILE}" -u "${USERNAME}:${PASSWORD}" "http://${HOST}/${REPOSITORY}" | tee /dev/null
Expand Up @@ -655,13 +655,28 @@ object OntologyConstants {
val KnoraApiPrefix: String = KnoraApiOntologyLabel + ":"

/**
* Returns `true` if the specified IRI is `knora-api:Resource` in Knora API v2, in the simple
* or complex schema.
* The IRIs representing `knora-api:Resource` in Knora API v2, in the simple and complex schemas.
*/
def isKnoraApiV2Resource(iri: SmartIri): Boolean = {
val iriStr = iri.toString
iriStr == OntologyConstants.KnoraApiV2Simple.Resource || iriStr == OntologyConstants.KnoraApiV2Complex.Resource
}
lazy val KnoraApiV2ResourceIris: Set[IRI] = Set(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@benjamingeer question: why are using lazy val here in the following three cases? The initialization does not need any evaluation of an expression, it is just a set. In this case, it would not make any difference if the initialization happens immediately or is postponed to the first time the value is accessed.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great question! The problem is that KnoraApiV2ResourceIris is a static value (it's defined in an object rather than in a class), which refers to static values in another object. The order of initialisation of static values in the JVM depends on which object is loaded first by the JVM's class loader at run time. Without the lazy, if object KnoraApi is initialised before object KnoraApiV2Simple and object KnoraApiV2Complex, KnoraApiV2ResourceIris will just contain null values. It took me a little while to figure this out yesterday. A test was failing, and it turned out that it was because KnoraApiV2ResourceIris did not contain OntologyConstants.KnoraApiV2Complex.Resource, because it hadn't been initialised properly.

In the earlier code, there was a method, which didn't access those constants until it was called, so it was fine.

OntologyConstants.KnoraApiV2Simple.Resource,
OntologyConstants.KnoraApiV2Complex.Resource
)

/**
* The IRIs representing `knora-api:GravsearchOptions` in Knora API v2, in the simple and complex schemas.
*/
lazy val GravsearchOptionsIris: Set[IRI] = Set(
OntologyConstants.KnoraApiV2Simple.GravsearchOptions,
OntologyConstants.KnoraApiV2Complex.GravsearchOptions
)

/**
* The IRIs representing `knora-api:useInference` in Knora API v2, in the simple and complex schemas.
*/
lazy val UseInferenceIris: Set[IRI] = Set(
OntologyConstants.KnoraApiV2Simple.UseInference,
OntologyConstants.KnoraApiV2Complex.UseInference
)

/**
* Returns the IRI of `knora-api:subjectType` in the specified schema.
Expand Down Expand Up @@ -925,6 +940,9 @@ object OntologyConstants {
val MatchTextInStandoffFunction: IRI = KnoraApiV2PrefixExpansion + "matchTextInStandoff"
val MatchLabelFunction: IRI = KnoraApiV2PrefixExpansion + "matchLabel"
val StandoffLinkFunction: IRI = KnoraApiV2PrefixExpansion + "standoffLink"

val GravsearchOptions: IRI = KnoraApiV2PrefixExpansion + "GravsearchOptions"
val UseInference: IRI = KnoraApiV2PrefixExpansion + "useInference"
}

object SalsahGuiApiV2WithValueObjects {
Expand Down Expand Up @@ -1026,6 +1044,9 @@ object OntologyConstants {

val ArkUrl: IRI = KnoraApiV2PrefixExpansion + "arkUrl"
val VersionArkUrl: IRI = KnoraApiV2PrefixExpansion + "versionArkUrl"

val GravsearchOptions: IRI = KnoraApiV2PrefixExpansion + "GravsearchOptions"
val UseInference: IRI = KnoraApiV2PrefixExpansion + "useInference"
}

/**
Expand Down
Expand Up @@ -95,6 +95,13 @@ trait SelectToSelectTransformer extends WhereTransformer {
* @return the result of the transformation.
*/
def transformStatementInSelect(statementPattern: StatementPattern): Seq[StatementPattern]

/**
* Specifies a FROM clause, if needed.
*
* @return the FROM clause to be used, if any.
*/
def getFromClause: Option[FromClause]
}

/**
Expand Down Expand Up @@ -189,7 +196,7 @@ object QueryTraverser {
// remove statements that would otherwise be expanded by transformStatementInWhere
val optimisedPatterns = whereTransformer.optimiseQueryPatterns(patterns)

optimisedPatterns.flatMap {
optimisedPatterns.flatMap {
case statementPattern: StatementPattern =>
whereTransformer.transformStatementInWhere(
statementPattern = statementPattern,
Expand Down Expand Up @@ -338,6 +345,7 @@ object QueryTraverser {

def transformSelectToSelect(inputQuery: SelectQuery, transformer: SelectToSelectTransformer): SelectQuery = {
inputQuery.copy(
fromClause = transformer.getFromClause,
whereClause = WhereClause(
patterns = transformWherePatterns(
patterns = inputQuery.whereClause.patterns,
Expand Down
Expand Up @@ -573,19 +573,32 @@ case class OrderCriterion(queryVariable: QueryVariable, isAscending: Boolean) ex
}
}

/**
* Represents a FROM clause.
*
* @param defaultGraph the graph to be used as the default graph in the query.
*/
case class FromClause(defaultGraph: IriRef) extends SparqlGenerator {
override def toSparql: String = s"FROM ${defaultGraph.toSparql}\n"
}

/**
* Represents a SPARQL CONSTRUCT query.
*
* @param constructClause the CONSTRUCT clause.
* @param fromClause the FROM clause, if any.
* @param whereClause the WHERE clause.
* @param orderBy the variables that the results should be ordered by.
* @param offset if this is a Gravsearch query, represents the OFFSET specified in the query.
* @param querySchema if this is a Gravsearch query, represents the Knora API v2 ontology schema used in the query.
*/
case class ConstructQuery(constructClause: ConstructClause, whereClause: WhereClause, orderBy: Seq[OrderCriterion] = Seq.empty[OrderCriterion], offset: Long = 0, querySchema: Option[ApiV2Schema] = None) extends SparqlGenerator {
case class ConstructQuery(constructClause: ConstructClause, fromClause: Option[FromClause] = None, whereClause: WhereClause, orderBy: Seq[OrderCriterion] = Seq.empty[OrderCriterion], offset: Long = 0, querySchema: Option[ApiV2Schema] = None) extends SparqlGenerator {
override def toSparql: String = {
val stringBuilder = new StringBuilder
stringBuilder.append(constructClause.toSparql).append(whereClause.toSparql)

stringBuilder.append(constructClause.toSparql)
.append(fromClause.map(_.toSparql).getOrElse(""))
.append(whereClause.toSparql)

if (orderBy.nonEmpty) {
stringBuilder.append("ORDER BY ").append(orderBy.map(_.toSparql).mkString(" ")).append("\n")
Expand All @@ -604,12 +617,13 @@ case class ConstructQuery(constructClause: ConstructClause, whereClause: WhereCl
*
* @param variables the variables to be returned by the query.
* @param useDistinct indicates if DISTINCT should be used.
* @param fromClause the FROM clause, if any.
* @param whereClause the WHERE clause.
* @param orderBy the variables that the results should be ordered by.
* @param limit the maximum number of result rows to be returned.
* @param offset the offset to be used (limit of the previous query + 1 to do paging).
*/
case class SelectQuery(variables: Seq[SelectQueryColumn], useDistinct: Boolean = true, whereClause: WhereClause, groupBy: Seq[QueryVariable] = Seq.empty[QueryVariable], orderBy: Seq[OrderCriterion] = Seq.empty[OrderCriterion], limit: Option[Int] = None, offset: Long = 0) extends SparqlGenerator {
case class SelectQuery(variables: Seq[SelectQueryColumn], useDistinct: Boolean = true, fromClause: Option[FromClause] = None, whereClause: WhereClause, groupBy: Seq[QueryVariable] = Seq.empty[QueryVariable], orderBy: Seq[OrderCriterion] = Seq.empty[OrderCriterion], limit: Option[Int] = None, offset: Long = 0) extends SparqlGenerator {
override def toSparql: String = {
val stringBuilder = new StringBuilder

Expand All @@ -620,7 +634,9 @@ case class SelectQuery(variables: Seq[SelectQueryColumn], useDistinct: Boolean =

}

stringBuilder.append(variables.map(_.toSparql).mkString(" ")).append("\n").append(whereClause.toSparql)
stringBuilder.append(variables.map(_.toSparql).mkString(" ")).append("\n")
.append(fromClause.map(_.toSparql).getOrElse(""))
.append(whereClause.toSparql)

if (groupBy.nonEmpty) {
stringBuilder.append("GROUP BY ").append(groupBy.map(_.toSparql).mkString(" ")).append("\n")
Expand Down