1use std::collections::HashMap;
3
4use ailake_catalog::{decode_centroid, DataFileEntry};
5use ailake_core::VectorMetric;
6use ailake_vec::{cosine_distance, dot_product, euclidean_distance};
7use tracing::debug;
8
9pub struct VectorPruner;
10
11impl VectorPruner {
12 pub fn prune(
18 files: Vec<DataFileEntry>,
19 query: &[f32],
20 metric: VectorMetric,
21 threshold: f32,
22 ) -> Vec<DataFileEntry> {
23 files
24 .into_iter()
25 .filter(|entry| {
26 match decode_centroid(entry, metric) {
27 Some(centroid) => {
28 if centroid.values.len() != query.len() {
32 debug!(
33 "ailake: pruner {} — centroid dim={} != query dim={}, skipping (secondary column)",
34 entry.path,
35 centroid.values.len(),
36 query.len(),
37 );
38 return true;
39 }
40 let dist = compute_distance(query, ¢roid.values, metric);
41 let keep = dist - centroid.radius <= threshold;
42 debug!(
43 "ailake: pruner {} — dist={:.4} radius={:.4} edge={:.4} threshold={:.4} → {}",
44 entry.path,
45 dist,
46 centroid.radius,
47 dist - centroid.radius,
48 threshold,
49 if keep { "KEEP" } else { "PRUNE" }
50 );
51 keep
52 }
53 None => {
54 debug!(
55 "ailake: pruner {} — no centroid metadata, keeping (conservative fallback)",
56 entry.path
57 );
58 true }
60 }
61 })
62 .collect()
63 }
64}
65
66fn compute_distance(a: &[f32], b: &[f32], metric: VectorMetric) -> f32 {
67 match metric {
68 VectorMetric::Cosine | VectorMetric::NormalizedCosine => cosine_distance(a, b),
69 VectorMetric::Euclidean => euclidean_distance(a, b),
70 VectorMetric::DotProduct => -dot_product(a, b),
71 }
72}
73
74pub struct BloomPruner;
82
83impl BloomPruner {
84 pub fn prune(
89 files: Vec<DataFileEntry>,
90 query_text: &str,
91 bloom_map: &HashMap<String, crate::bloom::BloomFilter>,
92 ) -> Vec<DataFileEntry> {
93 let query_terms: Vec<String> = crate::bm25::tokenize(query_text);
94 if query_terms.is_empty() || bloom_map.is_empty() {
95 return files;
96 }
97 let before = files.len();
98 let surviving: Vec<DataFileEntry> = files
99 .into_iter()
100 .filter(|entry| match bloom_map.get(&entry.path) {
101 Some(bloom) => {
102 let keep = query_terms.iter().any(|t| bloom.may_contain(t));
103 debug!(
104 "ailake: bloom pruner {} — {} query terms, keep={}",
105 entry.path,
106 query_terms.len(),
107 keep
108 );
109 keep
110 }
111 None => true,
112 })
113 .collect();
114 debug!(
115 "ailake: bloom pruning — {}/{} files survive",
116 surviving.len(),
117 before
118 );
119 surviving
120 }
121}
122
123#[cfg(test)]
124mod tests {
125 use super::*;
126 use ailake_catalog::{make_data_file_entry, VectorIndexInfo};
127 use ailake_core::VectorMetric;
128 use ailake_vec::compute_centroid_and_radius;
129
130 fn make_entry(path: &str, vecs: &[Vec<f32>], metric: VectorMetric) -> DataFileEntry {
131 let centroid = compute_centroid_and_radius(vecs, metric);
132 make_data_file_entry(
133 path,
134 vecs.len() as u64,
135 1024,
136 ¢roid,
137 VectorIndexInfo {
138 column: "embedding",
139 dim: vecs[0].len() as u32,
140 hnsw_offset: 0,
141 hnsw_len: 0,
142 },
143 )
144 }
145
146 #[test]
147 fn prunes_far_file() {
148 let vecs = vec![vec![1.0f32, 0.0, 0.0], vec![0.9, 0.1, 0.0]];
150 let entry = make_entry("far.parquet", &vecs, VectorMetric::Cosine);
151 let query = vec![0.0f32, 0.0, 1.0];
152 let pruned = VectorPruner::prune(vec![entry], &query, VectorMetric::Cosine, 0.1);
153 assert!(pruned.is_empty(), "far file should be pruned");
154 }
155
156 #[test]
157 fn keeps_nearby_file() {
158 let vecs = vec![vec![1.0f32, 0.0, 0.0], vec![0.99, 0.1, 0.0]];
159 let entry = make_entry("near.parquet", &vecs, VectorMetric::Cosine);
160 let query = vec![1.0f32, 0.0, 0.0];
161 let kept = VectorPruner::prune(vec![entry], &query, VectorMetric::Cosine, 0.5);
162 assert_eq!(kept.len(), 1, "nearby file should be kept");
163 }
164
165 #[test]
166 fn no_centroid_always_kept() {
167 let entry = DataFileEntry {
168 path: "unknown.parquet".into(),
169 record_count: 10,
170 file_size_bytes: 512,
171 centroid_b64: None,
172 radius: None,
173 hnsw_offset: None,
174 hnsw_len: None,
175 vector_column: None,
176 vector_dim: None,
177 extra_vector_indexes: vec![],
178 index_status: ailake_catalog::IndexStatus::Ready,
179 index_error: None,
180 batch_id: None,
181 embedding_model: None,
182 partition_value: None,
183 deletion_vector: None,
184 first_row_id: None,
185 column_stats: None,
186 };
187 let query = vec![0.0f32, 0.0, 1.0];
188 let kept = VectorPruner::prune(vec![entry], &query, VectorMetric::Cosine, 0.0);
189 assert_eq!(kept.len(), 1);
190 }
191}