use std::collections::HashMap;
use std::sync::Arc;
use crate::vector::core::quantization::{QuantizedVectorMeta, ScalarQuantParams};
use crate::vector::core::vector::Vector;
#[derive(Debug)]
pub struct QuantizedVectorPool {
pub params: ScalarQuantParams,
pub dim: usize,
pub pad_dim: usize,
pub int8_data: Vec<u8>,
pub sum_q: Vec<u32>,
pub norm_q: Vec<f32>,
pub field_index: HashMap<String, Arc<HashMap<u64, u32>>>,
pub vector_count: usize,
}
impl QuantizedVectorPool {
#[inline]
pub const fn padded_dim(dim: usize) -> usize {
crate::vector::core::distance_quantized::padded_dim(dim)
}
pub fn build(
params: ScalarQuantParams,
dim: usize,
records: impl IntoIterator<Item = (u64, String, Vec<u8>, QuantizedVectorMeta)>,
) -> Self {
let pad_dim = Self::padded_dim(dim);
let mut int8_data: Vec<u8> = Vec::new();
let mut sum_q: Vec<u32> = Vec::new();
let mut norm_q: Vec<f32> = Vec::new();
let mut by_field: HashMap<String, HashMap<u64, u32>> = HashMap::new();
for (doc_id, field, int8, meta) in records {
debug_assert_eq!(int8.len(), dim, "int8 payload length must equal dim");
let pos = sum_q.len() as u32;
int8_data.extend_from_slice(&int8);
int8_data.resize((pos as usize + 1) * pad_dim, 0);
sum_q.push(meta.sum_q);
norm_q.push(meta.norm_q);
by_field.entry(field).or_default().insert(doc_id, pos);
}
let vector_count = sum_q.len();
let field_index: HashMap<String, Arc<HashMap<u64, u32>>> = by_field
.into_iter()
.map(|(field, map)| (field, Arc::new(map)))
.collect();
Self {
params,
dim,
pad_dim,
int8_data,
sum_q,
norm_q,
field_index,
vector_count,
}
}
#[inline]
pub fn get_record(&self, doc_id: u64, field: &str) -> Option<(&[u8], QuantizedVectorMeta)> {
let pos = self.field_index.get(field)?.get(&doc_id).copied()?;
Some(self.record_at(pos))
}
#[inline]
pub fn record_at(&self, pos: u32) -> (&[u8], QuantizedVectorMeta) {
let start = (pos as usize) * self.pad_dim;
let int8 = &self.int8_data[start..start + self.pad_dim];
let sum_q = self.sum_q[pos as usize];
let norm_q = self.norm_q[pos as usize];
(int8, QuantizedVectorMeta { sum_q, norm_q })
}
#[inline]
pub fn field_position_index(&self, field: &str) -> Option<Arc<HashMap<u64, u32>>> {
self.field_index.get(field).cloned()
}
#[inline]
pub fn contains(&self, doc_id: u64, field: &str) -> bool {
self.field_index
.get(field)
.is_some_and(|m| m.contains_key(&doc_id))
}
pub fn keys(&self) -> Vec<(u64, String)> {
let mut keys: Vec<(u64, String)> = self
.field_index
.iter()
.flat_map(|(field, map)| map.keys().map(move |id| (*id, field.clone())))
.collect();
keys.sort_by_key(|(id, _)| *id);
keys
}
pub fn dequantize_to_vector(&self, doc_id: u64, field: &str) -> Option<Vector> {
let (int8, _meta) = self.get_record(doc_id, field)?;
let data: Vec<f32> = int8[..self.dim]
.iter()
.map(|&b| self.params.dequantize_value(b))
.collect();
Some(Vector::new(data))
}
#[inline]
pub fn field_count(&self) -> usize {
self.field_index.len()
}
pub fn field_names(&self) -> Vec<String> {
let mut names: Vec<String> = self.field_index.keys().cloned().collect();
names.sort();
names
}
pub fn doc_ids_for_field(&self, field: &str) -> Arc<[u64]> {
let Some(map) = self.field_index.get(field) else {
return Arc::<[u64]>::from(Vec::<u64>::new());
};
let mut ids: Vec<u64> = map.keys().copied().collect();
ids.sort_unstable();
Arc::<[u64]>::from(ids)
}
#[inline]
pub fn heap_size(&self) -> usize {
self.int8_data.len() + self.sum_q.len() * 4 + self.norm_q.len() * 4
}
}
#[cfg(test)]
mod tests {
use super::*;
fn meta(sum_q: u32, norm_q: f32) -> QuantizedVectorMeta {
QuantizedVectorMeta { sum_q, norm_q }
}
fn sample_params() -> ScalarQuantParams {
ScalarQuantParams {
offset: -1.0,
scale: 2.0 / 255.0,
}
}
#[test]
fn build_packs_records_in_iteration_order() {
let q = QuantizedVectorPool::build(
sample_params(),
4,
vec![
(10, "embedding".to_string(), vec![0, 1, 2, 3], meta(6, 1.0)),
(
20,
"embedding".to_string(),
vec![10, 20, 30, 40],
meta(100, 2.0),
),
],
);
assert_eq!(q.vector_count, 2);
assert_eq!(q.dim, 4);
assert_eq!(q.pad_dim, 32);
assert_eq!(q.int8_data.len(), 2 * q.pad_dim);
let (int8_0, meta_0) = q.get_record(10, "embedding").unwrap();
assert_eq!(int8_0.len(), 32);
assert_eq!(&int8_0[..4], &[0u8, 1, 2, 3]);
assert!(int8_0[4..].iter().all(|&b| b == 0), "padding is zero");
assert_eq!(meta_0.sum_q, 6);
assert_eq!(meta_0.norm_q, 1.0);
let (int8_1, meta_1) = q.get_record(20, "embedding").unwrap();
assert_eq!(&int8_1[..4], &[10u8, 20, 30, 40]);
assert_eq!(meta_1.sum_q, 100);
assert_eq!(meta_1.norm_q, 2.0);
}
#[test]
fn build_pads_non_multiple_of_32_dim() {
let int8: Vec<u8> = (0..100).map(|i| (i % 256) as u8).collect();
let q = QuantizedVectorPool::build(
sample_params(),
100,
vec![(1, "f".to_string(), int8.clone(), meta(42, 1.0))],
);
assert_eq!(q.pad_dim, 128);
assert_eq!(q.int8_data.len(), 128);
let (payload, m) = q.record_at(0);
assert_eq!(payload.len(), 128);
assert_eq!(&payload[..100], &int8[..]);
assert!(payload[100..].iter().all(|&b| b == 0), "tail padding zero");
assert_eq!(m.sum_q, 42);
}
#[test]
fn get_record_returns_none_for_missing_keys() {
let q = QuantizedVectorPool::build(
sample_params(),
2,
vec![(1, "f".to_string(), vec![5, 6], meta(11, 1.0))],
);
assert!(q.get_record(2, "f").is_none(), "missing doc_id");
assert!(q.get_record(1, "other").is_none(), "missing field");
}
#[test]
fn field_position_index_supports_hot_loop_lookup() {
let q = QuantizedVectorPool::build(
sample_params(),
2,
vec![
(1, "embedding".to_string(), vec![5, 6], meta(11, 1.0)),
(2, "embedding".to_string(), vec![7, 8], meta(15, 1.5)),
(1, "thumbnail".to_string(), vec![9, 10], meta(19, 0.5)),
],
);
let idx = q.field_position_index("embedding").unwrap();
assert_eq!(idx.len(), 2);
let pos = *idx.get(&2).unwrap();
let (int8, meta_back) = q.record_at(pos);
assert_eq!(&int8[..2], &[7u8, 8]);
assert_eq!(meta_back.sum_q, 15);
}
#[test]
fn dequantize_to_vector_inverts_quantize_value() {
let q = QuantizedVectorPool::build(
sample_params(),
3,
vec![(1, "f".to_string(), vec![0, 128, 255], meta(383, 1.0))],
);
let v = q.dequantize_to_vector(1, "f").unwrap();
assert_eq!(v.data.len(), 3);
assert!((v.data[0] - (-1.0)).abs() < 1e-6);
assert!((v.data[1] - (-1.0 + 128.0 * 2.0 / 255.0)).abs() < 1e-6);
assert!((v.data[2] - 1.0).abs() < 1e-6);
}
#[test]
fn keys_sorted_by_doc_id() {
let q = QuantizedVectorPool::build(
sample_params(),
1,
vec![
(5, "a".to_string(), vec![10], meta(10, 0.0)),
(1, "a".to_string(), vec![20], meta(20, 0.0)),
(3, "b".to_string(), vec![30], meta(30, 0.0)),
],
);
let keys = q.keys();
let ids: Vec<u64> = keys.iter().map(|(id, _)| *id).collect();
assert_eq!(ids, vec![1, 3, 5]);
}
#[test]
fn doc_ids_for_field_returns_sorted_arc() {
let q = QuantizedVectorPool::build(
sample_params(),
1,
vec![
(5, "f".to_string(), vec![10], meta(10, 0.0)),
(1, "f".to_string(), vec![20], meta(20, 0.0)),
(3, "f".to_string(), vec![30], meta(30, 0.0)),
],
);
let ids = q.doc_ids_for_field("f");
assert_eq!(ids.as_ref(), &[1u64, 3, 5][..]);
assert!(q.doc_ids_for_field("missing").is_empty());
}
#[test]
fn field_names_sorted_alphabetically() {
let q = QuantizedVectorPool::build(
sample_params(),
1,
vec![
(1, "z".to_string(), vec![0], meta(0, 0.0)),
(1, "a".to_string(), vec![0], meta(0, 0.0)),
(1, "m".to_string(), vec![0], meta(0, 0.0)),
],
);
assert_eq!(q.field_names(), vec!["a", "m", "z"]);
}
#[test]
fn padded_dim_rounds_up_to_block() {
assert_eq!(QuantizedVectorPool::padded_dim(0), 0);
assert_eq!(QuantizedVectorPool::padded_dim(1), 32);
assert_eq!(QuantizedVectorPool::padded_dim(32), 32);
assert_eq!(QuantizedVectorPool::padded_dim(33), 64);
assert_eq!(QuantizedVectorPool::padded_dim(100), 128);
assert_eq!(QuantizedVectorPool::padded_dim(128), 128);
}
#[test]
fn contains_reflects_field_and_doc_id() {
let q = QuantizedVectorPool::build(
sample_params(),
1,
vec![(1, "f".to_string(), vec![0], meta(0, 0.0))],
);
assert!(q.contains(1, "f"));
assert!(!q.contains(2, "f"));
assert!(!q.contains(1, "g"));
}
}