use crate::math::{dot, l2_distance_sqr};
use crate::{Metric, QuantizedVector};
#[derive(Debug, Clone)]
pub struct QueryContext<'a> {
pub query: &'a [f32],
pub sum_query: f32,
pub ex_bits: u8,
pub binary_scale: f32,
pub cb: f32,
pub c1: f32,
}
impl<'a> QueryContext<'a> {
#[inline]
pub fn new(query: &'a [f32], ex_bits: u8) -> Self {
let sum_query: f32 = query.iter().sum();
let binary_scale = (1 << ex_bits) as f32;
let cb = -((1 << ex_bits) as f32 - 0.5);
let c1 = -0.5f32;
Self {
query,
sum_query,
ex_bits,
binary_scale,
cb,
c1,
}
}
}
#[inline]
pub fn estimate_distance(
ctx: &QueryContext,
centroid: &[f32],
quantized: &QuantizedVector,
metric: Metric,
) -> f32 {
let g_add = match metric {
Metric::L2 => l2_distance_sqr(ctx.query, centroid),
Metric::InnerProduct => -dot(ctx.query, centroid),
};
let binary_code = quantized.unpack_binary_code();
let binary_dot: f32 = binary_code
.iter()
.zip(ctx.query.iter())
.map(|(&bit, &q)| (bit as f32) * q)
.sum();
let binary_term = binary_dot + ctx.c1 * ctx.sum_query;
let distance_1bit = quantized.f_add + g_add + quantized.f_rescale * binary_term;
let final_distance = if ctx.ex_bits > 0 {
let ex_code = quantized.unpack_ex_code();
let ex_dot: f32 = ex_code
.iter()
.zip(ctx.query.iter())
.map(|(&code, &q)| (code as f32) * q)
.sum();
let total_term = ctx.binary_scale * binary_dot + ex_dot + ctx.cb * ctx.sum_query;
let distance_ex = quantized.f_add_ex + g_add + quantized.f_rescale_ex * total_term;
if distance_ex.is_finite() {
distance_ex
} else {
distance_1bit
}
} else {
distance_1bit
};
if metric == Metric::L2 {
final_distance.max(0.0)
} else {
final_distance
}
}
pub fn estimate_distances_batch(
query: &[f32],
centroid: &[f32],
quantized_vecs: &[QuantizedVector],
ex_bits: u8,
metric: Metric,
) -> Vec<f32> {
let ctx = QueryContext::new(query, ex_bits);
quantized_vecs
.iter()
.map(|qvec| estimate_distance(&ctx, centroid, qvec, metric))
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::quantizer::quantize_with_centroid;
use crate::RabitqConfig;
use rand::prelude::*;
fn generate_test_vectors(n: usize, dim: usize) -> (Vec<f32>, Vec<f32>, Vec<QuantizedVector>) {
let mut rng = StdRng::seed_from_u64(42);
let query: Vec<f32> = (0..dim).map(|_| rng.gen()).collect();
let centroid: Vec<f32> = (0..dim).map(|_| rng.gen()).collect();
let config = RabitqConfig::new(7);
let vectors: Vec<QuantizedVector> = (0..n)
.map(|_| {
let data: Vec<f32> = (0..dim).map(|_| rng.gen()).collect();
quantize_with_centroid(&data, ¢roid, &config, Metric::L2)
})
.collect();
(query, centroid, vectors)
}
#[test]
fn test_query_context_creation() {
let query = vec![1.0, 2.0, 3.0, 4.0];
let ctx = QueryContext::new(&query, 6);
assert_eq!(ctx.query, &query);
assert_eq!(ctx.sum_query, 10.0);
assert_eq!(ctx.ex_bits, 6);
assert_eq!(ctx.binary_scale, 64.0);
assert_eq!(ctx.cb, -63.5);
assert_eq!(ctx.c1, -0.5);
}
#[test]
fn test_estimate_distance_l2() {
let (query, centroid, vectors) = generate_test_vectors(10, 128);
let ctx = QueryContext::new(&query, 6);
for qvec in &vectors {
let dist = estimate_distance(&ctx, ¢roid, qvec, Metric::L2);
assert!(dist.is_finite(), "Distance should be finite");
assert!(dist >= 0.0, "L2 distance should be non-negative");
}
}
#[test]
fn test_estimate_distance_inner_product() {
let (query, centroid, vectors) = generate_test_vectors(10, 128);
let ctx = QueryContext::new(&query, 6);
for qvec in &vectors {
let dist = estimate_distance(&ctx, ¢roid, qvec, Metric::InnerProduct);
assert!(dist.is_finite(), "Distance should be finite");
}
}
#[test]
fn test_batch_estimation() {
let (query, centroid, vectors) = generate_test_vectors(100, 960);
let distances = estimate_distances_batch(&query, ¢roid, &vectors, 6, Metric::L2);
assert_eq!(distances.len(), 100);
for dist in &distances {
assert!(dist.is_finite());
}
}
#[test]
fn test_consistency_with_individual_calls() {
let (query, centroid, vectors) = generate_test_vectors(50, 256);
let batch_distances = estimate_distances_batch(&query, ¢roid, &vectors, 6, Metric::L2);
let ctx = QueryContext::new(&query, 6);
let individual_distances: Vec<f32> = vectors
.iter()
.map(|qvec| estimate_distance(&ctx, ¢roid, qvec, Metric::L2))
.collect();
for (batch, individual) in batch_distances.iter().zip(individual_distances.iter()) {
assert!(
(batch - individual).abs() < 1e-6,
"Batch and individual should match"
);
}
}
}