@@ -586,31 +586,21 @@ class MediaApi(
586586 .getOrElse(TextSearch (query))
587587 }
588588
589- def emptyAiSearchResponse (searchParams : SearchParams ) =
590- Future .successful(
591- respondCollection(
592- List .empty[EmbeddedEntity [JsValue ]],
593- Some (searchParams.offset),
594- Some (0 ),
595- None ,
596- List ()
597- )
598- )
589+ def emptyAiSearchResponse =
590+ Future .successful(respondCollection(List .empty[EmbeddedEntity [JsValue ]], Some (0 ), Some (0 ), None , List ()))
599591
600- def aiSearchResponseFromResults (searchParams : SearchParams , searchResults : SearchResults ): Result = {
592+ def aiSearchResponseFromResults (searchResults : SearchResults ): Result = {
601593 val imageEntities = searchResults.hits map (hitToImageEntity _).tupled
602594 respondCollection(
603595 data = imageEntities,
604- offset = Some (searchParams.offset ),
596+ offset = Some (0 ),
605597 total = Some (searchResults.total),
606598 maybeExtraCounts = None ,
607599 links = Nil
608600 )
609601 }
610602
611- def semanticSearchByImage (searchParams : SearchParams , imageId : String ): Future [SearchResults ] = {
612- val semanticRetrievalWindow = Math .max(config.aiSearchSemanticRetrievalWindow, searchParams.length)
613- val countAll = searchParams.countAll.getOrElse(true )
603+ def semanticSearchByImage (imageId : String , k : Int ): Future [SearchResults ] = {
614604 for {
615605 maybeImage <- elasticSearch.getImageById(imageId)
616606 maybeEmbedding = maybeImage
@@ -621,23 +611,14 @@ class MediaApi(
621611 searchResults <- maybeEmbedding match {
622612 // If we have an embedding, perform the KNN search. If not, return an empty result set.
623613 case Some (embedding) =>
624- elasticSearch.knnSearch(
625- embedding,
626- k = semanticRetrievalWindow,
627- offset = searchParams.offset,
628- length = searchParams.length,
629- countAll = countAll,
630- numCandidates = Math .max(semanticRetrievalWindow * 2 , 100 )
631- )
614+ elasticSearch.knnSearch(embedding, k = k, numCandidates = Math .max(k * 2 , 100 ))
632615 case None =>
633616 Future .successful(SearchResults (Nil , total = 0 , extraCounts = None ))
634617 }
635618 } yield searchResults
636619 }
637620
638- def semanticSearchByText (searchParams : SearchParams , query : String ): Future [SearchResults ] = {
639- val semanticRetrievalWindow = Math .max(config.aiSearchSemanticRetrievalWindow, searchParams.length)
640- val countAll = searchParams.countAll.getOrElse(true )
621+ def semanticSearchByText (query : String , k : Int ): Future [SearchResults ] = {
641622 // Normalise key so that "Dogs" and "dogs " share a cache entry.
642623 val cacheKey = query.trim.toLowerCase
643624
@@ -655,56 +636,47 @@ class MediaApi(
655636
656637 for {
657638 embedding <- embeddingFuture
658- searchResults <- elasticSearch.knnSearch(
659- embedding,
660- k = semanticRetrievalWindow,
661- offset = searchParams.offset,
662- length = searchParams.length,
663- countAll = countAll,
664- numCandidates = Math .max(semanticRetrievalWindow * 2 , 100 )
665- )
639+ searchResults <- elasticSearch.knnSearch(embedding, k = k, numCandidates = Math .max(k * 2 , 100 ))
666640 } yield searchResults
667641 }
668642
669- def performAiSearchAndRespond (searchParams : SearchParams , query : String ): Future [Result ] = {
643+ def performAiSearchAndRespond (query : String ): Future [Result ] = {
644+ val k = 200
670645 val searchResultsFuture = parseAiSearchMode(query) match {
671- case SimilarSearch (imageId) => semanticSearchByImage(searchParams, imageId )
672- case TextSearch (textQuery) => semanticSearchByText(searchParams, textQuery )
646+ case SimilarSearch (imageId) => semanticSearchByImage(imageId, k )
647+ case TextSearch (textQuery) => semanticSearchByText(textQuery, k )
673648 }
674649
675- searchResultsFuture.map(aiSearchResponseFromResults(searchParams, _) )
650+ searchResultsFuture.map(aiSearchResponseFromResults)
676651 }
677652
678- val searchParams = if (canViewDeletedImages) {
679- _searchParams.copy(uploadedBy = Some (Authentication .getIdentity(request.user)))
680- }
681- else {
682- _searchParams
683- }
684-
685- SearchParams .validate(searchParams).fold(
686- errors => Future .successful(respondError(UnprocessableEntity , InvalidUriParams .errorKey,
687- errors.map(_.message).mkString(" , " ))
688- ),
689- params => {
690- if (params.useAISearch.contains(true )) {
691- params.query match {
692- // Short-circuit polling requests (length=0) and empty queries to avoid unnecessary Bedrock/KNN calls
693- case _ if params.length == 0 =>
694- emptyAiSearchResponse(params)
695- case Some (q) if ! q.isBlank =>
696- performAiSearchAndRespond(params, q)
697- // Empty queries do not make sense for AI search as we can
698- // only rank results once we have a meaningful vector to compare with.
699- // So return 0 results if the query was empty.
700- case _ =>
701- emptyAiSearchResponse(params)
702- }
703- } else {
704- performSearchAndRespond(params)
705- }
653+ if (_searchParams.useAISearch.contains(true )) {
654+ _searchParams.query match {
655+ // Short-circuit polling requests (length=0) and empty queries to avoid unnecessary Bedrock/KNN calls
656+ case _ if _searchParams.length == 0 =>
657+ emptyAiSearchResponse
658+ case Some (q) if ! q.isBlank =>
659+ performAiSearchAndRespond(q)
660+ // Empty queries do not make sense for AI search as we can
661+ // only rank results once we have a meaningful vector to compare with.
662+ // So return 0 results if the query was empty.
663+ case _ =>
664+ emptyAiSearchResponse
706665 }
707- )
666+ } else {
667+ val searchParams = if (canViewDeletedImages) {
668+ _searchParams.copy(uploadedBy = Some (Authentication .getIdentity(request.user)))
669+ } else {
670+ _searchParams
671+ }
672+ SearchParams .validate(searchParams).fold(
673+ // TODO: respondErrorCollection?
674+ errors => Future .successful(respondError(UnprocessableEntity , InvalidUriParams .errorKey,
675+ errors.map(_.message).mkString(" , " ))
676+ ),
677+ params => performSearchAndRespond(params)
678+ )
679+ }
708680 }
709681
710682 private def getImageResponseFromES (
0 commit comments