1use std::collections::{HashMap, HashSet, VecDeque};
7
8#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
14pub enum EntityKind {
15 Concept,
16 Document,
17 Person,
18 Organization,
19 Event,
20}
21
22#[derive(Clone, Debug)]
28pub struct GraphEntity {
29 pub entity_id: u64,
31 pub name: String,
33 pub kind: EntityKind,
35 pub embedding: Option<Vec<f32>>,
37 pub properties: Vec<(String, String)>,
39}
40
41#[derive(Clone, Debug)]
47pub struct GraphEdge {
48 pub edge_id: u64,
50 pub from_id: u64,
52 pub to_id: u64,
54 pub relation: String,
56 pub weight: f32,
58}
59
60#[derive(Clone, Debug)]
66pub struct GraphQuery {
67 pub start_entity_id: u64,
69 pub relation_filter: Option<String>,
71 pub max_hops: usize,
73 pub entity_kind_filter: Option<EntityKind>,
75}
76
77#[derive(Clone, Debug)]
83pub struct KnowledgeGraphStats {
84 pub total_entities: usize,
86 pub total_edges: usize,
88 pub entities_with_embeddings: usize,
90 pub avg_degree: f64,
93}
94
95pub fn cosine_sim(a: &[f32], b: &[f32]) -> f32 {
103 let len = a.len().min(b.len());
104 if len == 0 {
105 return 0.0;
106 }
107
108 let mut dot = 0.0_f32;
109 let mut mag_a = 0.0_f32;
110 let mut mag_b = 0.0_f32;
111
112 for i in 0..len {
113 dot += a[i] * b[i];
114 mag_a += a[i] * a[i];
115 mag_b += b[i] * b[i];
116 }
117
118 let denom = mag_a.sqrt() * mag_b.sqrt();
119 if denom < f32::EPSILON {
120 0.0
121 } else {
122 dot / denom
123 }
124}
125
126pub struct SemanticKnowledgeGraph {
138 pub entities: HashMap<u64, GraphEntity>,
140 pub edges: Vec<GraphEdge>,
142 pub next_entity_id: u64,
144 pub next_edge_id: u64,
146}
147
148impl SemanticKnowledgeGraph {
149 pub fn new() -> Self {
155 Self {
156 entities: HashMap::new(),
157 edges: Vec::new(),
158 next_entity_id: 0,
159 next_edge_id: 0,
160 }
161 }
162
163 pub fn add_entity(&mut self, name: &str, kind: EntityKind, embedding: Option<Vec<f32>>) -> u64 {
169 let id = self.next_entity_id;
170 self.next_entity_id += 1;
171
172 self.entities.insert(
173 id,
174 GraphEntity {
175 entity_id: id,
176 name: name.to_owned(),
177 kind,
178 embedding,
179 properties: Vec::new(),
180 },
181 );
182
183 id
184 }
185
186 pub fn add_edge(&mut self, from_id: u64, to_id: u64, relation: &str, weight: f32) -> u64 {
192 let id = self.next_edge_id;
193 self.next_edge_id += 1;
194
195 self.edges.push(GraphEdge {
196 edge_id: id,
197 from_id,
198 to_id,
199 relation: relation.to_owned(),
200 weight,
201 });
202
203 id
204 }
205
206 pub fn get_entity(&self, id: u64) -> Option<&GraphEntity> {
208 self.entities.get(&id)
209 }
210
211 pub fn remove_entity(&mut self, entity_id: u64) -> bool {
215 if self.entities.remove(&entity_id).is_none() {
216 return false;
217 }
218 self.edges
219 .retain(|e| e.from_id != entity_id && e.to_id != entity_id);
220 true
221 }
222
223 pub fn neighbors(&self, entity_id: u64, relation: Option<&str>) -> Vec<&GraphEntity> {
233 let mut candidates: Vec<(f32, &GraphEntity)> = self
235 .edges
236 .iter()
237 .filter(|e| e.from_id == entity_id && relation.is_none_or(|r| e.relation == r))
238 .filter_map(|e| self.entities.get(&e.to_id).map(|entity| (e.weight, entity)))
239 .collect();
240
241 candidates.sort_by(|(wa, ea), (wb, eb)| {
243 wb.partial_cmp(wa)
244 .unwrap_or(std::cmp::Ordering::Equal)
245 .then_with(|| ea.entity_id.cmp(&eb.entity_id))
246 });
247
248 candidates.into_iter().map(|(_, e)| e).collect()
249 }
250
251 pub fn traverse(&self, query: &GraphQuery) -> Vec<&GraphEntity> {
258 let mut visited: HashSet<u64> = HashSet::new();
259 let mut result_ids: Vec<u64> = Vec::new();
260
261 visited.insert(query.start_entity_id);
262
263 let mut queue: VecDeque<(u64, usize)> = VecDeque::new();
265 queue.push_back((query.start_entity_id, query.max_hops));
266
267 while let Some((current_id, hops_left)) = queue.pop_front() {
268 if hops_left == 0 {
269 continue;
270 }
271
272 for edge in self.edges.iter().filter(|e| e.from_id == current_id) {
273 if let Some(ref rel) = query.relation_filter {
275 if &edge.relation != rel {
276 continue;
277 }
278 }
279
280 let dest_id = edge.to_id;
281
282 if visited.contains(&dest_id) {
283 continue;
284 }
285 visited.insert(dest_id);
286
287 if let Some(entity) = self.entities.get(&dest_id) {
290 let kind_ok = query.entity_kind_filter.is_none_or(|k| entity.kind == k);
291 if kind_ok {
292 result_ids.push(dest_id);
293 }
294 queue.push_back((dest_id, hops_left - 1));
297 }
298 }
299 }
300
301 result_ids.sort_unstable();
302 result_ids
303 .iter()
304 .filter_map(|id| self.entities.get(id))
305 .collect()
306 }
307
308 pub fn similar_entities(&self, entity_id: u64, threshold: f32) -> Vec<(&GraphEntity, f32)> {
314 let query_emb = match self
315 .entities
316 .get(&entity_id)
317 .and_then(|e| e.embedding.as_ref())
318 {
319 Some(emb) => emb,
320 None => return Vec::new(),
321 };
322
323 let mut results: Vec<(&GraphEntity, f32)> = self
324 .entities
325 .values()
326 .filter(|e| e.entity_id != entity_id)
327 .filter_map(|e| {
328 e.embedding
329 .as_ref()
330 .map(|emb| (e, cosine_sim(query_emb, emb)))
331 })
332 .filter(|(_, sim)| *sim >= threshold)
333 .collect();
334
335 results.sort_by(|(_, sa), (_, sb)| sb.partial_cmp(sa).unwrap_or(std::cmp::Ordering::Equal));
336
337 results
338 }
339
340 pub fn stats(&self) -> KnowledgeGraphStats {
346 let total_entities = self.entities.len();
347 let total_edges = self.edges.len();
348 let entities_with_embeddings = self
349 .entities
350 .values()
351 .filter(|e| e.embedding.is_some())
352 .count();
353
354 let avg_degree = if total_entities == 0 {
355 0.0
356 } else {
357 (total_edges as f64 * 2.0) / total_entities as f64
358 };
359
360 KnowledgeGraphStats {
361 total_entities,
362 total_edges,
363 entities_with_embeddings,
364 avg_degree,
365 }
366 }
367}
368
369impl Default for SemanticKnowledgeGraph {
370 fn default() -> Self {
371 Self::new()
372 }
373}
374
375#[cfg(test)]
380mod tests {
381 use super::*;
382
383 fn small_graph() -> SemanticKnowledgeGraph {
388 let mut g = SemanticKnowledgeGraph::new();
389 let ai = g.add_entity("AI", EntityKind::Concept, Some(vec![1.0, 0.0]));
391 let paper = g.add_entity("ML paper", EntityKind::Document, Some(vec![0.9, 0.1]));
393 let alice = g.add_entity("Alice", EntityKind::Person, Some(vec![0.0, 1.0]));
395 let openai = g.add_entity("OpenAI", EntityKind::Organization, None);
397 g.add_edge(ai, paper, "is_about", 0.9);
399 g.add_edge(paper, alice, "authored_by", 0.8);
400 g.add_edge(ai, openai, "mentions", 0.5);
401 g
402 }
403
404 #[test]
409 fn test_new_starts_empty() {
410 let g = SemanticKnowledgeGraph::new();
411 assert!(g.entities.is_empty());
412 assert!(g.edges.is_empty());
413 assert_eq!(g.next_entity_id, 0);
414 assert_eq!(g.next_edge_id, 0);
415 }
416
417 #[test]
422 fn test_add_entity_returns_incrementing_ids() {
423 let mut g = SemanticKnowledgeGraph::new();
424 let id0 = g.add_entity("A", EntityKind::Concept, None);
425 let id1 = g.add_entity("B", EntityKind::Document, None);
426 let id2 = g.add_entity("C", EntityKind::Person, None);
427 assert_eq!(id0, 0);
428 assert_eq!(id1, 1);
429 assert_eq!(id2, 2);
430 }
431
432 #[test]
433 fn test_add_entity_stores_properties() {
434 let mut g = SemanticKnowledgeGraph::new();
435 let id = g.add_entity("E", EntityKind::Event, Some(vec![0.1, 0.2]));
436 let e = g.get_entity(id).expect("entity must exist");
437 assert_eq!(e.name, "E");
438 assert_eq!(e.kind, EntityKind::Event);
439 assert_eq!(e.embedding, Some(vec![0.1, 0.2]));
440 assert!(e.properties.is_empty());
441 }
442
443 #[test]
448 fn test_add_edge_stores_edge() {
449 let mut g = SemanticKnowledgeGraph::new();
450 let a = g.add_entity("A", EntityKind::Concept, None);
451 let b = g.add_entity("B", EntityKind::Concept, None);
452 let eid = g.add_edge(a, b, "related", 0.7);
453 assert_eq!(eid, 0);
454 assert_eq!(g.edges.len(), 1);
455 let edge = &g.edges[0];
456 assert_eq!(edge.from_id, a);
457 assert_eq!(edge.to_id, b);
458 assert_eq!(edge.relation, "related");
459 assert!((edge.weight - 0.7).abs() < f32::EPSILON);
460 }
461
462 #[test]
467 fn test_get_entity_some() {
468 let g = small_graph();
469 assert!(g.get_entity(0).is_some());
470 assert!(g.get_entity(1).is_some());
471 }
472
473 #[test]
474 fn test_get_entity_none() {
475 let g = small_graph();
476 assert!(g.get_entity(999).is_none());
477 }
478
479 #[test]
484 fn test_neighbors_returns_correct_entities() {
485 let g = small_graph();
486 let nbrs = g.neighbors(0, None);
488 let ids: Vec<u64> = nbrs.iter().map(|e| e.entity_id).collect();
489 assert!(ids.contains(&1));
490 assert!(ids.contains(&3));
491 }
492
493 #[test]
494 fn test_neighbors_with_relation_filter() {
495 let g = small_graph();
496 let nbrs = g.neighbors(0, Some("is_about"));
497 assert_eq!(nbrs.len(), 1);
498 assert_eq!(nbrs[0].entity_id, 1);
499 }
500
501 #[test]
502 fn test_neighbors_sorted_by_weight_desc() {
503 let mut g = SemanticKnowledgeGraph::new();
504 let src = g.add_entity("src", EntityKind::Concept, None);
505 let a = g.add_entity("a", EntityKind::Concept, None);
506 let b = g.add_entity("b", EntityKind::Concept, None);
507 let c = g.add_entity("c", EntityKind::Concept, None);
508 g.add_edge(src, a, "r", 0.3);
509 g.add_edge(src, b, "r", 0.9);
510 g.add_edge(src, c, "r", 0.6);
511 let nbrs = g.neighbors(src, None);
512 let weights: Vec<f32> = nbrs
513 .iter()
514 .map(|e| {
515 g.edges
516 .iter()
517 .find(|edge| edge.from_id == src && edge.to_id == e.entity_id)
518 .map(|edge| edge.weight)
519 .unwrap_or(0.0)
520 })
521 .collect();
522 assert!(weights[0] >= weights[1]);
523 assert!(weights[1] >= weights[2]);
524 }
525
526 #[test]
531 fn test_traverse_single_hop() {
532 let g = small_graph();
533 let query = GraphQuery {
534 start_entity_id: 0,
535 relation_filter: None,
536 max_hops: 1,
537 entity_kind_filter: None,
538 };
539 let result = g.traverse(&query);
540 let ids: Vec<u64> = result.iter().map(|e| e.entity_id).collect();
541 assert!(ids.contains(&1));
543 assert!(ids.contains(&3));
544 assert!(!ids.contains(&2));
546 }
547
548 #[test]
549 fn test_traverse_multiple_hops() {
550 let g = small_graph();
551 let query = GraphQuery {
552 start_entity_id: 0,
553 relation_filter: None,
554 max_hops: 2,
555 entity_kind_filter: None,
556 };
557 let result = g.traverse(&query);
558 let ids: Vec<u64> = result.iter().map(|e| e.entity_id).collect();
559 assert!(ids.contains(&1));
561 assert!(ids.contains(&2));
562 assert!(ids.contains(&3));
563 }
564
565 #[test]
566 fn test_traverse_with_relation_filter() {
567 let g = small_graph();
568 let query = GraphQuery {
569 start_entity_id: 0,
570 relation_filter: Some("is_about".to_owned()),
571 max_hops: 2,
572 entity_kind_filter: None,
573 };
574 let result = g.traverse(&query);
575 let ids: Vec<u64> = result.iter().map(|e| e.entity_id).collect();
576 assert!(ids.contains(&1));
579 assert!(!ids.contains(&2));
580 assert!(!ids.contains(&3));
581 }
582
583 #[test]
584 fn test_traverse_with_entity_kind_filter() {
585 let g = small_graph();
586 let query = GraphQuery {
587 start_entity_id: 0,
588 relation_filter: None,
589 max_hops: 2,
590 entity_kind_filter: Some(EntityKind::Person),
591 };
592 let result = g.traverse(&query);
593 let ids: Vec<u64> = result.iter().map(|e| e.entity_id).collect();
594 assert_eq!(ids, vec![2]);
596 }
597
598 #[test]
599 fn test_traverse_excludes_start_entity() {
600 let g = small_graph();
601 let query = GraphQuery {
602 start_entity_id: 0,
603 relation_filter: None,
604 max_hops: 5,
605 entity_kind_filter: None,
606 };
607 let result = g.traverse(&query);
608 assert!(!result.iter().any(|e| e.entity_id == 0));
609 }
610
611 #[test]
612 fn test_traverse_sorted_by_entity_id() {
613 let g = small_graph();
614 let query = GraphQuery {
615 start_entity_id: 0,
616 relation_filter: None,
617 max_hops: 5,
618 entity_kind_filter: None,
619 };
620 let result = g.traverse(&query);
621 let ids: Vec<u64> = result.iter().map(|e| e.entity_id).collect();
622 let mut sorted = ids.clone();
623 sorted.sort_unstable();
624 assert_eq!(ids, sorted);
625 }
626
627 #[test]
628 fn test_traverse_zero_hops_returns_empty() {
629 let g = small_graph();
630 let query = GraphQuery {
631 start_entity_id: 0,
632 relation_filter: None,
633 max_hops: 0,
634 entity_kind_filter: None,
635 };
636 let result = g.traverse(&query);
637 assert!(result.is_empty());
638 }
639
640 #[test]
645 fn test_similar_entities_above_threshold() {
646 let g = small_graph();
647 let sims = g.similar_entities(0, 0.9);
650 assert!(!sims.is_empty());
651 let ids: Vec<u64> = sims.iter().map(|(e, _)| e.entity_id).collect();
652 assert!(ids.contains(&1));
653 }
654
655 #[test]
656 fn test_similar_entities_excludes_self() {
657 let g = small_graph();
658 let sims = g.similar_entities(0, 0.0);
659 assert!(sims.iter().all(|(e, _)| e.entity_id != 0));
660 }
661
662 #[test]
663 fn test_similar_entities_sorted_by_sim_desc() {
664 let g = small_graph();
665 let sims = g.similar_entities(0, 0.0);
666 if sims.len() > 1 {
667 for i in 0..sims.len() - 1 {
668 assert!(sims[i].1 >= sims[i + 1].1);
669 }
670 }
671 }
672
673 #[test]
674 fn test_similar_entities_empty_when_no_embeddings() {
675 let mut g = SemanticKnowledgeGraph::new();
676 let a = g.add_entity("A", EntityKind::Concept, None);
677 let _b = g.add_entity("B", EntityKind::Concept, None);
678 let sims = g.similar_entities(a, 0.0);
680 assert!(sims.is_empty());
681 }
682
683 #[test]
684 fn test_similar_entities_returns_empty_for_missing_entity() {
685 let g = small_graph();
686 let sims = g.similar_entities(9999, 0.0);
687 assert!(sims.is_empty());
688 }
689
690 #[test]
695 fn test_remove_entity_removes_node() {
696 let mut g = small_graph();
697 let removed = g.remove_entity(0);
698 assert!(removed);
699 assert!(g.get_entity(0).is_none());
700 }
701
702 #[test]
703 fn test_remove_entity_removes_connected_edges() {
704 let mut g = small_graph();
705 let edges_before = g.edges.len();
706 g.remove_entity(0);
707 assert!(g.edges.len() < edges_before);
709 assert!(g.edges.iter().all(|e| e.from_id != 0 && e.to_id != 0));
710 }
711
712 #[test]
713 fn test_remove_entity_false_for_unknown() {
714 let mut g = small_graph();
715 assert!(!g.remove_entity(999));
716 }
717
718 #[test]
723 fn test_stats_total_entities_and_edges() {
724 let g = small_graph();
725 let s = g.stats();
726 assert_eq!(s.total_entities, 4);
728 assert_eq!(s.total_edges, 3);
729 }
730
731 #[test]
732 fn test_stats_entities_with_embeddings() {
733 let g = small_graph();
734 let s = g.stats();
735 assert_eq!(s.entities_with_embeddings, 3);
737 }
738
739 #[test]
740 fn test_stats_avg_degree() {
741 let g = small_graph();
742 let s = g.stats();
743 let expected = (3_f64 * 2.0) / 4.0;
744 assert!((s.avg_degree - expected).abs() < 1e-9);
745 }
746
747 #[test]
748 fn test_stats_avg_degree_empty_graph() {
749 let g = SemanticKnowledgeGraph::new();
750 let s = g.stats();
751 assert_eq!(s.avg_degree, 0.0);
752 }
753
754 #[test]
759 fn test_cosine_sim_identical_vectors() {
760 let v = vec![1.0_f32, 2.0, 3.0];
761 let sim = cosine_sim(&v, &v);
762 assert!((sim - 1.0).abs() < 1e-6);
763 }
764
765 #[test]
766 fn test_cosine_sim_orthogonal_vectors() {
767 let a = vec![1.0_f32, 0.0];
768 let b = vec![0.0_f32, 1.0];
769 let sim = cosine_sim(&a, &b);
770 assert!(sim.abs() < 1e-6);
771 }
772
773 #[test]
774 fn test_cosine_sim_zero_vector() {
775 let a = vec![0.0_f32, 0.0];
776 let b = vec![1.0_f32, 0.0];
777 let sim = cosine_sim(&a, &b);
778 assert_eq!(sim, 0.0);
779 }
780}