Skip to content

Commit

Permalink
feat: Add http metrics to all search endpoints by migrating to tapir D…
Browse files Browse the repository at this point in the history
  • Loading branch information
seakayone authored Dec 6, 2023
1 parent e4ca484 commit 20f8d5c
Show file tree
Hide file tree
Showing 11 changed files with 411 additions and 375 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,6 @@ import org.knora.webapi.messages.util.rdf.JsonLDUtil
import org.knora.webapi.messages.util.search.SparqlQueryConstants
import org.knora.webapi.routing.UnsafeZioRun
import org.knora.webapi.routing.v2.ResourcesRouteV2
import org.knora.webapi.routing.v2.SearchRouteV2
import org.knora.webapi.routing.v2.StandoffRouteV2
import org.knora.webapi.routing.v2.ValuesRouteV2
import org.knora.webapi.sharedtestdata.SharedTestDataADM
Expand All @@ -54,13 +53,9 @@ import pekko.http.scaladsl.model.headers.BasicHttpCredentials
*/
class SearchRouteV2R2RSpec extends R2RSpec {
private implicit val stringFormatter: StringFormatter = StringFormatter.getGeneralInstance
private val searchPathNew = UnsafeZioRun
private val searchPath = UnsafeZioRun
.runOrThrow(ZIO.serviceWith[SearchApiRoutes](_.routes))
.reduce(_ ~ _)
private val searchPathOld = DSPApiDirectives.handleErrors(appConfig)(
SearchRouteV2(routeData.appConfig.v2.fulltextSearch.searchValueMinLength).makeRoute
)
private val searchPath = searchPathNew ~ searchPathOld

private val resourcePath =
DSPApiDirectives.handleErrors(appConfig)(ResourcesRouteV2(appConfig).makeRoute)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import org.knora.webapi.responders.v2.ResourcesResponseCheckerV2.compareReadReso
import org.knora.webapi.routing.UnsafeZioRun
import org.knora.webapi.sharedtestdata.SharedTestDataADM
import org.knora.webapi.sharedtestdata.SharedTestDataADM.anonymousUser
import org.knora.webapi.slice.resourceinfo.domain.IriConverter
import org.knora.webapi.util.ZioScalaTestUtil.assertFailsWithA

class SearchResponderV2Spec extends CoreSpec {
Expand Down Expand Up @@ -151,60 +152,84 @@ class SearchResponderV2Spec extends CoreSpec {
}

"perform a search by label for incunabula:book that contain 'Narrenschiff'" in {
val result = UnsafeZioRun.runOrThrow(
SearchResponderV2.searchResourcesByLabelV2(
searchValue = "Narrenschiff",
offset = 0,
limitToProject = None,
limitToResourceClass = Some("http://www.knora.org/ontology/0803/incunabula#book".toSmartIri), // internal Iri!
targetSchema = ApiV2Complex,
requestingUser = anonymousUser
)
)

assert(result.resources.size == 3)
val actual = UnsafeZioRun.runOrThrow {
for {
limitToResourceClass <- IriConverter
.asSmartIri("http://www.knora.org/ontology/0803/incunabula#book")
.mapAttempt(_.toOntologySchema(ApiV2Complex))
.map(Some(_))
result <- SearchResponderV2.searchResourcesByLabelV2(
searchValue = "Narrenschiff",
offset = 0,
limitToProject = None,
limitToResourceClass,
targetSchema = ApiV2Complex,
requestingUser = anonymousUser
)
} yield result
}

assert(actual.resources.size == 3)
}

"perform a search by label for incunabula:book that contain 'Das Narrenschiff'" in {
val result = UnsafeZioRun.runOrThrow(
SearchResponderV2.searchResourcesByLabelV2(
searchValue = "Narrenschiff",
offset = 0,
limitToProject = None,
limitToResourceClass = Some("http://www.knora.org/ontology/0803/incunabula#book".toSmartIri), // internal Iri!
targetSchema = ApiV2Complex,
requestingUser = anonymousUser
)
)

assert(result.resources.size == 3)
val actual = UnsafeZioRun.runOrThrow {
for {
limitToResourceClass <- IriConverter
.asSmartIri("http://www.knora.org/ontology/0803/incunabula#book")
.mapAttempt(_.toOntologySchema(ApiV2Complex))
.map(Some(_))
result <- SearchResponderV2.searchResourcesByLabelV2(
searchValue = "Narrenschiff",
offset = 0,
limitToProject = None,
limitToResourceClass,
targetSchema = ApiV2Complex,
requestingUser = anonymousUser
)
} yield result
}

assert(actual.resources.size == 3)
}

"perform a count search query by label for incunabula:book that contain 'Narrenschiff'" in {

val result = UnsafeZioRun.runOrThrow(
SearchResponderV2.searchResourcesByLabelCountV2(
searchValue = "Narrenschiff",
limitToProject = None,
limitToResourceClass = Some("http://www.knora.org/ontology/0803/incunabula#book".toSmartIri)
)
)

assert(result.numberOfResources == 3)
val actual = UnsafeZioRun.runOrThrow {
for {
limitToResourceClass <- IriConverter
.asSmartIri("http://www.knora.org/ontology/0803/incunabula#book")
.mapAttempt(_.toOntologySchema(ApiV2Complex))
.map(Some(_))
result <- SearchResponderV2.searchResourcesByLabelCountV2(
searchValue = "Narrenschiff",
limitToProject = None,
limitToResourceClass
)
} yield result
}

assert(actual.numberOfResources == 3)

}

"perform a a count search query by label for incunabula:book that contain 'Passio sancti Meynrhadi martyris et heremite'" in {

val result = UnsafeZioRun.runOrThrow(
SearchResponderV2.searchResourcesByLabelCountV2(
searchValue = "Passio sancti Meynrhadi martyris et heremite",
limitToProject = None,
limitToResourceClass = Some("http://www.knora.org/ontology/0803/incunabula#book".toSmartIri)
)
)

assert(result.numberOfResources == 1)
val actual = UnsafeZioRun.runOrThrow {
for {
limitToResourceClass <- IriConverter
.asSmartIri("http://www.knora.org/ontology/0803/incunabula#book")
.mapAttempt(_.toOntologySchema(ApiV2Complex))
.map(Some(_))
result <- SearchResponderV2.searchResourcesByLabelCountV2(
searchValue = "Passio sancti Meynrhadi martyris et heremite",
limitToProject = None,
limitToResourceClass
)
} yield result
}

assert(actual.numberOfResources == 1)
}

"search by project and resource class" in {
Expand Down
9 changes: 9 additions & 0 deletions webapi/src/main/scala/dsp/valueobjects/Iri.scala
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,15 @@ object Iri {
if (isUserIri(iri)) toSparqlEncodedString(iri)
else None

/**
*/
final case class SimpleIri private (value: String) extends Iri
object SimpleIri {
def from(value: String): Either[String, Iri] =
if (isIri(value)) Right(SimpleIri(value))
else Left(s"Invalid IRI: $value")
}

/**
* GroupIri value object.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ import org.knora.webapi.IRI
import org.knora.webapi.RdfMediaTypes
import org.knora.webapi.Rendering
import org.knora.webapi.SchemaOptions
import org.knora.webapi.slice.common.api.ApiV2

/**
* A trait for supported RDF formats.
Expand Down Expand Up @@ -57,11 +56,12 @@ sealed trait QuadFormat extends NonJsonLD

object RdfFormat {

val default: RdfFormat = JsonLD

val values: Seq[RdfFormat] = Seq(JsonLD, Turtle, TriG, RdfXml, NQuads)

def from(mediaType: model.MediaType): RdfFormat = values
def from(mediaType: model.MediaType): Option[RdfFormat] = values
.find(_.mediaType.equalsIgnoreParameters(mediaType))
.getOrElse(ApiV2.Inputs.defaultRdfFormat)

/**
* Converts a [[MediaType]] to an [[RdfFormat]].
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import dsp.errors.AssertionException
import dsp.errors.BadRequestException
import dsp.errors.GravsearchException
import dsp.errors.InconsistentRepositoryDataException
import dsp.valueobjects.Iri
import org.knora.webapi.*
import org.knora.webapi.config.AppConfig
import org.knora.webapi.core.MessageRelay
Expand Down Expand Up @@ -168,7 +169,7 @@ trait SearchResponderV2 {
* @return a [[ResourceCountV2]] representing the resources that have been found.
*/
def searchResourcesByLabelCountV2(
searchValue: IRI,
searchValue: String,
limitToProject: Option[ProjectIri],
limitToResourceClass: Option[SmartIri]
): Task[ResourceCountV2]
Expand All @@ -185,7 +186,7 @@ trait SearchResponderV2 {
* @return a [[ReadResourcesSequenceV2]] representing the resources that have been found.
*/
def searchResourcesByLabelV2(
searchValue: IRI,
searchValue: String,
offset: RuntimeFlags,
limitToProject: Option[ProjectIri],
limitToResourceClass: Option[SmartIri],
Expand Down Expand Up @@ -225,12 +226,14 @@ final case class SearchResponderV2Live(
private val sparqlTransformerLive: OntologyInferencer,
private val gravsearchTypeInspectionRunner: GravsearchTypeInspectionRunner,
private val inferenceOptimizationService: InferenceOptimizationService,
implicit private val stringFormatter: StringFormatter,
private val stringFormatter: StringFormatter,
private val iriConverter: IriConverter,
private val constructTransformer: ConstructTransformer
) extends SearchResponderV2
with LazyLogging {

private implicit val sf: StringFormatter = stringFormatter

/**
* Performs a fulltext search and returns the resources count (how many resources match the search criteria),
* without taking into consideration permission checking.
Expand All @@ -250,6 +253,10 @@ final case class SearchResponderV2Live(
limitToStandoffClass: Option[SmartIri]
): Task[ResourceCountV2] =
for {
_ <- ensureIsFulltextSearch(searchValue)
searchValue <- validateSearchString(searchValue)
limitToResourceClass <- ZIO.foreach(limitToResourceClass)(ensureResourceClassIri)
limitToStandoffClass <- ZIO.foreach(limitToStandoffClass)(ensureStandoffClass)
countSparql <- ZIO.attempt(
sparql.v2.txt
.searchFulltext(
Expand Down Expand Up @@ -299,6 +306,10 @@ final case class SearchResponderV2Live(
): Task[ReadResourcesSequenceV2] = {
import org.knora.webapi.messages.util.search.FullTextMainQueryGenerator.FullTextSearchConstants
for {
_ <- ensureIsFulltextSearch(searchValue)
searchValue <- validateSearchString(searchValue)
limitToResourceClass <- ZIO.foreach(limitToResourceClass)(ensureResourceClassIri)
limitToStandoffClass <- ZIO.foreach(limitToStandoffClass)(ensureStandoffClass)
searchSparql <-
ZIO.attempt(
sparql.v2.txt
Expand Down Expand Up @@ -849,14 +860,17 @@ final case class SearchResponderV2Live(
}

override def searchResourcesByLabelCountV2(
searchValue: IRI,
searchValue: String,
limitToProject: Option[ProjectIri],
limitToResourceClass: Option[SmartIri]
): Task[ResourceCountV2] = {
val searchTerm = MatchStringWhileTyping(searchValue).generateLiteralForLuceneIndexWithoutExactSequence
val countSparql =
SearchQueries.selectCountByLabel(searchTerm, limitToProject.map(_.value), limitToResourceClass.map(_.toString))
): Task[ResourceCountV2] =
for {
searchValue <- validateSearchString(searchValue)
_ <- ensureIsFulltextSearch(searchValue)
limitToResourceClass <- ZIO.foreach(limitToResourceClass)(ensureResourceClassIri)
searchTerm = MatchStringWhileTyping(searchValue).generateLiteralForLuceneIndexWithoutExactSequence
countSparql =
SearchQueries.selectCountByLabel(searchTerm, limitToProject.map(_.value), limitToResourceClass.map(_.toString))
countResponse <- triplestore.query(countSparql)

count <- // query response should contain one result with one row with the name "count"
Expand All @@ -870,6 +884,36 @@ final case class SearchResponderV2Live(
.as(countResponse.results.bindings.head.rowMap("count"))

} yield ResourceCountV2(count.toInt)

private def ensureResourceClassIri(resourceClassIri: SmartIri): Task[SmartIri] = {
val errMsg = s"Resource class IRI <$resourceClassIri> is not a valid Knora API v2 entity IRI"
if (resourceClassIri.isKnoraApiV2EntityIri) {
iriConverter.asInternalSmartIri(resourceClassIri).orElseFail(BadRequestException(errMsg))
} else { ZIO.fail(BadRequestException(errMsg)) }
}

private def ensureStandoffClass(standoffClassIri: SmartIri): Task[SmartIri] = {
val errMsg = s"Invalid standoff class IRI: $standoffClassIri"
if (standoffClassIri.isApiV2ComplexSchema) {
iriConverter.asInternalSmartIri(standoffClassIri).orElseFail(BadRequestException(errMsg))
} else { ZIO.fail(BadRequestException(errMsg)) }
}

private def ensureIsFulltextSearch(searchStr: String) =
ZIO
.fail(BadRequestException("It looks like you are submitting a Gravsearch request to a full-text search route"))
.when(searchStr.contains(OntologyConstants.KnoraApi.ApiOntologyHostname))

private def validateSearchString(searchStr: String) = {
val searchValueMinLength = appConfig.v2.fulltextSearch.searchValueMinLength
ZIO
.fromOption(Iri.toSparqlEncodedString(searchStr))
.orElseFail(throw BadRequestException(s"Invalid search string: '$searchStr'"))
.filterOrElseWith(_.length >= searchValueMinLength) { it =>
val errorMsg =
s"A search value is expected to have at least length of $searchValueMinLength, but '$it' given of length ${it.length}."
ZIO.fail(BadRequestException(errorMsg))
}
}

override def searchResourcesByLabelV2(
Expand All @@ -882,15 +926,18 @@ final case class SearchResponderV2Live(
): Task[ReadResourcesSequenceV2] = {
val searchLimit = appConfig.v2.resourcesSequence.resultsPerPage
val searchOffset = offset * appConfig.v2.resourcesSequence.resultsPerPage
val searchTerm = MatchStringWhileTyping(searchValue).generateLiteralForLuceneIndexWithoutExactSequence
val searchResourceByLabelSparql = SearchQueries.constructSearchByLabel(
searchTerm,
limitToResourceClass.map(_.toIri),
limitToProject.map(_.value),
searchLimit,
searchOffset
)
for {
searchValue <- validateSearchString(searchValue)
_ <- ensureIsFulltextSearch(searchValue)
limitToResourceClass <- ZIO.foreach(limitToResourceClass)(ensureResourceClassIri)
searchTerm = MatchStringWhileTyping(searchValue).generateLiteralForLuceneIndexWithoutExactSequence
searchResourceByLabelSparql = SearchQueries.constructSearchByLabel(
searchTerm,
limitToResourceClass.map(_.toIri),
limitToProject.map(_.value),
searchLimit,
searchOffset
)
searchResourceByLabelResponse <- triplestore.query(searchResourceByLabelSparql).flatMap(_.asExtended)

// collect the IRIs of main resources returned
Expand Down Expand Up @@ -1012,42 +1059,5 @@ final case class SearchResponderV2Live(
}

object SearchResponderV2Live {
val layer: ZLayer[
AppConfig & TriplestoreService & MessageRelay & ConstructResponseUtilV2 & OntologyCache & StandoffTagUtilV2 &
QueryTraverser & OntologyInferencer & GravsearchTypeInspectionRunner & InferenceOptimizationService &
IriConverter & ConstructTransformer & StringFormatter,
Nothing,
SearchResponderV2Live
] =
ZLayer.fromZIO(
for {
appConfig <- ZIO.service[AppConfig]
triplestoreService <- ZIO.service[TriplestoreService]
messageRelay <- ZIO.service[MessageRelay]
constructResponseUtilV2 <- ZIO.service[ConstructResponseUtilV2]
ontologyCache <- ZIO.service[OntologyCache]
standoffTagUtilV2 <- ZIO.service[StandoffTagUtilV2]
queryTraverser <- ZIO.service[QueryTraverser]
sparqlTransformerLive <- ZIO.service[OntologyInferencer]
stringFormatter <- ZIO.service[StringFormatter]
typeInspectionRunner <- ZIO.service[GravsearchTypeInspectionRunner]
inferenceOptimizationService <- ZIO.service[InferenceOptimizationService]
iriConverter <- ZIO.service[IriConverter]
constructTransformer <- ZIO.service[ConstructTransformer]
} yield new SearchResponderV2Live(
appConfig,
triplestoreService,
messageRelay,
constructResponseUtilV2,
ontologyCache,
standoffTagUtilV2,
queryTraverser,
sparqlTransformerLive,
typeInspectionRunner,
inferenceOptimizationService,
stringFormatter,
iriConverter,
constructTransformer
)
)
val layer = ZLayer.derive[SearchResponderV2Live]
}
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,6 @@ private final case class ApiRoutesImpl(
PermissionsRouteADM(routeData, runtime).makeRoute ~
RejectingRoute(appConfig, runtime).makeRoute ~
ResourcesRouteV2(appConfig).makeRoute ~
SearchRouteV2(appConfig.v2.fulltextSearch.searchValueMinLength).makeRoute ~
StandoffRouteV2().makeRoute ~
StoreRouteADM(routeData, runtime).makeRoute ~
UsersRouteADM().makeRoute ~
Expand Down
Loading

0 comments on commit 20f8d5c

Please sign in to comment.