use crate::AletheiaDB;
use crate::core::error::{Error, Result, StorageError, VectorError};
use crate::core::id::NodeId;
use crate::core::vector::ops::cosine_similarity;
use std::collections::HashSet;
pub struct Voyager<'a> {
db: &'a AletheiaDB,
}
impl<'a> Voyager<'a> {
pub fn new(db: &'a AletheiaDB) -> Self {
Self { db }
}
pub fn traverse(
&self,
start_node: NodeId,
vector_property: &str,
max_steps: usize,
) -> Result<Vec<NodeId>> {
let mut path = Vec::new();
let mut visited = HashSet::new();
let mut current = start_node;
path.push(current);
visited.insert(current);
for _ in 0..max_steps {
let current_vec = self.get_vector(current, vector_property)?;
let edges = self.db.get_outgoing_edges(current);
if edges.is_empty() {
break; }
let mut least_similar_neighbor = None;
let mut lowest_similarity = f32::MAX;
for edge_id in edges {
if let Ok(edge) = self.db.get_edge(edge_id) {
let neighbor = edge.target;
if visited.contains(&neighbor) {
continue;
}
if let Ok(neighbor_vec) = self.get_vector(neighbor, vector_property) {
let sim = cosine_similarity(¤t_vec, &neighbor_vec)?;
if sim < lowest_similarity {
lowest_similarity = sim;
least_similar_neighbor = Some(neighbor);
}
}
}
}
match least_similar_neighbor {
Some(next_node) => {
path.push(next_node);
visited.insert(next_node);
current = next_node;
}
None => {
break;
}
}
}
Ok(path)
}
fn get_vector(&self, node_id: NodeId, property: &str) -> Result<Vec<f32>> {
let node = self
.db
.get_node(node_id)
.map_err(|_| Error::Storage(StorageError::NodeNotFound(node_id)))?;
let val = node.properties.get(property).ok_or_else(|| {
Error::Vector(VectorError::IndexError(format!(
"Property '{}' missing on node {}",
property, node_id
)))
})?;
let vec = val.as_vector().ok_or_else(|| {
Error::Vector(VectorError::IndexError(format!(
"Property '{}' is not a vector on node {}",
property, node_id
)))
})?;
Ok(vec.to_vec())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::AletheiaDB;
use crate::core::property::PropertyMapBuilder;
use crate::index::vector::{DistanceMetric, HnswConfig};
#[test]
fn test_voyager_chooses_lowest_similarity() {
let db = AletheiaDB::new().unwrap();
db.enable_vector_index("vec", HnswConfig::new(2, DistanceMetric::Cosine))
.unwrap();
let props_a = PropertyMapBuilder::new()
.insert_vector("vec", &[1.0, 0.0])
.build();
let a = db.create_node("Node", props_a).unwrap();
let props_b = PropertyMapBuilder::new()
.insert_vector("vec", &[0.9, 0.1])
.build();
let b = db.create_node("Node", props_b).unwrap();
let props_c = PropertyMapBuilder::new()
.insert_vector("vec", &[0.0, 1.0])
.build();
let c = db.create_node("Node", props_c).unwrap();
let props_d = PropertyMapBuilder::new()
.insert_vector("vec", &[0.0, -1.0])
.build();
let d = db.create_node("Node", props_d).unwrap();
db.create_edge(a, b, "LINK", PropertyMapBuilder::new().build())
.unwrap();
db.create_edge(a, c, "LINK", PropertyMapBuilder::new().build())
.unwrap();
db.create_edge(c, b, "LINK", PropertyMapBuilder::new().build())
.unwrap();
db.create_edge(c, d, "LINK", PropertyMapBuilder::new().build())
.unwrap();
let voyager = Voyager::new(&db);
let path = voyager.traverse(a, "vec", 2).unwrap();
assert_eq!(path.len(), 3); assert_eq!(path[0], a);
assert_eq!(path[1], c);
assert_eq!(path[2], d);
}
#[test]
fn test_voyager_avoids_cycles() {
let db = AletheiaDB::new().unwrap();
db.enable_vector_index("vec", HnswConfig::new(2, DistanceMetric::Cosine))
.unwrap();
let props_a = PropertyMapBuilder::new()
.insert_vector("vec", &[1.0, 0.0])
.build();
let a = db.create_node("Node", props_a).unwrap();
let props_b = PropertyMapBuilder::new()
.insert_vector("vec", &[0.0, 1.0])
.build();
let b = db.create_node("Node", props_b).unwrap();
db.create_edge(a, b, "LINK", PropertyMapBuilder::new().build())
.unwrap();
db.create_edge(b, a, "LINK", PropertyMapBuilder::new().build())
.unwrap();
let voyager = Voyager::new(&db);
let path = voyager.traverse(a, "vec", 10).unwrap();
assert_eq!(path.len(), 2);
assert_eq!(path[0], a);
assert_eq!(path[1], b);
}
}