๐Ÿ“„ rrf.ts  โ€ข  2673 bytes
/**
 * CmdCode ๅ‘้‡่ฎฐๅฟ†็ณป็ปŸ - RRF ่žๅˆๆŽ’ๅบ
 * 
 * RRF (Reciprocal Rank Fusion): 
 * ๅคš่ทฏๆœ็ดข็ป“ๆžœ่žๅˆ็ฎ—ๆณ•๏ผŒๅฏนไธๅŒๆœ็ดขๆ–นๆณ•็š„็ป“ๆžœ่ฟ›่กŒ็ปผๅˆๆŽ’ๅ
 */

export interface SearchResult {
  id: number
  session_id: string
  role: string
  content: string
  created_at: string
  score: number
  source: 'fts' | 'vec' | 'mixed'
}

/**
 * RRF ่žๅˆๆŽ’ๅบ
 * @param resultsMap Map<source, SearchResult[]> ๅ„่ทฏๆœ็ดข็ป“ๆžœ
 * @param k RRF ๅ‚ๆ•ฐ๏ผŒ้ป˜่ฎค 60
 */
export function rrfFusion(
  resultsMap: Map<string, SearchResult[]>,
  k: number = 60
): SearchResult[] {
  // ่šๅˆๆ‰€ๆœ‰็ป“ๆžœ
  const scoreMap = new Map<number, { result: SearchResult; totalScore: number; sources: Set<string> }>()

  for (const [source, rawResults] of resultsMap) {
    // ๅŒไธ€่ทฏ็ป“ๆžœๅ†…ๆŒ‰ id ๅŽป้‡๏ผˆไฟ็•™้ฆ–ๆฌกๅ‡บ็Žฐ=ๆœ€้ซ˜ๆŽ’ๅ๏ผ‰
    const seen = new Set<number>()
    const results = rawResults.filter(r => {
      if (seen.has(r.id)) return false
      seen.add(r.id)
      return true
    })
    results.forEach((result, rank) => {
      const key = result.id
      const rrfScore = 1 / (k + rank + 1)

      if (scoreMap.has(key)) {
        const existing = scoreMap.get(key)!
        existing.totalScore += rrfScore
        existing.sources.add(source)
        existing.result.score = existing.totalScore
        existing.result.source = existing.sources.size > 1 ? 'mixed' : source as 'fts' | 'vec'
      } else {
        scoreMap.set(key, {
          result: { ...result, score: rrfScore, source: source as 'fts' | 'vec' },
          totalScore: rrfScore,
          sources: new Set([source])
        })
      }
    })
  }

  // ๆŽ’ๅบ
  const sorted = Array.from(scoreMap.values())
    .sort((a, b) => b.totalScore - a.totalScore)

  return sorted.map(item => ({
    ...item.result,
    score: item.totalScore,
    source: item.sources.size > 1 ? 'mixed' : item.result.source
  }))
}

/**
 * ็ฎ€ๅ•่žๅˆ๏ผˆFTS + ๅ‘้‡็ป“ๆžœๅˆๅนถๅŽป้‡๏ผ‰
 */
export function simpleFusion(ftsResults: SearchResult[], vecResults: SearchResult[], limit = 20): SearchResult[] {
  const map = new Map<number, SearchResult>()

  // FTS ็ป“ๆžœ๏ผˆๆƒ้‡ 1.0๏ผ‰
  ftsResults.forEach((r, i) => {
    map.set(r.id, { ...r, score: 1.0 / (60 + i + 1), source: 'fts' })
  })

  // ๅ‘้‡็ป“ๆžœ๏ผˆๆƒ้‡ 1.2๏ผŒ็•ฅ้ซ˜๏ผ‰
  vecResults.forEach((r, i) => {
    const existing = map.get(r.id)
    if (existing) {
      existing.score += 1.2 / (60 + i + 1)
      existing.source = 'mixed'
    } else {
      map.set(r.id, { ...r, score: 1.2 / (60 + i + 1), source: 'vec' })
    }
  })

  return Array.from(map.values())
    .sort((a, b) => b.score - a.score)
    .slice(0, limit)
}