@@ -269,40 +269,33 @@ class ElasticSearch(
269269
270270 private case class HybridResult (id : String , lexicalScore : Double , semanticScore : Double , image : SourceWrapper [Image ])
271271 private case object HybridResult {
272- def fromSearchHit (hit : SearchHit , queryEmbedding : List [Double ]): Option [HybridResult ] = {
273- val lexicalScore = hit.score
274- resolveHit(hit) map { image =>
275- val semanticScore = (for {
276- embedding <- image.instance.embedding
277- cohereEmbedV4 <- embedding.cohereEmbedV4
278- } yield {
272+ def fromSearchHit (hit : SearchHit , queryEmbedding : List [Double ]): Option [HybridResult ] =
273+ resolveHit(hit).map { image =>
274+ val semanticScore = image.instance.embedding
275+ .flatMap(_.cohereEmbedV4)
279276 // Save some computation by assuming normalised vectors
280277 // TODO: double check this assumption
281- VectorUtils .dotProduct(cohereEmbedV4 .image, queryEmbedding)
282- }) .getOrElse(- 1.0 )
283- HybridResult (hit.id, lexicalScore = lexicalScore , semanticScore = semanticScore, image = image)
278+ .map(e => VectorUtils .dotProduct(e .image, queryEmbedding) )
279+ .getOrElse(- 1.0 )
280+ HybridResult (hit.id, lexicalScore = hit.score , semanticScore = semanticScore, image = image)
284281 }
285- }
286282
287283 def getTopK (results : List [HybridResult ], vecWeight : Double , k : Int )(implicit logMarker : LogMarker ): List [HybridResult ] = {
288- // Account for the rare case in which KNN doesn't return the true closest vector,
289- // and that true closest vector happens to be among the lexical-only results.
290- // Max lexical score could still be pulled from the top of the lexical results,
291- // but it reads better to do it in a uniform way for both.
292- val maxLexicalScore = results.maxBy(_.lexicalScore).lexicalScore
293- val maxSemanticScore = results.maxBy(_.semanticScore).semanticScore
294-
295284 val dedupedResults = results.distinctBy(_.id)
296285 logger.info(logMarker, s " ${dedupedResults.length} deduped results " )
297286
298- dedupedResults.sortBy { result =>
299- val maxNormedSemanticScore = (result.semanticScore + 1 ) / (maxSemanticScore + 1 )
300- val maxNormedLexicalScore = result.lexicalScore / maxLexicalScore
287+ // Account for the rare case in which KNN doesn't return the true closest vector,
288+ // and that true closest vector happens to be among the lexical-only results.
289+ val maxLexicalScore = dedupedResults.maxBy(_.lexicalScore).lexicalScore
290+ val maxSemanticScore = dedupedResults.maxBy(_.semanticScore).semanticScore
301291
302- // Negative to get a descending sort order
303- // Only other way to do this is to .reverse afterwards, apparently
304- - ((vecWeight * maxNormedSemanticScore) + ((1 - vecWeight) * maxNormedLexicalScore))
305- }.take(k)
292+ def combinedScore (result : HybridResult ): Double = {
293+ val normedSemanticScore = (result.semanticScore + 1 ) / (maxSemanticScore + 1 )
294+ val normedLexicalScore = result.lexicalScore / maxLexicalScore
295+ (vecWeight * normedSemanticScore) + ((1 - vecWeight) * normedLexicalScore)
296+ }
297+
298+ dedupedResults.sortBy(combinedScore)(Ordering [Double ].reverse).take(k)
306299 }
307300 }
308301
@@ -315,9 +308,11 @@ class ElasticSearch(
315308 vecWeight : Double ,
316309 filterOpt : Option [Query ]
317310 )(implicit ex : ExecutionContext , logMarker : LogMarker ): Future [SearchResults ] = {
311+ val lexicalQuery = createMultiMatchQuery(query, operator = Or )
312+
318313 val lexicalSearchRequest = ElasticDsl
319314 .search(imagesCurrentAlias)
320- .query(createMultiMatchQuery(query, operator = Or ) )
315+ .query(lexicalQuery )
321316 .size(k)
322317
323318 val semanticSearchRequest = ElasticDsl
@@ -327,7 +322,7 @@ class ElasticSearch(
327322 .k(k)
328323 .numCandidates(numCandidates)
329324 )
330- .rescore(Rescore (createMultiMatchQuery(query, operator = Or ) )
325+ .rescore(Rescore (lexicalQuery )
331326 .window(k)
332327 // We want to replace the knn score with the BM25 score,
333328 // because we can calculate cosine similarity clientside,
0 commit comments