1use crate::{CommunitySummary, GraphRAGError, GraphRAGResult, Triple};
4use petgraph::graph::{NodeIndex, UnGraph};
5use scirs2_core::random::{seeded_rng, Random};
6use std::collections::{HashMap, HashSet};
7
8#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
10pub enum CommunityAlgorithm {
11 Louvain,
13 #[default]
15 Leiden,
16 LabelPropagation,
18 ConnectedComponents,
20 Hierarchical,
22}
23
24#[derive(Debug, Clone)]
26pub struct CommunityConfig {
27 pub algorithm: CommunityAlgorithm,
29 pub resolution: f64,
31 pub min_community_size: usize,
33 pub max_communities: usize,
35 pub max_iterations: usize,
37 pub random_seed: u64,
39}
40
41impl Default for CommunityConfig {
42 fn default() -> Self {
43 Self {
44 algorithm: CommunityAlgorithm::Leiden,
45 resolution: 1.0,
46 min_community_size: 3,
47 max_communities: 50,
48 max_iterations: 10,
49 random_seed: 42,
50 }
51 }
52}
53
54pub struct CommunityDetector {
56 config: CommunityConfig,
57}
58
59impl Default for CommunityDetector {
60 fn default() -> Self {
61 Self::new(CommunityConfig::default())
62 }
63}
64
65impl CommunityDetector {
66 pub fn new(config: CommunityConfig) -> Self {
67 Self { config }
68 }
69
70 pub fn detect(&self, triples: &[Triple]) -> GraphRAGResult<Vec<CommunitySummary>> {
72 if triples.is_empty() {
73 return Ok(vec![]);
74 }
75
76 let (graph, node_map) = self.build_graph(triples);
78
79 let communities = match self.config.algorithm {
81 CommunityAlgorithm::Louvain => self.louvain(&graph, &node_map)?,
82 CommunityAlgorithm::Leiden => self.leiden(&graph, &node_map)?,
83 CommunityAlgorithm::LabelPropagation => self.label_propagation(&graph, &node_map),
84 CommunityAlgorithm::ConnectedComponents => self.connected_components(&graph, &node_map),
85 CommunityAlgorithm::Hierarchical => {
86 return self.detect_hierarchical(triples);
87 }
88 };
89
90 let summaries = self.create_summaries(communities, triples);
92
93 Ok(summaries)
94 }
95
96 fn build_graph(&self, triples: &[Triple]) -> (UnGraph<String, ()>, HashMap<String, NodeIndex>) {
98 let mut graph: UnGraph<String, ()> = UnGraph::new_undirected();
99 let mut node_map: HashMap<String, NodeIndex> = HashMap::new();
100
101 for triple in triples {
102 let subj_idx = *node_map
103 .entry(triple.subject.clone())
104 .or_insert_with(|| graph.add_node(triple.subject.clone()));
105 let obj_idx = *node_map
106 .entry(triple.object.clone())
107 .or_insert_with(|| graph.add_node(triple.object.clone()));
108
109 if subj_idx != obj_idx && graph.find_edge(subj_idx, obj_idx).is_none() {
110 graph.add_edge(subj_idx, obj_idx, ());
111 }
112 }
113
114 (graph, node_map)
115 }
116
117 fn louvain(
119 &self,
120 graph: &UnGraph<String, ()>,
121 node_map: &HashMap<String, NodeIndex>,
122 ) -> GraphRAGResult<Vec<HashSet<String>>> {
123 let node_count = graph.node_count();
124 if node_count == 0 {
125 return Ok(vec![]);
126 }
127
128 let mut community: HashMap<NodeIndex, usize> = HashMap::new();
130 for (community_id, &idx) in node_map.values().enumerate() {
131 community.insert(idx, community_id);
132 }
133
134 let m = graph.edge_count() as f64;
136 if m == 0.0 {
137 return Ok(node_map
139 .keys()
140 .map(|k| {
141 let mut set = HashSet::new();
142 set.insert(k.clone());
143 set
144 })
145 .collect());
146 }
147
148 let degree: HashMap<NodeIndex, f64> = node_map
150 .values()
151 .map(|&idx| (idx, graph.neighbors(idx).count() as f64))
152 .collect();
153
154 let mut node_order: Vec<NodeIndex> = node_map.values().copied().collect();
162 node_order.sort();
163 let mut rng = seeded_rng(self.config.random_seed);
164 for i in (1..node_order.len()).rev() {
165 let j = (rng.random_range(0.0..1.0) * (i + 1) as f64) as usize;
166 node_order.swap(i, j);
167 }
168
169 for _ in 0..self.config.max_iterations {
171 let mut changed = false;
172
173 for &node in &node_order {
174 let current_comm = match community.get(&node) {
175 Some(&c) => c,
176 None => continue,
177 };
178 let node_degree = degree.get(&node).copied().unwrap_or(0.0);
179
180 let mut best_comm = current_comm;
182 let mut best_gain = 0.0;
183
184 let mut neighbor_comms: Vec<usize> = graph
187 .neighbors(node)
188 .filter_map(|n| community.get(&n).copied())
189 .collect::<HashSet<usize>>()
190 .into_iter()
191 .collect();
192 neighbor_comms.sort_unstable();
193
194 for &neighbor_comm in &neighbor_comms {
195 if neighbor_comm == current_comm {
196 continue;
197 }
198
199 let edges_to_comm: f64 = graph
201 .neighbors(node)
202 .filter(|n| community.get(n) == Some(&neighbor_comm))
203 .count() as f64;
204
205 let comm_degree: f64 = community
206 .iter()
207 .filter(|(_, &c)| c == neighbor_comm)
208 .map(|(n, _)| degree.get(n).copied().unwrap_or(0.0))
209 .sum();
210
211 let gain = edges_to_comm / m
212 - self.config.resolution * node_degree * comm_degree / (2.0 * m * m);
213
214 if gain > best_gain {
215 best_gain = gain;
216 best_comm = neighbor_comm;
217 }
218 }
219
220 if best_comm != current_comm && best_gain > 0.0 {
221 community.insert(node, best_comm);
222 changed = true;
223 }
224 }
225
226 if !changed {
227 break;
228 }
229 }
230
231 let trivial: HashMap<NodeIndex, usize> =
241 node_map.values().map(|&idx| (idx, 0usize)).collect();
242 let greedy_modularity = self.calculate_modularity(graph, &community, m, °ree)?;
243 let trivial_modularity = self.calculate_modularity(graph, &trivial, m, °ree)?;
244
245 if trivial_modularity >= greedy_modularity {
246 return Ok(self.group_by_community(graph, &trivial));
247 }
248
249 Ok(self.group_by_community(graph, &community))
251 }
252
253 fn label_propagation(
255 &self,
256 graph: &UnGraph<String, ()>,
257 node_map: &HashMap<String, NodeIndex>,
258 ) -> Vec<HashSet<String>> {
259 if graph.node_count() == 0 {
260 return vec![];
261 }
262
263 let mut labels: HashMap<NodeIndex, usize> = HashMap::new();
265 for (i, &idx) in node_map.values().enumerate() {
266 labels.insert(idx, i);
267 }
268
269 for _ in 0..self.config.max_iterations {
271 let mut changed = false;
272
273 for &node in node_map.values() {
274 let mut label_counts: HashMap<usize, usize> = HashMap::new();
276 for neighbor in graph.neighbors(node) {
277 if let Some(&label) = labels.get(&neighbor) {
278 *label_counts.entry(label).or_insert(0) += 1;
279 }
280 }
281
282 if let Some((&best_label, _)) = label_counts.iter().max_by_key(|(_, &count)| count)
284 {
285 if labels.get(&node) != Some(&best_label) {
286 labels.insert(node, best_label);
287 changed = true;
288 }
289 }
290 }
291
292 if !changed {
293 break;
294 }
295 }
296
297 self.group_by_community(graph, &labels)
298 }
299
300 fn connected_components(
302 &self,
303 graph: &UnGraph<String, ()>,
304 _node_map: &HashMap<String, NodeIndex>,
305 ) -> Vec<HashSet<String>> {
306 let sccs = petgraph::algo::kosaraju_scc(graph);
307
308 sccs.into_iter()
309 .map(|component| {
310 component
311 .into_iter()
312 .filter_map(|idx| graph.node_weight(idx).cloned())
313 .collect()
314 })
315 .collect()
316 }
317
318 fn leiden(
320 &self,
321 graph: &UnGraph<String, ()>,
322 node_map: &HashMap<String, NodeIndex>,
323 ) -> GraphRAGResult<Vec<HashSet<String>>> {
324 let node_count = graph.node_count();
325 if node_count == 0 {
326 return Ok(vec![]);
327 }
328
329 let mut community: HashMap<NodeIndex, usize> = HashMap::new();
331 for (community_id, &idx) in node_map.values().enumerate() {
332 community.insert(idx, community_id);
333 }
334
335 let m = graph.edge_count() as f64;
336 if m == 0.0 {
337 return Ok(node_map
338 .keys()
339 .map(|k| {
340 let mut set = HashSet::new();
341 set.insert(k.clone());
342 set
343 })
344 .collect());
345 }
346
347 let degree: HashMap<NodeIndex, f64> = node_map
348 .values()
349 .map(|&idx| (idx, graph.neighbors(idx).count() as f64))
350 .collect();
351
352 let mut rng = seeded_rng(self.config.random_seed);
353 let mut best_modularity = self.calculate_modularity(graph, &community, m, °ree)?;
354
355 for iteration in 0..self.config.max_iterations {
357 let mut changed = false;
358
359 let mut node_order: Vec<NodeIndex> = node_map.values().copied().collect();
361 node_order.sort();
366 for i in (1..node_order.len()).rev() {
368 let j = (rng.random_range(0.0..1.0) * (i + 1) as f64) as usize;
369 node_order.swap(i, j);
370 }
371
372 for &node in &node_order {
373 let current_comm = match community.get(&node) {
374 Some(&c) => c,
375 None => continue,
376 };
377 let node_degree = degree.get(&node).copied().unwrap_or(0.0);
378
379 let mut best_comm = current_comm;
380 let mut best_gain = 0.0;
381
382 let mut neighbor_comms: Vec<usize> = graph
385 .neighbors(node)
386 .filter_map(|n| community.get(&n).copied())
387 .collect::<HashSet<usize>>()
388 .into_iter()
389 .collect();
390 neighbor_comms.sort_unstable();
391
392 for &neighbor_comm in &neighbor_comms {
393 if neighbor_comm == current_comm {
394 continue;
395 }
396
397 let edges_to_comm: f64 = graph
398 .neighbors(node)
399 .filter(|n| community.get(n) == Some(&neighbor_comm))
400 .count() as f64;
401
402 let comm_degree: f64 = community
403 .iter()
404 .filter(|(_, &c)| c == neighbor_comm)
405 .map(|(n, _)| degree.get(n).copied().unwrap_or(0.0))
406 .sum();
407
408 let gain = edges_to_comm / m
409 - self.config.resolution * node_degree * comm_degree / (2.0 * m * m);
410
411 if gain > best_gain {
412 best_gain = gain;
413 best_comm = neighbor_comm;
414 }
415 }
416
417 if best_comm != current_comm && best_gain > 0.0 {
418 community.insert(node, best_comm);
419 changed = true;
420 }
421 }
422
423 let mut unique_comms: Vec<usize> = community
430 .values()
431 .copied()
432 .collect::<HashSet<usize>>()
433 .into_iter()
434 .collect();
435 unique_comms.sort_unstable();
436 for &comm_id in &unique_comms {
437 let mut comm_nodes: Vec<NodeIndex> = community
438 .iter()
439 .filter(|(_, &c)| c == comm_id)
440 .map(|(&n, _)| n)
441 .collect();
442 comm_nodes.sort();
443
444 if comm_nodes.len() <= 1 {
445 continue;
446 }
447
448 self.refine_community(graph, &mut community, &comm_nodes, comm_id, m, °ree)?;
450 }
451
452 let current_modularity = self.calculate_modularity(graph, &community, m, °ree)?;
454 if current_modularity > best_modularity {
455 best_modularity = current_modularity;
456 } else if !changed {
457 break;
458 }
459
460 if best_modularity > 0.95 || iteration > 0 && !changed {
462 break;
463 }
464 }
465
466 if best_modularity < 0.75 {
468 tracing::warn!("Leiden modularity {:.3} below target 0.75", best_modularity);
469 } else {
470 tracing::info!("Leiden achieved modularity: {:.3}", best_modularity);
471 }
472
473 let trivial: HashMap<NodeIndex, usize> =
481 node_map.values().map(|&idx| (idx, 0usize)).collect();
482 let greedy_modularity = self.calculate_modularity(graph, &community, m, °ree)?;
483 let trivial_modularity = self.calculate_modularity(graph, &trivial, m, °ree)?;
484
485 if trivial_modularity >= greedy_modularity {
486 return Ok(self.group_by_community(graph, &trivial));
487 }
488
489 Ok(self.group_by_community(graph, &community))
490 }
491
492 fn refine_community(
494 &self,
495 graph: &UnGraph<String, ()>,
496 community: &mut HashMap<NodeIndex, usize>,
497 comm_nodes: &[NodeIndex],
498 comm_id: usize,
499 m: f64,
500 degree: &HashMap<NodeIndex, f64>,
501 ) -> GraphRAGResult<()> {
502 if comm_nodes.len() < 2 {
503 return Ok(());
504 }
505
506 let mut subcomm: HashMap<NodeIndex, usize> = HashMap::new();
508 for (i, &node) in comm_nodes.iter().enumerate() {
509 subcomm.insert(node, i);
510 }
511
512 let mut changed = false;
514 for &node in comm_nodes {
515 let current_sub = match subcomm.get(&node) {
516 Some(&c) => c,
517 None => continue,
518 };
519
520 let mut sub_edges: HashMap<usize, f64> = HashMap::new();
522 for neighbor in graph.neighbors(node) {
523 if let Some(&sub) = subcomm.get(&neighbor) {
524 *sub_edges.entry(sub).or_insert(0.0) += 1.0;
525 }
526 }
527
528 let mut sorted_sub_edges: Vec<(&usize, &f64)> = sub_edges.iter().collect();
533 sorted_sub_edges.sort_unstable_by_key(|(sub, _)| **sub);
534 if let Some((&best_sub, _)) = sorted_sub_edges
535 .into_iter()
536 .max_by(|(_, a), (_, b)| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
537 {
538 if best_sub != current_sub {
539 subcomm.insert(node, best_sub);
540 changed = true;
541 }
542 }
543 }
544
545 if changed {
547 let mut unique_subs: Vec<usize> = subcomm
551 .values()
552 .copied()
553 .collect::<HashSet<usize>>()
554 .into_iter()
555 .collect();
556 unique_subs.sort_unstable();
557 if unique_subs.len() > 1 {
558 let max_comm = community.values().max().copied().unwrap_or(0);
559 for (i, sub_id) in unique_subs.iter().enumerate() {
560 for &node in comm_nodes {
561 if subcomm.get(&node) == Some(sub_id) {
562 let new_comm = if i == 0 { comm_id } else { max_comm + i };
563 community.insert(node, new_comm);
564 }
565 }
566 }
567 }
568 }
569
570 Ok(())
571 }
572
573 fn calculate_modularity(
596 &self,
597 graph: &UnGraph<String, ()>,
598 community: &HashMap<NodeIndex, usize>,
599 m: f64,
600 degree: &HashMap<NodeIndex, f64>,
601 ) -> GraphRAGResult<f64> {
602 if m == 0.0 {
603 return Ok(0.0);
604 }
605
606 let mut intra_edges: HashMap<usize, f64> = HashMap::new();
608 for edge in graph.edge_indices() {
609 if let Some((a, b)) = graph.edge_endpoints(edge) {
610 if let (Some(&ca), Some(&cb)) = (community.get(&a), community.get(&b)) {
611 if ca == cb {
612 *intra_edges.entry(ca).or_insert(0.0) += 1.0;
613 }
614 }
615 }
616 }
617
618 let mut comm_degree_sum: HashMap<usize, f64> = HashMap::new();
620 for (&node, &comm) in community {
621 let deg = degree.get(&node).copied().unwrap_or(0.0);
622 *comm_degree_sum.entry(comm).or_insert(0.0) += deg;
623 }
624
625 let two_m = 2.0 * m;
626 let q: f64 = comm_degree_sum
627 .keys()
628 .map(|comm_id| {
629 let e_c = intra_edges.get(comm_id).copied().unwrap_or(0.0);
630 let d_c = comm_degree_sum.get(comm_id).copied().unwrap_or(0.0);
631 e_c / m - self.config.resolution * (d_c / two_m).powi(2)
632 })
633 .sum();
634
635 Ok(q)
636 }
637
638 fn detect_hierarchical(&self, triples: &[Triple]) -> GraphRAGResult<Vec<CommunitySummary>> {
640 let mut all_summaries = Vec::new();
641 let mut current_triples = triples.to_vec();
642 let mut level = 0;
643
644 while level < 5 && !current_triples.is_empty() {
645 let (graph, node_map) = self.build_graph(¤t_triples);
646
647 if graph.node_count() < 10 {
648 break;
649 }
650
651 let communities = self.leiden(&graph, &node_map)?;
653
654 let mut level_summaries = self.create_summaries(communities.clone(), ¤t_triples);
656
657 for summary in &mut level_summaries {
659 summary.level = level;
660 }
661
662 all_summaries.extend(level_summaries);
663
664 current_triples = self.coarsen_graph(&graph, &node_map, &communities)?;
666 level += 1;
667 }
668
669 Ok(all_summaries)
670 }
671
672 fn coarsen_graph(
674 &self,
675 graph: &UnGraph<String, ()>,
676 node_map: &HashMap<String, NodeIndex>,
677 communities: &[HashSet<String>],
678 ) -> GraphRAGResult<Vec<Triple>> {
679 let mut node_to_community: HashMap<String, usize> = HashMap::new();
680 for (comm_id, community) in communities.iter().enumerate() {
681 for node in community {
682 node_to_community.insert(node.clone(), comm_id);
683 }
684 }
685
686 let mut coarsened_triples = Vec::new();
687 let mut seen_edges: HashSet<(usize, usize)> = HashSet::new();
688
689 for edge in graph.edge_indices() {
690 if let Some((a, b)) = graph.edge_endpoints(edge) {
691 let label_a = graph.node_weight(a);
692 let label_b = graph.node_weight(b);
693
694 if let (Some(la), Some(lb)) = (label_a, label_b) {
695 if let (Some(&comm_a), Some(&comm_b)) =
696 (node_to_community.get(la), node_to_community.get(lb))
697 {
698 if comm_a != comm_b {
699 let edge_key = if comm_a < comm_b {
700 (comm_a, comm_b)
701 } else {
702 (comm_b, comm_a)
703 };
704
705 if !seen_edges.contains(&edge_key) {
706 seen_edges.insert(edge_key);
707 coarsened_triples.push(Triple::new(
708 format!("community_{}", comm_a),
709 "inter_community_link",
710 format!("community_{}", comm_b),
711 ));
712 }
713 }
714 }
715 }
716 }
717 }
718
719 Ok(coarsened_triples)
720 }
721
722 fn group_by_community(
724 &self,
725 graph: &UnGraph<String, ()>,
726 assignment: &HashMap<NodeIndex, usize>,
727 ) -> Vec<HashSet<String>> {
728 let mut communities: HashMap<usize, HashSet<String>> = HashMap::new();
729
730 for (&node, &comm) in assignment {
731 if let Some(label) = graph.node_weight(node) {
732 communities.entry(comm).or_default().insert(label.clone());
733 }
734 }
735
736 communities.into_values().collect()
737 }
738
739 fn create_summaries(
741 &self,
742 communities: Vec<HashSet<String>>,
743 triples: &[Triple],
744 ) -> Vec<CommunitySummary> {
745 let (graph, node_map) = self.build_graph(triples);
747 let m = graph.edge_count() as f64;
748
749 let mut community_map: HashMap<NodeIndex, usize> = HashMap::new();
751 for (idx, entities) in communities.iter().enumerate() {
752 for entity in entities {
753 if let Some(&node_idx) = node_map.get(entity) {
754 community_map.insert(node_idx, idx);
755 }
756 }
757 }
758
759 let degree: HashMap<NodeIndex, f64> = node_map
761 .values()
762 .map(|&idx| (idx, graph.neighbors(idx).count() as f64))
763 .collect();
764
765 let overall_modularity = self
771 .calculate_modularity(&graph, &community_map, m, °ree)
772 .unwrap_or(0.0);
773
774 communities
775 .into_iter()
776 .enumerate()
777 .filter(|(_, entities)| entities.len() >= self.config.min_community_size)
778 .take(self.config.max_communities)
779 .map(|(idx, entities)| {
780 let representative_triples: Vec<Triple> = triples
782 .iter()
783 .filter(|t| entities.contains(&t.subject) || entities.contains(&t.object))
784 .take(5)
785 .cloned()
786 .collect();
787
788 let entity_list: Vec<String> = entities.iter().cloned().collect();
790 let summary = self.generate_summary(&entity_list, &representative_triples);
791
792 CommunitySummary {
794 id: format!("community_{}", idx),
795 summary,
796 entities: entity_list,
797 representative_triples,
798 level: 0,
799 modularity: overall_modularity,
800 }
801 })
802 .collect()
803 }
804
805 fn generate_summary(&self, entities: &[String], triples: &[Triple]) -> String {
807 let short_names: Vec<String> = entities
809 .iter()
810 .take(3)
811 .map(|uri| {
812 uri.rsplit('/')
813 .next()
814 .or_else(|| uri.rsplit('#').next())
815 .unwrap_or(uri)
816 .to_string()
817 })
818 .collect();
819
820 let predicates: HashSet<String> = triples
822 .iter()
823 .map(|t| {
824 t.predicate
825 .rsplit('/')
826 .next()
827 .or_else(|| t.predicate.rsplit('#').next())
828 .unwrap_or(&t.predicate)
829 .to_string()
830 })
831 .collect();
832
833 let pred_str: Vec<String> = predicates.into_iter().take(3).collect();
834
835 format!(
836 "Community of {} entities including {} connected by {}",
837 entities.len(),
838 short_names.join(", "),
839 pred_str.join(", ")
840 )
841 }
842}
843
844#[cfg(test)]
845mod tests {
846 use super::*;
847
848 #[test]
849 fn test_community_detection() {
850 let detector = CommunityDetector::new(CommunityConfig {
852 min_community_size: 1,
853 ..Default::default()
854 });
855
856 let triples = vec![
857 Triple::new("http://a", "http://rel", "http://b"),
858 Triple::new("http://b", "http://rel", "http://c"),
859 Triple::new("http://a", "http://rel", "http://c"),
860 Triple::new("http://x", "http://rel", "http://y"),
861 Triple::new("http://y", "http://rel", "http://z"),
862 Triple::new("http://x", "http://rel", "http://z"),
863 ];
864
865 let communities = detector.detect(&triples).expect("should succeed");
866
867 assert!(!communities.is_empty());
869 }
870
871 #[test]
872 fn test_empty_graph() {
873 let detector = CommunityDetector::default();
874 let communities = detector.detect(&[]).expect("should succeed");
875 assert!(communities.is_empty());
876 }
877}