use crate::geometry::Geometry;
use crate::lattice::PaddedTileLattice;
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Hit {
pub id: usize,
pub score: f32,
}
#[derive(Clone)]
pub struct TensorIndex {
dim: usize,
geom: Geometry,
items: Vec<Vec<f32>>,
}
impl TensorIndex {
pub fn new(dim: usize) -> Self {
TensorIndex {
dim,
geom: Geometry::TPU_V,
items: Vec::new(),
}
}
#[inline]
pub fn dim(&self) -> usize {
self.dim
}
#[inline]
pub fn len(&self) -> usize {
self.items.len()
}
#[inline]
pub fn is_empty(&self) -> bool {
self.items.is_empty()
}
pub fn add(&mut self, vector: Vec<f32>) -> usize {
assert_eq!(vector.len(), self.dim, "vector dimension must match index");
let id = self.items.len();
self.items.push(vector);
id
}
pub fn vector(&self, id: usize) -> &[f32] {
&self.items[id]
}
fn corpus_matrix(&self) -> PaddedTileLattice<f32> {
let n = self.items.len();
let mut dense = vec![0.0f32; self.dim * n];
for (j, item) in self.items.iter().enumerate() {
for (p, &value) in item.iter().enumerate() {
dense[p * n + j] = value;
}
}
PaddedTileLattice::from_dense(self.dim, n, &dense, self.geom).unwrap()
}
pub fn score_batch(&self, queries: &[&[f32]]) -> Vec<Vec<f32>> {
let b = queries.len();
if b == 0 || self.items.is_empty() {
return vec![Vec::new(); b];
}
let n = self.items.len();
let mut qdense = vec![0.0f32; b * self.dim];
for (i, q) in queries.iter().enumerate() {
assert_eq!(q.len(), self.dim, "query dimension must match index");
qdense[i * self.dim..(i + 1) * self.dim].copy_from_slice(q);
}
let qlat = PaddedTileLattice::from_dense(b, self.dim, &qdense, self.geom).unwrap();
let scores = qlat.matmul(&self.corpus_matrix()).unwrap().to_dense();
scores.chunks_exact(n).map(|row| row.to_vec()).collect()
}
pub fn search_batch(&self, queries: &[&[f32]], k: usize) -> Vec<Vec<Hit>> {
self.score_batch(queries)
.into_iter()
.map(|row| {
let mut hits: Vec<Hit> = row
.into_iter()
.enumerate()
.map(|(id, score)| Hit { id, score })
.collect();
hits.sort_by(|a, b| b.score.partial_cmp(&a.score).unwrap());
hits.truncate(k);
hits
})
.collect()
}
pub fn search(&self, query: &[f32], k: usize) -> Vec<Hit> {
self.search_batch(&[query], k).pop().unwrap_or_default()
}
pub fn nearest(&self, query: &[f32]) -> Option<Hit> {
self.search(query, 1).into_iter().next()
}
}
impl core::fmt::Debug for TensorIndex {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
write!(
f,
"TensorIndex {{ dim: {}, items: {} }}",
self.dim,
self.items.len()
)
}
}