@@ -341,7 +341,16 @@ class ElasticSearch(
341341 case _ => fusedLexicalAndSemanticSearch(query, queryEmbedding, k, numCandidates, vecWeight, filterOpt)
342342 }
343343
344- searchResults.andThen { case _ =>
344+ // Run in parallel: how many images match the filters in total (so we can
345+ // show "Best k of N matches"). The ticker count badges are computed over
346+ // the top-k results we actually return, not the whole filtered set.
347+ val filterTotal = countMatchingFilter(filterOpt)
348+
349+ (for {
350+ results <- searchResults
351+ total <- filterTotal
352+ extraCounts <- extraCountsForIds(results.hits.map(_._1))
353+ } yield results.copy(total = total, extraCounts = Some (extraCounts))).andThen { case _ =>
345354 val elapsed = stopwatch.elapsed
346355 logger.info(
347356 combineMarkers(logMarker, elapsed),
@@ -351,6 +360,32 @@ class ElasticSearch(
351360 }
352361 }
353362
363+ // How many images match the active filters in total, so we can show
364+ // "Best k of N matches".
365+ private def countMatchingFilter (filterOpt : Option [Query ])(implicit ex : ExecutionContext , logMarker : LogMarker ): Future [Long ] = {
366+ val countRequest = ElasticDsl .count(imagesCurrentAlias).query(filterOpt.getOrElse(matchAllQuery()))
367+
368+ executeAndLog(countRequest, " hybrid AI search filter count" ).map(_.result.count)
369+ }
370+
371+ // The ticker count badges restricted to the given (top-k) result ids.
372+ // In principle, we could compute this here, without an extra request.
373+ // But it would require awkward duplication of the search logic
374+ // from `maybeOrgOwnedExtraCount` and `maybeAgencyPicksExtraCount`
375+ private def extraCountsForIds (ids : Seq [String ])(implicit ex : ExecutionContext , logMarker : LogMarker ): Future [ExtraCounts ] = {
376+ if (ids.isEmpty) Future .successful(ExtraCounts (tickerCounts = Map .empty))
377+ else {
378+ val searchRequest = ElasticDsl .search(imagesCurrentAlias)
379+ .query(filters.ids(ids.toList))
380+ .size(0 )
381+ .aggregations(extraCountAggregations)
382+
383+ executeAndLog(withSearchQueryTimeout(searchRequest), " hybrid AI search ticker counts" ).map { r =>
384+ extraCountsFrom(r.result.aggregations)
385+ }
386+ }
387+ }
388+
354389 def search (params : SearchParams )(implicit ex : ExecutionContext , request : AuthenticatedRequest [AnyContent , Principal ], logMarker : LogMarker = MarkerMap ()): Future [SearchResults ] = {
355390 val query : Query = queryBuilder.makeQuery(params.structuredQuery)
356391
@@ -391,10 +426,7 @@ class ElasticSearch(
391426 .runtimeMappings(runtimeMappings)
392427 .storedFields(" _source" ) // this needs to be explicit when using script fields
393428 .scriptfields(graphicImagesScriptFields)
394- .aggregations(aggregationsNameToSearchClauseMap.map {
395- case (name, ExtraCountConfig (searchClause, _, maybeSubAggregation)) =>
396- filterAgg(name, queryBuilder.makeQuery(Parser .run(searchClause))).subAggregations(maybeSubAggregation)
397- })
429+ .aggregations(extraCountAggregations)
398430 .from(params.offset)
399431 .size(params.length)
400432 .sortBy(sort)
@@ -408,27 +440,36 @@ class ElasticSearch(
408440 SearchResults (
409441 hits = imageHits,
410442 total = if (trackTotalHits) r.result.totalHits else 0 ,
411- extraCounts = Some (ExtraCounts (
412- tickerCounts = aggregationsNameToSearchClauseMap.map {
413- case (name, ExtraCountConfig (searchClause, backgroundColour, maybeSubAggregation)) =>
414- val aggResult = r.result.aggregations.filter(name)
415- val maybeSubAggResult = maybeSubAggregation.map(_.name).map(aggResult.result[Terms ])
416- name -> ExtraCount (
417- value = aggResult.docCount,
418- searchClause,
419- backgroundColour,
420- subCounts = maybeSubAggResult.map { termsAgg =>
421- ListMap (termsAgg.buckets.sortBy(_.docCount).reverse.map { bucket =>
422- (bucket.key, bucket.docCount)
423- }: _* ) + (" other" -> termsAgg.otherDocCount)
424- }.filter(_.exists { case (_, count) => count > 0 })
425- )
426- }
427- ))
443+ extraCounts = Some (extraCountsFrom(r.result.aggregations))
428444 )
429445 }
430446 }
431447
448+ // The aggregations used to compute the ticker count badges ("GNM-owned",
449+ // "agency picks" etc) shown above search results.
450+ private def extraCountAggregations = aggregationsNameToSearchClauseMap.map {
451+ case (name, ExtraCountConfig (searchClause, _, maybeSubAggregation)) =>
452+ filterAgg(name, queryBuilder.makeQuery(Parser .run(searchClause))).subAggregations(maybeSubAggregation)
453+ }
454+
455+ private def extraCountsFrom (aggregations : Aggregations ): ExtraCounts = ExtraCounts (
456+ tickerCounts = aggregationsNameToSearchClauseMap.map {
457+ case (name, ExtraCountConfig (searchClause, backgroundColour, maybeSubAggregation)) =>
458+ val aggResult = aggregations.filter(name)
459+ val maybeSubAggResult = maybeSubAggregation.map(_.name).map(aggResult.result[Terms ])
460+ name -> ExtraCount (
461+ value = aggResult.docCount,
462+ searchClause,
463+ backgroundColour,
464+ subCounts = maybeSubAggResult.map { termsAgg =>
465+ ListMap (termsAgg.buckets.sortBy(_.docCount).reverse.map { bucket =>
466+ (bucket.key, bucket.docCount)
467+ }: _* ) + (" other" -> termsAgg.otherDocCount)
468+ }.filter(_.exists { case (_, count) => count > 0 })
469+ )
470+ }
471+ )
472+
432473 def usageForSupplier (id : String , numDays : Int )(implicit ex : ExecutionContext , logMarker : LogMarker ): Future [SupplierUsageSummary ] = {
433474 val supplier = Agencies .get(id)
434475 val supplierName = supplier.supplier
0 commit comments