Skip to content

Commit

Permalink
feat(api-v2): Allow querying for rdfs:label in Gravsearch (#1649)
Browse files Browse the repository at this point in the history
* feat(api-v2): Make the inferring type inspector know the object type of rdfs:label.

* feat(api-v2): Support Gravsearch queries using rdfs:label (ongoing).

* feat(api-v2): Support wildcard searches in resource labels.

* feat(api-v2): Support Lucene searches in rdfs:label (ongoing).

* test(gravsearch): Add tests for wildcard searches with rdfs:label.

* feat(gravsearch): Support the regex function with rdfs:label.

- Add tests.
- Add docs.

* test(gravsearch): Fix tests.

* style(gravsearch): Use apply() instead of newInstance().
  • Loading branch information
Benjamin Geer committed Jun 3, 2020
1 parent bd08f2c commit d56004b
Show file tree
Hide file tree
Showing 12 changed files with 1,579 additions and 672 deletions.
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#"
}
}

0 comments on commit d56004b

Please sign in to comment.