use crate::{Vector, VectorData, VectorError};
use half::f16;
use std::io::{Read, Write};
#[derive(Debug, Clone, Default)]
pub enum CompressionMethod {
#[default]
None,
Zstd {
level: i32,
},
Quantization {
bits: u8,
},
ProductQuantization {
subvectors: usize,
codebook_size: usize,
},
Pca {
components: usize,
},
Adaptive {
quality_level: AdaptiveQuality,
analysis_samples: usize,
},
}
#[derive(Debug, Clone)]
pub enum AdaptiveQuality {
Fast, Balanced, BestRatio, }
pub trait VectorCompressor: Send + Sync {
fn compress(&self, vector: &Vector) -> Result<Vec<u8>, VectorError>;
fn decompress(&self, data: &[u8], dimensions: usize) -> Result<Vector, VectorError>;
fn compression_ratio(&self) -> f32;
}
pub struct ZstdCompressor {
level: i32,
}
impl ZstdCompressor {
pub fn new(level: i32) -> Self {
Self {
level: level.clamp(1, 22),
}
}
}
impl VectorCompressor for ZstdCompressor {
fn compress(&self, vector: &Vector) -> Result<Vec<u8>, VectorError> {
let bytes = vector_to_bytes(vector)?;
oxiarc_zstd::encode_all(&bytes, self.level)
.map_err(|e| VectorError::CompressionError(e.to_string()))
}
fn decompress(&self, data: &[u8], dimensions: usize) -> Result<Vector, VectorError> {
let decompressed = oxiarc_zstd::decode_all(data)
.map_err(|e| VectorError::CompressionError(e.to_string()))?;
bytes_to_vector(&decompressed, dimensions)
}
fn compression_ratio(&self) -> f32 {
match self.level {
1..=3 => 0.7,
4..=9 => 0.5,
10..=15 => 0.4,
16..=22 => 0.3,
_ => 1.0,
}
}
}
pub struct ScalarQuantizer {
bits: u8,
min_val: f32,
max_val: f32,
}
impl ScalarQuantizer {
pub fn new(bits: u8) -> Self {
Self {
bits: bits.clamp(1, 16),
min_val: 0.0,
max_val: 1.0,
}
}
pub fn with_range(bits: u8, min_val: f32, max_val: f32) -> Self {
Self {
bits: bits.clamp(1, 16),
min_val,
max_val,
}
}
pub fn train(&mut self, vectors: &[Vector]) -> Result<(), VectorError> {
if vectors.is_empty() {
return Err(VectorError::InvalidDimensions(
"No vectors to train on".to_string(),
));
}
let mut min = f32::INFINITY;
let mut max = f32::NEG_INFINITY;
for vector in vectors {
match &vector.values {
VectorData::F32(v) => {
for &val in v {
min = min.min(val);
max = max.max(val);
}
}
VectorData::F64(v) => {
for &val in v {
min = min.min(val as f32);
max = max.max(val as f32);
}
}
_ => {}
}
}
self.min_val = min;
self.max_val = max;
Ok(())
}
fn quantize_value(&self, value: f32) -> u16 {
let normalized = ((value - self.min_val) / (self.max_val - self.min_val)).clamp(0.0, 1.0);
let max_quant_val = (1u32 << self.bits) - 1;
(normalized * max_quant_val as f32).round() as u16
}
fn dequantize_value(&self, quantized: u16) -> f32 {
let max_quant_val = (1u32 << self.bits) - 1;
let normalized = quantized as f32 / max_quant_val as f32;
normalized * (self.max_val - self.min_val) + self.min_val
}
}
impl VectorCompressor for ScalarQuantizer {
fn compress(&self, vector: &Vector) -> Result<Vec<u8>, VectorError> {
let values = match &vector.values {
VectorData::F32(v) => v.clone(),
VectorData::F64(v) => v.iter().map(|&x| x as f32).collect(),
_ => {
return Err(VectorError::UnsupportedOperation(
"Quantization only supports float vectors".to_string(),
))
}
};
let mut compressed = Vec::new();
compressed.write_all(&self.bits.to_le_bytes())?;
compressed.write_all(&self.min_val.to_le_bytes())?;
compressed.write_all(&self.max_val.to_le_bytes())?;
if self.bits <= 8 {
for val in values {
let quantized = self.quantize_value(val) as u8;
compressed.push(quantized);
}
} else {
for val in values {
let quantized = self.quantize_value(val);
compressed.write_all(&quantized.to_le_bytes())?;
}
}
Ok(compressed)
}
fn decompress(&self, data: &[u8], dimensions: usize) -> Result<Vector, VectorError> {
let mut cursor = std::io::Cursor::new(data);
let mut bits_buf = [0u8; 1];
cursor.read_exact(&mut bits_buf)?;
let bits = bits_buf[0];
let mut min_buf = [0u8; 4];
cursor.read_exact(&mut min_buf)?;
let min_val = f32::from_le_bytes(min_buf);
let mut max_buf = [0u8; 4];
cursor.read_exact(&mut max_buf)?;
let max_val = f32::from_le_bytes(max_buf);
let quantizer = ScalarQuantizer {
bits,
min_val,
max_val,
};
let mut values = Vec::with_capacity(dimensions);
if bits <= 8 {
let mut buf = [0u8; 1];
for _ in 0..dimensions {
cursor.read_exact(&mut buf)?;
let quantized = buf[0] as u16;
values.push(quantizer.dequantize_value(quantized));
}
} else {
let mut buf = [0u8; 2];
for _ in 0..dimensions {
cursor.read_exact(&mut buf)?;
let quantized = u16::from_le_bytes(buf);
values.push(quantizer.dequantize_value(quantized));
}
}
Ok(Vector::new(values))
}
fn compression_ratio(&self) -> f32 {
self.bits as f32 / 32.0
}
}
pub struct PcaCompressor {
components: usize,
mean: Vec<f32>,
components_matrix: Vec<Vec<f32>>,
explained_variance_ratio: Vec<f32>,
}
impl PcaCompressor {
pub fn new(components: usize) -> Self {
Self {
components,
mean: Vec::new(),
components_matrix: Vec::new(),
explained_variance_ratio: Vec::new(),
}
}
pub fn train(&mut self, vectors: &[Vector]) -> Result<(), VectorError> {
if vectors.is_empty() {
return Err(VectorError::InvalidDimensions(
"No vectors to train on".to_string(),
));
}
let data: Vec<Vec<f32>> = vectors
.iter()
.map(|v| match &v.values {
VectorData::F32(vals) => Ok(vals.clone()),
VectorData::F64(vals) => Ok(vals.iter().map(|&x| x as f32).collect()),
_ => Err(VectorError::UnsupportedOperation(
"PCA only supports float vectors".to_string(),
)),
})
.collect::<Result<Vec<_>, _>>()?;
let n_samples = data.len();
let n_features = data[0].len();
self.mean = vec![0.0; n_features];
for sample in &data {
for (i, &val) in sample.iter().enumerate() {
self.mean[i] += val;
}
}
for val in &mut self.mean {
*val /= n_samples as f32;
}
let mut centered = data.clone();
for sample in &mut centered {
for (i, val) in sample.iter_mut().enumerate() {
*val -= self.mean[i];
}
}
self.components_matrix = Vec::with_capacity(self.components);
use nalgebra::DMatrix;
let training_data: Result<Vec<Vec<f32>>, _> = vectors
.iter()
.map(|v| match &v.values {
VectorData::F32(vals) => Ok(vals.clone()),
VectorData::F64(vals) => Ok(vals.iter().map(|&x| x as f32).collect()),
_ => Err(VectorError::UnsupportedOperation(
"PCA only supports float vectors".to_string(),
)),
})
.collect();
let training_data = training_data?;
let n_samples = training_data.len();
if n_samples == 0 {
return Err(VectorError::InvalidDimensions(
"No training data provided for PCA".to_string(),
));
}
let mut data_matrix = DMatrix::<f32>::zeros(n_samples, n_features);
for (i, sample) in training_data.iter().enumerate() {
for (j, &val) in sample.iter().enumerate() {
data_matrix[(i, j)] = val - self.mean[j];
}
}
let covariance = data_matrix.transpose() * &data_matrix / (n_samples as f32 - 1.0);
let svd = covariance.svd(true, true);
if let Some(u) = svd.u {
let num_components = self.components.min(u.ncols());
let singular_values = &svd.singular_values;
let total_variance: f32 = singular_values.iter().sum();
let mut explained_variance = Vec::with_capacity(num_components);
for i in 0..num_components {
let component: Vec<f32> = u.column(i).iter().cloned().collect();
self.components_matrix.push(component);
let variance_ratio = singular_values[i] / total_variance;
explained_variance.push(variance_ratio);
}
self.explained_variance_ratio = explained_variance;
} else {
return Err(VectorError::CompressionError(
"SVD decomposition failed for PCA".to_string(),
));
}
Ok(())
}
fn project(&self, vector: &[f32]) -> Vec<f32> {
let mut centered = vector.to_vec();
for (i, val) in centered.iter_mut().enumerate() {
*val -= self.mean.get(i).unwrap_or(&0.0);
}
let mut projected = vec![0.0; self.components];
for (i, component) in self.components_matrix.iter().enumerate() {
let mut dot = 0.0;
for (j, &val) in centered.iter().enumerate() {
dot += val * component.get(j).unwrap_or(&0.0);
}
projected[i] = dot;
}
projected
}
fn reconstruct(&self, projected: &[f32]) -> Vec<f32> {
let n_features = self.mean.len();
let mut reconstructed = self.mean.clone();
for (i, &coeff) in projected.iter().enumerate() {
if let Some(component) = self.components_matrix.get(i) {
for (j, &comp_val) in component.iter().enumerate() {
if j < n_features {
reconstructed[j] += coeff * comp_val;
}
}
}
}
reconstructed
}
pub fn explained_variance_ratio(&self) -> &[f32] {
&self.explained_variance_ratio
}
pub fn total_explained_variance(&self) -> f32 {
self.explained_variance_ratio.iter().sum()
}
}
impl VectorCompressor for PcaCompressor {
fn compress(&self, vector: &Vector) -> Result<Vec<u8>, VectorError> {
let values = match &vector.values {
VectorData::F32(v) => v.clone(),
VectorData::F64(v) => v.iter().map(|&x| x as f32).collect(),
_ => {
return Err(VectorError::UnsupportedOperation(
"PCA only supports float vectors".to_string(),
))
}
};
let projected = self.project(&values);
let mut compressed = Vec::new();
compressed.write_all(&(self.components as u32).to_le_bytes())?;
for val in projected {
compressed.write_all(&val.to_le_bytes())?;
}
Ok(compressed)
}
fn decompress(&self, data: &[u8], _dimensions: usize) -> Result<Vector, VectorError> {
let mut cursor = std::io::Cursor::new(data);
let mut components_buf = [0u8; 4];
cursor.read_exact(&mut components_buf)?;
let components = u32::from_le_bytes(components_buf) as usize;
let mut projected = Vec::with_capacity(components);
let mut val_buf = [0u8; 4];
for _ in 0..components {
cursor.read_exact(&mut val_buf)?;
projected.push(f32::from_le_bytes(val_buf));
}
let reconstructed = self.reconstruct(&projected);
Ok(Vector::new(reconstructed))
}
fn compression_ratio(&self) -> f32 {
if self.mean.is_empty() {
1.0
} else {
self.components as f32 / self.mean.len() as f32
}
}
}
pub struct ProductQuantizer {
subvectors: usize,
codebook_size: usize,
codebooks: Vec<Vec<Vec<f32>>>, subvector_dim: usize,
}
impl ProductQuantizer {
pub fn new(subvectors: usize, codebook_size: usize) -> Self {
Self {
subvectors,
codebook_size,
codebooks: Vec::new(),
subvector_dim: 0,
}
}
pub fn train(&mut self, vectors: &[Vector]) -> Result<(), VectorError> {
if vectors.is_empty() {
return Err(VectorError::InvalidDimensions(
"No training data provided for Product Quantization".to_string(),
));
}
let vector_dim = vectors[0].dimensions;
if vector_dim % self.subvectors != 0 {
return Err(VectorError::InvalidDimensions(format!(
"Vector dimension {} is not divisible by number of subvectors {}",
vector_dim, self.subvectors
)));
}
self.subvector_dim = vector_dim / self.subvectors;
self.codebooks = Vec::with_capacity(self.subvectors);
let training_data: Result<Vec<Vec<f32>>, _> = vectors
.iter()
.map(|v| match &v.values {
VectorData::F32(vals) => Ok(vals.clone()),
VectorData::F64(vals) => Ok(vals.iter().map(|&x| x as f32).collect()),
_ => Err(VectorError::UnsupportedOperation(
"Product quantization only supports float vectors".to_string(),
)),
})
.collect();
let training_data = training_data?;
for subvec_idx in 0..self.subvectors {
let start_dim = subvec_idx * self.subvector_dim;
let end_dim = start_dim + self.subvector_dim;
let subvectors: Vec<Vec<f32>> = training_data
.iter()
.map(|v| v[start_dim..end_dim].to_vec())
.collect();
let codebook = self.train_codebook(&subvectors)?;
self.codebooks.push(codebook);
}
Ok(())
}
fn train_codebook(&self, subvectors: &[Vec<f32>]) -> Result<Vec<Vec<f32>>, VectorError> {
use scirs2_core::random::Random;
let mut rng = Random::seed(42);
if subvectors.is_empty() {
return Err(VectorError::InvalidDimensions(
"No subvectors to train codebook".to_string(),
));
}
let dim = subvectors[0].len();
let mut centroids = Vec::with_capacity(self.codebook_size);
for _ in 0..self.codebook_size {
let mut centroid = vec![0.0; dim];
for val in &mut centroid {
*val = rng.gen_range(-1.0..1.0);
}
centroids.push(centroid);
}
for _ in 0..10 {
let mut assignments = vec![0; subvectors.len()];
for (i, subvec) in subvectors.iter().enumerate() {
let mut best_dist = f32::INFINITY;
let mut best_centroid = 0;
for (j, centroid) in centroids.iter().enumerate() {
let dist = euclidean_distance(subvec, centroid);
if dist < best_dist {
best_dist = dist;
best_centroid = j;
}
}
assignments[i] = best_centroid;
}
for (j, centroid) in centroids.iter_mut().enumerate() {
let assigned_points: Vec<&Vec<f32>> = subvectors
.iter()
.enumerate()
.filter(|(i, _)| assignments[*i] == j)
.map(|(_, v)| v)
.collect();
if !assigned_points.is_empty() {
for (d, centroid_val) in centroid.iter_mut().enumerate() {
*centroid_val = assigned_points.iter().map(|p| p[d]).sum::<f32>()
/ assigned_points.len() as f32;
}
}
}
}
Ok(centroids)
}
fn quantize_vector(&self, vector: &[f32]) -> Result<Vec<u8>, VectorError> {
if vector.len() != self.subvectors * self.subvector_dim {
return Err(VectorError::InvalidDimensions(format!(
"Vector dimension {} doesn't match expected {}",
vector.len(),
self.subvectors * self.subvector_dim
)));
}
let mut codes = Vec::with_capacity(self.subvectors);
for subvec_idx in 0..self.subvectors {
let start_dim = subvec_idx * self.subvector_dim;
let end_dim = start_dim + self.subvector_dim;
let subvector = &vector[start_dim..end_dim];
let codebook = &self.codebooks[subvec_idx];
let mut best_dist = f32::INFINITY;
let mut best_code = 0u8;
for (code, centroid) in codebook.iter().enumerate() {
let dist = euclidean_distance(subvector, centroid);
if dist < best_dist {
best_dist = dist;
best_code = code as u8;
}
}
codes.push(best_code);
}
Ok(codes)
}
fn dequantize_codes(&self, codes: &[u8]) -> Result<Vec<f32>, VectorError> {
if codes.len() != self.subvectors {
return Err(VectorError::InvalidDimensions(format!(
"Code length {} doesn't match expected {}",
codes.len(),
self.subvectors
)));
}
let mut reconstructed = Vec::with_capacity(self.subvectors * self.subvector_dim);
for (subvec_idx, &code) in codes.iter().enumerate() {
let codebook = &self.codebooks[subvec_idx];
if (code as usize) < codebook.len() {
reconstructed.extend_from_slice(&codebook[code as usize]);
} else {
return Err(VectorError::InvalidDimensions(format!(
"Invalid code {} for codebook of size {}",
code,
codebook.len()
)));
}
}
Ok(reconstructed)
}
}
fn euclidean_distance(a: &[f32], b: &[f32]) -> f32 {
a.iter()
.zip(b.iter())
.map(|(&x, &y)| (x - y).powi(2))
.sum::<f32>()
.sqrt()
}
impl VectorCompressor for ProductQuantizer {
fn compress(&self, vector: &Vector) -> Result<Vec<u8>, VectorError> {
let values = match &vector.values {
VectorData::F32(v) => v.clone(),
VectorData::F64(v) => v.iter().map(|&x| x as f32).collect(),
_ => {
return Err(VectorError::UnsupportedOperation(
"Product quantization only supports float vectors".to_string(),
))
}
};
let codes = self.quantize_vector(&values)?;
let mut compressed = Vec::new();
compressed.write_all(&(self.subvectors as u32).to_le_bytes())?;
compressed.write_all(&(self.codebook_size as u32).to_le_bytes())?;
compressed.write_all(&(self.subvector_dim as u32).to_le_bytes())?;
compressed.extend_from_slice(&codes);
Ok(compressed)
}
fn decompress(&self, data: &[u8], _dimensions: usize) -> Result<Vector, VectorError> {
if data.len() < 12 {
return Err(VectorError::InvalidData(
"Invalid compressed data format".to_string(),
));
}
let subvectors = u32::from_le_bytes([data[0], data[1], data[2], data[3]]) as usize;
let codebook_size = u32::from_le_bytes([data[4], data[5], data[6], data[7]]) as usize;
let subvector_dim = u32::from_le_bytes([data[8], data[9], data[10], data[11]]) as usize;
if subvectors != self.subvectors
|| codebook_size != self.codebook_size
|| subvector_dim != self.subvector_dim
{
return Err(VectorError::InvalidData(
"Metadata mismatch in compressed data".to_string(),
));
}
let codes = &data[12..];
if codes.len() != subvectors {
return Err(VectorError::InvalidData("Invalid code length".to_string()));
}
let values = self.dequantize_codes(codes)?;
Ok(Vector::new(values))
}
fn compression_ratio(&self) -> f32 {
(8.0 * self.subvectors as f32) / (32.0 * self.subvectors as f32 * self.subvector_dim as f32)
}
}
#[derive(Debug, Clone)]
pub struct VectorAnalysis {
pub sparsity: f32, pub range: f32, pub mean: f32, pub std_dev: f32, pub entropy: f32, pub dominant_patterns: Vec<f32>, pub recommended_method: CompressionMethod,
pub expected_ratio: f32, }
impl VectorAnalysis {
pub fn analyze(vectors: &[Vector], quality: &AdaptiveQuality) -> Result<Self, VectorError> {
if vectors.is_empty() {
return Err(VectorError::InvalidDimensions(
"No vectors to analyze".to_string(),
));
}
let mut all_values = Vec::new();
let mut dimensions = 0;
for vector in vectors {
let values = match &vector.values {
VectorData::F32(v) => v.clone(),
VectorData::F64(v) => v.iter().map(|&x| x as f32).collect(),
VectorData::F16(v) => v.iter().map(|&x| f16::from_bits(x).to_f32()).collect(),
VectorData::I8(v) => v.iter().map(|&x| x as f32).collect(),
VectorData::Binary(_) => {
return Ok(Self::binary_analysis(vectors.len()));
}
};
if dimensions == 0 {
dimensions = values.len();
}
all_values.extend(values);
}
if all_values.is_empty() {
return Err(VectorError::InvalidDimensions(
"No values to analyze".to_string(),
));
}
let min_val = all_values.iter().copied().fold(f32::INFINITY, f32::min);
let max_val = all_values.iter().copied().fold(f32::NEG_INFINITY, f32::max);
let range = max_val - min_val;
let mean = all_values.iter().sum::<f32>() / all_values.len() as f32;
let variance =
all_values.iter().map(|&x| (x - mean).powi(2)).sum::<f32>() / all_values.len() as f32;
let std_dev = variance.sqrt();
let epsilon = std_dev * 0.01; let near_zero_count = all_values.iter().filter(|&&x| x.abs() < epsilon).count();
let sparsity = near_zero_count as f32 / all_values.len() as f32;
let entropy = Self::calculate_entropy(&all_values);
let dominant_patterns = Self::find_dominant_patterns(&all_values);
let (recommended_method, expected_ratio) =
Self::select_optimal_method(sparsity, range, std_dev, entropy, dimensions, quality);
Ok(Self {
sparsity,
range,
mean,
std_dev,
entropy,
dominant_patterns,
recommended_method,
expected_ratio,
})
}
fn binary_analysis(_vector_count: usize) -> Self {
Self {
sparsity: 0.0,
range: 1.0,
mean: 0.5,
std_dev: 0.5,
entropy: 1.0,
dominant_patterns: vec![0.0, 1.0],
recommended_method: CompressionMethod::Zstd { level: 1 },
expected_ratio: 0.125, }
}
fn calculate_entropy(values: &[f32]) -> f32 {
let mut histogram = std::collections::HashMap::new();
let bins = 64;
if values.is_empty() {
return 0.0;
}
let min_val = values.iter().copied().fold(f32::INFINITY, f32::min);
let max_val = values.iter().copied().fold(f32::NEG_INFINITY, f32::max);
let range = max_val - min_val;
if range == 0.0 {
return 0.0; }
for &value in values {
let bin = ((value - min_val) / range * (bins - 1) as f32) as usize;
let bin = bin.min(bins - 1);
*histogram.entry(bin).or_insert(0) += 1;
}
let total = values.len() as f32;
let mut entropy = 0.0;
for count in histogram.values() {
let probability = *count as f32 / total;
if probability > 0.0 {
entropy -= probability * probability.log2();
}
}
entropy
}
fn find_dominant_patterns(values: &[f32]) -> Vec<f32> {
let mut value_counts = std::collections::HashMap::new();
for &value in values {
let quantized = (value * 1000.0).round() / 1000.0; *value_counts.entry(quantized.to_bits()).or_insert(0) += 1;
}
let mut patterns: Vec<_> = value_counts.into_iter().collect();
patterns.sort_by_key(|b| std::cmp::Reverse(b.1));
patterns
.into_iter()
.take(5) .map(|(bits, _)| f32::from_bits(bits))
.collect()
}
fn select_optimal_method(
sparsity: f32,
range: f32,
std_dev: f32,
entropy: f32,
dimensions: usize,
quality: &AdaptiveQuality,
) -> (CompressionMethod, f32) {
if sparsity > 0.7 {
return match quality {
AdaptiveQuality::Fast => (CompressionMethod::Zstd { level: 1 }, 0.3),
AdaptiveQuality::Balanced => (CompressionMethod::Zstd { level: 6 }, 0.2),
AdaptiveQuality::BestRatio => (CompressionMethod::Zstd { level: 19 }, 0.15),
};
}
if entropy < 2.0 {
return match quality {
AdaptiveQuality::Fast => (CompressionMethod::Zstd { level: 3 }, 0.4),
AdaptiveQuality::Balanced => (CompressionMethod::Zstd { level: 9 }, 0.3),
AdaptiveQuality::BestRatio => (CompressionMethod::Zstd { level: 22 }, 0.2),
};
}
if range < 2.0 && std_dev < 0.5 {
return match quality {
AdaptiveQuality::Fast => (CompressionMethod::Quantization { bits: 8 }, 0.25),
AdaptiveQuality::Balanced => (CompressionMethod::Quantization { bits: 6 }, 0.1875),
AdaptiveQuality::BestRatio => (CompressionMethod::Quantization { bits: 4 }, 0.125),
};
}
if dimensions > 128 {
let components = match quality {
AdaptiveQuality::Fast => dimensions * 7 / 10, AdaptiveQuality::Balanced => dimensions / 2, AdaptiveQuality::BestRatio => dimensions / 3, };
return (
CompressionMethod::Pca { components },
components as f32 / dimensions as f32,
);
}
match quality {
AdaptiveQuality::Fast => (CompressionMethod::Zstd { level: 3 }, 0.6),
AdaptiveQuality::Balanced => (CompressionMethod::Zstd { level: 6 }, 0.5),
AdaptiveQuality::BestRatio => (CompressionMethod::Zstd { level: 12 }, 0.4),
}
}
}
pub struct AdaptiveCompressor {
quality_level: AdaptiveQuality,
analysis_samples: usize,
current_method: Option<Box<dyn VectorCompressor>>,
analysis_cache: Option<VectorAnalysis>,
performance_metrics: CompressionMetrics,
}
#[derive(Debug, Clone)]
pub struct CompressionMetrics {
pub vectors_compressed: usize,
pub total_original_size: usize,
pub total_compressed_size: usize,
pub compression_time_ms: f64,
pub decompression_time_ms: f64,
pub current_ratio: f32,
pub method_switches: usize,
}
impl Default for CompressionMetrics {
fn default() -> Self {
Self {
vectors_compressed: 0,
total_original_size: 0,
total_compressed_size: 0,
compression_time_ms: 0.0,
decompression_time_ms: 0.0,
current_ratio: 1.0,
method_switches: 0,
}
}
}
impl AdaptiveCompressor {
pub fn new(quality_level: AdaptiveQuality, analysis_samples: usize) -> Self {
Self {
quality_level,
analysis_samples: analysis_samples.max(10), current_method: None,
analysis_cache: None,
performance_metrics: CompressionMetrics::default(),
}
}
pub fn with_fast_quality() -> Self {
Self::new(AdaptiveQuality::Fast, 50)
}
pub fn with_balanced_quality() -> Self {
Self::new(AdaptiveQuality::Balanced, 100)
}
pub fn with_best_ratio() -> Self {
Self::new(AdaptiveQuality::BestRatio, 200)
}
pub fn optimize_for_vectors(&mut self, sample_vectors: &[Vector]) -> Result<(), VectorError> {
if sample_vectors.is_empty() {
return Ok(());
}
let start_time = std::time::Instant::now();
let samples_to_analyze = sample_vectors.len().min(self.analysis_samples);
let analysis_vectors = &sample_vectors[..samples_to_analyze];
let analysis = VectorAnalysis::analyze(analysis_vectors, &self.quality_level)?;
let should_switch = match (&self.current_method, &self.analysis_cache) {
(Some(_), Some(cached)) => {
!methods_equivalent(&cached.recommended_method, &analysis.recommended_method)
}
_ => true, };
if should_switch {
self.current_method = Some(create_compressor(&analysis.recommended_method));
self.performance_metrics.method_switches += 1;
}
if self.current_method.is_some() {
}
self.analysis_cache = Some(analysis);
let analysis_time = start_time.elapsed().as_secs_f64() * 1000.0;
tracing::debug!("Adaptive compression analysis took {:.2}ms", analysis_time);
Ok(())
}
fn train_compressor(
&self,
_compressor: &mut dyn VectorCompressor,
_vectors: &[Vector],
) -> Result<(), VectorError> {
Ok(())
}
pub fn get_metrics(&self) -> &CompressionMetrics {
&self.performance_metrics
}
pub fn get_analysis(&self) -> Option<&VectorAnalysis> {
self.analysis_cache.as_ref()
}
pub fn adaptive_reanalysis(&mut self, recent_vectors: &[Vector]) -> Result<bool, VectorError> {
if recent_vectors.len() < self.analysis_samples / 4 {
return Ok(false); }
let old_method = self
.analysis_cache
.as_ref()
.map(|a| a.recommended_method.clone());
self.optimize_for_vectors(recent_vectors)?;
let method_changed = match (old_method, &self.analysis_cache) {
(Some(old), Some(new)) => !methods_equivalent(&old, &new.recommended_method),
_ => false,
};
Ok(method_changed)
}
}
impl VectorCompressor for AdaptiveCompressor {
fn compress(&self, vector: &Vector) -> Result<Vec<u8>, VectorError> {
if let Some(compressor) = &self.current_method {
let start = std::time::Instant::now();
let result = compressor.compress(vector);
let _compression_time = start.elapsed().as_secs_f64() * 1000.0;
result
} else {
let no_op = NoOpCompressor;
no_op.compress(vector)
}
}
fn decompress(&self, data: &[u8], dimensions: usize) -> Result<Vector, VectorError> {
if let Some(compressor) = &self.current_method {
let start = std::time::Instant::now();
let result = compressor.decompress(data, dimensions);
let _decompression_time = start.elapsed().as_secs_f64() * 1000.0;
result
} else {
let no_op = NoOpCompressor;
no_op.decompress(data, dimensions)
}
}
fn compression_ratio(&self) -> f32 {
if let Some(compressor) = &self.current_method {
compressor.compression_ratio()
} else {
1.0
}
}
}
fn methods_equivalent(method1: &CompressionMethod, method2: &CompressionMethod) -> bool {
match (method1, method2) {
(CompressionMethod::None, CompressionMethod::None) => true,
(CompressionMethod::Zstd { level: l1 }, CompressionMethod::Zstd { level: l2 }) => {
(l1 - l2).abs() <= 2 }
(
CompressionMethod::Quantization { bits: b1 },
CompressionMethod::Quantization { bits: b2 },
) => b1 == b2,
(CompressionMethod::Pca { components: c1 }, CompressionMethod::Pca { components: c2 }) => {
((*c1 as i32) - (*c2 as i32)).abs() <= (*c1 as i32) / 10 }
_ => false,
}
}
pub fn create_compressor(method: &CompressionMethod) -> Box<dyn VectorCompressor> {
match method {
CompressionMethod::None => Box::new(NoOpCompressor),
CompressionMethod::Zstd { level } => Box::new(ZstdCompressor::new(*level)),
CompressionMethod::Quantization { bits } => Box::new(ScalarQuantizer::new(*bits)),
CompressionMethod::Pca { components } => Box::new(PcaCompressor::new(*components)),
CompressionMethod::ProductQuantization {
subvectors,
codebook_size,
} => Box::new(ProductQuantizer::new(*subvectors, *codebook_size)),
CompressionMethod::Adaptive {
quality_level,
analysis_samples,
} => Box::new(AdaptiveCompressor::new(
quality_level.clone(),
*analysis_samples,
)),
}
}
struct NoOpCompressor;
impl VectorCompressor for NoOpCompressor {
fn compress(&self, vector: &Vector) -> Result<Vec<u8>, VectorError> {
vector_to_bytes(vector)
}
fn decompress(&self, data: &[u8], dimensions: usize) -> Result<Vector, VectorError> {
bytes_to_vector(data, dimensions)
}
fn compression_ratio(&self) -> f32 {
1.0
}
}
fn vector_to_bytes(vector: &Vector) -> Result<Vec<u8>, VectorError> {
let mut bytes = Vec::new();
let type_byte = match &vector.values {
VectorData::F32(_) => 0u8,
VectorData::F64(_) => 1u8,
VectorData::F16(_) => 2u8,
VectorData::I8(_) => 3u8,
VectorData::Binary(_) => 4u8,
};
bytes.push(type_byte);
match &vector.values {
VectorData::F32(v) => {
for val in v {
bytes.write_all(&val.to_le_bytes())?;
}
}
VectorData::F64(v) => {
for val in v {
bytes.write_all(&val.to_le_bytes())?;
}
}
VectorData::F16(v) => {
for val in v {
bytes.write_all(&val.to_le_bytes())?;
}
}
VectorData::I8(v) => {
for &val in v {
bytes.push(val as u8);
}
}
VectorData::Binary(v) => {
bytes.extend_from_slice(v);
}
}
Ok(bytes)
}
fn bytes_to_vector(data: &[u8], dimensions: usize) -> Result<Vector, VectorError> {
if data.is_empty() {
return Err(VectorError::InvalidDimensions("Empty data".to_string()));
}
let type_byte = data[0];
let data = &data[1..];
match type_byte {
0 => {
let mut values = Vec::with_capacity(dimensions);
let mut cursor = std::io::Cursor::new(data);
let mut buf = [0u8; 4];
for _ in 0..dimensions {
cursor.read_exact(&mut buf)?;
values.push(f32::from_le_bytes(buf));
}
Ok(Vector::new(values))
}
1 => {
let mut values = Vec::with_capacity(dimensions);
let mut cursor = std::io::Cursor::new(data);
let mut buf = [0u8; 8];
for _ in 0..dimensions {
cursor.read_exact(&mut buf)?;
values.push(f64::from_le_bytes(buf));
}
Ok(Vector::f64(values))
}
2 => {
let mut values = Vec::with_capacity(dimensions);
let mut cursor = std::io::Cursor::new(data);
let mut buf = [0u8; 2];
for _ in 0..dimensions {
cursor.read_exact(&mut buf)?;
values.push(u16::from_le_bytes(buf));
}
Ok(Vector::f16(values))
}
3 => {
Ok(Vector::i8(
data[..dimensions].iter().map(|&b| b as i8).collect(),
))
}
4 => {
Ok(Vector::binary(data[..dimensions].to_vec()))
}
_ => Err(VectorError::InvalidData(format!(
"Unknown vector type: {type_byte}"
))),
}
}
#[cfg(test)]
mod tests {
type Result<T> = std::result::Result<T, Box<dyn std::error::Error>>;
use super::*;
#[test]
fn test_zstd_compression() -> Result<()> {
let vector = Vector::new(vec![1.0, 2.0, 3.0, 4.0, 5.0]);
let compressor = ZstdCompressor::new(3);
let compressed = compressor.compress(&vector)?;
let decompressed = compressor.decompress(&compressed, 5)?;
let orig = vector.as_f32();
let dec = decompressed.as_f32();
assert_eq!(orig.len(), dec.len());
for (a, b) in orig.iter().zip(dec.iter()) {
assert!((a - b).abs() < 1e-6);
}
Ok(())
}
#[test]
fn test_scalar_quantization() -> Result<()> {
let vector = Vector::new(vec![0.1, 0.5, 0.9, 0.3, 0.7]);
let mut quantizer = ScalarQuantizer::new(8);
quantizer.train(std::slice::from_ref(&vector))?;
let compressed = quantizer.compress(&vector)?;
let decompressed = quantizer.decompress(&compressed, 5)?;
assert!(compressed.len() < 20);
let orig = vector.as_f32();
let dec = decompressed.as_f32();
assert_eq!(orig.len(), dec.len());
for (a, b) in orig.iter().zip(dec.iter()) {
assert!((a - b).abs() < 0.01);
}
Ok(())
}
#[test]
fn test_pca_compression() -> Result<()> {
let vectors = vec![
Vector::new(vec![1.0, 2.0, 3.0, 4.0, 5.0]),
Vector::new(vec![2.0, 3.0, 4.0, 5.0, 6.0]),
Vector::new(vec![3.0, 4.0, 5.0, 6.0, 7.0]),
];
let mut pca = PcaCompressor::new(3);
pca.train(&vectors)?;
let compressed = pca.compress(&vectors[0])?;
let decompressed = pca.decompress(&compressed, 5)?;
let dec = decompressed.as_f32();
assert_eq!(dec.len(), 5);
Ok(())
}
#[test]
fn test_adaptive_compression_sparse_data() -> Result<()> {
let vectors = vec![
Vector::new(vec![0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 2.0, 0.0, 0.0, 0.0]),
Vector::new(vec![0.0, 0.0, 0.0, 0.0, 3.0, 0.0, 0.0, 0.0, 0.0, 1.0]),
Vector::new(vec![1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 2.0, 0.0]),
];
let mut adaptive = AdaptiveCompressor::with_balanced_quality();
adaptive.optimize_for_vectors(&vectors)?;
let analysis = adaptive
.get_analysis()
.ok_or("get_analysis returned None")?;
assert!(analysis.sparsity > 0.5);
let compressed = adaptive.compress(&vectors[0])?;
let decompressed = adaptive.decompress(&compressed, 10)?;
let orig = vectors[0].as_f32();
let dec = decompressed.as_f32();
assert_eq!(orig.len(), dec.len());
Ok(())
}
#[test]
fn test_adaptive_compression_quantizable_data() -> Result<()> {
let vectors = vec![
Vector::new(vec![0.1, 0.2, 0.3, 0.4, 0.5]),
Vector::new(vec![0.2, 0.3, 0.4, 0.5, 0.6]),
Vector::new(vec![0.3, 0.4, 0.5, 0.6, 0.7]),
];
let mut adaptive = AdaptiveCompressor::with_balanced_quality();
adaptive.optimize_for_vectors(&vectors)?;
let analysis = adaptive
.get_analysis()
.ok_or("get_analysis returned None")?;
assert!(analysis.range < 1.0);
let compressed = adaptive.compress(&vectors[0])?;
let decompressed = adaptive.decompress(&compressed, 5)?;
let orig = vectors[0].as_f32();
let dec = decompressed.as_f32();
assert_eq!(orig.len(), dec.len());
assert!(adaptive.compression_ratio() < 0.5); Ok(())
}
#[test]
fn test_adaptive_compression_high_dimensional() -> Result<()> {
let mut vectors = Vec::new();
for i in 0..10 {
let mut data = vec![0.0; 200]; for (j, item) in data.iter_mut().enumerate().take(200) {
*item = (i * j) as f32 * 0.01;
}
vectors.push(Vector::new(data));
}
let mut adaptive = AdaptiveCompressor::with_best_ratio();
adaptive.optimize_for_vectors(&vectors)?;
let analysis = adaptive
.get_analysis()
.ok_or("get_analysis returned None")?;
match &analysis.recommended_method {
CompressionMethod::Pca { components } => {
assert!(*components < 200); }
_ => {
assert!(matches!(
analysis.recommended_method,
CompressionMethod::Pca { .. }
| CompressionMethod::Quantization { .. }
| CompressionMethod::Zstd { .. }
));
}
}
let original = &vectors[0];
println!("Original vector length: {}", original.dimensions);
println!("Recommended method: {:?}", analysis.recommended_method);
let compressed = adaptive.compress(original)?;
println!("Compressed size: {} bytes", compressed.len());
assert!(!compressed.is_empty());
assert!(compressed.len() < original.dimensions * 4);
match &analysis.recommended_method {
CompressionMethod::Pca { components } => {
assert!(*components < original.dimensions);
println!(
"PCA compression: {} → {} components",
original.dimensions, components
);
}
_ => {
let decompressed = adaptive.decompress(&compressed, original.dimensions)?;
let dec = decompressed.as_f32();
let orig = original.as_f32();
assert_eq!(dec.len(), orig.len());
}
}
Ok(())
}
#[test]
fn test_adaptive_compression_method_switching() -> Result<()> {
let mut adaptive = AdaptiveCompressor::with_fast_quality();
let sparse_vectors = vec![
Vector::new(vec![0.0, 0.0, 1.0, 0.0, 0.0]),
Vector::new(vec![0.0, 2.0, 0.0, 0.0, 0.0]),
];
adaptive.optimize_for_vectors(&sparse_vectors)?;
let initial_switches = adaptive.get_metrics().method_switches;
let dense_vectors = vec![
Vector::new(vec![0.1, 0.2, 0.3, 0.4, 0.5]),
Vector::new(vec![0.2, 0.3, 0.4, 0.5, 0.6]),
];
adaptive.optimize_for_vectors(&dense_vectors)?;
assert!(adaptive.get_metrics().method_switches > initial_switches);
Ok(())
}
#[test]
fn test_vector_analysis() -> Result<()> {
let vectors = vec![
Vector::new(vec![1.0, 2.0, 3.0]),
Vector::new(vec![2.0, 3.0, 4.0]),
Vector::new(vec![3.0, 4.0, 5.0]),
];
let analysis = VectorAnalysis::analyze(&vectors, &AdaptiveQuality::Balanced)?;
assert!(analysis.mean > 0.0);
assert!(analysis.std_dev > 0.0);
assert!(analysis.range > 0.0);
assert!(analysis.entropy >= 0.0);
assert!(!analysis.dominant_patterns.is_empty());
assert!(analysis.expected_ratio > 0.0 && analysis.expected_ratio <= 1.0);
Ok(())
}
#[test]
fn test_compression_method_equivalence() {
assert!(methods_equivalent(
&CompressionMethod::Zstd { level: 5 },
&CompressionMethod::Zstd { level: 6 }
));
assert!(!methods_equivalent(
&CompressionMethod::Zstd { level: 1 },
&CompressionMethod::Zstd { level: 10 }
));
assert!(methods_equivalent(
&CompressionMethod::Quantization { bits: 8 },
&CompressionMethod::Quantization { bits: 8 }
));
assert!(!methods_equivalent(
&CompressionMethod::Zstd { level: 5 },
&CompressionMethod::Quantization { bits: 8 }
)); }
#[test]
fn test_adaptive_compressor_convenience_constructors() {
let fast = AdaptiveCompressor::with_fast_quality();
assert!(matches!(fast.quality_level, AdaptiveQuality::Fast));
let balanced = AdaptiveCompressor::with_balanced_quality();
assert!(matches!(balanced.quality_level, AdaptiveQuality::Balanced));
let best = AdaptiveCompressor::with_best_ratio();
assert!(matches!(best.quality_level, AdaptiveQuality::BestRatio));
}
#[test]
fn test_product_quantization() -> Result<()> {
let vectors = vec![
Vector::new(vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0]),
Vector::new(vec![2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0]),
Vector::new(vec![3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0]),
Vector::new(vec![1.5, 2.5, 3.5, 4.5, 5.5, 6.5, 7.5, 8.5]),
];
let mut pq = ProductQuantizer::new(4, 4);
pq.train(&vectors)?;
let original = &vectors[0];
let compressed = pq.compress(original)?;
let decompressed = pq.decompress(&compressed, 8)?;
assert_eq!(decompressed.dimensions, original.dimensions);
let ratio = pq.compression_ratio();
assert!(
ratio > 0.0 && ratio < 1.0,
"Compression ratio should be between 0 and 1, got {ratio}"
);
for vector in &vectors {
let compressed = pq.compress(vector)?;
let decompressed = pq.decompress(&compressed, vector.dimensions)?;
assert_eq!(decompressed.dimensions, vector.dimensions);
}
Ok(())
}
#[test]
fn test_product_quantization_invalid_dimensions() {
let vectors = vec![
Vector::new(vec![1.0, 2.0, 3.0]), ];
let mut pq = ProductQuantizer::new(4, 4); let result = pq.train(&vectors);
assert!(result.is_err());
if let Err(VectorError::InvalidDimensions(_)) = result {
} else {
panic!("Expected InvalidDimensions error");
}
}
}