use std::collections::HashMap;
use std::sync::{Arc, Mutex};
use serde::{Deserialize, Serialize};
use crate::document::{Document, Value};
pub struct CachedVectors {
pub generation: u64,
pub vectors: Arc<VectorBlock>,
}
#[derive(Default)]
pub struct VectorBlock {
pub ids: Vec<ulid::Ulid>,
pub values: Vec<f32>,
pub dimensions: usize,
}
impl VectorBlock {
pub fn bytes(&self) -> usize {
self.ids.capacity() * std::mem::size_of::<ulid::Ulid>() + self.values.capacity() * 4
}
}
pub const DEFAULT_VECTOR_CACHE_BYTES: usize = crate::search_cache::DEFAULT_SEARCH_CACHE_BYTES;
#[derive(Clone, Copy, PartialEq)]
pub(crate) struct Candidate(pub ulid::Ulid, pub f32);
impl Eq for Candidate {}
impl PartialOrd for Candidate {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl Ord for Candidate {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
self.1
.total_cmp(&other.1)
.then_with(|| other.0.cmp(&self.0))
}
}
pub type VectorCacheMap = HashMap<String, CachedVectors>;
pub type SharedVectorCache = Arc<Mutex<VectorCacheMap>>;
pub fn new_shared_vector_cache() -> SharedVectorCache {
Arc::new(Mutex::new(HashMap::new()))
}
pub const META_VECTOR_TABLE: &str = "meta::vector_indexes";
pub const META_HNSW_TABLE: &str = "meta::hnsw_indexes";
pub fn hnsw_table_name(collection: &str, field: &str) -> String {
format!("hnsw::{collection}::{field}")
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
pub enum VectorMetric {
#[default]
Cosine,
Dot,
Euclidean,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct VectorDef {
pub collection: String,
pub field: String,
pub dimensions: usize,
pub metric: VectorMetric,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HnswOptions {
pub m: u32,
pub ef_construction: u32,
}
impl Default for HnswOptions {
fn default() -> Self {
Self {
m: 32,
ef_construction: 200,
}
}
}
pub fn vec_meta_key(collection: &str, field: &str) -> String {
format!("{collection}::{field}")
}
pub fn vec_table_name(collection: &str, field: &str) -> String {
format!("vec::{collection}::{field}")
}
pub fn encode_f32_vec(v: &[f32]) -> Vec<u8> {
let mut buf = Vec::with_capacity(v.len() * 4);
for f in v {
buf.extend_from_slice(&f.to_le_bytes());
}
buf
}
pub fn decode_f32_vec(bytes: &[u8]) -> Option<Vec<f32>> {
let (quads, rest) = bytes.as_chunks::<4>();
if !rest.is_empty() {
return None;
}
Some(quads.iter().copied().map(f32::from_le_bytes).collect())
}
pub fn value_to_f32_vec(v: &Value) -> Option<Vec<f32>> {
match v {
Value::Array(arr) => arr
.iter()
.map(|item| match item {
Value::Float(f) => Some(*f as f32),
Value::Int(n) => Some(*n as f32),
_ => None,
})
.collect(),
_ => None,
}
}
pub fn cosine_similarity(a: &[f32], b: &[f32]) -> f32 {
let dot: f32 = a.iter().zip(b.iter()).map(|(x, y)| x * y).sum();
let norm_a: f32 = a.iter().map(|x| x * x).sum::<f32>().sqrt();
let norm_b: f32 = b.iter().map(|x| x * x).sum::<f32>().sqrt();
if norm_a == 0.0 || norm_b == 0.0 {
0.0
} else {
dot / (norm_a * norm_b)
}
}
pub fn dot_similarity(a: &[f32], b: &[f32]) -> f32 {
let mut acc = [0.0f32; LANES];
let (a_lanes, a_rest) = a.as_chunks::<LANES>();
let (b_lanes, b_rest) = b.as_chunks::<LANES>();
for (x, y) in a_lanes.iter().zip(b_lanes) {
for i in 0..LANES {
acc[i] += x[i] * y[i];
}
}
let mut total: f32 = acc.iter().sum();
for (x, y) in a_rest.iter().zip(b_rest) {
total += x * y;
}
total
}
#[inline]
pub fn norm_sq(v: &[f32]) -> f32 {
let mut acc = [0.0f32; LANES];
let (lanes, rest) = v.as_chunks::<LANES>();
for s in lanes {
for i in 0..LANES {
acc[i] += s[i] * s[i];
}
}
let mut total: f32 = acc.iter().sum();
for s in rest {
total += s * s;
}
total
}
#[inline]
pub fn score_with_norms(
metric: &VectorMetric,
query: &[f32],
query_norm: f32,
stored: &[f32],
stored_norm_sq: f32,
) -> f32 {
match metric {
VectorMetric::Cosine => {
let norm_b = stored_norm_sq.sqrt();
if query_norm == 0.0 || norm_b == 0.0 {
0.0
} else {
dot_similarity(query, stored) / (query_norm * norm_b)
}
}
_ => score_with_query_norm(metric, query, query_norm, stored),
}
}
pub fn euclidean_similarity(a: &[f32], b: &[f32]) -> f32 {
let mut acc = [0.0f32; LANES];
let (a_lanes, a_rest) = a.as_chunks::<LANES>();
let (b_lanes, b_rest) = b.as_chunks::<LANES>();
for (x, y) in a_lanes.iter().zip(b_lanes) {
for i in 0..LANES {
let d = x[i] - y[i];
acc[i] += d * d;
}
}
let mut dist_sq: f32 = acc.iter().sum();
for (x, y) in a_rest.iter().zip(b_rest) {
let d = x - y;
dist_sq += d * d;
}
1.0 / (1.0 + dist_sq.sqrt())
}
pub fn compute_similarity(metric: &VectorMetric, a: &[f32], b: &[f32]) -> f32 {
match metric {
VectorMetric::Cosine => cosine_similarity(a, b),
VectorMetric::Dot => dot_similarity(a, b),
VectorMetric::Euclidean => euclidean_similarity(a, b),
}
}
#[inline]
pub fn l2_norm(a: &[f32]) -> f32 {
a.iter().map(|x| x * x).sum::<f32>().sqrt()
}
#[inline]
pub fn score_with_query_norm(
metric: &VectorMetric,
query: &[f32],
query_norm: f32,
stored: &[f32],
) -> f32 {
match metric {
VectorMetric::Cosine => {
let (dot, norm_sq) = dot_and_norm_sq(query, stored);
let norm_b = norm_sq.sqrt();
if query_norm == 0.0 || norm_b == 0.0 {
0.0
} else {
dot / (query_norm * norm_b)
}
}
VectorMetric::Dot => dot_similarity(query, stored),
VectorMetric::Euclidean => euclidean_similarity(query, stored),
}
}
const LANES: usize = 8;
#[inline]
fn dot_and_norm_sq(query: &[f32], stored: &[f32]) -> (f32, f32) {
let (q_lanes, q_rest) = query.as_chunks::<LANES>();
let (s_lanes, s_rest) = stored.as_chunks::<LANES>();
let mut dot = [0.0f32; LANES];
for (q, s) in q_lanes.iter().zip(s_lanes) {
for i in 0..LANES {
dot[i] += q[i] * s[i];
}
}
let mut norm_sq = [0.0f32; LANES];
for (_q, s) in q_lanes.iter().zip(s_lanes) {
for i in 0..LANES {
norm_sq[i] += s[i] * s[i];
}
}
let mut dot_total: f32 = dot.iter().sum();
let mut norm_total: f32 = norm_sq.iter().sum();
for (q, s) in q_rest.iter().zip(s_rest) {
dot_total += q * s;
norm_total += s * s;
}
(dot_total, norm_total)
}
#[derive(Debug, Clone)]
pub struct VectorSearchResult {
pub document: Document,
pub score: f32,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn encode_decode_roundtrip() {
let v = vec![0.1f32, -0.5, 1.0, 99.9];
let decoded = decode_f32_vec(&encode_f32_vec(&v)).unwrap();
for (a, b) in v.iter().zip(decoded.iter()) {
assert!((a - b).abs() < 1e-6, "{a} != {b}");
}
}
#[test]
fn decode_rejects_odd_length() {
assert!(decode_f32_vec(&[1, 2, 3]).is_none());
}
#[test]
fn cosine_identical() {
let a = vec![1.0f32, 2.0, 3.0];
assert!((cosine_similarity(&a, &a) - 1.0).abs() < 1e-6);
}
#[test]
fn cosine_orthogonal() {
let a = vec![1.0f32, 0.0];
let b = vec![0.0f32, 1.0];
assert!(cosine_similarity(&a, &b).abs() < 1e-6);
}
#[test]
fn euclidean_identical() {
let a = vec![1.0f32, 2.0, 3.0];
assert!((euclidean_similarity(&a, &a) - 1.0).abs() < 1e-6);
}
#[test]
fn value_extraction_floats() {
let v = Value::Array(vec![Value::Float(0.5), Value::Int(1), Value::Float(-0.25)]);
let r = value_to_f32_vec(&v).unwrap();
assert_eq!(r.len(), 3);
assert!((r[0] - 0.5f32).abs() < 1e-6);
assert_eq!(r[1], 1.0f32);
assert!((r[2] - (-0.25f32)).abs() < 1e-6);
}
#[test]
fn value_extraction_rejects_mixed() {
let v = Value::Array(vec![Value::Float(0.5), Value::Str("x".into())]);
assert!(value_to_f32_vec(&v).is_none());
}
#[test]
fn value_extraction_rejects_non_array() {
assert!(value_to_f32_vec(&Value::Str("vec".into())).is_none());
}
fn scalar_dot(a: &[f32], b: &[f32]) -> f32 {
a.iter().zip(b).map(|(x, y)| x * y).sum()
}
fn scalar_euclidean(a: &[f32], b: &[f32]) -> f32 {
let d: f32 = a.iter().zip(b).map(|(x, y)| (x - y) * (x - y)).sum();
1.0 / (1.0 + d.sqrt())
}
fn ramp(len: usize, offset: f32) -> Vec<f32> {
(0..len)
.map(|i| (i as f32).mul_add(0.125, offset))
.collect()
}
#[test]
fn lane_reductions_match_scalar_at_every_remainder_length() {
for len in std::iter::once(0).chain(1..=17) {
let a = ramp(len, 0.5);
let b = ramp(len, -0.25);
let dot = dot_similarity(&a, &b);
assert!(
(dot - scalar_dot(&a, &b)).abs() < 1e-4,
"dot mismatch at len {len}: {dot} vs {}",
scalar_dot(&a, &b)
);
let euc = euclidean_similarity(&a, &b);
assert!(
(euc - scalar_euclidean(&a, &b)).abs() < 1e-6,
"euclidean mismatch at len {len}"
);
let norm = l2_norm(&a);
let cos = score_with_query_norm(&VectorMetric::Cosine, &a, norm, &b);
let expected = cosine_similarity(&a, &b);
assert!(
(cos - expected).abs() < 1e-5,
"cosine mismatch at len {len}: {cos} vs {expected}"
);
}
}
#[test]
fn identical_vectors_still_score_perfectly_after_lane_split() {
for len in [4usize, 8, 12, 384] {
let v = ramp(len, 1.0);
let cos = score_with_query_norm(&VectorMetric::Cosine, &v, l2_norm(&v), &v);
assert!(
(cos - 1.0).abs() < 1e-5,
"cosine self-similarity at len {len}: {cos}"
);
let euc = euclidean_similarity(&v, &v);
assert!(
(euc - 1.0).abs() < 1e-6,
"euclidean self-similarity at len {len}"
);
}
}
#[test]
fn dot_and_norm_sq_matches_the_chunks_exact_original_bit_for_bit() {
#[allow(clippy::chunks_exact_to_as_chunks)]
fn original(query: &[f32], stored: &[f32]) -> (f32, f32) {
let mut dot = [0.0f32; LANES];
let mut norm_sq = [0.0f32; LANES];
let mut q_chunks = query.chunks_exact(LANES);
let mut s_chunks = stored.chunks_exact(LANES);
for (q, s) in q_chunks.by_ref().zip(s_chunks.by_ref()) {
for i in 0..LANES {
dot[i] += q[i] * s[i];
norm_sq[i] += s[i] * s[i];
}
}
let mut dot_total: f32 = dot.iter().sum();
let mut norm_total: f32 = norm_sq.iter().sum();
for (q, s) in q_chunks.remainder().iter().zip(s_chunks.remainder()) {
dot_total += q * s;
norm_total += s * s;
}
(dot_total, norm_total)
}
let mut seed = 0xC0FF_EE12_u64;
let mut next = || {
seed = seed
.wrapping_mul(6364136223846793005)
.wrapping_add(1442695040888963407);
let bits = (seed >> 33) as u32;
(f32::from_bits((bits & 0x007F_FFFF) | 0x3F80_0000) - 1.5) * 1e3
};
for len in [0usize, 1, 7, 8, 9, 15, 16, 17, 31, 384, 385, 768, 1536] {
let q: Vec<f32> = (0..len).map(|_| next()).collect();
let s: Vec<f32> = (0..len).map(|_| next()).collect();
let (new_dot, new_norm) = dot_and_norm_sq(&q, &s);
let (old_dot, old_norm) = original(&q, &s);
assert_eq!(
new_dot.to_bits(),
old_dot.to_bits(),
"dot diverged at len={len}"
);
assert_eq!(
new_norm.to_bits(),
old_norm.to_bits(),
"norm_sq diverged at len={len}"
);
}
}
#[test]
fn mismatched_lengths_match_the_fused_original_bit_for_bit() {
#[allow(clippy::chunks_exact_to_as_chunks)]
fn fused(query: &[f32], stored: &[f32]) -> (f32, f32) {
let mut dot = [0.0f32; LANES];
let mut norm_sq = [0.0f32; LANES];
let mut q_chunks = query.chunks_exact(LANES);
let mut s_chunks = stored.chunks_exact(LANES);
for (q, s) in q_chunks.by_ref().zip(s_chunks.by_ref()) {
for i in 0..LANES {
dot[i] += q[i] * s[i];
norm_sq[i] += s[i] * s[i];
}
}
let mut dot_total: f32 = dot.iter().sum();
let mut norm_total: f32 = norm_sq.iter().sum();
for (q, s) in q_chunks.remainder().iter().zip(s_chunks.remainder()) {
dot_total += q * s;
norm_total += s * s;
}
(dot_total, norm_total)
}
let mut seed = 0x5DEE_CE66_D125_u64;
let mut next = move || {
seed = seed
.wrapping_mul(6364136223846793005)
.wrapping_add(1442695040888963407);
let bits = (seed >> 33) as u32;
(f32::from_bits((bits & 0x007F_FFFF) | 0x3F80_0000) - 1.5) * 1e3
};
for (ql, sl) in [
(8usize, 16usize),
(16, 8),
(7, 9),
(9, 7),
(384, 385),
(385, 384),
(0, 8),
(8, 0),
] {
let q: Vec<f32> = (0..ql).map(|_| next()).collect();
let s: Vec<f32> = (0..sl).map(|_| next()).collect();
let (dot, norm) = dot_and_norm_sq(&q, &s);
let (fd, fnm) = fused(&q, &s);
assert_eq!(dot.to_bits(), fd.to_bits(), "dot at ql={ql} sl={sl}");
assert_eq!(norm.to_bits(), fnm.to_bits(), "norm at ql={ql} sl={sl}");
}
}
}