1use super::{
10 build_nfa, simplify, subset_construction, value_as_f64, BTreeMap, Dfa, DfaState, EdgeId,
11 GraphPayload, GraphPostingList, GraphStore, GraphStoreError, GraphStoreResult,
12 PathWeightPredicate, Payload, PostingEntry, PostingList, RegularPathExpr, Value, VecDeque,
13 VertexId, DEFAULT_GRAPH_SCORE,
14};
15
16pub struct WeightedPathQuery<'a> {
26 pub path: RegularPathExpr,
27 pub graph: &'a str,
28 pub start_vertex: Option<VertexId>,
29 pub weight_property: &'a str,
30 pub default_edge_weight: f64,
31 pub max_hops: usize,
32 pub predicate: PathWeightPredicate,
33 pub score: f64,
34}
35
36impl<'a> WeightedPathQuery<'a> {
37 pub fn new(
38 path: RegularPathExpr,
39 graph: &'a str,
40 weight_property: &'a str,
41 predicate: PathWeightPredicate,
42 ) -> Self {
43 Self {
44 path,
45 graph,
46 start_vertex: None,
47 weight_property,
48 default_edge_weight: 1.0,
49 max_hops: 16,
50 predicate,
51 score: DEFAULT_GRAPH_SCORE,
52 }
53 }
54
55 pub fn from_vertex(mut self, start: VertexId) -> Self {
56 self.start_vertex = Some(start);
57 self
58 }
59
60 pub fn execute<G: GraphStore>(&self, store: &G) -> GraphStoreResult<GraphPostingList> {
61 if !self.default_edge_weight.is_finite() {
62 return Err(GraphStoreError::InvalidMutation(format!(
63 "default edge weight must be finite, got {}",
64 self.default_edge_weight
65 )));
66 }
67 if !self.score.is_finite() {
68 return Err(GraphStoreError::InvalidMutation(format!(
69 "weighted path score must be finite, got {}",
70 self.score
71 )));
72 }
73 let simplified = simplify(&self.path)
74 .map_err(|error| GraphStoreError::InvalidQuery(error.to_string()))?;
75 let nfa = build_nfa(&simplified)
76 .map_err(|error| GraphStoreError::InvalidQuery(error.to_string()))?;
77 let dfa = subset_construction(&nfa)
78 .map_err(|error| GraphStoreError::InvalidQuery(error.to_string()))?;
79 let starts: Vec<VertexId> = match self.start_vertex {
80 Some(vertex) => {
81 store.require_vertex_in_graph(vertex, self.graph)?;
82 vec![vertex]
83 }
84 None => store.vertex_ids_in_graph(self.graph)?.into_iter().collect(),
85 };
86 let mut accepted = BTreeMap::<VertexId, WeightedPathMatch>::new();
87 for start in starts {
88 self.simulate_from(store, start, &dfa, &mut accepted)?;
89 }
90
91 let mut entries = Vec::with_capacity(accepted.len());
92 let mut graph_payloads = BTreeMap::new();
93 for (end, path_match) in accepted {
94 let mut fields = BTreeMap::new();
95 fields.insert("_path_weight".to_string(), Value::Float(path_match.weight));
96 entries.push(PostingEntry::new(
97 end,
98 Payload {
99 score: self.score,
100 fields,
101 ..Default::default()
102 },
103 ));
104 graph_payloads.insert(
105 end,
106 GraphPayload {
107 subgraph_vertices: path_match.vertices,
108 subgraph_edges: path_match.edges,
109 graph_name: self.graph.to_string(),
110 score_override: Some(self.score),
111 },
112 );
113 }
114 GraphPostingList::try_from_parts(
115 PostingList::from_sorted_unchecked(entries),
116 graph_payloads,
117 )
118 .map_err(Into::into)
119 }
120
121 fn simulate_from<G: GraphStore>(
122 &self,
123 store: &G,
124 start: VertexId,
125 dfa: &Dfa,
126 accepted: &mut BTreeMap<VertexId, WeightedPathMatch>,
127 ) -> GraphStoreResult<()> {
128 let mut queue = VecDeque::from([WeightedWalk {
129 vertex: start,
130 state: dfa.start.clone(),
131 hops: 0,
132 weight: 0.0,
133 vertices: vec![start],
134 edges: Vec::new(),
135 }]);
136 if dfa.accepts.contains(&dfa.start) && (self.predicate)(0.0) {
137 record_weighted_match(accepted, start, 0.0, vec![start], Vec::new());
138 }
139
140 while let Some(walk) = queue.pop_front() {
141 if walk.hops >= self.max_hops {
142 continue;
143 }
144 let Some(transitions) = dfa.transitions.get(&walk.state) else {
145 continue;
146 };
147 for edge_id in store.out_edge_ids(walk.vertex, self.graph)? {
148 let edge = store.get_edge(edge_id).ok_or_else(|| {
149 GraphStoreError::CorruptGraph(format!("missing weighted-path edge {edge_id}"))
150 })?;
151 let Some(next_state) = transitions.get(&edge.label) else {
152 continue;
153 };
154 let edge_weight = match edge.properties.get(self.weight_property) {
155 Some(value) => value_as_f64(value)?.ok_or_else(|| {
156 GraphStoreError::InvalidMutation(format!(
157 "edge {edge_id} weight property {:?} is not numeric",
158 self.weight_property
159 ))
160 })?,
161 None => self.default_edge_weight,
162 };
163 let weight = walk.weight + edge_weight;
164 if !weight.is_finite() {
165 return Err(GraphStoreError::InvalidMutation(format!(
166 "weighted path accumulation is not finite at edge {edge_id}"
167 )));
168 }
169 let mut vertices = walk.vertices.clone();
170 vertices.push(edge.target_id);
171 let mut edges = walk.edges.clone();
172 edges.push(edge_id);
173 if dfa.accepts.contains(next_state) && (self.predicate)(weight) {
174 record_weighted_match(
175 accepted,
176 edge.target_id,
177 weight,
178 vertices.clone(),
179 edges.clone(),
180 );
181 }
182 queue.push_back(WeightedWalk {
183 vertex: edge.target_id,
184 state: next_state.clone(),
185 hops: walk.hops.checked_add(1).ok_or_else(|| {
186 GraphStoreError::CorruptGraph("weighted path hop count overflow".into())
187 })?,
188 weight,
189 vertices,
190 edges,
191 });
192 }
193 }
194 Ok(())
195 }
196}
197
198struct WeightedWalk {
199 vertex: VertexId,
200 state: DfaState,
201 hops: usize,
202 weight: f64,
203 vertices: Vec<VertexId>,
204 edges: Vec<EdgeId>,
205}
206
207struct WeightedPathMatch {
208 weight: f64,
209 vertices: Vec<VertexId>,
210 edges: Vec<EdgeId>,
211}
212
213fn record_weighted_match(
214 accepted: &mut BTreeMap<VertexId, WeightedPathMatch>,
215 endpoint: VertexId,
216 weight: f64,
217 vertices: Vec<VertexId>,
218 edges: Vec<EdgeId>,
219) {
220 let replace = accepted
221 .get(&endpoint)
222 .is_none_or(|current| weight.total_cmp(¤t.weight).is_gt());
223 if replace {
224 accepted.insert(
225 endpoint,
226 WeightedPathMatch {
227 weight,
228 vertices,
229 edges,
230 },
231 );
232 }
233}