use crate::metric::Metric;
use crate::resource::DocId;
use roaring::RoaringBitmap;
use std::cmp::Ordering;
use std::collections::BinaryHeap;
pub fn l2_normalize(v: &mut [f32]) {
let norm = dot(v, v).sqrt();
if norm > 0.0 {
let inv = 1.0 / norm;
for x in v.iter_mut() {
*x *= inv;
}
}
}
pub fn l2_normalize_rows(v: &mut [f32], dim: usize) {
if dim == 0 {
return;
}
#[cfg(feature = "parallel")]
{
use rayon::prelude::*;
v.par_chunks_exact_mut(dim).for_each(l2_normalize);
}
#[cfg(not(feature = "parallel"))]
for row in v.chunks_exact_mut(dim) {
l2_normalize(row);
}
}
#[inline]
#[must_use]
pub fn dot(a: &[f32], b: &[f32]) -> f32 {
debug_assert_eq!(a.len(), b.len());
let mut acc = 0.0f32;
for i in 0..a.len() {
acc += a[i] * b[i];
}
acc
}
#[inline]
#[must_use]
pub fn similarity(query: &[f32], doc: &[f32], metric: Metric) -> f32 {
match metric {
Metric::Cosine | Metric::Dot => dot(query, doc),
Metric::L2 => {
let mut acc = 0.0f32;
for i in 0..query.len() {
let d = query[i] - doc[i];
acc += d * d;
}
-acc }
}
}
#[derive(Clone, Copy, Debug)]
struct Scored {
score: f32,
id: DocId,
}
impl PartialEq for Scored {
fn eq(&self, other: &Self) -> bool {
self.score == other.score && self.id == other.id
}
}
impl Eq for Scored {}
impl PartialOrd for Scored {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl Ord for Scored {
fn cmp(&self, other: &Self) -> Ordering {
other
.score
.partial_cmp(&self.score)
.unwrap_or(Ordering::Less)
.then(self.id.cmp(&other.id))
}
}
#[must_use]
pub fn top_k(
query: &[f32],
dense: &[f32],
dim: usize,
k: usize,
metric: Metric,
allowed: Option<&RoaringBitmap>,
) -> Vec<(DocId, f32)> {
if k == 0 || dim == 0 || query.len() != dim {
return Vec::new();
}
let doc_count = dense.len() / dim;
let mut heap: BinaryHeap<Scored> = BinaryHeap::with_capacity(k + 1);
let consider = |id: DocId, heap: &mut BinaryHeap<Scored>| {
let start = id as usize * dim;
let doc = &dense[start..start + dim];
let score = similarity(query, doc, metric);
if heap.len() < k {
heap.push(Scored { score, id });
} else if let Some(top) = heap.peek() {
if score > top.score {
heap.pop();
heap.push(Scored { score, id });
}
}
};
match allowed {
Some(bitmap) => {
for id in bitmap {
if (id as usize) < doc_count {
consider(id, &mut heap);
}
}
}
None => {
for id in 0..doc_count as DocId {
consider(id, &mut heap);
}
}
}
let mut out: Vec<(DocId, f32)> = heap.into_iter().map(|s| (s.id, s.score)).collect();
out.sort_by(|a, b| {
b.1.partial_cmp(&a.1)
.unwrap_or(Ordering::Equal)
.then(a.0.cmp(&b.0))
});
out
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn normalize_yields_unit_length() {
let mut v = vec![3.0, 4.0];
l2_normalize(&mut v);
assert!((dot(&v, &v).sqrt() - 1.0).abs() < 1e-6);
assert!((v[0] - 0.6).abs() < 1e-6);
assert!((v[1] - 0.8).abs() < 1e-6);
}
#[test]
fn normalize_leaves_zero_vector() {
let mut v = vec![0.0, 0.0, 0.0];
l2_normalize(&mut v);
assert_eq!(v, vec![0.0, 0.0, 0.0]);
}
#[test]
fn normalize_rows_matches_per_row_exactly() {
let dim = 8;
let n = 600;
let mut seed = 0x1234_5678u64;
let mut a: Vec<f32> = (0..n * dim)
.map(|_| {
seed ^= seed << 13;
seed ^= seed >> 7;
seed ^= seed << 17;
(seed >> 40) as f32 / (1u64 << 24) as f32 - 0.5
})
.collect();
let mut b = a.clone();
l2_normalize_rows(&mut a, dim);
for row in b.chunks_exact_mut(dim) {
l2_normalize(row);
}
assert_eq!(a, b);
}
#[test]
fn dot_matches_f64_reference() {
let a = [1.0f32, 2.0, 3.0, 4.0];
let b = [4.0f32, 3.0, 2.0, 1.0];
let reference: f64 = a
.iter()
.zip(&b)
.map(|(x, y)| f64::from(*x) * f64::from(*y))
.sum();
assert!((f64::from(dot(&a, &b)) - reference).abs() < 1e-9);
}
#[test]
fn top_k_finds_nearest() {
let dense = vec![
1.0, 0.0, 0.0, 1.0, -1.0, 0.0, ];
let query = [0.0, 1.0];
let hits = top_k(&query, &dense, 2, 2, Metric::Dot, None);
assert_eq!(hits.len(), 2);
assert_eq!(hits[0].0, 1); assert!(hits[0].1 > hits[1].1);
}
#[test]
fn top_k_respects_allowed_filter() {
let dense = vec![1.0, 0.0, 0.0, 1.0, -1.0, 0.0];
let query = [0.0, 1.0];
let mut allowed = RoaringBitmap::new();
allowed.insert(0);
allowed.insert(2);
let hits = top_k(&query, &dense, 2, 5, Metric::Dot, Some(&allowed));
assert!(hits.iter().all(|(id, _)| *id != 1));
assert_eq!(hits.len(), 2);
}
#[test]
fn top_k_handles_k_larger_than_corpus() {
let dense = vec![1.0, 0.0, 0.0, 1.0];
let hits = top_k(&[1.0, 0.0], &dense, 2, 100, Metric::Dot, None);
assert_eq!(hits.len(), 2);
}
#[test]
fn top_k_empty_for_degenerate_inputs() {
assert!(top_k(&[1.0], &[1.0], 1, 0, Metric::Dot, None).is_empty());
assert!(top_k(&[1.0, 2.0], &[1.0], 1, 5, Metric::Dot, None).is_empty());
}
}