in media-api/app/lib/elasticsearch/ElasticSearch.scala [119:279]
def search(params: SearchParams)(implicit ex: ExecutionContext, request: AuthenticatedRequest[AnyContent, Principal], logMarker:MarkerMap = MarkerMap()): Future[SearchResults] = {
val isPotentiallyGraphicFieldName = "isPotentiallyGraphic"
def resolveHit(hit: SearchHit) = mapImageFrom(
hit.sourceAsString,
hit.id,
hit.index,
fields = hit.fields match {
case null => JsObject.empty
case _ => Json.obj(
isPotentiallyGraphicFieldName -> hit.fields.get(isPotentiallyGraphicFieldName).map(_.asInstanceOf[List[Boolean]].headOption)
)
}
)
val query: Query = queryBuilder.makeQuery(params.structuredQuery)
val uploadTimeFilter = filters.date("uploadTime", params.since, params.until)
val lastModTimeFilter = filters.date("lastModified", params.modifiedSince, params.modifiedUntil)
val takenTimeFilter = filters.date("metadata.dateTaken", params.takenSince, params.takenUntil)
// we only inject filters if there are actual date parameters
val dateFilterList = List(uploadTimeFilter, lastModTimeFilter, takenTimeFilter).flatten.toNel
val dateFilter = dateFilterList.map(dateFilters => filters.and(dateFilters.list.toList: _*))
val idsFilter = params.ids.map(filters.ids)
val labelFilter = params.labels.toNel.map(filters.terms("labels", _))
val metadataFilter = params.hasMetadata.map(metadataField).toNel.map(filters.exists)
val archivedFilter = params.archived.map(filters.existsOrMissing(editsField("archived"), _))
val hasExports = params.hasExports.map(filters.existsOrMissing("exports", _))
val hasIdentifier = params.hasIdentifier.map(idName => filters.exists(NonEmptyList(identifierField(idName))))
val missingIdentifier = params.missingIdentifier.map(idName => filters.missing(NonEmptyList(identifierField(idName))))
val uploadedByFilter = params.uploadedBy.map(uploadedBy => filters.terms("uploadedBy", NonEmptyList(uploadedBy)))
val simpleCostFilter = params.free.flatMap(free => if (free) searchFilters.freeFilter else searchFilters.nonFreeFilter)
val costFilter = params.payType match {
case Some(PayType.Free) => searchFilters.freeFilter
case Some(PayType.MaybeFree) => searchFilters.maybeFreeFilter
case Some(PayType.Pay) => searchFilters.nonFreeFilter
case _ => None
}
val printUsageFilter = params.printUsageFilters.map(searchFilters.printUsageFilters)
val hasRightsCategory = params.hasRightsCategory.filter(_ == true).map(_ => searchFilters.hasRightsCategoryFilter)
val validityFilter = params.valid.map(valid => if (valid) searchFilters.validFilter else searchFilters.invalidFilter)
val persistFilter = params.persisted map {
case true => searchFilters.persistedFilter
case false => searchFilters.nonPersistedFilter
}
val usageFilter: Iterable[Query] =
params.usageStatus.toNel.map(status => filters.terms("usagesStatus", status.map(_.toString))).toOption ++
params.usagePlatform.toNel.map(filters.terms("usagesPlatform", _)).toOption
val syndicationStatusFilter = params.syndicationStatus.map(status => syndicationFilter.statusFilter(status))
// Port of special case code in elastic1 sorts. Using the dateAddedToCollection sort implies an additional filter for reasons unknown
val dateAddedToCollectionFilter = {
params.orderBy match {
case Some("dateAddedToCollection") => {
val pathHierarchyOpt = params.structuredQuery.flatMap {
case Match(HierarchyField, Phrase(value)) => Some(value)
case _ => None
}.headOption
pathHierarchyOpt.map { pathHierarchy =>
termQuery("collections.pathHierarchy", pathHierarchy)
}
}
case _ => None
}
}
val filterOpt = (
metadataFilter.toOption.toList
++ persistFilter
++ labelFilter.toOption
++ archivedFilter
++ uploadedByFilter
++ idsFilter
++ validityFilter
++ simpleCostFilter
++ costFilter
++ hasExports
++ hasIdentifier
++ missingIdentifier
++ dateFilter.toOption
++ usageFilter
++ hasRightsCategory
++ searchFilters.tierFilter(params.tier)
++ syndicationStatusFilter
++ dateAddedToCollectionFilter
++ printUsageFilter
).toNel.map(filter => filter.list.toList.reduceLeft(filters.and(_, _))).toOption
val withFilter = filterOpt.map { f =>
boolQuery() must (query) filter f
}.getOrElse(query)
val sort = params.orderBy match {
case Some("dateAddedToCollection") => sorts.dateAddedToCollectionDescending
case _ => sorts.createSort(params.orderBy)
}
val runtimeMappings = if (params.syndicationStatus.contains(AwaitingReviewForSyndication) && config.useRuntimeFieldsToFixSyndicationReviewQueueQuery) {
Seq(syndicationFilter.syndicationReviewQueueFixMapping)
} else {
Seq.empty
}
// We need to set trackHits to ensure that the total number of hits we return to users is accurate.
// See https://www.elastic.co/guide/en/elasticsearch/reference/current/breaking-changes-7.0.html#hits-total-now-object-search-response
val trackTotalHits = params.countAll.getOrElse(true)
val graphicImagesScriptFields =
if (params.shouldFlagGraphicImages) {
Seq(ScriptField(
field = isPotentiallyGraphicFieldName,
// the rest of the logic is in the client (in image.js)
script = Script(
//language=groovy -- it's actually painless, but that's pretty similar to groovy and this provides syntax highlighting
script = "params['_source']?.fileMetadata?.xmp !=null && params['_source']?.fileMetadata?.xmp['pur:adultContentWarning'] != null",
lang = Some("painless")
)
))
} else {
Seq.empty
}
val searchRequest = prepareSearch(withFilter)
.trackTotalHits(trackTotalHits)
.runtimeMappings(runtimeMappings)
.storedFields("_source") // this needs to be explicit when using script fields
.scriptfields(graphicImagesScriptFields)
.aggregations(if (config.shouldDisplayOrgOwnedCountAndFilterCheckbox) List(filterAgg(
orgOwnedAggName,
queryBuilder.makeQuery(Parser.run(s"is:${config.staffPhotographerOrganisation}-owned"))
)) else Nil)
.from(params.offset)
.size(params.length)
.sortBy(sort)
executeAndLog(searchRequest, "image search").
toMetric(Some(mediaApiMetrics.searchQueries), List(mediaApiMetrics.searchTypeDimension("results")))(_.result.took).map { r =>
logSearchQueryIfTimedOut(searchRequest, r.result)
val imageHits = r.result.hits.hits.map(resolveHit).toSeq.flatten.map(i => (i.instance.id, i))
// setting trackTotalHits to false means we don't get any hit count at all.
// Requester has explicitly opted into not caring about the total hits, so give them what they want (nothing).
SearchResults(
hits = imageHits,
total = if (trackTotalHits) r.result.totalHits else 0,
maybeOrgOwnedCount =
if (config.shouldDisplayOrgOwnedCountAndFilterCheckbox)
Some(r.result.aggregations.filter(orgOwnedAggName).docCount)
else
None
)
}
}