1use std::collections::HashSet;
6
7#[cfg(test)]
8use super::DEFAULT_MAX_VISITED;
9use crate::bfs_params::BfsParams;
10use crate::csr::index::LabelFilter;
11use crate::csr::{CsrIndex, Direction};
12use crate::overlay_delta::GraphOverlayDelta;
13use crate::traversal_overlay::traverse_bfs_overlay;
14
15impl CsrIndex {
16 pub fn traverse_bfs(
30 &self,
31 params: BfsParams<'_>,
32 overlay: Option<&GraphOverlayDelta>,
33 ) -> Vec<String> {
34 Self::traverse_bfs_on(Some(self), params, overlay)
35 }
36
37 pub fn traverse_bfs_on(
41 partition: Option<&Self>,
42 params: BfsParams<'_>,
43 overlay: Option<&GraphOverlayDelta>,
44 ) -> Vec<String> {
45 match (partition, overlay) {
46 (_, Some(ov)) if !ov.is_empty() => traverse_bfs_overlay(partition, params, ov),
47 (Some(csr), _) => csr.traverse_bfs_dense(params),
48 (None, _) => Vec::new(),
49 }
50 }
51
52 fn traverse_bfs_dense(&self, params: BfsParams<'_>) -> Vec<String> {
60 let BfsParams {
61 start_nodes,
62 label_filter,
63 direction,
64 max_depth,
65 max_visited,
66 frontier_bitmap,
67 } = params;
68 let labels = self.label_filter(label_filter);
69 let in_bitmap = |id: u32| {
70 frontier_bitmap.is_none_or(|bm| {
71 bm.contains(nodedb_types::Surrogate::new(self.node_surrogate_raw(id)))
72 })
73 };
74 let mut visited: HashSet<u32> = HashSet::new();
75 let mut frontier: Vec<u32> = Vec::new();
76 for &node in start_nodes {
77 if let Some(&id) = self.node_to_id.get(node)
78 && visited.insert(id)
79 {
80 frontier.push(id);
81 }
82 }
83
84 for _depth in 0..max_depth {
85 if frontier.is_empty() || visited.len() >= max_visited {
86 break;
87 }
88 let candidates =
89 self.level_candidates(&frontier, &labels, direction, &visited, in_bitmap);
90 frontier = self.admit_by_name(candidates, &mut visited, max_visited);
91 }
92
93 visited
94 .into_iter()
95 .map(|id| self.id_to_node[id as usize].clone())
96 .collect()
97 }
98
99 fn level_candidates(
102 &self,
103 frontier: &[u32],
104 labels: &LabelFilter,
105 direction: Direction,
106 visited: &HashSet<u32>,
107 eligible: impl Fn(u32) -> bool,
108 ) -> Vec<u32> {
109 let mut candidates: Vec<u32> = Vec::new();
110 for &node_id in frontier {
111 self.record_access(node_id);
113 if matches!(direction, Direction::Out | Direction::Both) {
114 for (lid, dst) in self.dense_iter_out(node_id) {
115 if labels.keeps(lid) && !visited.contains(&dst) && eligible(dst) {
116 candidates.push(dst);
117 }
118 }
119 }
120 if matches!(direction, Direction::In | Direction::Both) {
121 for (lid, src) in self.dense_iter_in(node_id) {
122 if labels.keeps(lid) && !visited.contains(&src) && eligible(src) {
123 candidates.push(src);
124 }
125 }
126 }
127 }
128 candidates
129 }
130
131 pub(crate) fn admit_by_name(
134 &self,
135 mut candidates: Vec<u32>,
136 visited: &mut HashSet<u32>,
137 max_visited: usize,
138 ) -> Vec<u32> {
139 self.sort_by_name(&mut candidates);
140 candidates.dedup();
141 let mut admitted = Vec::with_capacity(candidates.len());
142 for id in candidates {
143 if visited.len() >= max_visited {
144 break;
145 }
146 if visited.insert(id) {
147 self.prefetch_node(id);
148 admitted.push(id);
149 }
150 }
151 admitted
152 }
153
154 pub fn traverse_bfs_with_depth(
165 &self,
166 start_nodes: &[&str],
167 label_filter: &[&str],
168 direction: Direction,
169 max_depth: usize,
170 max_visited: usize,
171 ) -> Vec<(String, u8)> {
172 let labels = self.label_filter(label_filter);
173 let mut visited: HashSet<u32> = HashSet::new();
174 let mut depths: Vec<(u32, u8)> = Vec::new();
175 let mut frontier: Vec<u32> = Vec::new();
176 for &node in start_nodes {
177 if let Some(&id) = self.node_to_id.get(node)
178 && visited.insert(id)
179 {
180 frontier.push(id);
181 depths.push((id, 0));
182 }
183 }
184
185 for depth in 0..max_depth {
186 if frontier.is_empty() || visited.len() >= max_visited {
187 break;
188 }
189 let tag = u8::try_from(depth.saturating_add(1)).unwrap_or(u8::MAX);
190 let candidates =
191 self.level_candidates(&frontier, &labels, direction, &visited, |_| true);
192 frontier = self.admit_by_name(candidates, &mut visited, max_visited);
193 depths.extend(frontier.iter().map(|&id| (id, tag)));
194 }
195
196 depths
197 .into_iter()
198 .map(|(id, depth)| (self.id_to_node[id as usize].clone(), depth))
199 .collect()
200 }
201}
202
203#[cfg(test)]
204mod tests {
205 use std::collections::HashMap;
206
207 use super::*;
208 use crate::test_support::{chain_csr, long_chain_csr, test_memory};
209
210 fn bfs(csr: &CsrIndex, labels: &[&str], max_depth: usize, max_visited: usize) -> Vec<String> {
211 let mut result = csr.traverse_bfs(
212 BfsParams {
213 start_nodes: &["a"],
214 label_filter: labels,
215 direction: Direction::Out,
216 max_depth,
217 max_visited,
218 frontier_bitmap: None,
219 },
220 None,
221 );
222 result.sort();
223 result
224 }
225
226 #[test]
227 fn a_label_set_reaches_both_labels() {
228 let csr = chain_csr();
229 assert_eq!(
230 bfs(&csr, &["KNOWS", "WORKS"], 1, DEFAULT_MAX_VISITED),
231 vec!["a", "b", "e"]
232 );
233 }
234
235 #[test]
236 fn an_unknown_label_in_a_set_adds_nothing() {
237 let csr = chain_csr();
238 assert_eq!(
239 bfs(&csr, &["KNOWS", "NOPE"], 3, DEFAULT_MAX_VISITED),
240 bfs(&csr, &["KNOWS"], 3, DEFAULT_MAX_VISITED)
241 );
242 }
243
244 #[test]
245 fn a_set_of_unknown_labels_returns_the_start_only() {
246 let csr = chain_csr();
247 assert_eq!(bfs(&csr, &["NOPE"], 3, DEFAULT_MAX_VISITED), vec!["a"]);
248 }
249
250 #[test]
252 fn a_capped_walk_over_a_set_ignores_insertion_order() {
253 let edges = [
254 ("a", "K", "z"),
255 ("a", "W", "m"),
256 ("a", "K", "b"),
257 ("a", "W", "c"),
258 ("a", "X", "a0"),
259 ];
260 let mut forward = CsrIndex::new(test_memory());
261 for (src, label, dst) in edges {
262 forward.add_edge(src, label, dst).unwrap();
263 }
264 let mut reverse = CsrIndex::new(test_memory());
265 for (src, label, dst) in edges.iter().rev() {
266 reverse.add_edge(src, label, dst).unwrap();
267 }
268 let forward_walk = bfs(&forward, &["K", "W"], 2, 3);
269 assert_eq!(forward_walk, vec!["a", "b", "c"]);
270 assert_eq!(bfs(&reverse, &["K", "W"], 2, 3), forward_walk);
271 }
272
273 #[test]
276 fn a_capped_depth_walk_admits_in_name_order() {
277 let mut csr = CsrIndex::new(test_memory());
278 csr.add_edge("a", "L", "z").unwrap();
279 csr.add_edge("a", "L", "m").unwrap();
280 csr.add_edge("a", "L", "b").unwrap();
281 let map: HashMap<String, u8> = csr
282 .traverse_bfs_with_depth(&["a"], &[], Direction::Out, 2, 3)
283 .into_iter()
284 .collect();
285 assert_eq!(map.len(), 3);
286 assert_eq!(map["a"], 0);
287 assert_eq!(map["b"], 1);
288 assert_eq!(map["m"], 1);
289 }
290
291 #[test]
293 fn the_depth_tag_saturates_past_255() {
294 let mut csr = CsrIndex::new(test_memory());
295 for i in 0..270 {
296 csr.add_edge(&format!("n{i}"), "NEXT", &format!("n{}", i + 1))
297 .unwrap();
298 }
299 let map: HashMap<String, u8> = csr
300 .traverse_bfs_with_depth(&["n0"], &["NEXT"], Direction::Out, 300, DEFAULT_MAX_VISITED)
301 .into_iter()
302 .collect();
303 assert_eq!(map.len(), 271);
304 assert_eq!(map["n0"], 0);
305 assert_eq!(map["n200"], 200);
306 assert_eq!(map["n255"], 255);
307 assert_eq!(map["n256"], u8::MAX);
308 assert_eq!(map["n270"], u8::MAX);
309 }
310
311 #[test]
312 fn bfs_traversal() {
313 let csr = chain_csr();
314 let mut result = csr.traverse_bfs(
315 BfsParams {
316 start_nodes: &["a"],
317 label_filter: &["KNOWS"],
318 direction: Direction::Out,
319 max_depth: 2,
320 max_visited: DEFAULT_MAX_VISITED,
321 frontier_bitmap: None,
322 },
323 None,
324 );
325 result.sort();
326 assert_eq!(result, vec!["a", "b", "c"]);
327 }
328
329 #[test]
330 fn bfs_all_labels() {
331 let csr = chain_csr();
332 let mut result = csr.traverse_bfs(
333 BfsParams {
334 start_nodes: &["a"],
335 label_filter: &[],
336 direction: Direction::Out,
337 max_depth: 1,
338 max_visited: DEFAULT_MAX_VISITED,
339 frontier_bitmap: None,
340 },
341 None,
342 );
343 result.sort();
344 assert_eq!(result, vec!["a", "b", "e"]);
345 }
346
347 #[test]
350 fn a_capped_bfs_admits_each_level_in_name_order() {
351 let mut csr = CsrIndex::new(test_memory());
352 csr.add_edge("a", "L", "z").unwrap();
353 csr.add_edge("a", "L", "m").unwrap();
354 csr.add_edge("a", "L", "b").unwrap();
355 let mut result = csr.traverse_bfs(
356 BfsParams {
357 start_nodes: &["a"],
358 label_filter: &[],
359 direction: Direction::Out,
360 max_depth: 2,
361 max_visited: 3,
362 frontier_bitmap: None,
363 },
364 None,
365 );
366 result.sort();
367 assert_eq!(result, vec!["a", "b", "m"]);
368 }
369
370 #[test]
371 fn bfs_cycle() {
372 let mut csr = CsrIndex::new(test_memory());
373 csr.add_edge("a", "L", "b").unwrap();
374 csr.add_edge("b", "L", "c").unwrap();
375 csr.add_edge("c", "L", "a").unwrap();
376 let mut result = csr.traverse_bfs(
377 BfsParams {
378 start_nodes: &["a"],
379 label_filter: &[],
380 direction: Direction::Out,
381 max_depth: 10,
382 max_visited: DEFAULT_MAX_VISITED,
383 frontier_bitmap: None,
384 },
385 None,
386 );
387 result.sort();
388 assert_eq!(result, vec!["a", "b", "c"]);
389 }
390
391 #[test]
392 fn bfs_with_depth() {
393 let csr = chain_csr();
394 let result =
395 csr.traverse_bfs_with_depth(&["a"], &["KNOWS"], Direction::Out, 3, DEFAULT_MAX_VISITED);
396 let map: HashMap<String, u8> = result.into_iter().collect();
397 assert_eq!(map["a"], 0);
398 assert_eq!(map["b"], 1);
399 assert_eq!(map["c"], 2);
400 assert_eq!(map["d"], 3);
401 }
402
403 #[test]
404 fn large_graph_bfs() {
405 let csr = long_chain_csr();
406 let result = csr.traverse_bfs(
407 BfsParams {
408 start_nodes: &["n0"],
409 label_filter: &["NEXT"],
410 direction: Direction::Out,
411 max_depth: 100,
412 max_visited: DEFAULT_MAX_VISITED,
413 frontier_bitmap: None,
414 },
415 None,
416 );
417 assert_eq!(result.len(), 101);
418 }
419
420 #[test]
423 fn bfs_frontier_bitmap_excludes_non_members() {
424 use nodedb_types::{Surrogate, SurrogateBitmap};
425
426 let mut csr = chain_csr();
427 csr.set_node_surrogate("b", Surrogate::new(10));
429 csr.set_node_surrogate("c", Surrogate::new(20));
430 csr.set_node_surrogate("d", Surrogate::new(30));
431
432 let mut bm = SurrogateBitmap::new();
434 bm.insert(Surrogate::new(10));
435
436 let mut result = csr.traverse_bfs(
437 BfsParams {
438 start_nodes: &["a"],
439 label_filter: &["KNOWS"],
440 direction: Direction::Out,
441 max_depth: 10,
442 max_visited: DEFAULT_MAX_VISITED,
443 frontier_bitmap: Some(&bm),
444 },
445 None,
446 );
447 result.sort();
448 assert_eq!(result, vec!["a", "b"]);
451 }
452}