use crate::AletheiaDB;
use crate::core::error::{Error, Result};
use crate::core::id::NodeId;
use crate::core::vector::ops;
use std::collections::{HashSet, VecDeque};
#[derive(Debug, Clone)]
pub struct ScenicRoute {
pub path: Vec<NodeId>,
pub serendipity_score: f32,
}
pub struct SerendipityEngine<'a> {
db: &'a AletheiaDB,
}
impl<'a> SerendipityEngine<'a> {
pub fn new(db: &'a AletheiaDB) -> Self {
Self { db }
}
pub fn find_scenic_route(
&self,
start: NodeId,
end: NodeId,
vector_property: &str,
max_depth: usize,
) -> Result<Option<ScenicRoute>> {
if start == end {
return Ok(Some(ScenicRoute {
path: vec![start],
serendipity_score: 0.0,
}));
}
if max_depth == 0 {
return Ok(None);
}
let mut queue = VecDeque::new();
queue.push_back((start, vec![start], 0.0_f32, 0));
let mut best_route: Option<ScenicRoute> = None;
while let Some((current, path, score, depth)) = queue.pop_front() {
if current == end {
if let Some(best) = &best_route {
if score > best.serendipity_score {
best_route = Some(ScenicRoute {
path: path.clone(),
serendipity_score: score,
});
}
} else {
best_route = Some(ScenicRoute {
path: path.clone(),
serendipity_score: score,
});
}
continue;
}
if depth >= max_depth {
continue; }
let current_vec = match self.get_vector(current, vector_property) {
Ok(v) => v,
Err(_) => continue, };
let outgoing = self.db.get_outgoing_edges(current);
let mut neighbors = HashSet::new();
for edge_id in outgoing {
if let Ok(target) = self.db.get_edge_target(edge_id) {
neighbors.insert(target);
}
}
for neighbor in neighbors {
if path.contains(&neighbor) {
continue;
}
if let Ok(neighbor_vec) = self.get_vector(neighbor, vector_property) {
let similarity =
ops::cosine_similarity(¤t_vec, &neighbor_vec).unwrap_or(0.0);
let distance = (1.0 - similarity).max(0.0);
let mut new_path = path.clone();
new_path.push(neighbor);
queue.push_back((neighbor, new_path, score + distance, depth + 1));
}
}
}
Ok(best_route)
}
fn get_vector(&self, node_id: NodeId, property: &str) -> Result<Vec<f32>> {
let node = self.db.get_node(node_id)?;
let val = node.properties.get(property).ok_or_else(|| {
Error::other(format!(
"Property '{}' not found on node {}",
property, node_id
))
})?;
let vec = val
.as_vector()
.ok_or_else(|| Error::other(format!("Property '{}' is not a vector", property)))?;
Ok(vec.to_vec())
}
}
#[cfg(not(feature = "semantic-search"))]
impl<'a> SerendipityEngine<'a> {
pub fn new(_db: &'a AletheiaDB) -> Self {
panic!(
"Experimental features like Serendipity require the 'nova' feature. Please enable it in your Cargo.toml."
);
}
pub fn find_scenic_route(
&self,
_start: NodeId,
_end: NodeId,
_vector_property: &str,
_max_depth: usize,
) -> Result<Option<ScenicRoute>> {
panic!("Experimental features like Serendipity require the 'nova' feature.");
}
}
#[cfg(all(test, feature = "semantic-search"))]
mod tests {
use super::*;
use crate::core::property::PropertyMapBuilder;
#[test]
fn test_serendipity_route() {
let db = AletheiaDB::new().unwrap();
let a = db
.create_node(
"Node",
PropertyMapBuilder::new()
.insert_vector("vec", &[1.0, 0.0])
.build(),
)
.unwrap();
let b = db
.create_node(
"Node",
PropertyMapBuilder::new()
.insert_vector("vec", &[0.9, 0.1])
.build(),
)
.unwrap();
let c = db
.create_node(
"Node",
PropertyMapBuilder::new()
.insert_vector("vec", &[0.8, 0.2])
.build(),
)
.unwrap();
let x = db
.create_node(
"Node",
PropertyMapBuilder::new()
.insert_vector("vec", &[0.0, 1.0])
.build(),
)
.unwrap();
let y = db
.create_node(
"Node",
PropertyMapBuilder::new()
.insert_vector("vec", &[-1.0, 0.0])
.build(),
)
.unwrap();
db.create_edge(a, b, "LINK", Default::default()).unwrap();
db.create_edge(b, c, "LINK", Default::default()).unwrap();
db.create_edge(a, x, "LINK", Default::default()).unwrap();
db.create_edge(x, y, "LINK", Default::default()).unwrap();
db.create_edge(y, c, "LINK", Default::default()).unwrap();
let engine = SerendipityEngine::new(&db);
let result = engine.find_scenic_route(a, c, "vec", 4).unwrap().unwrap();
assert_eq!(result.path, vec![a, x, y, c]);
assert!(result.serendipity_score > 3.0);
}
#[test]
fn test_serendipity_no_path() {
let db = AletheiaDB::new().unwrap();
let a = db
.create_node(
"Node",
PropertyMapBuilder::new()
.insert_vector("vec", &[1.0])
.build(),
)
.unwrap();
let b = db
.create_node(
"Node",
PropertyMapBuilder::new()
.insert_vector("vec", &[1.0])
.build(),
)
.unwrap();
let engine = SerendipityEngine::new(&db);
let result = engine.find_scenic_route(a, b, "vec", 3).unwrap();
assert!(result.is_none());
}
}
#[cfg(all(test, not(feature = "semantic-search")))]
mod stub_tests {
use super::*;
#[test]
#[should_panic(expected = "require the 'nova' feature")]
fn test_stub_new() {
let db = AletheiaDB::new().unwrap();
let _ = SerendipityEngine::new(&db);
}
}