From 166a26061dd434ca1d95fedf6dec75728760c78c Mon Sep 17 00:00:00 2001 From: Benjamin Geer Date: Tue, 1 Sep 2020 15:20:51 +0200 Subject: [PATCH] feat(api-v2): Make inference optional in Gravsearch (#1696) --- docs/03-apis/api-v2/query-language.md | 10 + docs/05-internals/design/api-v2/gravsearch.md | 8 +- webapi/scripts/fuseki-dump-repository.sh | 2 +- webapi/scripts/fuseki-upload-repository.sh | 2 +- .../webapi/messages/OntologyConstants.scala | 33 ++- .../messages/util/search/QueryTraverser.scala | 10 +- .../messages/util/search/SparqlQuery.scala | 24 +- .../util/search/SparqlTransformer.scala | 193 ++++++++------ .../prequery/AbstractPrequeryGenerator.scala | 80 ++++-- ...GravsearchToCountPrequeryTransformer.scala | 4 +- ...tationReadingGravsearchTypeInspector.scala | 5 +- .../types/GravsearchTypeInspectionUtil.scala | 14 +- .../InferringGravsearchTypeInspector.scala | 7 +- .../responders/v2/SearchResponderV2.scala | 40 ++- .../webapi/routing/v2/OntologiesRouteV2.scala | 2 +- .../webapi/e2e/v2/SearchRouteV2R2RSpec.scala | 23 ++ .../types/GravsearchTypeInspectorSpec.scala | 251 ++++++++++-------- 17 files changed, 452 insertions(+), 256 deletions(-) diff --git a/docs/03-apis/api-v2/query-language.md b/docs/03-apis/api-v2/query-language.md index 958a53b31e..304f330b5d 100644 --- a/docs/03-apis/api-v2/query-language.md +++ b/docs/03-apis/api-v2/query-language.md @@ -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 diff --git a/docs/05-internals/design/api-v2/gravsearch.md b/docs/05-internals/design/api-v2/gravsearch.md index 92c75ab93d..b26c073e5f 100644 --- a/docs/05-internals/design/api-v2/gravsearch.md +++ b/docs/05-internals/design/api-v2/gravsearch.md @@ -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 ``, and expands unmarked + `SparqlTransformer.transformStatementInWhereForNoInference` removes ``, 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 diff --git a/webapi/scripts/fuseki-dump-repository.sh b/webapi/scripts/fuseki-dump-repository.sh index 5adc702003..1a7260d7c8 100755 --- a/webapi/scripts/fuseki-dump-repository.sh +++ b/webapi/scripts/fuseki-dump-repository.sh @@ -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}" diff --git a/webapi/scripts/fuseki-upload-repository.sh b/webapi/scripts/fuseki-upload-repository.sh index 927a15c464..51401f7295 100755 --- a/webapi/scripts/fuseki-upload-repository.sh +++ b/webapi/scripts/fuseki-upload-repository.sh @@ -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 diff --git a/webapi/src/main/scala/org/knora/webapi/messages/OntologyConstants.scala b/webapi/src/main/scala/org/knora/webapi/messages/OntologyConstants.scala index f416d8eb30..3cd8ef4942 100644 --- a/webapi/src/main/scala/org/knora/webapi/messages/OntologyConstants.scala +++ b/webapi/src/main/scala/org/knora/webapi/messages/OntologyConstants.scala @@ -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( + 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. @@ -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 { @@ -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" } /** diff --git a/webapi/src/main/scala/org/knora/webapi/messages/util/search/QueryTraverser.scala b/webapi/src/main/scala/org/knora/webapi/messages/util/search/QueryTraverser.scala index d26da6ae96..b4fc12481e 100644 --- a/webapi/src/main/scala/org/knora/webapi/messages/util/search/QueryTraverser.scala +++ b/webapi/src/main/scala/org/knora/webapi/messages/util/search/QueryTraverser.scala @@ -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] } /** @@ -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, @@ -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, diff --git a/webapi/src/main/scala/org/knora/webapi/messages/util/search/SparqlQuery.scala b/webapi/src/main/scala/org/knora/webapi/messages/util/search/SparqlQuery.scala index 021066758d..bd42bee902 100644 --- a/webapi/src/main/scala/org/knora/webapi/messages/util/search/SparqlQuery.scala +++ b/webapi/src/main/scala/org/knora/webapi/messages/util/search/SparqlQuery.scala @@ -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") @@ -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 @@ -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") diff --git a/webapi/src/main/scala/org/knora/webapi/messages/util/search/SparqlTransformer.scala b/webapi/src/main/scala/org/knora/webapi/messages/util/search/SparqlTransformer.scala index ab8ca9d13d..4506c85a77 100644 --- a/webapi/src/main/scala/org/knora/webapi/messages/util/search/SparqlTransformer.scala +++ b/webapi/src/main/scala/org/knora/webapi/messages/util/search/SparqlTransformer.scala @@ -31,12 +31,16 @@ object SparqlTransformer { /** * Transforms a non-triplestore-specific SELECT query for GraphDB. + * + * @param useInference `true` if the triplestore's inference should be used. */ - class GraphDBSelectToSelectTransformer extends SelectToSelectTransformer { + class GraphDBSelectToSelectTransformer(useInference: Boolean) extends SelectToSelectTransformer { + private implicit val stringFormatter: StringFormatter = StringFormatter.getGeneralInstance + override def transformStatementInSelect(statementPattern: StatementPattern): Seq[StatementPattern] = Seq(statementPattern) override def transformStatementInWhere(statementPattern: StatementPattern, inputOrderBy: Seq[OrderCriterion]): Seq[StatementPattern] = { - transformKnoraExplicitToGraphDBExplicit(statementPattern) + transformKnoraExplicitToGraphDBExplicit(statement = statementPattern, useInference = useInference) } override def transformFilter(filterPattern: FilterPattern): Seq[QueryPattern] = Seq(filterPattern) @@ -45,18 +49,29 @@ object SparqlTransformer { override def transformLuceneQueryPattern(luceneQueryPattern: LuceneQueryPattern): Seq[QueryPattern] = transformLuceneQueryPatternForGraphDB(luceneQueryPattern) + + override def getFromClause: Option[FromClause] = { + if (useInference) { + None + } else { + // To turn off inference, add FROM . + Some(FromClause(IriRef(OntologyConstants.NamedGraphs.GraphDBExplicitNamedGraph.toSmartIri))) + } + } } /** * Transforms a non-triplestore-specific SELECT for a triplestore that does not have inference enabled (e.g., Fuseki). + * + * @param simulateInference `true` if RDFS inference should be simulated using property path syntax. */ - class NoInferenceSelectToSelectTransformer extends SelectToSelectTransformer { + class NoInferenceSelectToSelectTransformer(simulateInference: Boolean) extends SelectToSelectTransformer { private implicit val stringFormatter: StringFormatter = StringFormatter.getGeneralInstance override def transformStatementInSelect(statementPattern: StatementPattern): Seq[StatementPattern] = Seq(statementPattern) override def transformStatementInWhere(statementPattern: StatementPattern, inputOrderBy: Seq[OrderCriterion]): Seq[StatementPattern] = - transformStatementInWhereForNoInference(statementPattern) + transformStatementInWhereForNoInference(statementPattern = statementPattern, simulateInference = simulateInference) override def transformFilter(filterPattern: FilterPattern): Seq[QueryPattern] = Seq(filterPattern) @@ -66,16 +81,20 @@ object SparqlTransformer { override def transformLuceneQueryPattern(luceneQueryPattern: LuceneQueryPattern): Seq[QueryPattern] = transformLuceneQueryPatternForFuseki(luceneQueryPattern) + + override def getFromClause: Option[FromClause] = None } /** * Transforms a non-triplestore-specific CONSTRUCT query for GraphDB. */ class GraphDBConstructToConstructTransformer extends ConstructToConstructTransformer { + private implicit val stringFormatter: StringFormatter = StringFormatter.getGeneralInstance + override def transformStatementInConstruct(statementPattern: StatementPattern): Seq[StatementPattern] = Seq(statementPattern) override def transformStatementInWhere(statementPattern: StatementPattern, inputOrderBy: Seq[OrderCriterion]): Seq[StatementPattern] = { - transformKnoraExplicitToGraphDBExplicit(statementPattern) + transformKnoraExplicitToGraphDBExplicit(statement = statementPattern, useInference = true) } override def transformFilter(filterPattern: FilterPattern): Seq[QueryPattern] = Seq(filterPattern) @@ -95,7 +114,7 @@ object SparqlTransformer { override def transformStatementInConstruct(statementPattern: StatementPattern): Seq[StatementPattern] = Seq(statementPattern) override def transformStatementInWhere(statementPattern: StatementPattern, inputOrderBy: Seq[OrderCriterion]): Seq[StatementPattern] = - transformStatementInWhereForNoInference(statementPattern) + transformStatementInWhereForNoInference(statementPattern = statementPattern, simulateInference = true) override def transformFilter(filterPattern: FilterPattern): Seq[QueryPattern] = Seq(filterPattern) @@ -212,96 +231,94 @@ object SparqlTransformer { /** * Transforms a statement in a WHERE clause for a triplestore that does not provide inference. * - * @param statementPattern the statement pattern. + * @param statementPattern the statement pattern. + * @param simulateInference `true` if RDFS inference should be simulated using property path syntax. * @return the statement pattern as expanded to work without inference. */ - private def transformStatementInWhereForNoInference(statementPattern: StatementPattern): Seq[StatementPattern] = { + private def transformStatementInWhereForNoInference(statementPattern: StatementPattern, simulateInference: Boolean): Seq[StatementPattern] = { implicit val stringFormatter: StringFormatter = StringFormatter.getGeneralInstance statementPattern.pred match { case iriRef: IriRef if iriRef.iri.toString == OntologyConstants.KnoraBase.StandoffTagHasStartAncestor => + // Simulate knora-api:standoffTagHasStartAncestor, using knora-api:standoffTagHasStartParent. Seq( statementPattern.copy( pred = IriRef(OntologyConstants.KnoraBase.StandoffTagHasStartParent.toSmartIri, Some('*')) ) ) - case _ => expandStatementForNoInference(statementPattern) - } - } - - /** - * If inference is not being used, expands non-explicit statements to simulate inference using `rdfs:subClassOf*` - * and `rdfs:subPropertyOf*`. - * - * @param statementPattern the statement to be expanded. - * @return the result of the expansion. - */ - private def expandStatementForNoInference(statementPattern: StatementPattern)(implicit stringFormatter: StringFormatter): Seq[StatementPattern] = { - // Is the statement in KnoraExplicitNamedGraph? - statementPattern.namedGraph match { - case Some(graphIri: IriRef) if graphIri.iri.toString == OntologyConstants.NamedGraphs.KnoraExplicitNamedGraph => - // Yes. No expansion needed. Just remove KnoraExplicitNamedGraph. - Seq(statementPattern.copy(namedGraph = None)) - case _ => - // The statement isn't in KnoraExplicitNamedGraph, so it might need to be expanded. Is the predicate a property IRI? - statementPattern.pred match { - case iriRef: IriRef => - // Yes. - val propertyIri = iriRef.iri.toString - - // Is the property rdf:type? - if (propertyIri == OntologyConstants.Rdf.Type) { - // Yes. Expand using rdfs:subClassOf*. + // Is the statement in KnoraExplicitNamedGraph? + statementPattern.namedGraph match { + case Some(graphIri: IriRef) if graphIri.iri.toString == OntologyConstants.NamedGraphs.KnoraExplicitNamedGraph => + // Yes. No expansion needed. Just remove KnoraExplicitNamedGraph. + Seq(statementPattern.copy(namedGraph = None)) - val baseClassIri: IriRef = statementPattern.obj match { - case iriRef: IriRef => iriRef - case other => throw GravsearchException(s"The object of rdf:type must be an IRI, but $other was used") + case _ => + // Is inference enabled? + if (simulateInference) { + // Yes. The statement might need to be expanded. Is the predicate a property IRI? + statementPattern.pred match { + case iriRef: IriRef => + // Yes. + val propertyIri = iriRef.iri.toString + + // Is the property rdf:type? + if (propertyIri == OntologyConstants.Rdf.Type) { + // Yes. Expand using rdfs:subClassOf*. + + val baseClassIri: IriRef = statementPattern.obj match { + case iriRef: IriRef => iriRef + case other => throw GravsearchException(s"The object of rdf:type must be an IRI, but $other was used") + } + + val rdfTypeVariable: QueryVariable = createUniqueVariableNameForEntityAndBaseClass(base = statementPattern.subj, baseClassIri = baseClassIri) + + Seq( + StatementPattern( + subj = rdfTypeVariable, + pred = IriRef( + iri = OntologyConstants.Rdfs.SubClassOf.toSmartIri, + propertyPathOperator = Some('*') + ), + obj = statementPattern.obj + ), + StatementPattern( + subj = statementPattern.subj, + pred = statementPattern.pred, + obj = rdfTypeVariable + ) + ) + } else { + // No. Expand using rdfs:subPropertyOf*. + + val propertyVariable: QueryVariable = createUniqueVariableNameFromEntityAndProperty(base = statementPattern.pred, propertyIri = OntologyConstants.Rdfs.SubPropertyOf) + + Seq( + StatementPattern( + subj = propertyVariable, + pred = IriRef( + iri = OntologyConstants.Rdfs.SubPropertyOf.toSmartIri, + propertyPathOperator = Some('*') + ), + obj = statementPattern.pred + ), + StatementPattern( + subj = statementPattern.subj, + pred = propertyVariable, + obj = statementPattern.obj + ) + ) + } + + case _ => + // The predicate isn't a property IRI, so no expansion needed. + Seq(statementPattern) } - - val rdfTypeVariable: QueryVariable = createUniqueVariableNameForEntityAndBaseClass(base = statementPattern.subj, baseClassIri = baseClassIri) - - Seq( - StatementPattern( - subj = rdfTypeVariable, - pred = IriRef( - iri = OntologyConstants.Rdfs.SubClassOf.toSmartIri, - propertyPathOperator = Some('*') - ), - obj = statementPattern.obj - ), - StatementPattern( - subj = statementPattern.subj, - pred = statementPattern.pred, - obj = rdfTypeVariable - ) - ) } else { - // No. Expand using rdfs:subPropertyOf*. - - val propertyVariable: QueryVariable = createUniqueVariableNameFromEntityAndProperty(base = statementPattern.pred, propertyIri = OntologyConstants.Rdfs.SubPropertyOf) - - Seq( - StatementPattern( - subj = propertyVariable, - pred = IriRef( - iri = OntologyConstants.Rdfs.SubPropertyOf.toSmartIri, - propertyPathOperator = Some('*') - ), - obj = statementPattern.pred - ), - StatementPattern( - subj = statementPattern.subj, - pred = propertyVariable, - obj = statementPattern.obj - ) - ) + // Inference is disabled. Just return the statement as is. + Seq(statementPattern) } - - case _ => - // The predicate isn't a property IRI, so no expansion needed. - Seq(statementPattern) } } } @@ -309,18 +326,26 @@ object SparqlTransformer { /** * Transforms the the Knora explicit graph name to GraphDB explicit graph name. * - * @param statement the given statement whose graph name has to be renamed. + * @param statement the given statement whose graph name has to be renamed. + * @param useInference `true` if the triplestore's inference should be used. * @return the statement with the renamed graph, if given. */ - private def transformKnoraExplicitToGraphDBExplicit(statement: StatementPattern): Seq[StatementPattern] = { + private def transformKnoraExplicitToGraphDBExplicit(statement: StatementPattern, useInference: Boolean): Seq[StatementPattern] = { implicit val stringFormatter: StringFormatter = StringFormatter.getGeneralInstance val transformedPattern = statement.copy( - // Replace the deprecated property knora-base:matchesTextIndex with a GraphDB-specific one. pred = statement.pred, namedGraph = statement.namedGraph match { - case Some(IriRef(SmartIri(OntologyConstants.NamedGraphs.KnoraExplicitNamedGraph), _)) => Some(IriRef(OntologyConstants.NamedGraphs.GraphDBExplicitNamedGraph.toSmartIri)) + case Some(IriRef(SmartIri(OntologyConstants.NamedGraphs.KnoraExplicitNamedGraph), _)) => + if (useInference) { + Some(IriRef(OntologyConstants.NamedGraphs.GraphDBExplicitNamedGraph.toSmartIri)) + } else { + // Inference will be turned off in a FROM clause, so there's no need to specify a named graph here. + None + } + case Some(IriRef(_, _)) => throw AssertionException(s"Named graphs other than ${OntologyConstants.NamedGraphs.KnoraExplicitNamedGraph} cannot occur in non-triplestore-specific generated search query SPARQL") + case None => None } ) @@ -337,7 +362,7 @@ object SparqlTransformer { private def transformLuceneQueryPatternForGraphDB(luceneQueryPattern: LuceneQueryPattern): Seq[QueryPattern] = { implicit val stringFormatter: StringFormatter = StringFormatter.getGeneralInstance - Seq( + Seq( StatementPattern( subj = luceneQueryPattern.obj, // In GraphDB, an index entry is associated with a literal. pred = IriRef("http://www.ontotext.com/owlim/lucene#fullTextSearchIndex".toSmartIri), diff --git a/webapi/src/main/scala/org/knora/webapi/messages/util/search/gravsearch/prequery/AbstractPrequeryGenerator.scala b/webapi/src/main/scala/org/knora/webapi/messages/util/search/gravsearch/prequery/AbstractPrequeryGenerator.scala index d47ad2b185..36aaea6d43 100644 --- a/webapi/src/main/scala/org/knora/webapi/messages/util/search/gravsearch/prequery/AbstractPrequeryGenerator.scala +++ b/webapi/src/main/scala/org/knora/webapi/messages/util/search/gravsearch/prequery/AbstractPrequeryGenerator.scala @@ -72,6 +72,9 @@ abstract class AbstractPrequeryGenerator(constructClause: ConstructClause, // variables the represent resource metadata private val resourceMetadataVariables = mutable.Set.empty[QueryVariable] + // The query can set this to false to disable inference. + var useInference = true + /** * Saves a generated variable representing a value literal, if it hasn't been saved already. * @@ -417,36 +420,63 @@ abstract class AbstractPrequeryGenerator(constructClause: ConstructClause, } } - protected def processStatementPatternFromWhereClause(statementPattern: StatementPattern, inputOrderBy: Seq[OrderCriterion]): Seq[QueryPattern] = { + /** + * Processes Gravsearch options. + * + * @param statementPattern the statement specifying the option to be set. + */ + private def processGravsearchOption(statementPattern: StatementPattern): Unit = { + statementPattern.pred match { + case iriRef: IriRef if OntologyConstants.KnoraApi.UseInferenceIris.contains(iriRef.iri.toString) => + useInference = statementPattern.obj match { + case xsdLiteral: XsdLiteral if xsdLiteral.datatype.toString == OntologyConstants.Xsd.Boolean => + xsdLiteral.value.toBoolean + + case other => throw GravsearchException(s"Invalid object for knora-api:useInference: ${other.toSparql}") + } - // look at the statement's subject, predicate, and object and generate additional statements if needed based on the given type information. - // transform the originally given statement if necessary when processing the predicate + case other => throw GravsearchException(s"Invalid predicate for knora-api:GravsearchOptions: ${other.toSparql}") + } + } - // check if there exists type information for the given statement's subject - val additionalStatementsForSubj: Seq[QueryPattern] = checkForNonPropertyTypeInfoForEntity( - entity = statementPattern.subj, - typeInspectionResult = typeInspectionResult, - processedTypeInfo = processedTypeInformationKeysWhereClause, - conversionFuncForNonPropertyType = createAdditionalStatementsForNonPropertyType - ) + protected def processStatementPatternFromWhereClause(statementPattern: StatementPattern, inputOrderBy: Seq[OrderCriterion]): Seq[QueryPattern] = { - // check if there exists type information for the given statement's object - val additionalStatementsForObj: Seq[QueryPattern] = checkForNonPropertyTypeInfoForEntity( - entity = statementPattern.obj, - typeInspectionResult = typeInspectionResult, - processedTypeInfo = processedTypeInformationKeysWhereClause, - conversionFuncForNonPropertyType = createAdditionalStatementsForNonPropertyType - ) + // Does this statement set a Gravsearch option? + statementPattern.subj match { + case iriRef: IriRef if OntologyConstants.KnoraApi.GravsearchOptionsIris.contains(iriRef.iri.toString) => + // Yes. Process the option. + processGravsearchOption(statementPattern) + Seq.empty[QueryPattern] + + case _ => + // No. look at the statement's subject, predicate, and object and generate additional statements if needed based on the given type information. + // transform the originally given statement if necessary when processing the predicate + + // check if there exists type information for the given statement's subject + val additionalStatementsForSubj: Seq[QueryPattern] = checkForNonPropertyTypeInfoForEntity( + entity = statementPattern.subj, + typeInspectionResult = typeInspectionResult, + processedTypeInfo = processedTypeInformationKeysWhereClause, + conversionFuncForNonPropertyType = createAdditionalStatementsForNonPropertyType + ) - // Add additional statements based on the whole input statement, e.g. to deal with the value object or the link value, and transform the original statement. - val additionalStatementsForWholeStatement: Seq[QueryPattern] = checkForPropertyTypeInfoForStatement( - statementPattern = statementPattern, - typeInspectionResult = typeInspectionResult, - conversionFuncForPropertyType = convertStatementForPropertyType(inputOrderBy) - ) + // check if there exists type information for the given statement's object + val additionalStatementsForObj: Seq[QueryPattern] = checkForNonPropertyTypeInfoForEntity( + entity = statementPattern.obj, + typeInspectionResult = typeInspectionResult, + processedTypeInfo = processedTypeInformationKeysWhereClause, + conversionFuncForNonPropertyType = createAdditionalStatementsForNonPropertyType + ) - additionalStatementsForSubj ++ additionalStatementsForWholeStatement ++ additionalStatementsForObj + // Add additional statements based on the whole input statement, e.g. to deal with the value object or the link value, and transform the original statement. + val additionalStatementsForWholeStatement: Seq[QueryPattern] = checkForPropertyTypeInfoForStatement( + statementPattern = statementPattern, + typeInspectionResult = typeInspectionResult, + conversionFuncForPropertyType = convertStatementForPropertyType(inputOrderBy) + ) + additionalStatementsForSubj ++ additionalStatementsForWholeStatement ++ additionalStatementsForObj + } } /** @@ -579,7 +609,7 @@ abstract class AbstractPrequeryGenerator(constructClause: ConstructClause, // check if the objectTypeIri of propInfo is knora-api:Resource // if so, it is a linking property and its link value property must be restricted too - if (OntologyConstants.KnoraApi.isKnoraApiV2Resource(propInfo.objectTypeIri)) { + if (propInfo.objectIsResourceType) { // it is a linking property, restrict the link value property val restrictionForLinkValueProp = CompareExpression( leftArg = createLinkValuePropertyVariableFromLinkingPropertyVariable(queryVar), // the same variable was created during statement processing in WHERE clause in `convertStatementForPropertyType` diff --git a/webapi/src/main/scala/org/knora/webapi/messages/util/search/gravsearch/prequery/NonTriplestoreSpecificGravsearchToCountPrequeryTransformer.scala b/webapi/src/main/scala/org/knora/webapi/messages/util/search/gravsearch/prequery/NonTriplestoreSpecificGravsearchToCountPrequeryTransformer.scala index fcbf363078..83c0495089 100644 --- a/webapi/src/main/scala/org/knora/webapi/messages/util/search/gravsearch/prequery/NonTriplestoreSpecificGravsearchToCountPrequeryTransformer.scala +++ b/webapi/src/main/scala/org/knora/webapi/messages/util/search/gravsearch/prequery/NonTriplestoreSpecificGravsearchToCountPrequeryTransformer.scala @@ -86,10 +86,8 @@ class NonTriplestoreSpecificGravsearchToCountPrequeryTransformer(constructClause } override def optimiseQueryPatterns(patterns: Seq[QueryPattern]): Seq[QueryPattern] = { - val noTypePatterns = removeEntitiesInferredFromProperty(patterns) - moveLuceneToBeginning(noTypePatterns) + removeEntitiesInferredFromProperty(patterns) } override def transformLuceneQueryPattern(luceneQueryPattern: LuceneQueryPattern): Seq[QueryPattern] = Seq(luceneQueryPattern) - } diff --git a/webapi/src/main/scala/org/knora/webapi/messages/util/search/gravsearch/types/AnnotationReadingGravsearchTypeInspector.scala b/webapi/src/main/scala/org/knora/webapi/messages/util/search/gravsearch/types/AnnotationReadingGravsearchTypeInspector.scala index 05839c3d61..013accc4eb 100644 --- a/webapi/src/main/scala/org/knora/webapi/messages/util/search/gravsearch/types/AnnotationReadingGravsearchTypeInspector.scala +++ b/webapi/src/main/scala/org/knora/webapi/messages/util/search/gravsearch/types/AnnotationReadingGravsearchTypeInspector.scala @@ -77,13 +77,12 @@ class AnnotationReadingGravsearchTypeInspector(nextInspector: Option[GravsearchT case (acc: IntermediateTypeInspectionResult, typeAnnotation: GravsearchTypeAnnotation) => typeAnnotation.annotationProp match { case TypeAnnotationProperties.RdfType => - - val isResource = OntologyConstants.KnoraApi.isKnoraApiV2Resource(typeAnnotation.typeIri) + val isResource = OntologyConstants.KnoraApi.KnoraApiV2ResourceIris.contains(typeAnnotation.typeIri.toString) val isValue = GravsearchTypeInspectionUtil.GravsearchValueTypeIris.contains(typeAnnotation.typeIri.toString) acc.addTypes(typeAnnotation.typeableEntity, Set(NonPropertyTypeInfo(typeAnnotation.typeIri, isResourceType = isResource, isValueType = isValue))) case TypeAnnotationProperties.ObjectType => - val isResource = OntologyConstants.KnoraApi.isKnoraApiV2Resource(typeAnnotation.typeIri) + val isResource = OntologyConstants.KnoraApi.KnoraApiV2ResourceIris.contains(typeAnnotation.typeIri.toString) val isValue = GravsearchTypeInspectionUtil.GravsearchValueTypeIris.contains(typeAnnotation.typeIri.toString) acc.addTypes(typeAnnotation.typeableEntity, Set(PropertyTypeInfo(typeAnnotation.typeIri, objectIsResourceType = isResource, objectIsValueType = isValue))) } diff --git a/webapi/src/main/scala/org/knora/webapi/messages/util/search/gravsearch/types/GravsearchTypeInspectionUtil.scala b/webapi/src/main/scala/org/knora/webapi/messages/util/search/gravsearch/types/GravsearchTypeInspectionUtil.scala index f660e05eb5..21ac850acc 100644 --- a/webapi/src/main/scala/org/knora/webapi/messages/util/search/gravsearch/types/GravsearchTypeInspectionUtil.scala +++ b/webapi/src/main/scala/org/knora/webapi/messages/util/search/gravsearch/types/GravsearchTypeInspectionUtil.scala @@ -112,10 +112,22 @@ object GravsearchTypeInspectionUtil { OntologyConstants.KnoraApiV2Complex.KnoraProject ) + /** + * IRIs that are used to set Gravsearch options. + */ + val GravsearchOptionIris: Set[IRI] = Set( + OntologyConstants.KnoraApiV2Simple.GravsearchOptions, + OntologyConstants.KnoraApiV2Complex.GravsearchOptions, + OntologyConstants.KnoraApiV2Simple.UseInference, + OntologyConstants.KnoraApiV2Complex.UseInference + ) + /** * IRIs that do not need to be annotated to specify their types. */ - val ApiV2NonTypeableIris: Set[IRI] = GravsearchAnnotationTypeIris ++ TypeAnnotationProperties.allTypeAnnotationIris + val ApiV2NonTypeableIris: Set[IRI] = GravsearchAnnotationTypeIris ++ + TypeAnnotationProperties.allTypeAnnotationIris ++ + GravsearchOptionIris /** * Given a Gravsearch entity that is known to need type information, converts it to a [[TypeableEntity]]. diff --git a/webapi/src/main/scala/org/knora/webapi/messages/util/search/gravsearch/types/InferringGravsearchTypeInspector.scala b/webapi/src/main/scala/org/knora/webapi/messages/util/search/gravsearch/types/InferringGravsearchTypeInspector.scala index 464391f9cf..fd230f2d32 100644 --- a/webapi/src/main/scala/org/knora/webapi/messages/util/search/gravsearch/types/InferringGravsearchTypeInspector.scala +++ b/webapi/src/main/scala/org/knora/webapi/messages/util/search/gravsearch/types/InferringGravsearchTypeInspector.scala @@ -1094,9 +1094,12 @@ class InferringGravsearchTypeInspector(nextInspector: Option[GravsearchTypeInspe case _ => None }) - // If the statement's predicate is a Knora property, and isn't a type annotation predicate, add it to the set of Knora property IRIs. + // If the statement's predicate is a Knora property, and isn't a type annotation predicate or a Gravsearch option predicate, + // add it to the set of Knora property IRIs. val knoraPropertyIris: Set[SmartIri] = acc.knoraPropertyIris ++ (statementPattern.pred match { - case IriRef(predIri, _) if predIri.isKnoraEntityIri && !GravsearchTypeInspectionUtil.TypeAnnotationProperties.allTypeAnnotationIris.contains(predIri.toString) => + case IriRef(predIri, _) if predIri.isKnoraEntityIri && + !(GravsearchTypeInspectionUtil.TypeAnnotationProperties.allTypeAnnotationIris.contains(predIri.toString) || + GravsearchTypeInspectionUtil.GravsearchOptionIris.contains(predIri.toString)) => Some(predIri) case _ => None diff --git a/webapi/src/main/scala/org/knora/webapi/responders/v2/SearchResponderV2.scala b/webapi/src/main/scala/org/knora/webapi/responders/v2/SearchResponderV2.scala index 8ffb0d6945..9bbf444523 100644 --- a/webapi/src/main/scala/org/knora/webapi/responders/v2/SearchResponderV2.scala +++ b/webapi/src/main/scala/org/knora/webapi/responders/v2/SearchResponderV2.scala @@ -74,7 +74,11 @@ class SearchResponderV2(responderData: ResponderData) extends ResponderWithStand * @param requestingUser the the client making the request. * @return a [[ResourceCountV2]] representing the number of resources that have been found. */ - private def fulltextSearchCountV2(searchValue: String, limitToProject: Option[IRI], limitToResourceClass: Option[SmartIri], limitToStandoffClass: Option[SmartIri], requestingUser: UserADM): Future[ResourceCountV2] = { + private def fulltextSearchCountV2(searchValue: String, + limitToProject: Option[IRI], + limitToResourceClass: Option[SmartIri], + limitToStandoffClass: Option[SmartIri], + requestingUser: UserADM): Future[ResourceCountV2] = { val searchTerms: LuceneQueryString = LuceneQueryString(searchValue) @@ -298,17 +302,21 @@ class SearchResponderV2(responderData: ResponderData) extends ResponderWithStand ) // Convert the non-triplestore-specific query to a triplestore-specific one. + triplestoreSpecificQueryPatternTransformerSelect: SelectToSelectTransformer = { if (settings.triplestoreType.startsWith("graphdb")) { // GraphDB - new SparqlTransformer.GraphDBSelectToSelectTransformer + new SparqlTransformer.GraphDBSelectToSelectTransformer( + useInference = nonTriplestoreSpecificConstructToSelectTransformer.useInference + ) } else { // Other - new SparqlTransformer.NoInferenceSelectToSelectTransformer + new SparqlTransformer.NoInferenceSelectToSelectTransformer( + simulateInference = nonTriplestoreSpecificConstructToSelectTransformer.useInference + ) } } - // Convert the preprocessed query to a non-triplestore-specific query. triplestoreSpecificCountQuery = QueryTraverser.transformSelectToSelect( inputQuery = nonTriplestoreSpecficPrequery, transformer = triplestoreSpecificQueryPatternTransformerSelect @@ -381,22 +389,28 @@ class SearchResponderV2(responderData: ResponderData) extends ResponderWithStand triplestoreSpecificQueryPatternTransformerSelect: SelectToSelectTransformer = { if (settings.triplestoreType.startsWith("graphdb")) { // GraphDB - new SparqlTransformer.GraphDBSelectToSelectTransformer + new SparqlTransformer.GraphDBSelectToSelectTransformer( + useInference = nonTriplestoreSpecificConstructToSelectTransformer.useInference + ) } else { // Other - new SparqlTransformer.NoInferenceSelectToSelectTransformer + new SparqlTransformer.NoInferenceSelectToSelectTransformer( + simulateInference = nonTriplestoreSpecificConstructToSelectTransformer.useInference + ) } } // Convert the preprocessed query to a non-triplestore-specific query. + triplestoreSpecificPrequery = QueryTraverser.transformSelectToSelect( inputQuery = nonTriplestoreSpecificPrequery, transformer = triplestoreSpecificQueryPatternTransformerSelect ) - // _ = println(triplestoreSpecificPrequery.toSparql) + triplestoreSpecificPrequerySparql = triplestoreSpecificPrequery.toSparql + _ = log.debug(triplestoreSpecificPrequerySparql) - prequeryResponseNotMerged: SparqlSelectResponse <- (storeManager ? SparqlSelectRequest(triplestoreSpecificPrequery.toSparql)).mapTo[SparqlSelectResponse] + prequeryResponseNotMerged: SparqlSelectResponse <- (storeManager ? SparqlSelectRequest(triplestoreSpecificPrequerySparql)).mapTo[SparqlSelectResponse] pageSizeBeforeFiltering: Int = prequeryResponseNotMerged.results.bindings.size // Merge rows with the same main resource IRI. This could happen if there are unbound variables in a UNION. @@ -470,19 +484,17 @@ class SearchResponderV2(responderData: ResponderData) extends ResponderWithStand } } - val triplestoreSpecificQuery = QueryTraverser.transformConstructToConstruct( + val triplestoreSpecificMainQuery = QueryTraverser.transformConstructToConstruct( inputQuery = mainQuery, transformer = triplestoreSpecificQueryPatternTransformerConstruct ) // Convert the result to a SPARQL string and send it to the triplestore. - val triplestoreSpecificSparql: String = triplestoreSpecificQuery.toSparql - - // println("++++++++") - // println(triplestoreSpecificSparql) + val triplestoreSpecificMainQuerySparql: String = triplestoreSpecificMainQuery.toSparql + log.debug(triplestoreSpecificMainQuerySparql) for { - mainQueryResponse: SparqlExtendedConstructResponse <- (storeManager ? SparqlExtendedConstructRequest(triplestoreSpecificSparql)).mapTo[SparqlExtendedConstructResponse] + mainQueryResponse: SparqlExtendedConstructResponse <- (storeManager ? SparqlExtendedConstructRequest(triplestoreSpecificMainQuerySparql)).mapTo[SparqlExtendedConstructResponse] // Filter out values that the user doesn't have permission to see. queryResultsFilteredForPermissions: ConstructResponseUtilV2.MainResourcesAndValueRdfData = ConstructResponseUtilV2.splitMainResourcesAndValueRdfData( diff --git a/webapi/src/main/scala/org/knora/webapi/routing/v2/OntologiesRouteV2.scala b/webapi/src/main/scala/org/knora/webapi/routing/v2/OntologiesRouteV2.scala index 47d6f05424..fa2c3618a3 100644 --- a/webapi/src/main/scala/org/knora/webapi/routing/v2/OntologiesRouteV2.scala +++ b/webapi/src/main/scala/org/knora/webapi/routing/v2/OntologiesRouteV2.scala @@ -95,7 +95,7 @@ class OntologiesRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) val params: Map[String, String] = requestContext.request.uri.query().toMap val allLanguagesStr = params.get(ALL_LANGUAGES) - val allLanguages = stringFormatter.optionStringToBoolean(params.get(ALL_LANGUAGES), throw BadRequestException(s"Invalid boolean for $ALL_LANGUAGES: $allLanguagesStr")) + val allLanguages = stringFormatter.optionStringToBoolean(allLanguagesStr, throw BadRequestException(s"Invalid boolean for $ALL_LANGUAGES: $allLanguagesStr")) val requestMessageFuture: Future[OntologyEntitiesGetRequestV2] = for { requestingUser <- getUserADM(requestContext) diff --git a/webapi/src/test/scala/org/knora/webapi/e2e/v2/SearchRouteV2R2RSpec.scala b/webapi/src/test/scala/org/knora/webapi/e2e/v2/SearchRouteV2R2RSpec.scala index 96614f308d..7fa8c04f32 100644 --- a/webapi/src/test/scala/org/knora/webapi/e2e/v2/SearchRouteV2R2RSpec.scala +++ b/webapi/src/test/scala/org/knora/webapi/e2e/v2/SearchRouteV2R2RSpec.scala @@ -6397,6 +6397,29 @@ class SearchRouteV2R2RSpec extends R2RSpec { } + "search for an anything:Thing that has a decimal value of 2.1 (submitting the complex schema), without inference" in { + val gravsearchQuery = + """PREFIX anything: + |PREFIX knora-api: + | + |CONSTRUCT { + | ?thing knora-api:isMainResource true . + | ?thing anything:hasDecimal ?decimal . + |} WHERE { + | knora-api:GravsearchOptions knora-api:useInference false . + | ?thing a anything:Thing . + | ?thing anything:hasDecimal ?decimal . + | ?decimal knora-api:decimalValueAsDecimal "2.1"^^xsd:decimal . + |}""".stripMargin + + Post("/v2/searchextended", HttpEntity(SparqlQueryConstants.`application/sparql-query`, gravsearchQuery)) ~> addCredentials(BasicHttpCredentials(anythingUserEmail, password)) ~> searchPath ~> check { + assert(status == StatusCodes.OK, response.toString) + val expectedAnswerJSONLD = readOrWriteTextFile(responseAs[String], new File("test_data/searchR2RV2/ThingEqualsDecimal.jsonld"), writeTestDataFiles) + compareJSONLDForResourcesResponse(expectedJSONLD = expectedAnswerJSONLD, receivedJSONLD = responseAs[String]) + checkSearchResponseNumberOfResults(responseAs[String], 1) + } + } + "search for an anything:Thing that has a decimal value bigger than 2.0 (submitting the complex schema)" in { val gravsearchQuery = """ diff --git a/webapi/src/test/scala/org/knora/webapi/messages/util/search/gravsearch/types/GravsearchTypeInspectorSpec.scala b/webapi/src/test/scala/org/knora/webapi/messages/util/search/gravsearch/types/GravsearchTypeInspectorSpec.scala index 9d71e2a16d..3b8e807f39 100644 --- a/webapi/src/test/scala/org/knora/webapi/messages/util/search/gravsearch/types/GravsearchTypeInspectorSpec.scala +++ b/webapi/src/test/scala/org/knora/webapi/messages/util/search/gravsearch/types/GravsearchTypeInspectorSpec.scala @@ -21,13 +21,12 @@ package org.knora.webapi.util.search.gravsearch.types import akka.testkit.ImplicitSender import org.knora.webapi._ - import org.knora.webapi.exceptions.GravsearchException import org.knora.webapi.messages.IriConversions._ import org.knora.webapi.messages.StringFormatter +import org.knora.webapi.messages.util.search._ import org.knora.webapi.messages.util.search.gravsearch.GravsearchParser import org.knora.webapi.messages.util.search.gravsearch.types._ -import org.knora.webapi.messages.util.search._ import org.knora.webapi.sharedtestdata.SharedTestDataADM import scala.concurrent.duration._ @@ -593,7 +592,7 @@ class GravsearchTypeInspectorSpec extends CoreSpec() with ImplicitSender { TypeableVariable(variableName = "label") -> NonPropertyTypeInfo(typeIri = "http://www.w3.org/2001/XMLSchema#string".toSmartIri, isValueType = true) )) - val queryWithReduntentTypes: String = + val QueryWithRedundantTypes: String = """ |PREFIX beol: |PREFIX knora-api: @@ -616,25 +615,44 @@ class GravsearchTypeInspectorSpec extends CoreSpec() with ImplicitSender { |} ORDER BY ?date """.stripMargin - val queryWithInconsistentTypes = - """ - |PREFIX knora-api: - |PREFIX beol: - | - |CONSTRUCT { - | ?person knora-api:isMainResource true . - | ?document beol:hasAuthor ?person . - |} WHERE { - | ?person a knora-api:Resource . - | ?person a beol:person . - | - | ?document beol:hasAuthor ?person . - | beol:hasAuthor knora-api:objectType knora-api:Resource . - | ?document a knora-api:Resource . - | { ?document a beol:manuscript . } UNION { ?document a beol:letter .} - |} + val QueryWithInconsistentTypes3: String = + """ + |PREFIX knora-api: + |PREFIX beol: + | + |CONSTRUCT { + | ?person knora-api:isMainResource true . + | ?document beol:hasAuthor ?person . + |} WHERE { + | ?person a knora-api:Resource . + | ?person a beol:person . + | + | ?document beol:hasAuthor ?person . + | beol:hasAuthor knora-api:objectType knora-api:Resource . + | ?document a knora-api:Resource . + | { ?document a beol:manuscript . } UNION { ?document a beol:letter .} + |} """.stripMargin + val QueryWithGravsearchOptions: String = + """PREFIX anything: + |PREFIX knora-api: + | + |CONSTRUCT { + | ?thing knora-api:isMainResource true . + |} WHERE { + | knora-api:GravsearchOptions knora-api:useInference false . + | ?thing a anything:Thing . + |}""".stripMargin + + val GravsearchOptionsResult: GravsearchTypeInspectionResult = GravsearchTypeInspectionResult( + entities = Map(TypeableVariable(variableName = "thing") -> NonPropertyTypeInfo( + typeIri = "http://0.0.0.0:3333/ontology/0001/anything/v2#Thing".toSmartIri, + isResourceType = true + )), + entitiesInferredFromProperties = Map() + ) + "The type inspection utility" should { "remove the type annotations from a WHERE clause" in { val parsedQuery = GravsearchParser.parseQuery(QueryWithExplicitTypeAnnotations) @@ -655,146 +673,158 @@ class GravsearchTypeInspectorSpec extends CoreSpec() with ImplicitSender { "The inferring type inspector" should { "remove a type from IntermediateTypeInspectionResult" in { - val multipleDetectedTypes: IntermediateTypeInspectionResult = IntermediateTypeInspectionResult(entities = Map( - TypeableVariable(variableName = "mainRes") -> Set( - NonPropertyTypeInfo(typeIri = "http://api.knora.org/ontology/knora-api/simple/v2#Resource".toSmartIri, isResourceType = true), - NonPropertyTypeInfo(typeIri = "http://0.0.0.0:3333/ontology/0801/beol/simple/v2#letter".toSmartIri, isResourceType = true), - NonPropertyTypeInfo(typeIri = "http://0.0.0.0:3333/ontology/0801/beol/simple/v2#basicLetter".toSmartIri, isResourceType = true)) + val multipleDetectedTypes: IntermediateTypeInspectionResult = IntermediateTypeInspectionResult( + entities = Map( + TypeableVariable(variableName = "mainRes") -> Set( + NonPropertyTypeInfo(typeIri = "http://api.knora.org/ontology/knora-api/simple/v2#Resource".toSmartIri, isResourceType = true), + NonPropertyTypeInfo(typeIri = "http://0.0.0.0:3333/ontology/0801/beol/simple/v2#letter".toSmartIri, isResourceType = true), + NonPropertyTypeInfo(typeIri = "http://0.0.0.0:3333/ontology/0801/beol/simple/v2#basicLetter".toSmartIri, isResourceType = true)) ), entitiesInferredFromProperties = Map(TypeableVariable(variableName = "mainRes") -> Set( NonPropertyTypeInfo(typeIri = "http://0.0.0.0:3333/ontology/0801/beol/simple/v2#letter".toSmartIri, isResourceType = true), NonPropertyTypeInfo(typeIri = "http://0.0.0.0:3333/ontology/0801/beol/simple/v2#basicLetter".toSmartIri, isResourceType = true)) ) ) - //remove type basicLettr + + // remove type basicLetter val intermediateTypesWithoutResource: IntermediateTypeInspectionResult = multipleDetectedTypes.removeType(TypeableVariable(variableName = "mainRes"), NonPropertyTypeInfo(typeIri = "http://0.0.0.0:3333/ontology/0801/beol/simple/v2#basicLetter".toSmartIri, isResourceType = true)) - //Is it removed from entities? - intermediateTypesWithoutResource.entities should contain (TypeableVariable(variableName = "mainRes") -> Set( + // Is it removed from entities? + intermediateTypesWithoutResource.entities should contain(TypeableVariable(variableName = "mainRes") -> Set( NonPropertyTypeInfo(typeIri = "http://api.knora.org/ontology/knora-api/simple/v2#Resource".toSmartIri, isResourceType = true), NonPropertyTypeInfo(typeIri = "http://0.0.0.0:3333/ontology/0801/beol/simple/v2#letter".toSmartIri, isResourceType = true) )) - //Is it removed from entitiesInferredFromProperties? - intermediateTypesWithoutResource.entitiesInferredFromProperties should contain (TypeableVariable(variableName = "mainRes") -> Set( + // Is it removed from entitiesInferredFromProperties? + intermediateTypesWithoutResource.entitiesInferredFromProperties should contain(TypeableVariable(variableName = "mainRes") -> Set( NonPropertyTypeInfo(typeIri = "http://0.0.0.0:3333/ontology/0801/beol/simple/v2#letter".toSmartIri, isResourceType = true))) } "refine the inspected types for each typeableEntity" in { val typeInspectionRunner = new InferringGravsearchTypeInspector(nextInspector = None, responderData = responderData) val parsedQuery = GravsearchParser.parseQuery(QueryRdfTypeRule) - val (usageIndex , entityInfo) = Await.result(typeInspectionRunner.getUsageIndexAndEntityInfos(parsedQuery.whereClause, requestingUser = anythingAdminUser), timeout) - val multipleDetectedTypes: IntermediateTypeInspectionResult = IntermediateTypeInspectionResult(entities = Map( - TypeableVariable(variableName = "letter") -> Set( - NonPropertyTypeInfo(typeIri = "http://0.0.0.0:3333/ontology/0801/beol/simple/v2#letter".toSmartIri, isResourceType = true), - NonPropertyTypeInfo(typeIri = "http://api.knora.org/ontology/knora-api/simple/v2#Resource".toSmartIri, isResourceType = true), - NonPropertyTypeInfo(typeIri = "http://0.0.0.0:3333/ontology/0801/beol/simple/v2#basicLetter".toSmartIri, isResourceType = true) + val (_, entityInfo) = Await.result(typeInspectionRunner.getUsageIndexAndEntityInfos(parsedQuery.whereClause, requestingUser = anythingAdminUser), timeout) + val multipleDetectedTypes: IntermediateTypeInspectionResult = IntermediateTypeInspectionResult( + entities = Map( + TypeableVariable(variableName = "letter") -> Set( + NonPropertyTypeInfo(typeIri = "http://0.0.0.0:3333/ontology/0801/beol/simple/v2#letter".toSmartIri, isResourceType = true), + NonPropertyTypeInfo(typeIri = "http://api.knora.org/ontology/knora-api/simple/v2#Resource".toSmartIri, isResourceType = true), + NonPropertyTypeInfo(typeIri = "http://0.0.0.0:3333/ontology/0801/beol/simple/v2#basicLetter".toSmartIri, isResourceType = true) ) ), - entitiesInferredFromProperties = Map(TypeableVariable(variableName = "letter") -> Set(NonPropertyTypeInfo(typeIri = "http://0.0.0.0:3333/ontology/0801/beol/simple/v2#basicLetter".toSmartIri, isResourceType = true) - )) + entitiesInferredFromProperties = Map( + TypeableVariable(variableName = "letter") -> Set(NonPropertyTypeInfo(typeIri = "http://0.0.0.0:3333/ontology/0801/beol/simple/v2#basicLetter".toSmartIri, isResourceType = true)) + ) ) val refinedIntermediateResults = typeInspectionRunner.refineDeterminedTypes( intermediateResult = multipleDetectedTypes, - entityInfo = entityInfo) + entityInfo = entityInfo + ) + assert(refinedIntermediateResults.entities.size == 1) - refinedIntermediateResults.entities should contain (TypeableVariable(variableName = "letter") -> Set( + refinedIntermediateResults.entities should contain(TypeableVariable(variableName = "letter") -> Set( NonPropertyTypeInfo(typeIri = "http://0.0.0.0:3333/ontology/0801/beol/simple/v2#letter".toSmartIri, isResourceType = true))) assert(refinedIntermediateResults.entitiesInferredFromProperties.isEmpty) } "sanitize inconsistent resource types that only have knora-base:Resource as base class in common" in { - val typeInspectionRunner = new InferringGravsearchTypeInspector(nextInspector = None, responderData = responderData) - val parsedQuery = GravsearchParser.parseQuery(QueryRdfTypeRule) - val (usageIndex , entityInfo) = Await.result(typeInspectionRunner.getUsageIndexAndEntityInfos(parsedQuery.whereClause, requestingUser = anythingAdminUser), timeout) - val inconsistentTypes: IntermediateTypeInspectionResult = IntermediateTypeInspectionResult(entities = Map( + val typeInspectionRunner = new InferringGravsearchTypeInspector(nextInspector = None, responderData = responderData) + val parsedQuery = GravsearchParser.parseQuery(QueryRdfTypeRule) + val (usageIndex, entityInfo) = Await.result(typeInspectionRunner.getUsageIndexAndEntityInfos(parsedQuery.whereClause, requestingUser = anythingAdminUser), timeout) + val inconsistentTypes: IntermediateTypeInspectionResult = IntermediateTypeInspectionResult( + entities = Map( TypeableVariable(variableName = "letter") -> Set( NonPropertyTypeInfo(typeIri = "http://0.0.0.0:3333/ontology/0801/beol/simple/v2#letter".toSmartIri, isResourceType = true), NonPropertyTypeInfo(typeIri = "http://0.0.0.0:3333/ontology/0801/beol/simple/v2#person".toSmartIri, isResourceType = true) - ), + ), TypeableVariable(variableName = "linkingProp1") -> Set( PropertyTypeInfo(objectTypeIri = "http://0.0.0.0:3333/ontology/0801/beol/simple/v2#letter".toSmartIri, objectIsResourceType = true), PropertyTypeInfo(objectTypeIri = "http://0.0.0.0:3333/ontology/0801/beol/simple/v2#person".toSmartIri, objectIsResourceType = true) ) - ), - entitiesInferredFromProperties = Map(TypeableVariable(variableName = "letter") -> Set(NonPropertyTypeInfo(typeIri = "http://0.0.0.0:3333/ontology/0801/beol/simple/v2#letter".toSmartIri, isResourceType = true))) - ) + ), + entitiesInferredFromProperties = Map(TypeableVariable(variableName = "letter") -> Set(NonPropertyTypeInfo(typeIri = "http://0.0.0.0:3333/ontology/0801/beol/simple/v2#letter".toSmartIri, isResourceType = true))) + ) - val refinedIntermediateResults = typeInspectionRunner.refineDeterminedTypes( - intermediateResult = inconsistentTypes, - entityInfo = entityInfo) + val refinedIntermediateResults = typeInspectionRunner.refineDeterminedTypes( + intermediateResult = inconsistentTypes, + entityInfo = entityInfo) - val sanitizedResults = typeInspectionRunner.sanitizeInconsistentResourceTypes( - lastResults = refinedIntermediateResults, - usageIndex.querySchema, - entityInfo = entityInfo) + val sanitizedResults = typeInspectionRunner.sanitizeInconsistentResourceTypes( + lastResults = refinedIntermediateResults, + usageIndex.querySchema, + entityInfo = entityInfo) - val expectedResult: IntermediateTypeInspectionResult = IntermediateTypeInspectionResult(entities = Map( - TypeableVariable(variableName = "letter") -> Set( - NonPropertyTypeInfo(typeIri = "http://api.knora.org/ontology/knora-api/simple/v2#Resource".toSmartIri, isResourceType = true) - ), - TypeableVariable(variableName = "linkingProp1") -> Set( - PropertyTypeInfo(objectTypeIri = "http://api.knora.org/ontology/knora-api/simple/v2#Resource".toSmartIri, objectIsResourceType = true) - ) + val expectedResult: IntermediateTypeInspectionResult = IntermediateTypeInspectionResult( + entities = Map( + TypeableVariable(variableName = "letter") -> Set(NonPropertyTypeInfo(typeIri = "http://api.knora.org/ontology/knora-api/simple/v2#Resource".toSmartIri, isResourceType = true)), + TypeableVariable(variableName = "linkingProp1") -> Set(PropertyTypeInfo(objectTypeIri = "http://api.knora.org/ontology/knora-api/simple/v2#Resource".toSmartIri, objectIsResourceType = true)) ), - entitiesInferredFromProperties = Map(TypeableVariable(variableName = "letter") -> Set(NonPropertyTypeInfo(typeIri = "http://0.0.0.0:3333/ontology/0801/beol/simple/v2#letter".toSmartIri, isResourceType = true))) + entitiesInferredFromProperties = Map( + TypeableVariable(variableName = "letter") -> Set(NonPropertyTypeInfo(typeIri = "http://0.0.0.0:3333/ontology/0801/beol/simple/v2#letter".toSmartIri, isResourceType = true)) + ) ) - assert(sanitizedResults.entities == expectedResult.entities ) + assert(sanitizedResults.entities == expectedResult.entities) } "sanitize inconsistent resource types that have common base classes other than knora-base:Resource" in { val typeInspectionRunner = new InferringGravsearchTypeInspector(nextInspector = None, responderData = responderData) - val parsedQuery = GravsearchParser.parseQuery(queryWithInconsistentTypes) - val (usageIndex , entityInfo) = Await.result(typeInspectionRunner.getUsageIndexAndEntityInfos(parsedQuery.whereClause, requestingUser = anythingAdminUser), timeout) - val inconsistentTypes: IntermediateTypeInspectionResult = IntermediateTypeInspectionResult(entities = Map( - TypeableVariable(variableName = "document") -> Set( - NonPropertyTypeInfo(typeIri = "http://0.0.0.0:3333/ontology/0801/beol/simple/v2#letter".toSmartIri, isResourceType = true), - NonPropertyTypeInfo(typeIri = "http://0.0.0.0:3333/ontology/0801/beol/simple/v2#manuscript".toSmartIri, isResourceType = true) - ) - ), + val parsedQuery = GravsearchParser.parseQuery(QueryWithInconsistentTypes3) + val (usageIndex, entityInfo) = Await.result(typeInspectionRunner.getUsageIndexAndEntityInfos(parsedQuery.whereClause, requestingUser = anythingAdminUser), timeout) + val inconsistentTypes: IntermediateTypeInspectionResult = IntermediateTypeInspectionResult( + entities = Map( + TypeableVariable(variableName = "document") -> Set( + NonPropertyTypeInfo(typeIri = "http://0.0.0.0:3333/ontology/0801/beol/simple/v2#letter".toSmartIri, isResourceType = true), + NonPropertyTypeInfo(typeIri = "http://0.0.0.0:3333/ontology/0801/beol/simple/v2#manuscript".toSmartIri, isResourceType = true) + ) + ), entitiesInferredFromProperties = Map(TypeableVariable(variableName = "document") -> Set(NonPropertyTypeInfo(typeIri = "http://0.0.0.0:3333/ontology/0801/beol/simple/v2#letter".toSmartIri, isResourceType = true))) ) val refinedIntermediateResults = typeInspectionRunner.refineDeterminedTypes( intermediateResult = inconsistentTypes, entityInfo = entityInfo) + val sanitizedResults = typeInspectionRunner.sanitizeInconsistentResourceTypes( lastResults = refinedIntermediateResults, usageIndex.querySchema, entityInfo = entityInfo) - val expectedResult: IntermediateTypeInspectionResult = IntermediateTypeInspectionResult(entities = Map( - TypeableVariable(variableName = "document") -> Set( - NonPropertyTypeInfo(typeIri = "http://0.0.0.0:3333/ontology/0801/beol/simple/v2#writtenSource".toSmartIri, isResourceType = true) + + val expectedResult: IntermediateTypeInspectionResult = IntermediateTypeInspectionResult( + entities = Map( + TypeableVariable(variableName = "document") -> Set(NonPropertyTypeInfo(typeIri = "http://0.0.0.0:3333/ontology/0801/beol/simple/v2#writtenSource".toSmartIri, isResourceType = true)) + ), + entitiesInferredFromProperties = Map( + TypeableVariable(variableName = "document") -> Set(NonPropertyTypeInfo(typeIri = "http://0.0.0.0:3333/ontology/0801/beol/simple/v2#letter".toSmartIri, isResourceType = true)) ) - ), - entitiesInferredFromProperties = Map(TypeableVariable(variableName = "document") -> Set(NonPropertyTypeInfo(typeIri = "http://0.0.0.0:3333/ontology/0801/beol/simple/v2#letter".toSmartIri, isResourceType = true))) ) - assert(sanitizedResults.entities == expectedResult.entities ) + assert(sanitizedResults.entities == expectedResult.entities) } "sanitize inconsistent types resulted from a union" in { + val typeInspectionRunner = new GravsearchTypeInspectionRunner(responderData = responderData, inferTypes = true) + val parsedQuery = GravsearchParser.parseQuery(QueryWithInconsistentTypes3) + val resultFuture: Future[GravsearchTypeInspectionResult] = typeInspectionRunner.inspectTypes(parsedQuery.whereClause, requestingUser = anythingAdminUser) + val result = Await.result(resultFuture, timeout) - val typeInspectionRunner = new GravsearchTypeInspectionRunner(responderData = responderData, inferTypes = true) - val parsedQuery = GravsearchParser.parseQuery(queryWithInconsistentTypes) - val resultFuture: Future[GravsearchTypeInspectionResult] = typeInspectionRunner.inspectTypes(parsedQuery.whereClause, requestingUser = anythingAdminUser) - val result = Await.result(resultFuture, timeout) - - val expectedResult: GravsearchTypeInspectionResult = GravsearchTypeInspectionResult(entities = Map( + val expectedResult: GravsearchTypeInspectionResult = GravsearchTypeInspectionResult( + entities = Map( TypeableVariable(variableName = "person") -> NonPropertyTypeInfo(typeIri = "http://0.0.0.0:3333/ontology/0801/beol/simple/v2#person".toSmartIri, isResourceType = true), TypeableVariable(variableName = "document") -> NonPropertyTypeInfo(typeIri = "http://0.0.0.0:3333/ontology/0801/beol/simple/v2#writtenSource".toSmartIri, isResourceType = true), TypeableIri(iri = "http://0.0.0.0:3333/ontology/0801/beol/simple/v2#hasAuthor".toSmartIri) -> PropertyTypeInfo(objectTypeIri = "http://0.0.0.0:3333/ontology/0801/beol/simple/v2#person".toSmartIri, objectIsResourceType = true) - ), - entitiesInferredFromProperties = Map(TypeableVariable(variableName = "person") -> Set(NonPropertyTypeInfo(typeIri = "http://0.0.0.0:3333/ontology/0801/beol/simple/v2#person".toSmartIri, isResourceType = true)), - ) + ), + entitiesInferredFromProperties = Map( + TypeableVariable(variableName = "person") -> Set(NonPropertyTypeInfo(typeIri = "http://0.0.0.0:3333/ontology/0801/beol/simple/v2#person".toSmartIri, isResourceType = true)), ) - assert(result.entities == expectedResult.entities) + ) + + assert(result.entities == expectedResult.entities) } "types resulted from a query with optional" in { @@ -818,6 +848,7 @@ class GravsearchTypeInspectorSpec extends CoreSpec() with ImplicitSender { |} |} """.stripMargin + val typeInspectionRunner = new GravsearchTypeInspectionRunner(responderData = responderData, inferTypes = true) val parsedQuery = GravsearchParser.parseQuery(queryWithOptional) val resultFuture: Future[GravsearchTypeInspectionResult] = typeInspectionRunner.inspectTypes(parsedQuery.whereClause, requestingUser = anythingAdminUser) @@ -827,37 +858,39 @@ class GravsearchTypeInspectorSpec extends CoreSpec() with ImplicitSender { // since writtenSource is a base class of basicLetter, it is ignored and type "beol:basicLetter" is considered for ?document. // The OPTIONAL would become meaningless here. Therefore, in cases where property is in OPTIONAL block, // the rdf:type statement for ?document must not be removed from query even though ?document is in entitiesInferredFromProperties. - val expectedResult: GravsearchTypeInspectionResult = GravsearchTypeInspectionResult(entities = Map( - TypeableVariable(variableName = "document") -> - NonPropertyTypeInfo(typeIri = "http://0.0.0.0:3333/ontology/0801/beol/simple/v2#basicLetter".toSmartIri, isResourceType = true), - TypeableVariable(variableName = "familyName") -> - NonPropertyTypeInfo(typeIri = "http://www.w3.org/2001/XMLSchema#string".toSmartIri, isValueType = true), - TypeableVariable(variableName = "recipient") -> - NonPropertyTypeInfo(typeIri = "http://0.0.0.0:3333/ontology/0801/beol/simple/v2#person".toSmartIri, isResourceType = true), - TypeableIri(iri = "http://0.0.0.0:3333/ontology/0801/beol/simple/v2#hasRecipient".toSmartIri) -> - PropertyTypeInfo(objectTypeIri = "http://0.0.0.0:3333/ontology/0801/beol/simple/v2#person".toSmartIri, objectIsResourceType = true), - TypeableIri(iri = "http://0.0.0.0:3333/ontology/0801/beol/simple/v2#hasFamilyName".toSmartIri) -> - PropertyTypeInfo(objectTypeIri = "http://www.w3.org/2001/XMLSchema#string".toSmartIri, objectIsValueType = true) - ), - entitiesInferredFromProperties = Map(TypeableVariable(variableName = "document") -> Set(NonPropertyTypeInfo(typeIri = "http://0.0.0.0:3333/ontology/0801/beol/simple/v2#basicLetter".toSmartIri, isResourceType = true)), + val expectedResult: GravsearchTypeInspectionResult = GravsearchTypeInspectionResult( + entities = Map( + TypeableVariable(variableName = "document") -> + NonPropertyTypeInfo(typeIri = "http://0.0.0.0:3333/ontology/0801/beol/simple/v2#basicLetter".toSmartIri, isResourceType = true), + TypeableVariable(variableName = "familyName") -> + NonPropertyTypeInfo(typeIri = "http://www.w3.org/2001/XMLSchema#string".toSmartIri, isValueType = true), + TypeableVariable(variableName = "recipient") -> + NonPropertyTypeInfo(typeIri = "http://0.0.0.0:3333/ontology/0801/beol/simple/v2#person".toSmartIri, isResourceType = true), + TypeableIri(iri = "http://0.0.0.0:3333/ontology/0801/beol/simple/v2#hasRecipient".toSmartIri) -> + PropertyTypeInfo(objectTypeIri = "http://0.0.0.0:3333/ontology/0801/beol/simple/v2#person".toSmartIri, objectIsResourceType = true), + TypeableIri(iri = "http://0.0.0.0:3333/ontology/0801/beol/simple/v2#hasFamilyName".toSmartIri) -> + PropertyTypeInfo(objectTypeIri = "http://www.w3.org/2001/XMLSchema#string".toSmartIri, objectIsValueType = true) + ), + entitiesInferredFromProperties = Map( + TypeableVariable(variableName = "document") -> Set(NonPropertyTypeInfo(typeIri = "http://0.0.0.0:3333/ontology/0801/beol/simple/v2#basicLetter".toSmartIri, isResourceType = true)), TypeableVariable(variableName = "familyName") -> Set(NonPropertyTypeInfo(typeIri = "http://www.w3.org/2001/XMLSchema#string".toSmartIri, isValueType = true)), TypeableVariable(variableName = "recipient") -> Set(NonPropertyTypeInfo(typeIri = "http://0.0.0.0:3333/ontology/0801/beol/simple/v2#person".toSmartIri, isResourceType = true)), ) ) + assert(result.entities == expectedResult.entities) assert(result.entitiesInferredFromProperties == expectedResult.entitiesInferredFromProperties) - } "infer the most specific type from redundant ones given in a query" in { val typeInspectionRunner = new GravsearchTypeInspectionRunner(responderData = responderData, inferTypes = true) - val parsedQuery = GravsearchParser.parseQuery(queryWithReduntentTypes) + val parsedQuery = GravsearchParser.parseQuery(QueryWithRedundantTypes) val resultFuture: Future[GravsearchTypeInspectionResult] = typeInspectionRunner.inspectTypes(parsedQuery.whereClause, requestingUser = anythingAdminUser) val result = Await.result(resultFuture, timeout) assert(result.entities.size == 5) - result.entitiesInferredFromProperties.keySet should not contain(TypeableVariable("letter")) + result.entitiesInferredFromProperties.keySet should not contain TypeableVariable("letter") } "infer that an entity is a knora-api:Resource if there is an rdf:type statement about it and the specified type is a Knora resource class" in { @@ -868,7 +901,7 @@ class GravsearchTypeInspectorSpec extends CoreSpec() with ImplicitSender { assert(result.entities == TypeInferenceResult1.entities) result.entitiesInferredFromProperties.keySet should contain(TypeableVariable("date")) result.entitiesInferredFromProperties.keySet should contain(TypeableIri("http://rdfh.ch/0801/H7s3FmuWTkaCXa54eFANOA".toSmartIri)) - result.entitiesInferredFromProperties.keySet should not contain(TypeableVariable("linkingProp1")) + result.entitiesInferredFromProperties.keySet should not contain TypeableVariable("linkingProp1") } "infer a property's knora-api:objectType if the property's IRI is used as a predicate" in { @@ -976,6 +1009,14 @@ class GravsearchTypeInspectorSpec extends CoreSpec() with ImplicitSender { assert(result.entities == TypeInferenceResult3.entities) } + "ignore Gravsearch options" in { + val typeInspectionRunner = new GravsearchTypeInspectionRunner(responderData = responderData, inferTypes = true) + val parsedQuery = GravsearchParser.parseQuery(QueryWithGravsearchOptions) + val resultFuture: Future[GravsearchTypeInspectionResult] = typeInspectionRunner.inspectTypes(parsedQuery.whereClause, requestingUser = anythingAdminUser) + val result = Await.result(resultFuture, timeout) + assert(result.entities == GravsearchOptionsResult.entities) + } + "reject a query with inconsistent types inferred from statements" in { val typeInspectionRunner = new GravsearchTypeInspectionRunner(responderData = responderData, inferTypes = true) val parsedQuery = GravsearchParser.parseQuery(QueryWithInconsistentTypes1)