#![allow(clippy::collapsible_if)]
use crate::AletheiaDB;
use crate::api::transaction::{ReadOps, WriteOps};
use crate::core::error::Result;
use crate::core::id::NodeId;
use crate::core::interning::GLOBAL_INTERNER;
use crate::core::property::PropertyMapBuilder;
use crate::experimental::wormhole::WormholeDetector;
pub struct Alchemist<'a> {
db: &'a AletheiaDB,
}
impl<'a> Alchemist<'a> {
pub fn new(db: &'a AletheiaDB) -> Self {
Self { db }
}
pub fn crystallize_wormholes(
&self,
candidates: &[NodeId],
similarity_threshold: f32,
max_hops: usize,
edge_label: &str,
) -> Result<usize> {
if candidates.is_empty() {
return Ok(0);
}
let detector = WormholeDetector::new(self.db);
let wormholes = detector.find_wormholes(candidates, 10, max_hops)?;
let created_count = self.db.write(|tx| {
let mut count = 0;
for wormhole in wormholes {
if wormhole.similarity >= similarity_threshold {
let props = PropertyMapBuilder::new()
.insert("similarity", wormhole.similarity as f64)
.insert("crystallized", true)
.build();
tx.create_edge(wormhole.source, wormhole.target, edge_label, props)?;
count += 1;
}
}
Ok::<_, crate::core::error::Error>(count)
})?;
Ok(created_count)
}
pub fn fuse_synonyms(&self, candidates: &[NodeId], similarity_threshold: f32) -> Result<usize> {
use std::collections::HashSet;
if candidates.len() < 2 {
return Ok(0);
}
let candidate_set: HashSet<NodeId> = candidates.iter().cloned().collect();
let mut pairs = Vec::new();
let mut processed = HashSet::new();
for &node_id in candidates {
if processed.contains(&node_id) {
continue;
}
let neighbors = self.db.find_similar(node_id, 5)?;
for (neighbor, sim) in neighbors {
if sim >= similarity_threshold
&& candidate_set.contains(&neighbor)
&& !processed.contains(&neighbor)
{
pairs.push((node_id, neighbor));
processed.insert(node_id);
processed.insert(neighbor);
break;
}
}
}
if pairs.is_empty() {
return Ok(0);
}
let merged_count = self.db.write(|tx| {
let mut count = 0;
for (n1, n2) in pairs {
let (survivor, victim) = if n1 < n2 { (n1, n2) } else { (n2, n1) };
let outgoing = tx.get_outgoing_edges(victim);
for edge_id in outgoing {
if let Ok(edge) = tx.get_edge(edge_id) {
let label_str = GLOBAL_INTERNER
.resolve_with(edge.label, |s| s.to_string())
.unwrap_or_else(|| "UNKNOWN".to_string());
if edge.target != survivor {
tx.create_edge(
survivor,
edge.target,
&label_str,
edge.properties.clone(),
)?;
}
}
}
let incoming = tx.get_incoming_edges(victim);
for edge_id in incoming {
if let Ok(edge) = tx.get_edge(edge_id) {
let label_str = GLOBAL_INTERNER
.resolve_with(edge.label, |s| s.to_string())
.unwrap_or_else(|| "UNKNOWN".to_string());
if edge.source != survivor {
tx.create_edge(
edge.source,
survivor,
&label_str,
edge.properties.clone(),
)?;
}
}
}
tx.delete_node_cascade(victim)?;
count += 1;
}
Ok::<_, crate::core::error::Error>(count)
})?;
Ok(merged_count)
}
}
#[cfg(test)]
#[allow(clippy::collapsible_if)]
mod tests {
use super::*;
use crate::core::property::PropertyMapBuilder;
use crate::index::vector::{DistanceMetric, HnswConfig};
fn create_test_db() -> AletheiaDB {
let db = AletheiaDB::new().unwrap();
let config = HnswConfig::new(2, DistanceMetric::Cosine);
db.enable_vector_index("vec", config).unwrap();
db
}
#[test]
#[allow(clippy::collapsible_if)]
fn test_crystallize_wormholes_creates_edges() {
let db = create_test_db();
let alchemist = Alchemist::new(&db);
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.99, 0.1]) .build();
let b = db.create_node("Node", props_b).unwrap();
let candidates = vec![a, b];
let count = alchemist
.crystallize_wormholes(&candidates, 0.9, 2, "RELATED")
.unwrap();
assert!(
count >= 1,
"Should create at least one edge (may be bidirectional)"
);
let found = db
.read(|tx| {
let outgoing = tx.get_outgoing_edges(a);
let mut found_edge = false;
for eid in outgoing {
#[allow(clippy::collapsible_if)]
if let Ok(edge) = tx.get_edge(eid) {
if edge.target == b && edge.has_label_str("RELATED") {
found_edge = true;
if let Some(sim) = edge.properties.get("similarity") {
assert!(sim.as_float().unwrap() > 0.9);
}
}
}
}
Ok::<_, crate::core::error::Error>(found_edge)
})
.unwrap();
assert!(found, "Edge A->B with label RELATED should exist");
}
#[test]
#[allow(clippy::collapsible_if)]
fn test_fuse_synonyms_merges_nodes() {
let db = create_test_db();
let alchemist = Alchemist::new(&db);
let props_a = PropertyMapBuilder::new()
.insert("name", "Survivor")
.insert_vector("vec", &[1.0, 0.0])
.build();
let a = db.create_node("Node", props_a).unwrap();
let props_b = PropertyMapBuilder::new()
.insert("name", "Victim")
.insert_vector("vec", &[0.99, 0.01]) .build();
let b = db.create_node("Node", props_b).unwrap();
let c = db
.create_node("Other", PropertyMapBuilder::new().build())
.unwrap();
let d = db
.create_node("Other", PropertyMapBuilder::new().build())
.unwrap();
db.create_edge(c, a, "LINKS", Default::default()).unwrap();
db.create_edge(c, b, "LINKS_TO_B", Default::default())
.unwrap();
db.create_edge(b, d, "LINKS_FROM_B", Default::default())
.unwrap();
let candidates = vec![a, b];
let merged = alchemist.fuse_synonyms(&candidates, 0.95).unwrap();
assert_eq!(merged, 1, "Should merge 1 pair (delete 1 node)");
assert!(db.get_node(b).is_err(), "Node B should be deleted");
let has_a_to_d = db
.read(|tx| {
let edges = tx.get_outgoing_edges(a);
let mut found = false;
for eid in edges {
#[allow(clippy::collapsible_if)]
if let Ok(edge) = tx.get_edge(eid) {
if edge.target == d && edge.has_label_str("LINKS_FROM_B") {
found = true;
}
}
}
Ok::<_, crate::core::error::Error>(found)
})
.unwrap();
assert!(has_a_to_d, "Survivor A should inherit outgoing edge to D");
let has_c_to_a_inherited = db
.read(|tx| {
let edges = tx.get_outgoing_edges(c);
let mut found = false;
for eid in edges {
#[allow(clippy::collapsible_if)]
if let Ok(edge) = tx.get_edge(eid) {
if edge.target == a && edge.has_label_str("LINKS_TO_B") {
found = true;
}
}
}
Ok::<_, crate::core::error::Error>(found)
})
.unwrap();
assert!(
has_c_to_a_inherited,
"Survivor A should inherit incoming edge from C"
);
}
}