use std::collections::HashMap;
use std::path::{Path, PathBuf};
use anndists::dist::DistCosine;
use anyhow::{Context, Result};
use diskann_rs::IncrementalDiskANN;
const DISKANN_FILE_EXT: &str = ".diskann";
pub struct DiskAnnIndex {
index: IncrementalDiskANN<DistCosine>,
id_to_url: HashMap<u64, String>,
dim: usize,
path: PathBuf,
}
impl DiskAnnIndex {
pub async fn build_or_open(
turso: &crate::storage::turso_store::TursoStore,
turso_path: &str,
) -> Result<Self> {
let path: PathBuf = format!("{turso_path}{DISKANN_FILE_EXT}").into();
let path_str = path.to_str().unwrap_or("");
let mapping = load_mapping(turso).await?;
let (index, id_to_url) = if path.exists() {
let index = IncrementalDiskANN::<DistCosine>::open(path_str)
.with_context(|| format!("open DiskANN index at `{path_str}`"))?;
(index, mapping.into_iter().collect::<HashMap<_, _>>())
} else {
let (index, id_to_url) = Self::build_from_turso(turso, path_str, &mapping).await?;
(index, id_to_url)
};
let dim = index.dim();
Ok(Self {
index,
id_to_url,
dim,
path,
})
}
async fn build_from_turso(
turso: &crate::storage::turso_store::TursoStore,
path_str: &str,
persisted_mapping: &[(u64, String)],
) -> Result<(IncrementalDiskANN<DistCosine>, HashMap<u64, String>)> {
let embeddings = collect_embeddings(turso).await?;
if embeddings.is_empty() {
let placeholder = vec![0.0_f32; 384];
let index = IncrementalDiskANN::<DistCosine>::build_default(&[placeholder], path_str)
.context("create initial empty DiskANN index")?;
return Ok((index, HashMap::new()));
}
let vectors: Vec<Vec<f32>> = embeddings.iter().map(|(_, v)| v.clone()).collect();
let index = IncrementalDiskANN::<DistCosine>::build_default(&vectors, path_str)
.with_context(|| format!("build DiskANN index at `{path_str}`"))?;
let id_to_url: HashMap<u64, String> = if !persisted_mapping.is_empty() {
persisted_mapping.iter().cloned().collect()
} else {
embeddings
.into_iter()
.enumerate()
.map(|(i, (url, _))| (i as u64, url))
.collect()
};
Ok((index, id_to_url))
}
pub fn add_vector(&mut self, url: &str, vector: Vec<f32>) -> Result<u64> {
let ids = self
.index
.add_vectors(&[vector])
.context("add vector to DiskANN index")?;
let id = *ids
.first()
.context("DiskANN returned no id for added vector")?;
self.id_to_url.insert(id, url.to_string());
Ok(id)
}
pub fn search(&self, query: &[f32], k: usize) -> Vec<(String, f64)> {
let beam_width = (k * 4).max(64);
let results = self.index.search_with_dists(query, k, beam_width);
let mut scored: Vec<(String, f64)> = results
.into_iter()
.filter_map(|(id, dist)| {
self.id_to_url.get(&id).map(|url| {
let similarity = (1.0 - dist).clamp(0.0, 1.0);
(url.clone(), similarity as f64)
})
})
.collect();
scored.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
scored
}
pub fn len(&self) -> usize {
self.id_to_url.len()
}
pub fn is_empty(&self) -> bool {
self.id_to_url.is_empty()
}
pub fn dim(&self) -> usize {
self.dim
}
pub fn path(&self) -> &Path {
&self.path
}
}
async fn load_mapping(
turso: &crate::storage::turso_store::TursoStore,
) -> Result<Vec<(u64, String)>> {
let conn = turso.conn();
let mut map = Vec::new();
let mut rows = match conn.query("SELECT id, url FROM vector_index_map", ()).await {
Ok(r) => r,
Err(_) => return Ok(map),
};
loop {
match rows.next().await {
Ok(Some(row)) => {
let id: i64 = row.get(0).unwrap_or(-1);
let url: String = match row.get(1) {
Ok(u) => u,
Err(_) => continue,
};
if id >= 0 {
map.push((id as u64, url));
}
}
Ok(None) => break,
Err(_) => break,
}
}
Ok(map)
}
pub async fn save_mapping(
turso: &crate::storage::turso_store::TursoStore,
id: u64,
url: &str,
) -> Result<()> {
let conn = turso.conn();
conn.execute(
"INSERT OR REPLACE INTO vector_index_map (id, url) VALUES (?1, ?2)",
libsql::params![id as i64, url],
)
.await
.context("persist DiskANN id→url mapping")?;
Ok(())
}
async fn collect_embeddings(
turso: &crate::storage::turso_store::TursoStore,
) -> Result<Vec<(String, Vec<f32>)>> {
let conn = turso.conn();
let mut rows = conn
.query(
"SELECT url, embedding FROM url_nodes WHERE embedding IS NOT NULL",
(),
)
.await
.context("read embeddings for DiskANN build")?;
let mut result = Vec::new();
loop {
match rows.next().await {
Ok(Some(row)) => {
let url: String = match row.get(0) {
Ok(u) => u,
Err(_) => continue,
};
let bytes: Vec<u8> = match row.get(1) {
Ok(b) => b,
Err(_) => continue,
};
let vec = match bytes_to_vec(&bytes) {
Some(v) if !v.is_empty() => v,
_ => continue,
};
result.push((url, vec));
}
Ok(None) => break,
Err(e) => return Err(e).context("iterate embeddings for DiskANN build"),
}
}
Ok(result)
}
fn bytes_to_vec(bytes: &[u8]) -> Option<Vec<f32>> {
if !bytes.len().is_multiple_of(4) {
return None;
}
let floats: &[f32] = bytemuck::try_cast_slice(bytes).ok()?;
Some(floats.to_vec())
}