lean_ctx/core/extractive/
ranker.rs1use super::segment::Segment;
20use crate::core::embeddings::cosine_similarity;
21use crate::core::surprise::ScoringCtx;
22
23const REDUNDANCY_COSINE: f64 = 0.92;
27
28#[derive(Debug, Clone, Copy, PartialEq, Eq)]
30pub enum RankMode {
31 Centrality,
34 Query,
36}
37
38fn quantize(score: f32) -> i64 {
43 (f64::from(score) * 10_000.0).round() as i64
44}
45
46pub(super) fn centrality_scores(embs: &[Vec<f32>]) -> Vec<f32> {
49 let n = embs.len();
50 if n <= 1 {
51 return vec![0.0; n];
52 }
53 let mut scores = vec![0.0f32; n];
54 for i in 0..n {
55 let mut sum = 0.0f32;
56 for j in 0..n {
57 if i != j {
58 sum += cosine_similarity(&embs[i], &embs[j]);
59 }
60 }
61 scores[i] = sum / (n as f32 - 1.0);
62 }
63 scores
64}
65
66pub(super) fn query_scores(embs: &[Vec<f32>], anchor: &[f32]) -> Vec<f32> {
68 embs.iter().map(|e| cosine_similarity(e, anchor)).collect()
69}
70
71fn cost_of(seg: &Segment) -> usize {
73 seg.text.len() + 1
74}
75
76pub(super) fn select(
81 segs: &[Segment],
82 embs: &[Vec<f32>],
83 mode: RankMode,
84 anchor: Option<&[f32]>,
85 budget_chars: usize,
86) -> Vec<usize> {
87 debug_assert_eq!(segs.len(), embs.len());
88 let n = segs.len();
89
90 let scores = match mode {
91 RankMode::Centrality => centrality_scores(embs),
92 RankMode::Query => {
93 let Some(a) = anchor else {
94 return Vec::new();
95 };
96 query_scores(embs, a)
97 }
98 };
99
100 let mut kept: Vec<usize> = Vec::new();
101 let mut ctx = ScoringCtx::new();
102 let mut used = 0usize;
103
104 for (i, seg) in segs.iter().enumerate() {
106 if seg.protected {
107 kept.push(i);
108 used += cost_of(seg);
109 ctx.push_kept(embs[i].clone());
110 }
111 }
112
113 let mut ranked: Vec<usize> = (0..n).filter(|&i| !segs[i].protected).collect();
115 ranked.sort_by(|&a, &b| {
116 quantize(scores[b])
117 .cmp(&quantize(scores[a]))
118 .then(a.cmp(&b))
119 });
120
121 for i in ranked {
124 let cost = cost_of(&segs[i]);
125 if used + cost > budget_chars && !kept.is_empty() {
126 continue;
127 }
128 if ctx.max_cosine(&embs[i]) >= REDUNDANCY_COSINE {
129 continue;
130 }
131 kept.push(i);
132 used += cost;
133 ctx.push_kept(embs[i].clone());
134 }
135
136 kept.sort_unstable();
137 kept
138}
139
140#[cfg(test)]
141mod tests {
142 use super::*;
143
144 fn unit(v: [f32; 3]) -> Vec<f32> {
147 let norm = (v[0] * v[0] + v[1] * v[1] + v[2] * v[2]).sqrt();
148 vec![v[0] / norm, v[1] / norm, v[2] / norm]
149 }
150
151 fn seg(idx: usize, text: &str, protected: bool) -> Segment {
152 Segment {
153 idx,
154 para: 0,
155 text: text.to_string(),
156 protected,
157 }
158 }
159
160 #[test]
161 fn centrality_ranks_the_outlier_lowest() {
162 let embs = vec![
164 unit([1.0, 0.0, 0.0]),
165 unit([0.98, 0.02, 0.0]),
166 unit([0.97, 0.0, 0.03]),
167 unit([0.0, 0.0, 1.0]),
168 ];
169 let scores = centrality_scores(&embs);
170 let outlier = scores
171 .iter()
172 .enumerate()
173 .min_by(|a, b| a.1.partial_cmp(b.1).unwrap())
174 .unwrap()
175 .0;
176 assert_eq!(outlier, 3, "the orthogonal vector is least central");
177 }
178
179 #[test]
180 fn query_mode_ranks_by_anchor() {
181 let embs = vec![unit([1.0, 0.0, 0.0]), unit([0.0, 1.0, 0.0])];
182 let anchor = unit([0.9, 0.1, 0.0]);
183 let scores = query_scores(&embs, &anchor);
184 assert!(scores[0] > scores[1], "segment aligned with anchor wins");
185 }
186
187 #[test]
188 fn select_keeps_protected_and_central_drops_peripheral() {
189 let segs = vec![
191 seg(0, "PROTECTED", true),
192 seg(1, "central a", false),
193 seg(2, "central b", false),
194 seg(3, "peripheral", false),
195 ];
196 let embs = vec![
199 unit([1.0, 0.0, 0.0]),
200 unit([0.6, 0.8, 0.0]),
201 unit([0.6, -0.8, 0.0]),
202 unit([0.0, 0.0, 1.0]),
203 ];
204 let budget = "PROTECTED".len() + "central a".len() + "central b".len() + 3;
205 let kept = select(&segs, &embs, RankMode::Centrality, None, budget);
206 assert!(kept.contains(&0), "protected always kept");
207 assert!(kept.contains(&1) && kept.contains(&2), "central kept");
208 assert!(!kept.contains(&3), "peripheral dropped under budget");
209 let mut sorted = kept.clone();
211 sorted.sort_unstable();
212 assert_eq!(kept, sorted);
213 }
214
215 #[test]
216 fn mmr_drops_near_duplicate_segment() {
217 let segs = vec![
218 seg(0, "unique sentence one", false),
219 seg(1, "duplicate", false),
220 seg(2, "duplicate copy", false),
221 ];
222 let embs = vec![
224 unit([1.0, 0.0, 0.0]),
225 unit([0.0, 1.0, 0.0]),
226 unit([0.0, 1.0, 0.0]),
227 ];
228 let kept = select(&segs, &embs, RankMode::Centrality, None, 10_000);
229 let dups = [1usize, 2].iter().filter(|i| kept.contains(i)).count();
230 assert_eq!(dups, 1, "MMR keeps only one of the duplicates");
231 }
232
233 #[test]
234 fn select_is_deterministic() {
235 let segs = vec![
236 seg(0, "alpha", false),
237 seg(1, "beta", false),
238 seg(2, "gamma", false),
239 ];
240 let embs = vec![
241 unit([1.0, 0.1, 0.0]),
242 unit([0.9, 0.2, 0.0]),
243 unit([0.0, 0.0, 1.0]),
244 ];
245 let a = select(&segs, &embs, RankMode::Centrality, None, 12);
246 let b = select(&segs, &embs, RankMode::Centrality, None, 12);
247 assert_eq!(a, b);
248 }
249}