#![allow(clippy::collapsible_if)]
use crate::AletheiaDB;
use crate::api::transaction::WriteOps;
use crate::core::error::{Error, Result, VectorError};
use crate::core::id::NodeId;
use crate::core::property::PropertyMapBuilder;
pub struct Luna<'a> {
db: &'a AletheiaDB,
}
#[cfg(feature = "semantic-reasoning")]
impl<'a> Luna<'a> {
pub fn new(db: &'a AletheiaDB) -> Self {
Self { db }
}
pub fn synthesize_core(&self, seeds: &[NodeId], property: &str, label: &str) -> Result<NodeId> {
if seeds.is_empty() {
return Err(Error::other(
"Cannot synthesize from an empty set of seeds.",
));
}
let mut vectors = Vec::with_capacity(seeds.len());
for &node_id in seeds {
let node = self.db.get_node(node_id)?;
if let Some(val) = node.properties.get(property) {
if let Some(vec) = val.as_vector() {
vectors.push(vec.to_vec());
}
}
}
if vectors.is_empty() {
return Err(Error::Vector(VectorError::IndexError(format!(
"No vectors found in property '{}' for the given seeds.",
property
))));
}
let dim = vectors[0].len();
let mut centroid = vec![0.0; dim];
for vec in &vectors {
if vec.len() != dim {
return Err(Error::Vector(VectorError::DimensionMismatch {
expected: dim,
actual: vec.len(),
}));
}
for (i, val) in vec.iter().enumerate() {
centroid[i] += val;
}
}
let centroid = crate::core::vector::ops::normalize(¢roid);
let new_node_id = self.db.write(|tx| {
let props = PropertyMapBuilder::new()
.insert_vector(property, ¢roid)
.build();
let core_node_id = tx.create_node(label, props)?;
let edge_props = PropertyMapBuilder::new().build();
for &seed_id in seeds {
tx.create_edge(core_node_id, seed_id, "CORE_OF", edge_props.clone())?;
}
Ok::<NodeId, Error>(core_node_id)
})?;
Ok(new_node_id)
}
}
#[cfg(all(test, feature = "semantic-reasoning"))]
mod tests {
use super::*;
use crate::index::vector::{DistanceMetric, HnswConfig};
#[test]
fn test_luna_synthesize_core() {
let db = AletheiaDB::new().unwrap();
let config = HnswConfig::new(2, DistanceMetric::Cosine);
db.enable_vector_index("vec", config).unwrap();
let props_a = PropertyMapBuilder::new()
.insert_vector("vec", &[1.0, 0.0])
.build();
let a = db.create_node("Concept", props_a).unwrap();
let props_b = PropertyMapBuilder::new()
.insert_vector("vec", &[0.0, 1.0])
.build();
let b = db.create_node("Concept", props_b).unwrap();
let luna = Luna::new(&db);
let core_id = luna.synthesize_core(&[a, b], "vec", "CoreConcept").unwrap();
let core_node = db.get_node(core_id).unwrap();
let label_str = crate::core::GLOBAL_INTERNER
.resolve_with(core_node.label, |s| s.to_string())
.unwrap();
assert_eq!(label_str, "CoreConcept");
let vec_val = core_node.properties.get("vec").unwrap();
let vec = vec_val.as_vector().unwrap();
assert!((vec[0] - std::f32::consts::FRAC_1_SQRT_2).abs() < 0.01);
assert!((vec[1] - std::f32::consts::FRAC_1_SQRT_2).abs() < 0.01);
let out_edges = db.get_outgoing_edges(core_id);
assert_eq!(out_edges.len(), 2, "Core node should have 2 outgoing edges");
let mut targets = std::collections::HashSet::new();
for edge_id in out_edges {
let edge = db.get_edge(edge_id).unwrap();
let edge_label_str = crate::core::GLOBAL_INTERNER
.resolve_with(edge.label, |s| s.to_string())
.unwrap();
assert_eq!(edge_label_str, "CORE_OF");
targets.insert(edge.target);
}
assert!(targets.contains(&a));
assert!(targets.contains(&b));
}
}