Skip to content

Commit d56004b

Browse files
author
Benjamin Geer
authored
feat(api-v2): Allow querying for rdfs:label in Gravsearch (#1649)
* 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().
1 parent bd08f2c commit d56004b

12 files changed

Lines changed: 1579 additions & 672 deletions

File tree

‎docs/src/paradox/03-apis/api-v2/query-language.md‎

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -616,6 +616,39 @@ CONSTRUCT {
616616
}
617617
```
618618

619+
### Filtering on `rdfs:label`
620+
621+
The `rdfs:label` of a resource is not a Knora value, but you can still search for it.
622+
This can be done in the same ways in the simple or complex schema:
623+
624+
Using a string literal object:
625+
626+
```
627+
?book rdfs:label "Zeitglöcklein des Lebens und Leidens Christi" .
628+
```
629+
630+
Using a variable and a FILTER:
631+
632+
```
633+
?book rdfs:label ?label .
634+
FILTER(?label = "Zeitglöcklein des Lebens und Leidens Christi")
635+
```
636+
637+
Using the `regex` function:
638+
639+
```
640+
?book rdfs:label ?bookLabel .
641+
FILTER regex(?bookLabel, "Zeit", "i")
642+
```
643+
644+
To match words in an `rdfs:label` using the full-text search index, use the
645+
`knora-api:matchLabel` function, which works like `knora-api:matchText`,
646+
except that the first argument is a variable representing a resource:
647+
648+
```
649+
FILTER knora-api:matchLabel(?book, "Zeitglöcklein")
650+
```
651+
619652
### CONSTRUCT Clause
620653

621654
In the `CONSTRUCT` clause of a Gravsearch query, the variable representing the

‎webapi/src/main/scala/org/knora/webapi/OntologyConstants.scala‎

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,13 @@ object OntologyConstants {
133133
val InternalOntologyStart = "http://www.knora.org/ontology"
134134
}
135135

136+
/**
137+
* The object types of resource metadata properties.
138+
*/
139+
val ResourceMetadataPropertyAxioms: Map[IRI, IRI] = Map(
140+
OntologyConstants.Rdfs.Label -> OntologyConstants.Xsd.String
141+
)
142+
136143
/**
137144
* Ontology labels that are reserved for built-in ontologies.
138145
*/
@@ -922,6 +929,7 @@ object OntologyConstants {
922929
val MatchTextFunction: IRI = KnoraApiV2PrefixExpansion + "matchText"
923930
val MatchInStandoffFunction: IRI = KnoraApiV2PrefixExpansion + "matchInStandoff"
924931
val MatchTextInStandoffFunction: IRI = KnoraApiV2PrefixExpansion + "matchTextInStandoff"
932+
val MatchLabelFunction: IRI = KnoraApiV2PrefixExpansion + "matchLabel"
925933
val StandoffLinkFunction: IRI = KnoraApiV2PrefixExpansion + "standoffLink"
926934
}
927935

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

965974
val ResourceProperty: IRI = KnoraApiV2PrefixExpansion + "resourceProperty"
966975

@@ -1135,5 +1144,4 @@ object OntologyConstants {
11351144
val KnoraExplicitNamedGraph: IRI = "http://www.knora.org/explicit"
11361145
val GraphDBExplicitNamedGraph: IRI = "http://www.ontotext.com/explicit"
11371146
}
1138-
11391147
}

‎webapi/src/main/scala/org/knora/webapi/responders/v2/search/gravsearch/GravsearchQueryChecker.scala‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,6 @@ object GravsearchQueryChecker {
135135

136136
// A set of predicates that aren't allowed in Gravsearch.
137137
val forbiddenPredicates: Set[IRI] = Set(
138-
OntologyConstants.Rdfs.Label,
139138
OntologyConstants.KnoraApiV2Complex.AttachedToUser,
140139
OntologyConstants.KnoraApiV2Complex.HasPermissions,
141140
OntologyConstants.KnoraApiV2Complex.CreationDate,

‎webapi/src/main/scala/org/knora/webapi/responders/v2/search/gravsearch/prequery/AbstractPrequeryGenerator.scala‎

Lines changed: 245 additions & 145 deletions
Large diffs are not rendered by default.

‎webapi/src/main/scala/org/knora/webapi/responders/v2/search/gravsearch/types/GravsearchTypeInspectionRunner.scala‎

Lines changed: 35 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -22,16 +22,17 @@ package org.knora.webapi.responders.v2.search.gravsearch.types
2222
import org.knora.webapi.messages.admin.responder.usersmessages.UserADM
2323
import org.knora.webapi.responders.ResponderData
2424
import org.knora.webapi.responders.v2.search._
25+
import org.knora.webapi.util.StringFormatter
2526
import org.knora.webapi.{GravsearchException, KnoraDispatchers, OntologyConstants}
2627

2728
import scala.concurrent.{ExecutionContext, Future}
2829

2930
/**
30-
* Runs Gravsearch type inspection using one or more type inspector implementations.
31-
*
32-
* @param system the Akka actor system.
33-
* @param inferTypes if true, use type inference.
34-
*/
31+
* Runs Gravsearch type inspection using one or more type inspector implementations.
32+
*
33+
* @param responderData the Knora [[ResponderData]].
34+
* @param inferTypes if true, use type inference.
35+
*/
3536
class GravsearchTypeInspectionRunner(responderData: ResponderData,
3637
inferTypes: Boolean = true) {
3738
private implicit val executionContext: ExecutionContext = responderData.system.dispatchers.lookup(KnoraDispatchers.KnoraActorDispatcher)
@@ -55,15 +56,17 @@ class GravsearchTypeInspectionRunner(responderData: ResponderData,
5556
)
5657

5758
/**
58-
* Given the WHERE clause from a parsed Gravsearch query, returns information about the types found
59-
* in the query.
60-
*
61-
* @param whereClause the Gravsearch WHERE clause.
62-
* @param requestingUser the requesting user.
63-
* @return the result of the type inspection.
64-
*/
59+
* Given the WHERE clause from a parsed Gravsearch query, returns information about the types found
60+
* in the query.
61+
*
62+
* @param whereClause the Gravsearch WHERE clause.
63+
* @param requestingUser the requesting user.
64+
* @return the result of the type inspection.
65+
*/
6566
def inspectTypes(whereClause: WhereClause,
6667
requestingUser: UserADM): Future[GravsearchTypeInspectionResult] = {
68+
implicit val stringFormatter: StringFormatter = StringFormatter.getGeneralInstance
69+
6770
for {
6871
// Get the set of typeable entities in the Gravsearch query.
6972
typeableEntities: Set[TypeableEntity] <- Future {
@@ -110,16 +113,16 @@ class GravsearchTypeInspectionRunner(responderData: ResponderData,
110113

111114

112115
/**
113-
* A [[WhereVisitor]] that collects typeable entities from a Gravsearch WHERE clause.
114-
*/
116+
* A [[WhereVisitor]] that collects typeable entities from a Gravsearch WHERE clause.
117+
*/
115118
private class TypeableEntityCollectingWhereVisitor extends WhereVisitor[Set[TypeableEntity]] {
116119
/**
117-
* Collects typeable entities from a statement.
118-
*
119-
* @param statementPattern the pattern to be visited.
120-
* @param acc the accumulator.
121-
* @return the accumulator.
122-
*/
120+
* Collects typeable entities from a statement.
121+
*
122+
* @param statementPattern the pattern to be visited.
123+
* @param acc the accumulator.
124+
* @return the accumulator.
125+
*/
123126
override def visitStatementInWhere(statementPattern: StatementPattern, acc: Set[TypeableEntity]): Set[TypeableEntity] = {
124127
statementPattern.pred match {
125128
case iriRef: IriRef if iriRef.iri.toString == OntologyConstants.Rdf.Type =>
@@ -133,23 +136,23 @@ class GravsearchTypeInspectionRunner(responderData: ResponderData,
133136
}
134137

135138
/**
136-
* Collects typeable entities from a `FILTER`.
137-
*
138-
* @param filterPattern the pattern to be visited.
139-
* @param acc the accumulator.
140-
* @return the accumulator.
141-
*/
139+
* Collects typeable entities from a `FILTER`.
140+
*
141+
* @param filterPattern the pattern to be visited.
142+
* @param acc the accumulator.
143+
* @return the accumulator.
144+
*/
142145
override def visitFilter(filterPattern: FilterPattern, acc: Set[TypeableEntity]): Set[TypeableEntity] = {
143146
visitFilterExpression(filterPattern.expression, acc)
144147
}
145148

146149
/**
147-
* Collects typeable entities from a filter expression.
148-
*
149-
* @param filterExpression the filter expression to be visited.
150-
* @param acc the accumulator.
151-
* @return the accumulator.
152-
*/
150+
* Collects typeable entities from a filter expression.
151+
*
152+
* @param filterExpression the filter expression to be visited.
153+
* @param acc the accumulator.
154+
* @return the accumulator.
155+
*/
153156
private def visitFilterExpression(filterExpression: Expression, acc: Set[TypeableEntity]): Set[TypeableEntity] = {
154157
filterExpression match {
155158
case compareExpr: CompareExpression =>

‎webapi/src/main/scala/org/knora/webapi/responders/v2/search/gravsearch/types/InferringGravsearchTypeInspector.scala‎

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -265,7 +265,7 @@ class InferringGravsearchTypeInspector(nextInspector: Option[GravsearchTypeInspe
265265
None
266266
}
267267

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

1007+
case OntologyConstants.KnoraApiV2Simple.MatchLabelFunction =>
1008+
// The first argument is a variable representing a resource.
1009+
val resourceVar = TypeableVariable(functionCallExpression.getArgAsQueryVar(0).variableName)
1010+
val currentResourceVarTypesFromFilters: Set[SmartIri] = acc.typedEntitiesInFilters.getOrElse(resourceVar, Set.empty[SmartIri])
1011+
1012+
acc.copy(
1013+
typedEntitiesInFilters = acc.typedEntitiesInFilters +
1014+
(resourceVar -> (currentResourceVarTypesFromFilters + OntologyConstants.KnoraApiV2Simple.Resource.toSmartIri))
1015+
)
1016+
1017+
case OntologyConstants.KnoraApiV2Complex.MatchLabelFunction =>
1018+
// The first argument is a variable representing a resource.
1019+
val resourceVar = TypeableVariable(functionCallExpression.getArgAsQueryVar(0).variableName)
1020+
val currentResourceVarTypesFromFilters: Set[SmartIri] = acc.typedEntitiesInFilters.getOrElse(resourceVar, Set.empty[SmartIri])
1021+
1022+
acc.copy(
1023+
typedEntitiesInFilters = acc.typedEntitiesInFilters +
1024+
(resourceVar -> (currentResourceVarTypesFromFilters + OntologyConstants.KnoraApiV2Complex.Resource.toSmartIri))
1025+
)
1026+
10071027
case OntologyConstants.KnoraApiV2Complex.MatchInStandoffFunction =>
10081028
// The first argument is a variable representing a string.
10091029
val textVar = TypeableVariable(functionCallExpression.getArgAsQueryVar(0).variableName)

‎webapi/src/main/scala/org/knora/webapi/responders/v2/search/gravsearch/types/IntermediateTypeInspectionResult.scala‎

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,9 @@
1919

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

22-
import org.knora.webapi.AssertionException
22+
import org.knora.webapi.util.IriConversions._
23+
import org.knora.webapi.util.StringFormatter
24+
import org.knora.webapi.{AssertionException, IRI, OntologyConstants}
2325

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

8082
object IntermediateTypeInspectionResult {
8183
/**
82-
* Constructs an [[IntermediateTypeInspectionResult]] for the given set of typeable entities, with no
83-
* types specified.
84+
* Constructs an [[IntermediateTypeInspectionResult]] for the given set of typeable entities, with built-in
85+
* types specified (e.g. for `rdfs:label`).
8486
*
8587
* @param entities the set of typeable entities found in the WHERE clause of a Gravsearch query.
8688
*/
87-
def apply(entities: Set[TypeableEntity]): IntermediateTypeInspectionResult = {
88-
new IntermediateTypeInspectionResult(entities = entities.map(entity => entity -> Set.empty[GravsearchEntityTypeInfo]).toMap)
89+
def apply(entities: Set[TypeableEntity])(implicit stringFormatter: StringFormatter): IntermediateTypeInspectionResult = {
90+
// Make an IntermediateTypeInspectionResult in which each typeable entity has no types.
91+
val emptyResult = new IntermediateTypeInspectionResult(entities = entities.map(entity => entity -> Set.empty[GravsearchEntityTypeInfo]).toMap)
92+
93+
// Collect the typeable IRIs used.
94+
val irisUsed: Set[IRI] = entities.collect {
95+
case typeableIri: TypeableIri => typeableIri.iri.toString
96+
}
97+
98+
// Find the IRIs that represent resource metadata properties, and get their object types.
99+
val resourceMetadataPropertyTypesUsed: Map[TypeableIri, PropertyTypeInfo] = OntologyConstants.ResourceMetadataPropertyAxioms.filterKeys(irisUsed).map {
100+
case (propertyIri, objectTypeIri) => TypeableIri(propertyIri.toSmartIri) -> PropertyTypeInfo(objectTypeIri = objectTypeIri.toSmartIri)
101+
}
102+
103+
// Add those types to the IntermediateTypeInspectionResult.
104+
resourceMetadataPropertyTypesUsed.foldLeft(emptyResult) {
105+
case (acc: IntermediateTypeInspectionResult, (entity: TypeableIri, entityType: PropertyTypeInfo)) =>
106+
acc.addTypes(entity, Set(entityType))
107+
}
89108
}
90109
}
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
{
2+
"@graph" : [ {
3+
"@id" : "http://rdfh.ch/0803/c5058f3a",
4+
"@type" : "incunabula:book",
5+
"knora-api:arkUrl" : {
6+
"@type" : "xsd:anyURI",
7+
"@value" : "http://0.0.0.0:3336/ark:/72163/1/0803/c5058f3a5"
8+
},
9+
"knora-api:attachedToProject" : {
10+
"@id" : "http://rdfh.ch/projects/0803"
11+
},
12+
"knora-api:attachedToUser" : {
13+
"@id" : "http://rdfh.ch/users/91e19f1e01"
14+
},
15+
"knora-api:creationDate" : {
16+
"@type" : "xsd:dateTimeStamp",
17+
"@value" : "2016-03-02T15:05:10Z"
18+
},
19+
"knora-api:hasPermissions" : "CR knora-admin:Creator|M knora-admin:ProjectMember|V knora-admin:KnownUser|RV knora-admin:UnknownUser",
20+
"knora-api:userHasPermission" : "RV",
21+
"knora-api:versionArkUrl" : {
22+
"@type" : "xsd:anyURI",
23+
"@value" : "http://0.0.0.0:3336/ark:/72163/1/0803/c5058f3a5.20160302T150510Z"
24+
},
25+
"rdfs:label" : "Zeitglöcklein des Lebens und Leidens Christi"
26+
}, {
27+
"@id" : "http://rdfh.ch/0803/ff17e5ef9601",
28+
"@type" : "incunabula:book",
29+
"knora-api:arkUrl" : {
30+
"@type" : "xsd:anyURI",
31+
"@value" : "http://0.0.0.0:3336/ark:/72163/1/0803/ff17e5ef9601j"
32+
},
33+
"knora-api:attachedToProject" : {
34+
"@id" : "http://rdfh.ch/projects/0803"
35+
},
36+
"knora-api:attachedToUser" : {
37+
"@id" : "http://rdfh.ch/users/91e19f1e01"
38+
},
39+
"knora-api:creationDate" : {
40+
"@type" : "xsd:dateTimeStamp",
41+
"@value" : "2016-03-02T15:05:23Z"
42+
},
43+
"knora-api:hasPermissions" : "CR knora-admin:Creator|M knora-admin:ProjectMember|V knora-admin:KnownUser|RV knora-admin:UnknownUser",
44+
"knora-api:userHasPermission" : "RV",
45+
"knora-api:versionArkUrl" : {
46+
"@type" : "xsd:anyURI",
47+
"@value" : "http://0.0.0.0:3336/ark:/72163/1/0803/ff17e5ef9601j.20160302T150523Z"
48+
},
49+
"rdfs:label" : "Zeitglöcklein des Lebens und Leidens Christi"
50+
} ],
51+
"@context" : {
52+
"rdf" : "http://www.w3.org/1999/02/22-rdf-syntax-ns#",
53+
"knora-api" : "http://api.knora.org/ontology/knora-api/v2#",
54+
"rdfs" : "http://www.w3.org/2000/01/rdf-schema#",
55+
"incunabula" : "http://0.0.0.0:3333/ontology/0803/incunabula/v2#",
56+
"xsd" : "http://www.w3.org/2001/XMLSchema#"
57+
}
58+
}

0 commit comments

Comments
 (0)