Plan a graph-aware shard split. Uses BFS-based community detection to partition nodes into two groups that minimize cross-shard edges. Starting from a seed node, BFS assigns the first half of discovered nodes to group A, the rest to group B. This is a greedy heuristic — not optimal like METIS, but O(V+E) and practical for online splitting without blocking queries.
(
source_vshard: u32,
new_vshard: u32,
target_node: u64,
node_ids: &[String],
edges: &[(String, String)],
)
| 110 | /// This is a greedy heuristic — not optimal like METIS, but O(V+E) and |
| 111 | /// practical for online splitting without blocking queries. |
| 112 | pub fn plan_graph_split( |
| 113 | source_vshard: u32, |
| 114 | new_vshard: u32, |
| 115 | target_node: u64, |
| 116 | node_ids: &[String], |
| 117 | edges: &[(String, String)], |
| 118 | ) -> SplitPlan { |
| 119 | if node_ids.is_empty() { |
| 120 | return SplitPlan { |
| 121 | source_vshard, |
| 122 | new_vshard, |
| 123 | target_node, |
| 124 | documents_to_move: Vec::new(), |
| 125 | strategy: SplitStrategy::GraphCommunity, |
| 126 | estimated_bytes: 0, |
| 127 | }; |
| 128 | } |
| 129 | |
| 130 | // Build adjacency list for BFS. |
| 131 | let mut adj: HashMap<&str, Vec<&str>> = HashMap::new(); |
| 132 | for (src, dst) in edges { |
| 133 | adj.entry(src.as_str()).or_default().push(dst.as_str()); |
| 134 | adj.entry(dst.as_str()).or_default().push(src.as_str()); |
| 135 | } |
| 136 | |
| 137 | // BFS from first node, assign first half to group A. |
| 138 | let mut visited: Vec<String> = Vec::with_capacity(node_ids.len()); |
| 139 | let mut seen: HashSet<&str> = HashSet::new(); |
| 140 | let mut queue: std::collections::VecDeque<&str> = std::collections::VecDeque::new(); |
| 141 | |
| 142 | // Start BFS from the node with the most edges (hub). |
| 143 | let start = node_ids |
| 144 | .iter() |
| 145 | .max_by_key(|id| adj.get(id.as_str()).map(|v| v.len()).unwrap_or(0)) |
| 146 | .map(|s| s.as_str()) |
| 147 | .unwrap_or(node_ids[0].as_str()); |
| 148 | |
| 149 | queue.push_back(start); |
| 150 | seen.insert(start); |
| 151 | |
| 152 | while let Some(node) = queue.pop_front() { |
| 153 | visited.push(node.to_string()); |
| 154 | if let Some(neighbors) = adj.get(node) { |
| 155 | for &neighbor in neighbors { |
| 156 | if seen.insert(neighbor) { |
| 157 | queue.push_back(neighbor); |
| 158 | } |
| 159 | } |
| 160 | } |
| 161 | } |
| 162 | |
| 163 | // Add any disconnected nodes not reached by BFS. |
| 164 | for id in node_ids { |
| 165 | if seen.insert(id.as_str()) { |
| 166 | visited.push(id.clone()); |
| 167 | } |
| 168 | } |
| 169 |