nodedb_vector/rerank/codecs/
pq.rs1use nodedb_codec::vector_quant::layout::UnifiedQuantizedVectorRef;
17use nodedb_mem::ScopedMemory;
18
19use crate::{
20 quantize::pq::PqCodec,
21 rerank::codec::{CodecName, PreparedQuery, RerankCodec},
22 rerank::types::RerankError,
23};
24
25#[inline]
29fn pq_packed_bits_len(m: usize) -> usize {
30 m
31}
32
33pub struct PqRerank {
44 codec: Option<PqCodec>,
45 dim: usize,
46 m: usize,
47 k: usize,
48 max_iter: usize,
49 memory: ScopedMemory,
52}
53
54impl PqRerank {
55 pub fn new(dim: usize, m: usize, k: usize, memory: ScopedMemory) -> Self {
62 Self {
63 codec: None,
64 dim,
65 m,
66 k,
67 max_iter: 25,
68 memory,
69 }
70 }
71
72 pub fn from_codec(codec: PqCodec, memory: ScopedMemory) -> Self {
74 let dim = codec.dim;
75 let m = codec.m;
76 let k = codec.k;
77 Self {
78 codec: Some(codec),
79 dim,
80 m,
81 k,
82 max_iter: 25,
83 memory,
84 }
85 }
86}
87
88impl RerankCodec for PqRerank {
89 fn encode(&self, v: &[f32]) -> Result<Vec<u8>, RerankError> {
94 if v.len() != self.dim {
95 return Err(RerankError::BadInput(format!(
96 "pq encode: vector len {} != codec dim {}",
97 v.len(),
98 self.dim
99 )));
100 }
101 let codec = self.codec.as_ref().ok_or_else(|| {
102 RerankError::NotTrained(
103 "pq: codec must be trained before encoding (call train() with a sample of vectors)"
104 .to_string(),
105 )
106 })?;
107 use nodedb_codec::vector_quant::codec::VectorCodec;
108 let quantized = <PqCodec as VectorCodec>::encode(codec, v);
109 Ok(quantized.as_ref().as_bytes().to_vec())
110 }
111
112 fn prepare_query(&self, q: &[f32]) -> Result<PreparedQuery, RerankError> {
118 if q.len() != self.dim {
119 return Err(RerankError::BadInput(format!(
120 "pq prepare_query: query len {} != codec dim {}",
121 q.len(),
122 self.dim
123 )));
124 }
125 let codec = self.codec.as_ref().ok_or_else(|| {
126 RerankError::NotTrained(
127 "pq: codec must be trained before prepare_query (call train() with a sample of vectors)"
128 .to_string(),
129 )
130 })?;
131 use nodedb_codec::vector_quant::codec::VectorCodec;
132 let pq_query = <PqCodec as VectorCodec>::prepare_query(codec, q);
133 Ok(PreparedQuery::Lut(pq_query.distance_table))
134 }
135
136 fn distance_prepared(
141 &self,
142 prepared: &PreparedQuery,
143 encoded: &[u8],
144 ) -> Result<f32, RerankError> {
145 let lut = match prepared {
146 PreparedQuery::Lut(t) => t,
147 _ => {
148 return Err(RerankError::BadInput(
149 "pq distance: expected PreparedQuery::Lut".to_string(),
150 ));
151 }
152 };
153
154 let packed_len = pq_packed_bits_len(self.m);
155 let uqv_ref = UnifiedQuantizedVectorRef::from_bytes(encoded, packed_len).map_err(|e| {
156 RerankError::BadInput(format!("pq distance: failed to parse encoded bytes: {e}"))
157 })?;
158
159 let packed = uqv_ref.packed_bits();
160 let dist = packed
162 .iter()
163 .enumerate()
164 .map(|(sub, &code)| {
165 lut.get(sub)
166 .and_then(|row| row.get(code as usize).copied())
167 .unwrap_or(0.0)
168 })
169 .sum();
170 Ok(dist)
171 }
172
173 fn name(&self) -> CodecName {
174 CodecName::Pq
175 }
176
177 fn to_bytes(&self) -> Result<Vec<u8>, RerankError> {
178 let codec = self.codec.as_ref().ok_or_else(|| {
179 RerankError::NotTrained("pq sidecar serialize: codec not trained".to_string())
180 })?;
181 codec
182 .to_bytes()
183 .map_err(|e| RerankError::BadInput(format!("pq to_bytes: {e}")))
184 }
185
186 fn train(&mut self, samples: &[&[f32]]) -> Result<(), RerankError> {
197 if samples.is_empty() {
198 return Err(RerankError::BadInput(
199 "pq train: empty sample set".to_string(),
200 ));
201 }
202 for s in samples {
203 if s.len() != self.dim {
204 return Err(RerankError::BadInput(format!(
205 "pq train: sample has len {} but codec dim is {}",
206 s.len(),
207 self.dim
208 )));
209 }
210 }
211 if !self.dim.is_multiple_of(self.m) {
212 return Err(RerankError::BadInput(format!(
213 "pq train: dim ({}) must be divisible by m ({})",
214 self.dim, self.m
215 )));
216 }
217 if samples.len() < self.k {
218 return Err(RerankError::BadInput(format!(
219 "pq train: need >= k samples for k-means, got {}",
220 samples.len()
221 )));
222 }
223 let codec = PqCodec::train(
224 samples,
225 self.dim,
226 self.m,
227 self.k,
228 self.max_iter,
229 self.memory.clone(),
230 )
231 .map_err(|e| RerankError::BadInput(format!("pq train: {e}")))?;
232 self.codec = Some(codec);
233 Ok(())
234 }
235}
236
237#[cfg(test)]
240mod tests {
241 use super::*;
242 use crate::test_support::test_memory;
243
244 const DIM: usize = 32;
245 const M: usize = 4;
246 const K: usize = 8;
247 const N: usize = 64;
248
249 fn det_vec(i: usize, dim: usize) -> Vec<f32> {
250 (0..dim)
251 .map(|j| ((i * 31 + j) % 100) as f32 / 100.0)
252 .collect()
253 }
254
255 fn trained() -> PqRerank {
256 let vecs: Vec<Vec<f32>> = (0..N).map(|i| det_vec(i, DIM)).collect();
257 let refs: Vec<&[f32]> = vecs.iter().map(|v| v.as_slice()).collect();
258 let mut codec = PqRerank::new(DIM, M, K, test_memory());
259 codec.train(&refs).expect("train must succeed");
260 codec
261 }
262
263 #[test]
264 fn train_then_encode_roundtrip() {
265 let codec = trained();
266 let v = det_vec(0, DIM);
267 let enc = codec.encode(&v).expect("encode");
268 let prep = codec.prepare_query(&v).expect("prepare_query");
269 let dist = codec.distance_prepared(&prep, &enc).expect("distance");
270 assert!(dist.is_finite(), "distance must be finite, got {dist}");
271 assert!(dist >= 0.0, "distance must be non-negative, got {dist}");
272 assert!(dist < 1.0, "self-distance too large: {dist}");
274 }
275
276 #[test]
277 fn encode_before_train_returns_not_trained() {
278 let codec = PqRerank::new(DIM, M, K, test_memory());
279 let v = det_vec(0, DIM);
280 let err = codec.encode(&v).unwrap_err();
281 let msg = format!("{err}");
282 assert!(
283 msg.contains("not trained") || msg.contains("trained"),
284 "expected 'trained' in error, got: {msg}"
285 );
286 }
287
288 #[test]
289 fn train_with_wrong_dim_sample_fails() {
290 let vecs: Vec<Vec<f32>> = (0..N).map(|i| det_vec(i, DIM)).collect();
291 let mut refs: Vec<&[f32]> = vecs.iter().map(|v| v.as_slice()).collect();
292 let bad = det_vec(0, DIM + 4);
293 refs.push(bad.as_slice());
294 let mut codec = PqRerank::new(DIM, M, K, test_memory());
295 let err = codec.train(&refs).unwrap_err();
296 let msg = format!("{err}");
297 assert!(
298 msg.contains("bad input"),
299 "expected bad input error, got: {msg}"
300 );
301 }
302
303 #[test]
304 fn train_with_indivisible_dim_fails() {
305 let vecs: Vec<Vec<f32>> = (0..16).map(|i| det_vec(i, 33)).collect();
307 let refs: Vec<&[f32]> = vecs.iter().map(|v| v.as_slice()).collect();
308 let mut codec = PqRerank::new(33, 4, 8, test_memory());
309 let err = codec.train(&refs).unwrap_err();
310 let msg = format!("{err}");
311 assert!(
312 msg.contains("divisible"),
313 "expected divisibility error, got: {msg}"
314 );
315 }
316
317 #[test]
318 fn train_with_too_few_samples_fails() {
319 let vecs: Vec<Vec<f32>> = (0..4).map(|i| det_vec(i, DIM)).collect();
321 let refs: Vec<&[f32]> = vecs.iter().map(|v| v.as_slice()).collect();
322 let mut codec = PqRerank::new(DIM, M, 8, test_memory());
323 let err = codec.train(&refs).unwrap_err();
324 let msg = format!("{err}");
325 assert!(
326 msg.contains("k samples") || msg.contains("bad input"),
327 "expected sample count error, got: {msg}"
328 );
329 }
330
331 #[test]
332 fn name_is_pq() {
333 let codec = PqRerank::new(DIM, M, K, test_memory());
334 assert_eq!(codec.name(), CodecName::Pq);
335 }
336}