use embeddenator_vsa::{SparseVec, DIM};
use std::collections::HashMap;
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct SearchResult {
pub id: usize,
pub score: i32,
}
#[derive(Clone, Debug, PartialEq)]
pub struct RerankedResult {
pub id: usize,
pub approx_score: i32,
pub cosine: f64,
}
#[derive(Clone, Debug)]
pub struct TernaryInvertedIndex {
pos_postings: Vec<Vec<usize>>,
neg_postings: Vec<Vec<usize>>,
max_id: usize,
}
impl TernaryInvertedIndex {
pub fn new() -> Self {
Self {
pos_postings: vec![Vec::new(); DIM],
neg_postings: vec![Vec::new(); DIM],
max_id: 0,
}
}
pub fn build_from_pairs<I>(pairs: I) -> Self
where
I: IntoIterator<Item = (usize, SparseVec)>,
{
let mut index = Self::new();
for (id, vec) in pairs {
index.add(id, &vec);
}
index.finalize();
index
}
pub fn build_from_map(map: &HashMap<usize, SparseVec>) -> Self {
let mut index = Self::new();
for (&id, vec) in map {
index.add(id, vec);
}
index.finalize();
index
}
pub fn add(&mut self, id: usize, vec: &SparseVec) {
self.max_id = self.max_id.max(id);
for &d in &vec.pos {
if d < DIM {
self.pos_postings[d].push(id);
}
}
for &d in &vec.neg {
if d < DIM {
self.neg_postings[d].push(id);
}
}
}
pub fn finalize(&mut self) {
for posting in &mut self.pos_postings {
posting.sort_unstable();
posting.dedup();
}
for posting in &mut self.neg_postings {
posting.sort_unstable();
posting.dedup();
}
}
pub fn query_top_k(&self, query: &SparseVec, k: usize) -> Vec<SearchResult> {
if k == 0 {
return Vec::new();
}
let mut scores = vec![0i32; self.max_id + 1];
let mut touched = Vec::new();
let mut touched_flag = vec![false; self.max_id + 1];
for &d in &query.pos {
if d >= DIM {
continue;
}
for &id in &self.pos_postings[d] {
if !touched_flag[id] {
touched_flag[id] = true;
touched.push(id);
}
scores[id] += 1;
}
for &id in &self.neg_postings[d] {
if !touched_flag[id] {
touched_flag[id] = true;
touched.push(id);
}
scores[id] -= 1;
}
}
for &d in &query.neg {
if d >= DIM {
continue;
}
for &id in &self.pos_postings[d] {
if !touched_flag[id] {
touched_flag[id] = true;
touched.push(id);
}
scores[id] -= 1;
}
for &id in &self.neg_postings[d] {
if !touched_flag[id] {
touched_flag[id] = true;
touched.push(id);
}
scores[id] += 1;
}
}
let mut results: Vec<SearchResult> = touched
.into_iter()
.map(|id| SearchResult {
id,
score: scores[id],
})
.collect();
results.sort_by(|a, b| b.score.cmp(&a.score).then_with(|| a.id.cmp(&b.id)));
results.truncate(k);
results
}
pub fn query_top_k_reranked(
&self,
query: &SparseVec,
vectors: &HashMap<usize, SparseVec>,
candidate_k: usize,
k: usize,
) -> Vec<RerankedResult> {
let candidates = self.query_top_k(query, candidate_k);
rerank_candidates_by_cosine(query, &candidates, vectors, k)
}
}
impl Default for TernaryInvertedIndex {
fn default() -> Self {
Self::new()
}
}
pub fn rerank_candidates_by_cosine(
query: &SparseVec,
candidates: &[SearchResult],
vectors: &HashMap<usize, SparseVec>,
k: usize,
) -> Vec<RerankedResult> {
if k == 0 || candidates.is_empty() {
return Vec::new();
}
let mut out = Vec::with_capacity(candidates.len().min(k));
for cand in candidates {
let Some(vec) = vectors.get(&cand.id) else {
continue;
};
out.push(RerankedResult {
id: cand.id,
approx_score: cand.score,
cosine: query.cosine(vec),
});
}
out.sort_by(|a, b| {
b.cosine
.partial_cmp(&a.cosine)
.unwrap_or(std::cmp::Ordering::Equal)
.then_with(|| b.approx_score.cmp(&a.approx_score))
.then_with(|| a.id.cmp(&b.id))
});
out.truncate(k);
out
}