pub mod codecs;
use crate::error::{Result, RuvectorError};
use crate::types::DistanceMetric;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum CodecKind {
Fp32,
Fp16,
Int8,
Turbo4,
Pq,
RaBitQ1,
}
impl CodecKind {
pub fn encoded_len(&self, dim: usize) -> Option<usize> {
match self {
CodecKind::Fp32 => Some(dim * 4),
CodecKind::Fp16 => Some(dim * 2),
CodecKind::Int8 => Some(dim + 8),
CodecKind::Turbo4 => Some(dim / 2 + 8),
CodecKind::Pq | CodecKind::RaBitQ1 => None,
}
}
}
pub trait VectorCodec: Send + Sync {
fn kind(&self) -> CodecKind;
fn dim(&self) -> usize;
fn encoded_len(&self) -> usize;
fn encode(&self, v: &[f32]) -> Result<Vec<u8>>;
fn decode(&self, blob: &[u8]) -> Result<Vec<f32>>;
fn distance(&self, a: &[u8], b: &[u8]) -> Result<f32>;
fn make_query(&self, q: &[f32]) -> Result<Box<dyn EncodedQuery>>;
}
pub trait EncodedQuery: Send + Sync {
fn distance_to(&self, blob: &[u8]) -> f32;
}
pub fn codec_for(
kind: CodecKind,
dim: usize,
metric: DistanceMetric,
rotation_seed: u64,
) -> Result<Box<dyn VectorCodec>> {
match kind {
CodecKind::Fp32 => Ok(Box::new(codecs::Fp32Codec::new(dim, metric))),
CodecKind::Fp16 => Ok(Box::new(codecs::Fp16Codec::new(dim, metric))),
CodecKind::Int8 => Ok(Box::new(codecs::Int8Codec::new(dim, metric))),
CodecKind::Turbo4 => Ok(Box::new(codecs::Turbo4PlaneCodec::new(
dim,
metric,
rotation_seed,
)?)),
CodecKind::Pq | CodecKind::RaBitQ1 => Err(RuvectorError::InvalidParameter(format!(
"{kind:?} codec is provided by its own crate and not yet wired \
into the core plane (ADR-297 phase C)"
))),
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct VectorProvenance {
pub model_id: Option<String>,
pub codec: CodecKind,
pub codec_version: u16,
pub rotation_seed: Option<u64>,
pub dim: usize,
pub metric: DistanceMetric,
pub source_hash: Option<String>,
pub lineage: Vec<String>,
}
pub const CODEC_VERSION: u16 = 1;
impl VectorProvenance {
pub fn new(codec: &dyn VectorCodec, metric: DistanceMetric) -> Self {
Self {
model_id: None,
codec: codec.kind(),
codec_version: CODEC_VERSION,
rotation_seed: None,
dim: codec.dim(),
metric,
source_hash: None,
lineage: Vec::new(),
}
}
pub fn push_migration(&mut self, entry: impl Into<String>) {
self.lineage.push(entry.into());
}
}
pub(crate) fn metric_distance(metric: DistanceMetric, a: &[f32], b: &[f32]) -> f32 {
use crate::simd_intrinsics as si;
match metric {
DistanceMetric::Euclidean => si::euclidean_distance_simd(a, b),
DistanceMetric::Cosine => (1.0_f32 - si::cosine_similarity_simd(a, b)).max(0.0),
DistanceMetric::DotProduct => (-si::dot_product_simd(a, b)).max(0.0),
DistanceMetric::Manhattan => si::manhattan_distance_simd(a, b),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn encoded_len_matches_kind_table() {
for (kind, dim, expect) in [
(CodecKind::Fp32, 128, 512),
(CodecKind::Fp16, 128, 256),
(CodecKind::Int8, 128, 136),
(CodecKind::Turbo4, 128, 72),
] {
assert_eq!(kind.encoded_len(dim), Some(expect));
let codec = codec_for(kind, dim, DistanceMetric::Euclidean, 42).unwrap();
assert_eq!(codec.encoded_len(), expect);
assert_eq!(codec.kind(), kind);
}
assert_eq!(CodecKind::Pq.encoded_len(128), None);
assert!(codec_for(CodecKind::Pq, 128, DistanceMetric::Euclidean, 42).is_err());
}
#[test]
fn provenance_roundtrips_serde() {
let codec = codec_for(CodecKind::Turbo4, 64, DistanceMetric::Cosine, 42).unwrap();
let mut p = VectorProvenance::new(codec.as_ref(), DistanceMetric::Cosine);
p.rotation_seed = Some(42);
p.model_id = Some("test-model".into());
p.push_migration("fp32→turbo4@2026-08-06");
let json = serde_json::to_string(&p).unwrap();
let back: VectorProvenance = serde_json::from_str(&json).unwrap();
assert_eq!(p, back);
assert_eq!(back.lineage.len(), 1);
}
}