@@ -11,9 +11,7 @@ case class HybridResult(
1111 image : SourceWrapper [Image ]
1212)
1313
14- // Whether a result was originally returned by the lexical query, the semantic
15- // query, or both. A shared result tends to rank highly, so this is useful both
16- // for debugging and for understanding why something appears where it does.
14+
1715sealed trait ResultSource
1816object ResultSource {
1917 case object Lexical extends ResultSource
@@ -24,23 +22,17 @@ object ResultSource {
2422 (inLexical, inSemantic) match {
2523 case (true , true ) => Both
2624 case (true , false ) => Lexical
27- // A ranked result always comes from at least one side, so the remaining
28- // case is "semantic only".
29- case (false , _) => Semantic
25+ case (false , true ) => Semantic
26+ case _ => throw new IllegalArgumentException (" Result must be in either lexical or semantic" )
3027 }
3128}
3229
33- // The intermediate scoring detail for a single result: the two normalised
34- // component scores and the weighted blend of them that we actually rank by.
3530case class FusedScore (
3631 normedLexicalScore : Double ,
3732 normedSemanticScore : Double ,
3833 fusedScore : Double
3934)
4035
41- // A fully scored, ranked result. Carries the original scores (via `result`),
42- // where it came from (`source`), the normalised scores and the fused score
43- // (both via `score`) so the whole ranking decision is inspectable.
4436case class RankedResult (
4537 result : HybridResult ,
4638 source : ResultSource ,
@@ -65,15 +57,14 @@ object HybridResult {
6557 // are truncated 256-dim versions of a normalised 1536-dim vector,
6658 // meaning they will not have magnitude 1.
6759 // Note this is true cosine similarity from -1 to 1,
68- // *not* the ES-normalised score, but when we max- normalise
60+ // *not* the ES-normalised score, but when we normalise
6961 // later it will end up in the range 0-1.
7062 .map(e => VectorUtils .cosineSimilarity(e.image, queryEmbedding))
7163 .getOrElse(CosineSimilarityTheoreticalMin )
7264 HybridResult (hit.id, lexicalScore = hit.score, semanticScore = semanticScore, image = image)
7365 }
7466
75- // This is the "theoretical min-max" normalisation chosen by
76- // "An Analysis of Fusion Functions for Hybrid Retrieval"
67+ // This is the "theoretical min-max" normalisation chosen in
7768 // https://arxiv.org/pdf/2210.11934
7869 def normalise (score : Double , max : Double , theoreticalMin : Double ): Double =
7970 if (max == theoreticalMin) 0.0
@@ -93,13 +84,6 @@ object HybridResult {
9384 FusedScore (normedLexicalScore, normedSemanticScore, fusedScore)
9485 }
9586
96- // Combines the lexical and semantic result sets into a single, ranked list.
97- //
98- // Each input result already carries both scores (the semantic query is
99- // rescored to BM25 server-side, and resolveHitAndFillInSemanticScore fills in
100- // the cosine similarity client-side), so this function only has to: tag each
101- // result with where it came from, de-duplicate, normalise + fuse the scores,
102- // then sort and take the top k.
10387 def fuseAndRank (
10488 lexicalResults : List [HybridResult ],
10589 semanticResults : List [HybridResult ],
0 commit comments