// RRF 融合器(Reciprocal Rank Fusion):将两路召回的 id 列表融合重排。
// score(id) = Σ_lists w / (k + rank),rank 为该 id 在对应列表中的 1 基名次。
///|
/// 等权 RRF 融合:lists 为各路召回的 id 有序列表(已按相关性降序);
/// k 为平滑常数(缺省 60),top_k 为截断长度。
/// 排序确定性:融合分降序,相同分按 id 升序。
/// Package-private; used by MemoryStore::recall only.
fn rrf(lists : Array[Array[String]], k : Int32, top_k : Int32) -> Array[String] {
if k <= 0 {
return []
}
let scores : Map[String, Double] = Map([])
for list in lists {
for i = 0; i < list.length(); i = i + 1 {
let id = list[i]
let rank = i + 1
let contribution = 1.0 / (k.to_double() + rank.to_double())
let cur = match scores.get(id) {
Some(v) => v
None => 0.0
}
scores.set(id, cur + contribution)
}
}
rank_scores_to_ids(scores, top_k)
}
///|
/// 加权 RRF:score(id) = vector_weight/(k+vector_rank) + keyword_weight/(k+keyword_rank);缺席通道贡献 0。
/// 排序确定性:融合分降序,相同分按 id 升序。
fn rrf_weighted(
vector_ids : Array[String],
keyword_ids : Array[String],
k : Int32,
top_k : Int32,
vector_weight : Double,
keyword_weight : Double,
) -> Array[String] {
if k <= 0 {
return []
}
let scores : Map[String, Double] = Map([])
if vector_weight > 0.0 {
for i = 0; i < vector_ids.length(); i = i + 1 {
let id = vector_ids[i]
let rank = i + 1
let contribution = vector_weight / (k.to_double() + rank.to_double())
let cur = match scores.get(id) {
Some(v) => v
None => 0.0
}
scores.set(id, cur + contribution)
}
}
if keyword_weight > 0.0 {
for i = 0; i < keyword_ids.length(); i = i + 1 {
let id = keyword_ids[i]
let rank = i + 1
let contribution = keyword_weight / (k.to_double() + rank.to_double())
let cur = match scores.get(id) {
Some(v) => v
None => 0.0
}
scores.set(id, cur + contribution)
}
}
rank_scores_to_ids(scores, top_k)
}
///|
fn rank_scores_to_ids(
scores : Map[String, Double],
top_k : Int32,
) -> Array[String] {
let ranked : Array[(String, Double)] = []
for kv in scores.iter() {
ranked.push(kv)
}
ranked.sort_by((a, b) => {
if a.1 > b.1 {
-1
} else if a.1 < b.1 {
1
} else if a.0 < b.0 {
-1
} else if a.0 > b.0 {
1
} else {
0
}
})
let out : Array[String] = []
let limit = if top_k < ranked.length() { top_k } else { ranked.length() }
for i = 0; i < limit; i = i + 1 {
out.push(ranked[i].0)
}
out
}
///|
/// 词法强命中判定(仅用 BM25 分数尺度)。
/// bm25_scores 按相关性降序;空 → false。
/// lexical_strong ⇔ b1 >= floor 且(仅 1 命中 或 b1 >= b2 * gap)。
fn lexical_strong(
bm25_scores : Array[Double],
floor : Double,
gap : Double,
) -> Bool {
if bm25_scores.length() == 0 {
return false
}
let b1 = bm25_scores[0]
if b1 < floor {
return false
}
if bm25_scores.length() == 1 {
return true
}
let b2 = bm25_scores[1]
b1 >= b2 * gap
}
///|
/// 保护 BM25 top-k 成员:加权 RRF 结果若丢掉词法 top,则从尾部挤出非保护项换入。
/// 保证 adaptive 路径 Recall@5 不低于同库 BM25 单路(不改变计分子集)。
fn protect_bm25_topk(
fused : Array[String],
keyword_ids : Array[String],
k : Int32,
) -> Array[String] {
let out : Array[String] = []
let in_out : Map[String, Bool] = Map([])
let limit = if k < fused.length() { k } else { fused.length() }
for i = 0; i < limit; i = i + 1 {
out.push(fused[i])
in_out.set(fused[i], true)
}
let protect_n = if k < keyword_ids.length() {
k
} else {
keyword_ids.length()
}
let keep_set : Map[String, Bool] = Map([])
for i = 0; i < protect_n; i = i + 1 {
keep_set.set(keyword_ids[i], true)
}
for i = 0; i < protect_n; i = i + 1 {
let id = keyword_ids[i]
if in_out.contains(id) {
continue
}
// 找可替换的尾部位:非 BM25 保护集成员
let mut replaced = false
let mut j = out.length() - 1
while j >= 0 {
if !keep_set.contains(out[j]) {
in_out.remove(out[j])
out[j] = id
in_out.set(id, true)
replaced = true
break
}
j = j - 1
}
if !replaced {
// 结果全是保护成员且未满,或无法腾位:追加(截断到 k)
if out.length() < k {
out.push(id)
in_out.set(id, true)
}
}
}
// 若仍未满(BM25 不足 k),保持 fused 已填部分
out
}