#![allow(clippy::needless_range_loop, clippy::collapsible_if)]
use crate::AletheiaDB;
use crate::core::error::Result;
use crate::core::id::NodeId;
#[cfg(feature = "semantic-reasoning")]
use crate::core::vector::cosine_similarity;
#[cfg(feature = "semantic-reasoning")]
use std::collections::{HashMap, HashSet};
#[derive(Debug, Clone, PartialEq)]
pub struct Mapping {
pub source: NodeId,
pub target: NodeId,
pub score: f32,
}
#[derive(Debug, Clone)]
pub struct Alignment {
pub mappings: Vec<Mapping>,
pub global_score: f32,
}
pub struct Metaphor<'a> {
#[allow(dead_code)]
db: &'a AletheiaDB,
}
#[cfg(feature = "semantic-reasoning")]
impl<'a> Metaphor<'a> {
pub fn new(db: &'a AletheiaDB) -> Self {
Self { db }
}
#[allow(clippy::needless_range_loop, clippy::collapsible_if)]
pub fn align(
&self,
source_nodes: &[NodeId],
target_nodes: &[NodeId],
vector_property: &str,
structural_weight: f32,
) -> Result<Alignment> {
let source_data = self.fetch_subgraph_data(source_nodes, vector_property)?;
let target_data = self.fetch_subgraph_data(target_nodes, vector_property)?;
let target_len = target_nodes.len();
let mut scores = vec![0.0f32; source_nodes.len() * target_len];
for s_idx in 0..source_nodes.len() {
for t_idx in 0..target_len {
let s_vec = &source_data[s_idx].vector;
let t_vec = &target_data[t_idx].vector;
let sim = if let (Some(sv), Some(tv)) = (s_vec, t_vec) {
let s = cosine_similarity(sv, tv).unwrap_or(0.0);
if s.is_nan() { 0.0 } else { s }
} else {
0.0 };
scores[s_idx * target_len + t_idx] = sim;
}
}
let source_idx_map: HashMap<NodeId, usize> = source_nodes
.iter()
.enumerate()
.map(|(i, &id)| (id, i))
.collect();
let target_idx_map: HashMap<NodeId, usize> = target_nodes
.iter()
.enumerate()
.map(|(i, &id)| (id, i))
.collect();
let mut mappings = Vec::new();
let mut source_mapped = vec![false; source_nodes.len()];
let mut target_mapped = vec![false; target_nodes.len()];
let mut mapped_count = 0;
let min_len = source_nodes.len().min(target_nodes.len());
while mapped_count < min_len {
let mut best_pair = None;
let mut best_score = f32::NEG_INFINITY;
for s in 0..source_nodes.len() {
if source_mapped[s] {
continue;
}
for t in 0..target_len {
if target_mapped[t] {
continue;
}
let score = scores[s * target_len + t];
if score > best_score {
best_score = score;
best_pair = Some((s, t));
}
}
}
if let Some((best_s, best_t)) = best_pair {
source_mapped[best_s] = true;
target_mapped[best_t] = true;
mapped_count += 1;
mappings.push(Mapping {
source: source_nodes[best_s],
target: target_nodes[best_t],
score: best_score,
});
let s_neighbors = &source_data[best_s].neighbors;
let t_neighbors = &target_data[best_t].neighbors;
for &s_neighbor_id in s_neighbors {
if let Some(&s_neighbor_idx) = source_idx_map.get(&s_neighbor_id) {
if source_mapped[s_neighbor_idx] {
continue;
}
for &t_neighbor_id in t_neighbors {
if let Some(&t_neighbor_idx) = target_idx_map.get(&t_neighbor_id) {
if target_mapped[t_neighbor_idx] {
continue;
}
if s_neighbor_idx < source_nodes.len()
&& t_neighbor_idx < target_len
{
scores[s_neighbor_idx * target_len + t_neighbor_idx] +=
structural_weight;
}
}
}
}
}
} else {
break; }
}
let global_score = if mappings.is_empty() {
0.0
} else {
mappings.iter().map(|m| m.score).sum::<f32>() / mappings.len() as f32
};
Ok(Alignment {
mappings,
global_score,
})
}
fn fetch_subgraph_data(
&self,
nodes: &[NodeId],
vector_property: &str,
) -> Result<Vec<NodeData>> {
let mut data = Vec::with_capacity(nodes.len());
for &id in nodes {
let node = self.db.get_node(id)?;
let vector = node
.properties
.get(vector_property)
.and_then(|v| v.as_vector())
.map(|v| v.to_vec());
let mut neighbors = HashSet::new();
for edge_id in self.db.get_outgoing_edges(id) {
if let Ok(target) = self.db.get_edge_target(edge_id) {
neighbors.insert(target);
}
}
for edge_id in self.db.get_incoming_edges(id) {
if let Ok(source) = self.db.get_edge_source(edge_id) {
neighbors.insert(source);
}
}
let mut neighbors_vec: Vec<NodeId> = neighbors.into_iter().collect();
neighbors_vec.sort();
data.push(NodeData {
vector,
neighbors: neighbors_vec,
});
}
Ok(data)
}
}
#[cfg(not(feature = "semantic-reasoning"))]
impl<'a> Metaphor<'a> {
pub fn new(_db: &'a AletheiaDB) -> Self {
panic!(
"Experimental features like Metaphor require the 'nova' feature. Please enable it in your Cargo.toml:\n\n[dependencies]\naletheiadb = {{ version = \"...\", features = [\"nova\"] }}\n"
);
}
#[cfg(test)]
fn new_internal(db: &'a AletheiaDB) -> Self {
Self { db }
}
pub fn align(
&self,
_source_nodes: &[NodeId],
_target_nodes: &[NodeId],
_vector_property: &str,
_structural_weight: f32,
) -> Result<Alignment> {
panic!("Experimental features like Metaphor require the 'nova' feature.");
}
}
#[cfg(feature = "semantic-reasoning")]
struct NodeData {
vector: Option<Vec<f32>>,
neighbors: Vec<NodeId>,
}
#[cfg(feature = "semantic-reasoning")]
#[cfg(test)]
mod tests {
use super::*;
use crate::core::property::PropertyMapBuilder;
use crate::index::vector::{DistanceMetric, HnswConfig};
#[test]
fn test_metaphor_pure_semantic() {
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("Source", props_a).unwrap();
let props_x = PropertyMapBuilder::new()
.insert_vector("vec", &[1.0, 0.0])
.build();
let x = db.create_node("Target", props_x).unwrap();
let props_y = PropertyMapBuilder::new()
.insert_vector("vec", &[0.0, 1.0])
.build();
let y = db.create_node("Target", props_y).unwrap();
let metaphor = Metaphor::new(&db);
let alignment = metaphor.align(&[a], &[x, y], "vec", 0.5).unwrap();
assert_eq!(alignment.mappings.len(), 1);
assert_eq!(alignment.mappings[0].source, a);
assert_eq!(alignment.mappings[0].target, x);
assert!(alignment.mappings[0].score > 0.9);
}
#[test]
fn test_metaphor_exact_opposite_score() {
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("Source", props_a).unwrap();
let props_x = PropertyMapBuilder::new()
.insert_vector("vec", &[-1.0, 0.0])
.build();
let x = db.create_node("Target", props_x).unwrap();
let metaphor = Metaphor::new(&db);
let alignment = metaphor.align(&[a], &[x], "vec", 0.0).unwrap();
assert_eq!(alignment.mappings.len(), 1);
assert_eq!(alignment.mappings[0].source, a);
assert_eq!(alignment.mappings[0].target, x);
assert!((alignment.mappings[0].score - -1.0).abs() < 1e-6);
}
#[test]
fn test_metaphor_structural_disambiguation() {
let db = AletheiaDB::new().unwrap();
let config = HnswConfig::new(2, DistanceMetric::Cosine);
db.enable_vector_index("vec", config).unwrap();
let props_c = PropertyMapBuilder::new()
.insert_vector("vec", &[0.0, 1.0])
.build();
let c = db.create_node("Anchor", props_c).unwrap();
let props_z = PropertyMapBuilder::new()
.insert_vector("vec", &[0.0, 1.0])
.build();
let z = db.create_node("Anchor", props_z).unwrap();
let props_ambiguous = PropertyMapBuilder::new()
.insert_vector("vec", &[1.0, 0.0])
.build();
let a = db.create_node("Node", props_ambiguous.clone()).unwrap();
let b = db.create_node("Node", props_ambiguous.clone()).unwrap();
let x = db.create_node("Node", props_ambiguous.clone()).unwrap();
let y = db.create_node("Node", props_ambiguous.clone()).unwrap();
db.create_edge(a, c, "LINK", Default::default()).unwrap();
db.create_edge(x, z, "LINK", Default::default()).unwrap();
let metaphor = Metaphor::new(&db);
let alignment = metaphor
.align(
&[c, a, b],
&[z, x, y],
"vec",
0.5, )
.unwrap();
let map_c = alignment.mappings.iter().find(|m| m.source == c).unwrap();
assert_eq!(map_c.target, z);
let map_a = alignment.mappings.iter().find(|m| m.source == a).unwrap();
assert_eq!(map_a.target, x);
assert!(map_a.score > 1.0);
let map_b = alignment.mappings.iter().find(|m| m.source == b).unwrap();
assert_eq!(map_b.target, y);
}
}
#[cfg(not(feature = "semantic-reasoning"))]
#[cfg(test)]
mod stub_tests {
use super::*;
use crate::AletheiaDB;
#[test]
#[should_panic(expected = "Experimental features like Metaphor require the 'nova' feature")]
fn test_stub_new_panics() {
let db = AletheiaDB::new().unwrap();
let _ = Metaphor::new(&db);
}
#[test]
#[should_panic(expected = "Experimental features like Metaphor require the 'nova' feature")]
fn test_stub_align_panics() {
let db = AletheiaDB::new().unwrap();
let metaphor = Metaphor::new_internal(&db);
let _ = metaphor.align(&[], &[], "vec", 0.0);
}
}