๐ 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)
}