diff --git a/webapi/src/main/scala/org/knora/webapi/messages/util/search/gravsearch/prequery/GravsearchQueryOptimisationFactory.scala b/webapi/src/main/scala/org/knora/webapi/messages/util/search/gravsearch/prequery/GravsearchQueryOptimisationFactory.scala index 23c38cd519..6e29698d01 100644 --- a/webapi/src/main/scala/org/knora/webapi/messages/util/search/gravsearch/prequery/GravsearchQueryOptimisationFactory.scala +++ b/webapi/src/main/scala/org/knora/webapi/messages/util/search/gravsearch/prequery/GravsearchQueryOptimisationFactory.scala @@ -20,8 +20,9 @@ package org.knora.webapi.messages.util.search.gravsearch.prequery import org.knora.webapi.ApiV2Schema +import org.knora.webapi.exceptions.AssertionException import org.knora.webapi.feature.{Feature, FeatureFactory, FeatureFactoryConfig} -import org.knora.webapi.messages.OntologyConstants +import org.knora.webapi.messages.{OntologyConstants, SmartIri} import org.knora.webapi.messages.util.search._ import org.knora.webapi.messages.util.search.gravsearch.types.{ GravsearchTypeInspectionResult, @@ -73,17 +74,91 @@ object GravsearchQueryOptimisationFactory extends FeatureFactory { if (featureFactoryConfig.getToggle("gravsearch-dependency-optimisation").isEnabled) { new ReorderPatternsByDependencyOptimisationFeature(typeInspectionResult, querySchema).optimiseQueryPatterns( new RemoveEntitiesInferredFromPropertyOptimisationFeature(typeInspectionResult, querySchema) - .optimiseQueryPatterns(patterns) + .optimiseQueryPatterns( + new RemoveRedundantKnoraApiResourceOptimisationFeature(typeInspectionResult, querySchema) + .optimiseQueryPatterns(patterns)) ) } else { new RemoveEntitiesInferredFromPropertyOptimisationFeature(typeInspectionResult, querySchema) - .optimiseQueryPatterns(patterns) + .optimiseQueryPatterns( + new RemoveRedundantKnoraApiResourceOptimisationFeature(typeInspectionResult, querySchema) + .optimiseQueryPatterns(patterns)) } } } } } +/** + * Removes a statement with rdf:type knora-api:Resource if there is another rdf:type statement with the same subject + * and a different type. + * + * @param typeInspectionResult the type inspection result. + * @param querySchema the query schema. + */ +class RemoveRedundantKnoraApiResourceOptimisationFeature(typeInspectionResult: GravsearchTypeInspectionResult, + querySchema: ApiV2Schema) + extends GravsearchQueryOptimisationFeature(typeInspectionResult, querySchema) + with Feature { + + /** + * If the specified statement has rdf:type with an IRI as object, returns that IRI, otherwise None. + */ + private def getObjOfRdfType(statementPattern: StatementPattern): Option[SmartIri] = { + statementPattern.pred match { + case predicateIriRef: IriRef => + if (predicateIriRef.iri.toString == OntologyConstants.Rdf.Type) { + statementPattern.obj match { + case iriRef: IriRef => Some(iriRef.iri) + case _ => None + } + } else { + None + } + + case _ => None + } + } + + override def optimiseQueryPatterns(patterns: Seq[QueryPattern]): Seq[QueryPattern] = { + // Make a Set of subjects that have rdf:type statements whose objects are not knora-api:Resource. + val rdfTypesBySubj: Set[Entity] = patterns + .foldLeft(Set.empty[Entity]) { + case (acc, queryPattern: QueryPattern) => + queryPattern match { + case statementPattern: StatementPattern => + getObjOfRdfType(statementPattern) match { + case Some(typeIri) => + if (!OntologyConstants.KnoraApi.KnoraApiV2ResourceIris.contains(typeIri.toString)) { + acc + statementPattern.subj + } else { + acc + } + + case None => acc + } + + case _ => acc + } + } + + patterns.filterNot { + case statementPattern: StatementPattern => + // If this statement has rdf:type knora-api:Resource, and we also have another rdf:type statement + // with the same subject and a different type, remove this statement. + getObjOfRdfType(statementPattern) match { + case Some(typeIri) => + OntologyConstants.KnoraApi.KnoraApiV2ResourceIris + .contains(typeIri.toString) && rdfTypesBySubj.contains(statementPattern.subj) + + case None => false + } + + case _ => false + } + } +} + /** * Optimises a query by removing `rdf:type` statements that are known to be redundant. A redundant * `rdf:type` statement gives the type of a variable whose type is already restricted by its @@ -131,7 +206,8 @@ class RemoveEntitiesInferredFromPropertyOptimisationFeature(typeInspectionResult // Yes. Is this an rdf:type statement? if (predicateIriRef.iri.toString == OntologyConstants.Rdf.Type) { // Yes. Is the subject a typeable entity? - val subjectAsTypeableEntity = GravsearchTypeInspectionUtil.maybeTypeableEntity(statementPattern.subj) + val subjectAsTypeableEntity: Option[TypeableEntity] = + GravsearchTypeInspectionUtil.maybeTypeableEntity(statementPattern.subj) subjectAsTypeableEntity match { case Some(typeableEntity) => 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 b313e5fb7d..afd08f0db5 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 @@ -119,7 +119,7 @@ class AnnotationReadingGravsearchTypeInspector(nextInspector: Option[GravsearchT extends WhereVisitor[Vector[GravsearchTypeAnnotation]] { override def visitStatementInWhere(statementPattern: StatementPattern, acc: Vector[GravsearchTypeAnnotation]): Vector[GravsearchTypeAnnotation] = { - if (GravsearchTypeInspectionUtil.isAnnotationStatement(statementPattern)) { + if (GravsearchTypeInspectionUtil.canBeAnnotationStatement(statementPattern)) { acc :+ annotationStatementToAnnotation(statementPattern, querySchema) } else { acc 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 bb8d9c57ee..7fa9b6768f 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 @@ -173,10 +173,10 @@ object GravsearchTypeInspectionUtil { private class AnnotationRemovingWhereTransformer extends WhereTransformer { override def transformStatementInWhere(statementPattern: StatementPattern, inputOrderBy: Seq[OrderCriterion]): Seq[QueryPattern] = { - if (!isAnnotationStatement(statementPattern)) { - Seq(statementPattern) - } else { + if (mustBeAnnotationStatement(statementPattern)) { Seq.empty[QueryPattern] + } else { + Seq(statementPattern) } } @@ -209,12 +209,43 @@ object GravsearchTypeInspectionUtil { } /** - * Determines whether a statement pattern represents a Gravsearch type annotation. + * Determines whether a statement pattern must represent a Gravsearch type annotation. + * + * @param statementPattern the statement pattern. + * @return `true` if the statement pattern must represent a type annotation. + */ + def mustBeAnnotationStatement(statementPattern: StatementPattern): Boolean = { + // Does the statement have rdf:type knora-api:Resource (which is not necessarily a Gravsearch type annotation)? + def hasRdfTypeKnoraApiResource: Boolean = { + statementPattern.pred match { + case predIriRef: IriRef => + if (predIriRef.iri.toString == OntologyConstants.Rdf.Type) { + statementPattern.obj match { + case objIriRef: IriRef => + OntologyConstants.KnoraApi.KnoraApiV2ResourceIris.contains(objIriRef.iri.toString) + + case _ => false + } + } else { + false + } + + case _ => false + } + } + + // If the statement can be a type annotation and doesn't have rdf:type knora-api:Resource, return true. + // Otherwise, return false. + canBeAnnotationStatement(statementPattern) && !hasRdfTypeKnoraApiResource + } + + /** + * Determines whether a statement pattern can represent a Gravsearch type annotation. * * @param statementPattern the statement pattern. - * @return `true` if the statement pattern represents a type annotation. + * @return `true` if the statement pattern can represent a type annotation. */ - def isAnnotationStatement(statementPattern: StatementPattern): Boolean = { + def canBeAnnotationStatement(statementPattern: StatementPattern): Boolean = { /** * Returns `true` if an entity is an IRI representing a type that is valid for use in a type annotation. diff --git a/webapi/src/test/scala/org/knora/webapi/messages/util/search/gravsearch/prequery/NonTriplestoreSpecificGravsearchToPrequeryTransformerSpec.scala b/webapi/src/test/scala/org/knora/webapi/messages/util/search/gravsearch/prequery/NonTriplestoreSpecificGravsearchToPrequeryTransformerSpec.scala index 2893741de9..7e2ad8dbb2 100644 --- a/webapi/src/test/scala/org/knora/webapi/messages/util/search/gravsearch/prequery/NonTriplestoreSpecificGravsearchToPrequeryTransformerSpec.scala +++ b/webapi/src/test/scala/org/knora/webapi/messages/util/search/gravsearch/prequery/NonTriplestoreSpecificGravsearchToPrequeryTransformerSpec.scala @@ -3050,6 +3050,18 @@ class NonTriplestoreSpecificGravsearchToPrequeryTransformerSpec extends CoreSpec useDistinct = true ) + val queryWithKnoraApiResource: String = + """PREFIX knora-api: + |CONSTRUCT { + | ?resource knora-api:isMainResource true . + | ?resource ?p ?text . + |} WHERE { + | ?resource a knora-api:Resource . + | ?resource ?p ?text . + | ?p knora-api:objectType knora-api:TextValue . + | FILTER knora-api:matchText(?text, "der") + |}""".stripMargin + "The NonTriplestoreSpecificGravsearchToPrequeryGenerator object" should { "transform an input query with an optional property criterion without removing the rdf:type statement" in { @@ -3311,5 +3323,24 @@ class NonTriplestoreSpecificGravsearchToPrequeryTransformerSpec extends CoreSpec assert(transformedQuery == transformedQueryToReorderWithCycle) } + + "not remove rdf:type knora-api:Resource if it's needed" in { + val transformedQuery = + QueryHandler.transformQuery(queryWithKnoraApiResource, responderData, settings, defaultFeatureFactoryConfig) + + assert( + transformedQuery.whereClause.patterns.contains(StatementPattern( + subj = QueryVariable(variableName = "resource"), + pred = IriRef( + iri = "http://www.w3.org/1999/02/22-rdf-syntax-ns#type".toSmartIri, + propertyPathOperator = None + ), + obj = IriRef( + iri = "http://www.knora.org/ontology/knora-base#Resource".toSmartIri, + propertyPathOperator = None + ), + namedGraph = None + ))) + } } }