@@ -166,23 +166,24 @@ export async function mergeHybridResults(params: {
166166 : entry . pathScore ;
167167 // Make fusion modality-aware: non-text media (image/audio) only carries a
168168 // synthetic label as text, so its keyword signal is structurally near zero.
169- // Drop the text weight for such candidates and renormalize the remaining
170- // weights so the score collapses to the vector signal on the same [0,1]
171- // scale as text candidates. Only drop the signal when the candidate also has
172- // a vector signal, so a keyword-only media hit keeps its text-weighted score.
173- // Text candidates are unchanged: their weights already sum to 1, so dividing
174- // by weightSum is a no-op. Gate the drop on a positive configured vector
175- // weight so a valid keyword match is never removed when vectorWeight is 0.
169+ // For such candidates drop the text weight and renormalize by the remaining
170+ // vector weight so the score collapses to the vector signal on the same
171+ // [0,1] scale as text candidates. Only drop the signal when the candidate
172+ // has a vector signal but no keyword signal , so a keyword-only or both-signal
173+ // media hit keeps its text-weighted score. Gate the drop on a positive
174+ // configured vector weight so a valid keyword match is never removed when
175+ // vectorWeight is 0.
176176 const dropMediaTextSignal =
177177 entry . hasVector &&
178178 ! entry . hasKeyword &&
179179 params . vectorWeight > 0 &&
180180 params . isNonTextMediaPath ?.( entry . path ) === true ;
181- const effectiveTextWeight = dropMediaTextSignal ? 0 : params . textWeight ;
182- const weightSum = params . vectorWeight + effectiveTextWeight ;
183- const weightedContent =
184- params . vectorWeight * entry . vectorScore + effectiveTextWeight * keywordScore ;
185- const contentScore = weightSum > 0 ? weightedContent / weightSum : 0 ;
181+ // Renormalize only for the vector-only media case. Every other candidate
182+ // keeps the established unnormalized weighted formula, so custom weights
183+ // that do not sum to one preserve their score scale and result ordering.
184+ const contentScore = dropMediaTextSignal
185+ ? entry . vectorScore
186+ : params . vectorWeight * entry . vectorScore + params . textWeight * keywordScore ;
186187 const hasWeightedContentRelevance = contentScore > 0 ;
187188 // With decay enabled, reserve the lower half of an exact tier for path
188189 // identity and the upper half for content relevance. This lets recency beat
0 commit comments