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): Allow querying for rdfs:label in Gravsearch #1649

Merged
merged 10 commits into from Jun 3, 2020
33 changes: 33 additions & 0 deletions docs/src/paradox/03-apis/api-v2/query-language.md
Expand Up @@ -616,6 +616,39 @@ CONSTRUCT {
}
```

### Filtering on `rdfs:label`

The `rdfs:label` of a resource is not a Knora value, but you can still search for it.
This can be done in the same ways in the simple or complex schema:

Using a string literal object:

```
?book rdfs:label "Zeitglöcklein des Lebens und Leidens Christi" .
```

Using a variable and a FILTER:

```
?book rdfs:label ?label .
FILTER(?label = "Zeitglöcklein des Lebens und Leidens Christi")
```

Using the `regex` function:

```
?book rdfs:label ?bookLabel .
FILTER regex(?bookLabel, "Zeit", "i")
```

To match words in an `rdfs:label` using the full-text search index, use the
`knora-api:matchLabel` function, which works like `knora-api:matchText`,
except that the first argument is a variable representing a resource:

```
FILTER knora-api:matchLabel(?book, "Zeitglöcklein")
```

### CONSTRUCT Clause

In the `CONSTRUCT` clause of a Gravsearch query, the variable representing the
Expand Down
10 changes: 9 additions & 1 deletion webapi/src/main/scala/org/knora/webapi/OntologyConstants.scala
Expand Up @@ -133,6 +133,13 @@ object OntologyConstants {
val InternalOntologyStart = "http://www.knora.org/ontology"
}

/**
* The object types of resource metadata properties.
*/
val ResourceMetadataPropertyAxioms: Map[IRI, IRI] = Map(
OntologyConstants.Rdfs.Label -> OntologyConstants.Xsd.String
)

/**
* Ontology labels that are reserved for built-in ontologies.
*/
Expand Down Expand Up @@ -922,6 +929,7 @@ object OntologyConstants {
val MatchTextFunction: IRI = KnoraApiV2PrefixExpansion + "matchText"
val MatchInStandoffFunction: IRI = KnoraApiV2PrefixExpansion + "matchInStandoff"
val MatchTextInStandoffFunction: IRI = KnoraApiV2PrefixExpansion + "matchTextInStandoff"
val MatchLabelFunction: IRI = KnoraApiV2PrefixExpansion + "matchLabel"
val StandoffLinkFunction: IRI = KnoraApiV2PrefixExpansion + "standoffLink"
}

Expand Down Expand Up @@ -961,6 +969,7 @@ object OntologyConstants {
val MatchesTextIndex: IRI = KnoraApiV2PrefixExpansion + "matchesTextIndex" // virtual property to be replaced by a triplestore-specific one
val MatchFunction: IRI = KnoraApiV2PrefixExpansion + "match"
val MatchTextFunction: IRI = KnoraApiV2PrefixExpansion + "matchText"
val MatchLabelFunction: IRI = KnoraApiV2PrefixExpansion + "matchLabel"

val ResourceProperty: IRI = KnoraApiV2PrefixExpansion + "resourceProperty"

Expand Down Expand Up @@ -1135,5 +1144,4 @@ object OntologyConstants {
val KnoraExplicitNamedGraph: IRI = "http://www.knora.org/explicit"
val GraphDBExplicitNamedGraph: IRI = "http://www.ontotext.com/explicit"
}

}
Expand Up @@ -135,7 +135,6 @@ object GravsearchQueryChecker {

// A set of predicates that aren't allowed in Gravsearch.
val forbiddenPredicates: Set[IRI] = Set(
OntologyConstants.Rdfs.Label,
OntologyConstants.KnoraApiV2Complex.AttachedToUser,
OntologyConstants.KnoraApiV2Complex.HasPermissions,
OntologyConstants.KnoraApiV2Complex.CreationDate,
Expand Down

Large diffs are not rendered by default.

Expand Up @@ -22,16 +22,17 @@ package org.knora.webapi.responders.v2.search.gravsearch.types
import org.knora.webapi.messages.admin.responder.usersmessages.UserADM
import org.knora.webapi.responders.ResponderData
import org.knora.webapi.responders.v2.search._
import org.knora.webapi.util.StringFormatter
import org.knora.webapi.{GravsearchException, KnoraDispatchers, OntologyConstants}

import scala.concurrent.{ExecutionContext, Future}

/**
* Runs Gravsearch type inspection using one or more type inspector implementations.
*
* @param system the Akka actor system.
* @param inferTypes if true, use type inference.
*/
* Runs Gravsearch type inspection using one or more type inspector implementations.
*
* @param responderData the Knora [[ResponderData]].
* @param inferTypes if true, use type inference.
*/
class GravsearchTypeInspectionRunner(responderData: ResponderData,
inferTypes: Boolean = true) {
private implicit val executionContext: ExecutionContext = responderData.system.dispatchers.lookup(KnoraDispatchers.KnoraActorDispatcher)
Expand All @@ -55,15 +56,17 @@ class GravsearchTypeInspectionRunner(responderData: ResponderData,
)

/**
* Given the WHERE clause from a parsed Gravsearch query, returns information about the types found
* in the query.
*
* @param whereClause the Gravsearch WHERE clause.
* @param requestingUser the requesting user.
* @return the result of the type inspection.
*/
* Given the WHERE clause from a parsed Gravsearch query, returns information about the types found
* in the query.
*
* @param whereClause the Gravsearch WHERE clause.
* @param requestingUser the requesting user.
* @return the result of the type inspection.
*/
def inspectTypes(whereClause: WhereClause,
requestingUser: UserADM): Future[GravsearchTypeInspectionResult] = {
implicit val stringFormatter: StringFormatter = StringFormatter.getGeneralInstance

for {
// Get the set of typeable entities in the Gravsearch query.
typeableEntities: Set[TypeableEntity] <- Future {
Expand Down Expand Up @@ -110,16 +113,16 @@ class GravsearchTypeInspectionRunner(responderData: ResponderData,


/**
* A [[WhereVisitor]] that collects typeable entities from a Gravsearch WHERE clause.
*/
* A [[WhereVisitor]] that collects typeable entities from a Gravsearch WHERE clause.
*/
private class TypeableEntityCollectingWhereVisitor extends WhereVisitor[Set[TypeableEntity]] {
/**
* Collects typeable entities from a statement.
*
* @param statementPattern the pattern to be visited.
* @param acc the accumulator.
* @return the accumulator.
*/
* Collects typeable entities from a statement.
*
* @param statementPattern the pattern to be visited.
* @param acc the accumulator.
* @return the accumulator.
*/
override def visitStatementInWhere(statementPattern: StatementPattern, acc: Set[TypeableEntity]): Set[TypeableEntity] = {
statementPattern.pred match {
case iriRef: IriRef if iriRef.iri.toString == OntologyConstants.Rdf.Type =>
Expand All @@ -133,23 +136,23 @@ class GravsearchTypeInspectionRunner(responderData: ResponderData,
}

/**
* Collects typeable entities from a `FILTER`.
*
* @param filterPattern the pattern to be visited.
* @param acc the accumulator.
* @return the accumulator.
*/
* Collects typeable entities from a `FILTER`.
*
* @param filterPattern the pattern to be visited.
* @param acc the accumulator.
* @return the accumulator.
*/
override def visitFilter(filterPattern: FilterPattern, acc: Set[TypeableEntity]): Set[TypeableEntity] = {
visitFilterExpression(filterPattern.expression, acc)
}

/**
* Collects typeable entities from a filter expression.
*
* @param filterExpression the filter expression to be visited.
* @param acc the accumulator.
* @return the accumulator.
*/
* Collects typeable entities from a filter expression.
*
* @param filterExpression the filter expression to be visited.
* @param acc the accumulator.
* @return the accumulator.
*/
private def visitFilterExpression(filterExpression: Expression, acc: Set[TypeableEntity]): Set[TypeableEntity] = {
filterExpression match {
case compareExpr: CompareExpression =>
Expand Down
Expand Up @@ -265,7 +265,7 @@ class InferringGravsearchTypeInspector(nextInspector: Option[GravsearchTypeInspe
None
}

case other =>
case _ =>
// We don't have the predicate's type.
Set.empty[GravsearchEntityTypeInfo]
}
Expand Down Expand Up @@ -1004,6 +1004,26 @@ class InferringGravsearchTypeInspector(nextInspector: Option[GravsearchTypeInspe
(textVar -> (currentTextVarTypesFromFilters + OntologyConstants.KnoraApiV2Complex.TextValue.toSmartIri))
)

case OntologyConstants.KnoraApiV2Simple.MatchLabelFunction =>
// The first argument is a variable representing a resource.
val resourceVar = TypeableVariable(functionCallExpression.getArgAsQueryVar(0).variableName)
val currentResourceVarTypesFromFilters: Set[SmartIri] = acc.typedEntitiesInFilters.getOrElse(resourceVar, Set.empty[SmartIri])

acc.copy(
typedEntitiesInFilters = acc.typedEntitiesInFilters +
(resourceVar -> (currentResourceVarTypesFromFilters + OntologyConstants.KnoraApiV2Simple.Resource.toSmartIri))
)

case OntologyConstants.KnoraApiV2Complex.MatchLabelFunction =>
// The first argument is a variable representing a resource.
val resourceVar = TypeableVariable(functionCallExpression.getArgAsQueryVar(0).variableName)
val currentResourceVarTypesFromFilters: Set[SmartIri] = acc.typedEntitiesInFilters.getOrElse(resourceVar, Set.empty[SmartIri])

acc.copy(
typedEntitiesInFilters = acc.typedEntitiesInFilters +
(resourceVar -> (currentResourceVarTypesFromFilters + OntologyConstants.KnoraApiV2Complex.Resource.toSmartIri))
)

case OntologyConstants.KnoraApiV2Complex.MatchInStandoffFunction =>
// The first argument is a variable representing a string.
val textVar = TypeableVariable(functionCallExpression.getArgAsQueryVar(0).variableName)
Expand Down
Expand Up @@ -19,7 +19,9 @@

package org.knora.webapi.responders.v2.search.gravsearch.types

import org.knora.webapi.AssertionException
import org.knora.webapi.util.IriConversions._
import org.knora.webapi.util.StringFormatter
import org.knora.webapi.{AssertionException, IRI, OntologyConstants}

/**
* Represents an intermediate result during type inspection. This is different from [[GravsearchTypeInspectionResult]]
Expand Down Expand Up @@ -79,12 +81,29 @@ case class IntermediateTypeInspectionResult(entities: Map[TypeableEntity, Set[Gr

object IntermediateTypeInspectionResult {
/**
* Constructs an [[IntermediateTypeInspectionResult]] for the given set of typeable entities, with no
* types specified.
* Constructs an [[IntermediateTypeInspectionResult]] for the given set of typeable entities, with built-in
* types specified (e.g. for `rdfs:label`).
*
* @param entities the set of typeable entities found in the WHERE clause of a Gravsearch query.
*/
def apply(entities: Set[TypeableEntity]): IntermediateTypeInspectionResult = {
new IntermediateTypeInspectionResult(entities = entities.map(entity => entity -> Set.empty[GravsearchEntityTypeInfo]).toMap)
def apply(entities: Set[TypeableEntity])(implicit stringFormatter: StringFormatter): IntermediateTypeInspectionResult = {
// Make an IntermediateTypeInspectionResult in which each typeable entity has no types.
val emptyResult = new IntermediateTypeInspectionResult(entities = entities.map(entity => entity -> Set.empty[GravsearchEntityTypeInfo]).toMap)

// Collect the typeable IRIs used.
val irisUsed: Set[IRI] = entities.collect {
case typeableIri: TypeableIri => typeableIri.iri.toString
}

// Find the IRIs that represent resource metadata properties, and get their object types.
val resourceMetadataPropertyTypesUsed: Map[TypeableIri, PropertyTypeInfo] = OntologyConstants.ResourceMetadataPropertyAxioms.filterKeys(irisUsed).map {
case (propertyIri, objectTypeIri) => TypeableIri(propertyIri.toSmartIri) -> PropertyTypeInfo(objectTypeIri = objectTypeIri.toSmartIri)
}

// Add those types to the IntermediateTypeInspectionResult.
resourceMetadataPropertyTypesUsed.foldLeft(emptyResult) {
case (acc: IntermediateTypeInspectionResult, (entity: TypeableIri, entityType: PropertyTypeInfo)) =>
acc.addTypes(entity, Set(entityType))
}
}
}
@@ -0,0 +1,58 @@
{
"@graph" : [ {
"@id" : "http://rdfh.ch/0803/c5058f3a",
"@type" : "incunabula:book",
"knora-api:arkUrl" : {
"@type" : "xsd:anyURI",
"@value" : "http://0.0.0.0:3336/ark:/72163/1/0803/c5058f3a5"
},
"knora-api:attachedToProject" : {
"@id" : "http://rdfh.ch/projects/0803"
},
"knora-api:attachedToUser" : {
"@id" : "http://rdfh.ch/users/91e19f1e01"
},
"knora-api:creationDate" : {
"@type" : "xsd:dateTimeStamp",
"@value" : "2016-03-02T15:05:10Z"
},
"knora-api:hasPermissions" : "CR knora-admin:Creator|M knora-admin:ProjectMember|V knora-admin:KnownUser|RV knora-admin:UnknownUser",
"knora-api:userHasPermission" : "RV",
"knora-api:versionArkUrl" : {
"@type" : "xsd:anyURI",
"@value" : "http://0.0.0.0:3336/ark:/72163/1/0803/c5058f3a5.20160302T150510Z"
},
"rdfs:label" : "Zeitglöcklein des Lebens und Leidens Christi"
}, {
"@id" : "http://rdfh.ch/0803/ff17e5ef9601",
"@type" : "incunabula:book",
"knora-api:arkUrl" : {
"@type" : "xsd:anyURI",
"@value" : "http://0.0.0.0:3336/ark:/72163/1/0803/ff17e5ef9601j"
},
"knora-api:attachedToProject" : {
"@id" : "http://rdfh.ch/projects/0803"
},
"knora-api:attachedToUser" : {
"@id" : "http://rdfh.ch/users/91e19f1e01"
},
"knora-api:creationDate" : {
"@type" : "xsd:dateTimeStamp",
"@value" : "2016-03-02T15:05:23Z"
},
"knora-api:hasPermissions" : "CR knora-admin:Creator|M knora-admin:ProjectMember|V knora-admin:KnownUser|RV knora-admin:UnknownUser",
"knora-api:userHasPermission" : "RV",
"knora-api:versionArkUrl" : {
"@type" : "xsd:anyURI",
"@value" : "http://0.0.0.0:3336/ark:/72163/1/0803/ff17e5ef9601j.20160302T150523Z"
},
"rdfs:label" : "Zeitglöcklein des Lebens und Leidens Christi"
} ],
"@context" : {
"rdf" : "http://www.w3.org/1999/02/22-rdf-syntax-ns#",
"knora-api" : "http://api.knora.org/ontology/knora-api/v2#",
"rdfs" : "http://www.w3.org/2000/01/rdf-schema#",
"incunabula" : "http://0.0.0.0:3333/ontology/0803/incunabula/v2#",
"xsd" : "http://www.w3.org/2001/XMLSchema#"
}
}