velesdb_core/collection/graph/
streaming.rs1use super::edge_concurrent::ConcurrentEdgeStore;
7use super::traversal::{deadline_reached, reconstruct_path, BfsState, DEADLINE_CHECK_INTERVAL};
8use super::{EdgeStore, TraversalResult, DEFAULT_MAX_DEPTH};
9use rustc_hash::{FxHashMap, FxHashSet};
10use std::collections::VecDeque;
11use std::time::Instant;
12
13pub const MAX_VISITED_SIZE: usize = 100_000;
20
21#[derive(Debug, Clone)]
26pub struct StreamingConfig {
27 pub max_depth: u32,
29 pub limit: Option<usize>,
31 pub max_visited_size: usize,
35 pub rel_types: Vec<String>,
37 pub deadline: Option<Instant>,
41}
42
43impl Default for StreamingConfig {
44 fn default() -> Self {
45 Self {
46 max_depth: DEFAULT_MAX_DEPTH,
47 limit: None,
48 max_visited_size: MAX_VISITED_SIZE, rel_types: Vec::new(),
50 deadline: None,
51 }
52 }
53}
54
55impl StreamingConfig {
56 #[must_use]
58 pub fn with_limit(mut self, limit: usize) -> Self {
59 self.limit = Some(limit);
60 self
61 }
62
63 #[must_use]
65 pub fn with_max_depth(mut self, max_depth: u32) -> Self {
66 self.max_depth = max_depth;
67 self
68 }
69
70 #[must_use]
72 pub fn with_max_visited(mut self, max_visited: usize) -> Self {
73 self.max_visited_size = max_visited;
74 self
75 }
76
77 #[must_use]
79 pub fn with_rel_types(mut self, types: Vec<String>) -> Self {
80 self.rel_types = types;
81 self
82 }
83
84 #[must_use]
87 pub fn with_deadline(mut self, deadline: Instant) -> Self {
88 self.deadline = Some(deadline);
89 self
90 }
91}
92
93struct BfsBookkeeping {
101 config: StreamingConfig,
102 queue: VecDeque<BfsState>,
103 visited: FxHashSet<u64>,
104 rel_types_set: FxHashSet<String>,
105 visited_overflow: bool,
106 pending_results: VecDeque<TraversalResult>,
107 parent_map: FxHashMap<u64, (u64, u64)>,
108 source_id: u64,
109 yielded: usize,
110 nodes_since_check: u32,
112}
113
114impl BfsBookkeeping {
115 fn new(start_id: u64, config: StreamingConfig) -> Self {
117 let rel_types_set: FxHashSet<String> = config.rel_types.iter().cloned().collect();
118 let mut visited = FxHashSet::default();
119 visited.insert(start_id);
120 let mut queue = VecDeque::new();
121 queue.push_back(BfsState {
122 node_id: start_id,
123 depth: 0,
124 });
125 Self {
126 config,
127 queue,
128 visited,
129 rel_types_set,
130 visited_overflow: false,
131 pending_results: VecDeque::new(),
132 parent_map: FxHashMap::default(),
133 source_id: start_id,
134 yielded: 0,
135 nodes_since_check: DEADLINE_CHECK_INTERVAL,
138 }
139 }
140
141 #[inline]
143 fn label_passes_filter(&self, label: &str) -> bool {
144 self.rel_types_set.is_empty() || self.rel_types_set.contains(label)
145 }
146
147 #[inline]
150 fn try_visit(&mut self, target: u64) -> bool {
151 if self.visited_overflow {
152 return true;
153 }
154 if self.visited.contains(&target) {
155 return false;
156 }
157 if self.visited.len() >= self.config.max_visited_size {
158 self.visited_overflow = true;
159 self.visited.clear();
160 return true;
161 }
162 self.visited.insert(target);
163 true
164 }
165
166 fn process_candidate(
170 &mut self,
171 parent_id: u64,
172 target: u64,
173 edge_id: u64,
174 parent_depth: u32,
175 label: Option<&str>,
176 ) {
177 if let Some(label) = label {
178 if !self.label_passes_filter(label) {
179 return;
180 }
181 }
182 let new_depth = parent_depth + 1;
183 if new_depth > self.config.max_depth {
184 return;
185 }
186 if !self.try_visit(target) {
187 return;
188 }
189 self.parent_map.insert(target, (parent_id, edge_id));
190 if new_depth < self.config.max_depth {
191 self.queue.push_back(BfsState {
192 node_id: target,
193 depth: new_depth,
194 });
195 }
196 let path = reconstruct_path(target, self.source_id, &self.parent_map);
197 self.pending_results
198 .push_back(TraversalResult::new(target, path, new_depth));
199 }
200
201 #[inline]
203 fn next_pending(&mut self) -> Option<TraversalResult> {
204 let result = self.pending_results.pop_front()?;
205 self.yielded += 1;
206 Some(result)
207 }
208
209 fn drive(&mut self, mut expand: impl FnMut(&mut Self, &BfsState)) -> Option<TraversalResult> {
213 if self.config.limit.is_some_and(|limit| self.yielded >= limit) {
214 return None;
215 }
216 if let Some(result) = self.next_pending() {
217 return Some(result);
218 }
219 let deadline = self.config.deadline;
220 while let Some(state) = self.queue.pop_front() {
221 if deadline_reached(deadline, &mut self.nodes_since_check) {
222 self.queue.clear();
224 return None;
225 }
226 expand(self, &state);
227 if let Some(result) = self.next_pending() {
228 return Some(result);
229 }
230 }
231 None
232 }
233}
234
235fn expand_csr(edge_store: &EdgeStore, core: &mut BfsBookkeeping, state: &BfsState) {
237 let Some(snapshot) = edge_store.csr_snapshot() else {
238 return;
239 };
240 let targets = snapshot.neighbors(state.node_id);
241 let edge_ids = snapshot.edge_ids(state.node_id);
242 for (i, (&target, &eid)) in targets.iter().zip(edge_ids.iter()).enumerate() {
243 let label = snapshot.label_at(state.node_id, i);
244 core.process_candidate(state.node_id, target, eid, state.depth, label);
245 }
246}
247
248fn expand_legacy(edge_store: &EdgeStore, core: &mut BfsBookkeeping, state: &BfsState) {
250 for edge in edge_store.get_outgoing(state.node_id) {
251 core.process_candidate(
252 state.node_id,
253 edge.target(),
254 edge.id(),
255 state.depth,
256 Some(edge.label()),
257 );
258 }
259}
260
261fn expand_concurrent(
263 edge_store: &ConcurrentEdgeStore,
264 core: &mut BfsBookkeeping,
265 state: &BfsState,
266) {
267 for edge in &edge_store.get_outgoing(state.node_id) {
268 core.process_candidate(
269 state.node_id,
270 edge.target(),
271 edge.id(),
272 state.depth,
273 Some(edge.label()),
274 );
275 }
276}
277
278pub struct BfsIterator<'a> {
309 edge_store: &'a EdgeStore,
310 core: BfsBookkeeping,
311}
312
313impl<'a> BfsIterator<'a> {
314 #[must_use]
316 pub fn new(edge_store: &'a EdgeStore, start_id: u64, config: StreamingConfig) -> Self {
317 Self {
318 edge_store,
319 core: BfsBookkeeping::new(start_id, config),
320 }
321 }
322
323 #[must_use]
325 pub fn yielded_count(&self) -> usize {
326 self.core.yielded
327 }
328
329 #[must_use]
334 pub fn is_visited_overflow(&self) -> bool {
335 self.core.visited_overflow
336 }
337
338 #[must_use]
340 pub fn visited_size(&self) -> usize {
341 self.core.visited.len()
342 }
343}
344
345impl Iterator for BfsIterator<'_> {
346 type Item = TraversalResult;
347
348 fn next(&mut self) -> Option<Self::Item> {
349 let edge_store = self.edge_store;
350 self.core.drive(|core, state| {
352 if edge_store.has_csr_snapshot() {
353 expand_csr(edge_store, core, state);
354 } else {
355 expand_legacy(edge_store, core, state);
356 }
357 })
358 }
359}
360
361#[must_use]
363pub fn bfs_stream(
364 edge_store: &EdgeStore,
365 start_id: u64,
366 config: StreamingConfig,
367) -> BfsIterator<'_> {
368 BfsIterator::new(edge_store, start_id, config)
369}
370
371pub struct ConcurrentBfsIterator<'a> {
383 edge_store: &'a ConcurrentEdgeStore,
384 core: BfsBookkeeping,
385}
386
387impl<'a> ConcurrentBfsIterator<'a> {
388 #[must_use]
390 pub fn new(
391 edge_store: &'a ConcurrentEdgeStore,
392 start_id: u64,
393 config: StreamingConfig,
394 ) -> Self {
395 Self {
396 edge_store,
397 core: BfsBookkeeping::new(start_id, config),
398 }
399 }
400}
401
402impl Iterator for ConcurrentBfsIterator<'_> {
403 type Item = TraversalResult;
404
405 fn next(&mut self) -> Option<Self::Item> {
406 let edge_store = self.edge_store;
407 self.core
408 .drive(|core, state| expand_concurrent(edge_store, core, state))
409 }
410}
411
412#[must_use]
415pub fn concurrent_bfs_stream(
416 edge_store: &ConcurrentEdgeStore,
417 start_id: u64,
418 config: StreamingConfig,
419) -> ConcurrentBfsIterator<'_> {
420 ConcurrentBfsIterator::new(edge_store, start_id, config)
421}
422
423