1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
//! Rank fusion: combine results from multiple retrievers.
//!
//! Run: `cargo run --example fusion`
use rankops::{
borda, combmnz, copeland, median_rank, rrf, rrf_with_config, FusionMethod, RrfConfig,
};
fn main() {
// Two ranked lists from different retrievers.
// Scores are on incompatible scales (BM25 vs cosine similarity).
let bm25: Vec<(&str, f32)> = vec![
("doc_a", 12.5),
("doc_b", 11.0),
("doc_c", 9.2),
("doc_d", 7.1),
];
let dense: Vec<(&str, f32)> = vec![
("doc_b", 0.95),
("doc_e", 0.88),
("doc_a", 0.82),
("doc_f", 0.70),
];
// ── RRF (Reciprocal Rank Fusion) ────────────────────────────────────
// Score-agnostic: uses only rank positions, so incompatible scales are fine.
let fused_rrf = rrf(&bm25, &dense);
println!("RRF (default k=60):");
for (id, score) in &fused_rrf {
println!(" {id:8} {score:.6}");
}
// doc_b and doc_a appear in both lists, so they rank highest.
// RRF with custom k: lower k gives more weight to top positions.
let config = RrfConfig::new(20).with_top_k(3);
let top3 = rrf_with_config(&bm25, &dense, config);
println!("\nRRF (k=20, top_k=3):");
for (id, score) in &top3 {
println!(" {id:8} {score:.6}");
}
// ── CombMNZ ─────────────────────────────────────────────────────────
// Score-based: sums normalized scores, then multiplies by overlap count.
// Rewards documents that appear in multiple lists.
let fused_mnz = combmnz(&bm25, &dense);
println!("\nCombMNZ:");
for (id, score) in &fused_mnz {
println!(" {id:8} {score:.6}");
}
// ── Borda count ─────────────────────────────────────────────────────
// Score-agnostic: each document gets (N - rank) points per list.
let fused_borda = borda(&bm25, &dense);
println!("\nBorda:");
for (id, score) in &fused_borda {
println!(" {id:8} {score:.6}");
}
// ── Copeland (net pairwise wins) ────────────────────────────────────
// More discriminative than Condorcet: score = wins - losses.
let fused_copeland = copeland(&bm25, &dense);
println!("\nCopeland:");
for (id, score) in &fused_copeland {
println!(" {id:8} {score:.1}");
}
// ── Median Rank (outlier-robust) ──────────────────────────────────
// Takes the median rank across lists. Robust to a single bad retriever.
let fused_median = median_rank(&bm25, &dense);
println!("\nMedian Rank:");
for (id, score) in &fused_median {
println!(" {id:8} {score:.6}");
}
// ── FusionMethod enum (runtime dispatch) ────────────────────────────
// Useful when the fusion algorithm is selected at runtime (e.g. from config).
let methods = [
("RRF", FusionMethod::rrf()),
("CombMNZ", FusionMethod::CombMnz),
("Borda", FusionMethod::Borda),
("DBSF", FusionMethod::Dbsf),
("Copeland", FusionMethod::Copeland),
("MedRank", FusionMethod::MedianRank),
];
println!("\nFusionMethod dispatch — top result per method:");
for (name, method) in &methods {
let result = method.fuse(&bm25, &dense);
if let Some((id, score)) = result.first() {
println!(" {name:8} -> {id} ({score:.6})");
}
}
}