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