1use super::{
10 graph_id_value, synthetic_doc_id, BTreeMap, BTreeSet, DocId, Edge, EdgeId, EdgePattern,
11 GraphPattern, GraphPayload, GraphPostingList, GraphStore, GraphStoreError, GraphStoreResult,
12 Payload, PostingEntry, PostingList, VertexId, DEFAULT_GRAPH_SCORE,
13};
14
15struct BacktrackState {
19 unassigned: BTreeSet<String>,
20 assignment: BTreeMap<String, VertexId>,
21 assigned_values: BTreeSet<VertexId>,
22 matches: Vec<BTreeMap<String, VertexId>>,
23}
24
25pub struct GMatch<'a> {
32 pub pattern: GraphPattern,
33 pub graph: &'a str,
34 pub score: f64,
35}
36
37impl<'a> GMatch<'a> {
38 pub fn new(pattern: GraphPattern, graph: &'a str) -> Self {
39 Self {
40 pattern,
41 graph,
42 score: DEFAULT_GRAPH_SCORE,
43 }
44 }
45
46 pub fn execute<G: GraphStore>(&self, store: &G) -> GraphStoreResult<GraphPostingList> {
47 if let Some(result) = self.try_execute_single_edge(store)? {
48 return Ok(result);
49 }
50 if let Some(result) = self.try_execute_two_edge_path(store)? {
51 return Ok(result);
52 }
53
54 let candidates = self.compute_candidates(store)?;
55
56 let (positive_edges, negated_edges): (Vec<&EdgePattern>, Vec<&EdgePattern>) =
57 self.pattern.edge_patterns.iter().partition(|e| !e.negated);
58
59 let mut var_edges: BTreeMap<String, Vec<&EdgePattern>> = BTreeMap::new();
60 for ep in &positive_edges {
61 var_edges.entry(ep.source_var.clone()).or_default().push(ep);
62 var_edges.entry(ep.target_var.clone()).or_default().push(ep);
63 }
64
65 let variables: Vec<String> = self
66 .pattern
67 .vertex_patterns
68 .iter()
69 .map(|vp| vp.variable.clone())
70 .collect();
71 let mut state = BacktrackState {
72 unassigned: variables.iter().cloned().collect(),
73 assignment: BTreeMap::new(),
74 assigned_values: BTreeSet::new(),
75 matches: Vec::new(),
76 };
77 self.backtrack(store, &candidates, &var_edges, &mut state)?;
78 let mut matches = state.matches;
79
80 if !negated_edges.is_empty() {
81 let mut retained = Vec::with_capacity(matches.len());
82 for assignment in matches {
83 if Self::check_negated(store, self.graph, &negated_edges, &assignment)? {
84 retained.push(assignment);
85 }
86 }
87 matches = retained;
88 }
89
90 let mut entries: Vec<PostingEntry> = Vec::with_capacity(matches.len());
91 let mut graph_payloads: BTreeMap<DocId, GraphPayload> = BTreeMap::new();
92 for (i, m) in matches.iter().enumerate() {
93 let doc_id = synthetic_doc_id(i, "GMatch")?;
94 let mut fields = BTreeMap::new();
95 for (var, vid) in m {
96 fields.insert(var.clone(), graph_id_value(*vid, "GMatch assignment")?);
97 }
98 entries.push(PostingEntry::new(
99 doc_id,
100 Payload {
101 positions: Vec::new(),
102 score: self.score,
103 fields,
104 },
105 ));
106 let mut subgraph_vertices: Vec<VertexId> = m.values().copied().collect();
107 subgraph_vertices.sort_unstable();
108 subgraph_vertices.dedup();
109 let subgraph_edges = Self::collect_match_edges(store, self.graph, &positive_edges, m)?;
110 graph_payloads.insert(
111 doc_id,
112 GraphPayload {
113 subgraph_vertices,
114 subgraph_edges,
115 graph_name: self.graph.to_string(),
116 score_override: Some(self.score),
117 },
118 );
119 }
120 GraphPostingList::try_from_parts(
121 PostingList::from_sorted_unchecked(entries),
122 graph_payloads,
123 )
124 .map_err(Into::into)
125 }
126
127 fn try_execute_single_edge<G: GraphStore>(
128 &self,
129 store: &G,
130 ) -> GraphStoreResult<Option<GraphPostingList>> {
131 if self.pattern.vertex_patterns.len() != 2 || self.pattern.edge_patterns.len() != 1 {
132 return Ok(None);
133 }
134 let edge_pattern = &self.pattern.edge_patterns[0];
135 if edge_pattern.negated || edge_pattern.source_var == edge_pattern.target_var {
136 return Ok(None);
137 }
138 let Some(source_pattern) = self.vertex_pattern(&edge_pattern.source_var) else {
139 return Ok(None);
140 };
141 let Some(target_pattern) = self.vertex_pattern(&edge_pattern.target_var) else {
142 return Ok(None);
143 };
144 let edge_list = self.edge_list_for_pattern(store, edge_pattern)?;
145
146 let mut seen_assignments: BTreeSet<(VertexId, VertexId)> = BTreeSet::new();
147 let mut entries: Vec<PostingEntry> = Vec::new();
148 let mut graph_payloads: BTreeMap<DocId, GraphPayload> = BTreeMap::new();
149 for edge in edge_list {
150 if !edge_pattern.satisfies(&edge) {
151 continue;
152 }
153 let source = store.get_vertex(edge.source_id).ok_or_else(|| {
154 GraphStoreError::CorruptGraph(format!(
155 "edge {} references missing source vertex {}",
156 edge.edge_id, edge.source_id
157 ))
158 })?;
159 if !source_pattern.satisfies(source) {
160 continue;
161 }
162 let target = store.get_vertex(edge.target_id).ok_or_else(|| {
163 GraphStoreError::CorruptGraph(format!(
164 "edge {} references missing target vertex {}",
165 edge.edge_id, edge.target_id
166 ))
167 })?;
168 if !target_pattern.satisfies(target) {
169 continue;
170 }
171 if !seen_assignments.insert((edge.source_id, edge.target_id)) {
172 continue;
173 }
174
175 let doc_id = synthetic_doc_id(entries.len(), "single-edge GMatch")?;
176 let mut fields = BTreeMap::new();
177 fields.insert(
178 edge_pattern.source_var.clone(),
179 graph_id_value(edge.source_id, "GMatch source")?,
180 );
181 fields.insert(
182 edge_pattern.target_var.clone(),
183 graph_id_value(edge.target_id, "GMatch target")?,
184 );
185 entries.push(PostingEntry::new(
186 doc_id,
187 Payload {
188 positions: Vec::new(),
189 score: self.score,
190 fields,
191 },
192 ));
193 let mut subgraph_vertices = vec![edge.source_id, edge.target_id];
194 subgraph_vertices.sort_unstable();
195 subgraph_vertices.dedup();
196 graph_payloads.insert(
197 doc_id,
198 GraphPayload {
199 subgraph_vertices,
200 subgraph_edges: vec![edge.edge_id],
201 graph_name: self.graph.to_string(),
202 score_override: Some(self.score),
203 },
204 );
205 }
206
207 Ok(Some(GraphPostingList::try_from_parts(
208 PostingList::from_sorted_unchecked(entries),
209 graph_payloads,
210 )?))
211 }
212
213 fn try_execute_two_edge_path<G: GraphStore>(
214 &self,
215 store: &G,
216 ) -> GraphStoreResult<Option<GraphPostingList>> {
217 if self.pattern.vertex_patterns.len() != 3 || self.pattern.edge_patterns.len() != 2 {
218 return Ok(None);
219 }
220 let edge_patterns = &self.pattern.edge_patterns;
221 if edge_patterns.iter().any(|edge| edge.negated) {
222 return Ok(None);
223 }
224 let (first, second) = if edge_patterns[0].target_var == edge_patterns[1].source_var {
225 (&edge_patterns[0], &edge_patterns[1])
226 } else if edge_patterns[1].target_var == edge_patterns[0].source_var {
227 (&edge_patterns[1], &edge_patterns[0])
228 } else {
229 return Ok(None);
230 };
231 if first.source_var == first.target_var
232 || second.source_var == second.target_var
233 || first.source_var == second.target_var
234 {
235 return Ok(None);
236 }
237 let Some(source_pattern) = self.vertex_pattern(&first.source_var) else {
238 return Ok(None);
239 };
240 let Some(middle_pattern) = self.vertex_pattern(&first.target_var) else {
241 return Ok(None);
242 };
243 let Some(target_pattern) = self.vertex_pattern(&second.target_var) else {
244 return Ok(None);
245 };
246
247 let first_edges = self.edge_list_for_pattern(store, first)?;
248 let mut seen_assignments: BTreeSet<(VertexId, VertexId, VertexId)> = BTreeSet::new();
249 let mut entries: Vec<PostingEntry> = Vec::new();
250 let mut graph_payloads: BTreeMap<DocId, GraphPayload> = BTreeMap::new();
251
252 for first_edge in first_edges {
253 if !first.satisfies(&first_edge) {
254 continue;
255 }
256 let source = store.get_vertex(first_edge.source_id).ok_or_else(|| {
257 GraphStoreError::CorruptGraph(format!(
258 "edge {} references missing source vertex {}",
259 first_edge.edge_id, first_edge.source_id
260 ))
261 })?;
262 if !source_pattern.satisfies(source) {
263 continue;
264 }
265 let middle = store.get_vertex(first_edge.target_id).ok_or_else(|| {
266 GraphStoreError::CorruptGraph(format!(
267 "edge {} references missing middle vertex {}",
268 first_edge.edge_id, first_edge.target_id
269 ))
270 })?;
271 if !middle_pattern.satisfies(middle) {
272 continue;
273 }
274
275 for second_edge_id in store.out_edge_ids(first_edge.target_id, self.graph)? {
276 let second_edge = store.get_edge(second_edge_id).ok_or_else(|| {
277 GraphStoreError::CorruptGraph(format!("missing path edge {second_edge_id}"))
278 })?;
279 if !second.satisfies(second_edge) {
280 continue;
281 }
282 if first_edge.source_id == second_edge.target_id
283 || first_edge.target_id == second_edge.target_id
284 {
285 continue;
286 }
287 let target = store.get_vertex(second_edge.target_id).ok_or_else(|| {
288 GraphStoreError::CorruptGraph(format!(
289 "edge {} references missing target vertex {}",
290 second_edge.edge_id, second_edge.target_id
291 ))
292 })?;
293 if !target_pattern.satisfies(target) {
294 continue;
295 }
296 let assignment = (
297 first_edge.source_id,
298 first_edge.target_id,
299 second_edge.target_id,
300 );
301 if !seen_assignments.insert(assignment) {
302 continue;
303 }
304 self.push_two_edge_path_match(
305 first,
306 second,
307 &first_edge,
308 second_edge,
309 &mut entries,
310 &mut graph_payloads,
311 )?;
312 }
313 }
314
315 Ok(Some(GraphPostingList::try_from_parts(
316 PostingList::from_sorted_unchecked(entries),
317 graph_payloads,
318 )?))
319 }
320
321 fn push_two_edge_path_match(
322 &self,
323 first: &EdgePattern,
324 second: &EdgePattern,
325 first_edge: &Edge,
326 second_edge: &Edge,
327 entries: &mut Vec<PostingEntry>,
328 graph_payloads: &mut BTreeMap<DocId, GraphPayload>,
329 ) -> GraphStoreResult<()> {
330 let doc_id = synthetic_doc_id(entries.len(), "two-edge GMatch")?;
331 let mut fields = BTreeMap::new();
332 fields.insert(
333 first.source_var.clone(),
334 graph_id_value(first_edge.source_id, "GMatch source")?,
335 );
336 fields.insert(
337 first.target_var.clone(),
338 graph_id_value(first_edge.target_id, "GMatch middle")?,
339 );
340 fields.insert(
341 second.target_var.clone(),
342 graph_id_value(second_edge.target_id, "GMatch target")?,
343 );
344 entries.push(PostingEntry::new(
345 doc_id,
346 Payload {
347 positions: Vec::new(),
348 score: self.score,
349 fields,
350 },
351 ));
352
353 let mut subgraph_edges = vec![first_edge.edge_id, second_edge.edge_id];
354 subgraph_edges.sort_unstable();
355 subgraph_edges.dedup();
356 let mut subgraph_vertices = vec![
357 first_edge.source_id,
358 first_edge.target_id,
359 second_edge.target_id,
360 ];
361 subgraph_vertices.sort_unstable();
362 subgraph_vertices.dedup();
363 graph_payloads.insert(
364 doc_id,
365 GraphPayload {
366 subgraph_vertices,
367 subgraph_edges,
368 graph_name: self.graph.to_string(),
369 score_override: Some(self.score),
370 },
371 );
372 Ok(())
373 }
374
375 fn edge_list_for_pattern<G: GraphStore>(
376 &self,
377 store: &G,
378 pattern: &EdgePattern,
379 ) -> GraphStoreResult<Vec<Edge>> {
380 let edge_ids = match pattern.label.as_deref() {
381 Some(label) => store.edge_ids_by_label(label, self.graph)?,
382 None => return store.edges_in_graph(self.graph),
383 };
384 edge_ids
385 .into_iter()
386 .map(|edge_id| {
387 store.get_edge(edge_id).cloned().ok_or_else(|| {
388 GraphStoreError::CorruptGraph(format!("missing pattern edge {edge_id}"))
389 })
390 })
391 .collect()
392 }
393
394 fn vertex_pattern(&self, variable: &str) -> Option<&crate::pattern::VertexPattern> {
395 self.pattern
396 .vertex_patterns
397 .iter()
398 .find(|pattern| pattern.variable == variable)
399 }
400
401 fn compute_candidates<G: GraphStore>(
402 &self,
403 store: &G,
404 ) -> GraphStoreResult<BTreeMap<String, Vec<VertexId>>> {
405 let mut candidates: BTreeMap<String, Vec<VertexId>> = BTreeMap::new();
406 let graph_vids = store.vertex_ids_in_graph(self.graph)?;
407 for vp in &self.pattern.vertex_patterns {
408 let mut cands = Vec::new();
409 for vid in &graph_vids {
410 let vtx = store.get_vertex(*vid).ok_or_else(|| {
411 GraphStoreError::CorruptGraph(format!("missing candidate vertex {vid}"))
412 })?;
413 if vp.satisfies(vtx) {
414 cands.push(*vid);
415 }
416 }
417 candidates.insert(vp.variable.clone(), cands);
418 }
419
420 let mut changed = true;
422 while changed {
423 changed = false;
424 for ep in &self.pattern.edge_patterns {
425 if ep.negated {
426 continue;
427 }
428 let (src_var, tgt_var) = (&ep.source_var, &ep.target_var);
429 let Some(_src_cands) = candidates.get(src_var) else {
430 continue;
431 };
432 let Some(tgt_cands) = candidates.get(tgt_var) else {
433 continue;
434 };
435 let tgt_set: BTreeSet<VertexId> = tgt_cands.iter().copied().collect();
436 let mut new_src = Vec::new();
437 for vid in candidates[src_var].iter().copied() {
438 if Self::has_edge_out(store, self.graph, vid, &tgt_set, ep)? {
439 new_src.push(vid);
440 }
441 }
442 if new_src.len() < candidates[src_var].len() {
443 candidates.insert(src_var.clone(), new_src);
444 changed = true;
445 }
446 let src_set: BTreeSet<VertexId> = candidates[src_var].iter().copied().collect();
447 let mut new_tgt = Vec::new();
448 for vid in candidates[tgt_var].iter().copied() {
449 if Self::has_edge_in(store, self.graph, vid, &src_set, ep)? {
450 new_tgt.push(vid);
451 }
452 }
453 if new_tgt.len() < candidates[tgt_var].len() {
454 candidates.insert(tgt_var.clone(), new_tgt);
455 changed = true;
456 }
457 }
458 }
459 Ok(candidates)
460 }
461
462 fn has_edge_out<G: GraphStore>(
463 store: &G,
464 graph: &str,
465 src: VertexId,
466 tgt_set: &BTreeSet<VertexId>,
467 ep: &EdgePattern,
468 ) -> GraphStoreResult<bool> {
469 for eid in store.out_edge_ids(src, graph)? {
470 let edge = store.get_edge(eid).ok_or_else(|| {
471 GraphStoreError::CorruptGraph(format!("missing pattern edge {eid}"))
472 })?;
473 if !tgt_set.contains(&edge.target_id) {
474 continue;
475 }
476 if ep.satisfies(edge) {
477 return Ok(true);
478 }
479 }
480 Ok(false)
481 }
482
483 fn has_edge_in<G: GraphStore>(
484 store: &G,
485 graph: &str,
486 tgt: VertexId,
487 src_set: &BTreeSet<VertexId>,
488 ep: &EdgePattern,
489 ) -> GraphStoreResult<bool> {
490 for eid in store.in_edge_ids(tgt, graph)? {
491 let edge = store.get_edge(eid).ok_or_else(|| {
492 GraphStoreError::CorruptGraph(format!("missing pattern edge {eid}"))
493 })?;
494 if !src_set.contains(&edge.source_id) {
495 continue;
496 }
497 if ep.satisfies(edge) {
498 return Ok(true);
499 }
500 }
501 Ok(false)
502 }
503
504 fn backtrack<G: GraphStore>(
505 &self,
506 store: &G,
507 candidates: &BTreeMap<String, Vec<VertexId>>,
508 var_edges: &BTreeMap<String, Vec<&EdgePattern>>,
509 state: &mut BacktrackState,
510 ) -> GraphStoreResult<()> {
511 if state.unassigned.is_empty() {
512 state.matches.push(state.assignment.clone());
513 return Ok(());
514 }
515 let var = state
516 .unassigned
517 .iter()
518 .min_by_key(|v| candidates.get(*v).map_or(usize::MAX, Vec::len))
519 .cloned()
520 .ok_or_else(|| {
521 GraphStoreError::CorruptGraph(
522 "GMatch has no variable despite non-empty unassigned state".into(),
523 )
524 })?;
525 let cands = candidates.get(&var).cloned().ok_or_else(|| {
526 GraphStoreError::CorruptGraph(format!("missing candidate set for variable {var:?}"))
527 })?;
528 for vid in cands {
529 if state.assigned_values.contains(&vid) {
530 continue;
531 }
532 state.assignment.insert(var.clone(), vid);
533 state.assigned_values.insert(vid);
534 state.unassigned.remove(&var);
535 if Self::validate_edges_for(store, self.graph, &var, var_edges, &state.assignment)? {
536 self.backtrack(store, candidates, var_edges, state)?;
537 }
538 state.assignment.remove(&var);
539 state.assigned_values.remove(&vid);
540 state.unassigned.insert(var.clone());
541 }
542 Ok(())
543 }
544
545 fn validate_edges_for<G: GraphStore>(
546 store: &G,
547 graph: &str,
548 var: &str,
549 var_edges: &BTreeMap<String, Vec<&EdgePattern>>,
550 assignment: &BTreeMap<String, VertexId>,
551 ) -> GraphStoreResult<bool> {
552 let Some(edges) = var_edges.get(var) else {
553 return Ok(true);
554 };
555 for ep in edges {
556 let (Some(src_id), Some(tgt_id)) = (
557 assignment.get(&ep.source_var).copied(),
558 assignment.get(&ep.target_var).copied(),
559 ) else {
560 continue;
561 };
562 let mut found = false;
563 for eid in store.out_edge_ids(src_id, graph)? {
564 let edge = store.get_edge(eid).ok_or_else(|| {
565 GraphStoreError::CorruptGraph(format!("missing pattern edge {eid}"))
566 })?;
567 if edge.target_id != tgt_id {
568 continue;
569 }
570 if ep.satisfies(edge) {
571 found = true;
572 break;
573 }
574 }
575 if !found {
576 return Ok(false);
577 }
578 }
579 Ok(true)
580 }
581
582 fn check_negated<G: GraphStore>(
583 store: &G,
584 graph: &str,
585 negated: &[&EdgePattern],
586 assignment: &BTreeMap<String, VertexId>,
587 ) -> GraphStoreResult<bool> {
588 for ep in negated {
589 let (Some(src_id), Some(tgt_id)) = (
590 assignment.get(&ep.source_var).copied(),
591 assignment.get(&ep.target_var).copied(),
592 ) else {
593 continue;
594 };
595 for eid in store.out_edge_ids(src_id, graph)? {
596 let edge = store.get_edge(eid).ok_or_else(|| {
597 GraphStoreError::CorruptGraph(format!("missing pattern edge {eid}"))
598 })?;
599 if edge.target_id != tgt_id {
600 continue;
601 }
602 if ep.satisfies(edge) {
603 return Ok(false);
604 }
605 }
606 }
607 Ok(true)
608 }
609
610 fn collect_match_edges<G: GraphStore>(
611 store: &G,
612 graph: &str,
613 positive: &[&EdgePattern],
614 assignment: &BTreeMap<String, VertexId>,
615 ) -> GraphStoreResult<Vec<EdgeId>> {
616 let mut edges: BTreeSet<EdgeId> = BTreeSet::new();
617 for ep in positive {
618 let (Some(src_id), Some(tgt_id)) = (
619 assignment.get(&ep.source_var).copied(),
620 assignment.get(&ep.target_var).copied(),
621 ) else {
622 continue;
623 };
624 for eid in store.out_edge_ids(src_id, graph)? {
625 let edge = store.get_edge(eid).ok_or_else(|| {
626 GraphStoreError::CorruptGraph(format!("missing pattern edge {eid}"))
627 })?;
628 if edge.target_id == tgt_id && ep.satisfies(edge) {
629 edges.insert(eid);
630 break;
631 }
632 }
633 }
634 Ok(edges.into_iter().collect())
635 }
636}
637
638