Skip to main content

selene_graph/
json_search_candidates.rs

1//! Candidate-scoped exact JSON search over graph node properties.
2
3use selene_core::{CancellationChecker, DbString, JsonPathSelector, JsonValue, NodeId, Value};
4
5use crate::error::GraphResult;
6use crate::graph::SeleneGraph;
7use crate::json_search::{
8    JSON_SEARCH_CANCEL_STRIDE, JsonContainmentHit, JsonPathContainmentHit, JsonPathHit,
9    JsonPathValueHit, JsonSearchError,
10};
11use crate::shared::SharedGraph;
12
13/// Inputs for candidate-scoped JSON path-containment search.
14#[derive(Clone, Copy, Debug)]
15pub struct JsonPathContainmentCandidateOptions<'a> {
16    /// JSON path selector array.
17    pub path: &'a [JsonPathSelector],
18    /// JSON candidate the selected path value must contain.
19    pub candidate: &'a JsonValue,
20    /// Candidate nodes to filter.
21    pub candidates: &'a [NodeId],
22    /// Maximum result count.
23    pub k: usize,
24}
25
26impl<'a> JsonPathContainmentCandidateOptions<'a> {
27    /// Construct candidate-scoped JSON path-containment options.
28    #[must_use]
29    pub const fn new(
30        path: &'a [JsonPathSelector],
31        candidate: &'a JsonValue,
32        candidates: &'a [NodeId],
33        k: usize,
34    ) -> Self {
35        Self {
36            path,
37            candidate,
38            candidates,
39            k,
40        }
41    }
42}
43
44impl SeleneGraph {
45    /// Find candidate nodes whose JSON property contains `candidate`.
46    pub fn exact_json_contains_candidate_nodes(
47        &self,
48        label: &DbString,
49        property: &DbString,
50        candidate: &JsonValue,
51        candidates: &[NodeId],
52        k: usize,
53    ) -> GraphResult<Vec<JsonContainmentHit>> {
54        self.exact_json_contains_candidate_nodes_checked(
55            label,
56            property,
57            candidate,
58            candidates,
59            k,
60            CancellationChecker::disabled(),
61        )
62        .map_err(JsonSearchError::into_graph_error)
63    }
64
65    /// Find candidate JSON containment matches with cancellation checks.
66    pub fn exact_json_contains_candidate_nodes_checked(
67        &self,
68        label: &DbString,
69        property: &DbString,
70        candidate: &JsonValue,
71        candidates: &[NodeId],
72        k: usize,
73        checker: CancellationChecker<'_>,
74    ) -> Result<Vec<JsonContainmentHit>, JsonSearchError> {
75        self.filter_json_candidate_nodes(label, property, candidates, k, checker, |json| {
76            json.contains(candidate).then_some(None)
77        })
78        .map(|hits| {
79            hits.into_iter()
80                .map(|(node_id, _)| JsonContainmentHit { node_id })
81                .collect()
82        })
83    }
84
85    /// Find candidate nodes whose JSON property has `path`.
86    pub fn exact_json_path_exists_candidate_nodes(
87        &self,
88        label: &DbString,
89        property: &DbString,
90        path: &[JsonPathSelector],
91        candidates: &[NodeId],
92        k: usize,
93    ) -> GraphResult<Vec<JsonPathHit>> {
94        self.exact_json_path_exists_candidate_nodes_checked(
95            label,
96            property,
97            path,
98            candidates,
99            k,
100            CancellationChecker::disabled(),
101        )
102        .map_err(JsonSearchError::into_graph_error)
103    }
104
105    /// Find candidate JSON path-existence matches with cancellation checks.
106    pub fn exact_json_path_exists_candidate_nodes_checked(
107        &self,
108        label: &DbString,
109        property: &DbString,
110        path: &[JsonPathSelector],
111        candidates: &[NodeId],
112        k: usize,
113        checker: CancellationChecker<'_>,
114    ) -> Result<Vec<JsonPathHit>, JsonSearchError> {
115        if path.is_empty() {
116            return Ok(Vec::new());
117        }
118        self.filter_json_candidate_nodes(label, property, candidates, k, checker, |json| {
119            json.path_exists(path).then_some(None)
120        })
121        .map(|hits| {
122            hits.into_iter()
123                .map(|(node_id, _)| JsonPathHit { node_id })
124                .collect()
125        })
126    }
127
128    /// Find candidate nodes whose selected JSON path contains `candidate`.
129    pub fn exact_json_path_contains_candidate_nodes(
130        &self,
131        label: &DbString,
132        property: &DbString,
133        options: JsonPathContainmentCandidateOptions<'_>,
134    ) -> GraphResult<Vec<JsonPathContainmentHit>> {
135        self.exact_json_path_contains_candidate_nodes_checked(
136            label,
137            property,
138            options,
139            CancellationChecker::disabled(),
140        )
141        .map_err(JsonSearchError::into_graph_error)
142    }
143
144    /// Find candidate JSON path-containment matches with cancellation checks.
145    pub fn exact_json_path_contains_candidate_nodes_checked(
146        &self,
147        label: &DbString,
148        property: &DbString,
149        options: JsonPathContainmentCandidateOptions<'_>,
150        checker: CancellationChecker<'_>,
151    ) -> Result<Vec<JsonPathContainmentHit>, JsonSearchError> {
152        if options.path.is_empty() {
153            return Ok(Vec::new());
154        }
155        self.filter_json_candidate_nodes(
156            label,
157            property,
158            options.candidates,
159            options.k,
160            checker,
161            |json| {
162                json.path_contains(options.path, options.candidate)
163                    .then_some(None)
164            },
165        )
166        .map(|hits| {
167            hits.into_iter()
168                .map(|(node_id, _)| JsonPathContainmentHit { node_id })
169                .collect()
170        })
171    }
172
173    /// Return selected JSON values for matching candidate nodes.
174    pub fn exact_json_path_value_candidate_nodes(
175        &self,
176        label: &DbString,
177        property: &DbString,
178        path: &[JsonPathSelector],
179        candidates: &[NodeId],
180        k: usize,
181    ) -> GraphResult<Vec<JsonPathValueHit>> {
182        self.exact_json_path_value_candidate_nodes_checked(
183            label,
184            property,
185            path,
186            candidates,
187            k,
188            CancellationChecker::disabled(),
189        )
190        .map_err(JsonSearchError::into_graph_error)
191    }
192
193    /// Return selected candidate JSON path values with cancellation checks.
194    pub fn exact_json_path_value_candidate_nodes_checked(
195        &self,
196        label: &DbString,
197        property: &DbString,
198        path: &[JsonPathSelector],
199        candidates: &[NodeId],
200        k: usize,
201        checker: CancellationChecker<'_>,
202    ) -> Result<Vec<JsonPathValueHit>, JsonSearchError> {
203        if path.is_empty() {
204            return Ok(Vec::new());
205        }
206        self.filter_json_candidate_nodes(label, property, candidates, k, checker, |json| {
207            json.path_value(path).map(Some)
208        })
209        .map(|hits| {
210            hits.into_iter()
211                .filter_map(|(node_id, value)| {
212                    value.map(|value| JsonPathValueHit { node_id, value })
213                })
214                .collect()
215        })
216    }
217
218    fn filter_json_candidate_nodes(
219        &self,
220        label: &DbString,
221        property: &DbString,
222        candidates: &[NodeId],
223        k: usize,
224        checker: CancellationChecker<'_>,
225        mut predicate: impl FnMut(&JsonValue) -> Option<Option<JsonValue>>,
226    ) -> Result<Vec<(NodeId, Option<JsonValue>)>, JsonSearchError> {
227        checker.check()?;
228        if k == 0 || candidates.is_empty() {
229            return Ok(Vec::new());
230        }
231        let candidates = sorted_unique_candidates(candidates);
232        let mut hits = Vec::new();
233        for (offset, node_id) in candidates.into_iter().enumerate() {
234            if offset % JSON_SEARCH_CANCEL_STRIDE == 0 {
235                checker.check()?;
236            }
237            let Some(value) = self.json_candidate_value(label, property, node_id) else {
238                continue;
239            };
240            if let Some(selected) = predicate(value) {
241                hits.push((node_id, selected));
242                if hits.len() == k {
243                    break;
244                }
245            }
246        }
247        Ok(hits)
248    }
249
250    fn json_candidate_value(
251        &self,
252        label: &DbString,
253        property: &DbString,
254        node_id: NodeId,
255    ) -> Option<&JsonValue> {
256        let labels = self.node_labels(node_id)?;
257        if !labels.contains(label) {
258            return None;
259        }
260        let properties = self.node_properties(node_id)?;
261        match properties.get(property) {
262            Some(Value::Json(value)) => Some(value),
263            _ => None,
264        }
265    }
266}
267
268impl SharedGraph {
269    /// Find candidate nodes whose JSON property contains `candidate`.
270    pub fn exact_json_contains_candidate_nodes(
271        &self,
272        label: &DbString,
273        property: &DbString,
274        candidate: &JsonValue,
275        candidates: &[NodeId],
276        k: usize,
277    ) -> GraphResult<Vec<JsonContainmentHit>> {
278        self.read()
279            .exact_json_contains_candidate_nodes(label, property, candidate, candidates, k)
280    }
281
282    /// Find candidate JSON containment matches with cancellation checks.
283    pub fn exact_json_contains_candidate_nodes_checked(
284        &self,
285        label: &DbString,
286        property: &DbString,
287        candidate: &JsonValue,
288        candidates: &[NodeId],
289        k: usize,
290        checker: CancellationChecker<'_>,
291    ) -> Result<Vec<JsonContainmentHit>, JsonSearchError> {
292        self.read().exact_json_contains_candidate_nodes_checked(
293            label, property, candidate, candidates, k, checker,
294        )
295    }
296
297    /// Find candidate nodes whose JSON property has `path`.
298    pub fn exact_json_path_exists_candidate_nodes(
299        &self,
300        label: &DbString,
301        property: &DbString,
302        path: &[JsonPathSelector],
303        candidates: &[NodeId],
304        k: usize,
305    ) -> GraphResult<Vec<JsonPathHit>> {
306        self.read()
307            .exact_json_path_exists_candidate_nodes(label, property, path, candidates, k)
308    }
309
310    /// Find candidate JSON path-existence matches with cancellation checks.
311    pub fn exact_json_path_exists_candidate_nodes_checked(
312        &self,
313        label: &DbString,
314        property: &DbString,
315        path: &[JsonPathSelector],
316        candidates: &[NodeId],
317        k: usize,
318        checker: CancellationChecker<'_>,
319    ) -> Result<Vec<JsonPathHit>, JsonSearchError> {
320        self.read().exact_json_path_exists_candidate_nodes_checked(
321            label, property, path, candidates, k, checker,
322        )
323    }
324
325    /// Find candidate nodes whose selected JSON path contains `candidate`.
326    pub fn exact_json_path_contains_candidate_nodes(
327        &self,
328        label: &DbString,
329        property: &DbString,
330        options: JsonPathContainmentCandidateOptions<'_>,
331    ) -> GraphResult<Vec<JsonPathContainmentHit>> {
332        self.read()
333            .exact_json_path_contains_candidate_nodes(label, property, options)
334    }
335
336    /// Find candidate JSON path-containment matches with cancellation checks.
337    pub fn exact_json_path_contains_candidate_nodes_checked(
338        &self,
339        label: &DbString,
340        property: &DbString,
341        options: JsonPathContainmentCandidateOptions<'_>,
342        checker: CancellationChecker<'_>,
343    ) -> Result<Vec<JsonPathContainmentHit>, JsonSearchError> {
344        self.read()
345            .exact_json_path_contains_candidate_nodes_checked(label, property, options, checker)
346    }
347
348    /// Return selected JSON values for matching candidate nodes.
349    pub fn exact_json_path_value_candidate_nodes(
350        &self,
351        label: &DbString,
352        property: &DbString,
353        path: &[JsonPathSelector],
354        candidates: &[NodeId],
355        k: usize,
356    ) -> GraphResult<Vec<JsonPathValueHit>> {
357        self.read()
358            .exact_json_path_value_candidate_nodes(label, property, path, candidates, k)
359    }
360
361    /// Return selected candidate JSON path values with cancellation checks.
362    pub fn exact_json_path_value_candidate_nodes_checked(
363        &self,
364        label: &DbString,
365        property: &DbString,
366        path: &[JsonPathSelector],
367        candidates: &[NodeId],
368        k: usize,
369        checker: CancellationChecker<'_>,
370    ) -> Result<Vec<JsonPathValueHit>, JsonSearchError> {
371        self.read().exact_json_path_value_candidate_nodes_checked(
372            label, property, path, candidates, k, checker,
373        )
374    }
375}
376
377fn sorted_unique_candidates(candidates: &[NodeId]) -> Vec<NodeId> {
378    let mut candidates = candidates.to_vec();
379    candidates.sort_unstable();
380    candidates.dedup();
381    candidates
382}