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        let mut candidates_since_check = 0usize;
234        for node_id in candidates {
235            candidates_since_check += 1;
236            if candidates_since_check >= JSON_SEARCH_CANCEL_STRIDE {
237                checker.note_nodes_scanned(candidates_since_check)?;
238                candidates_since_check = 0;
239            }
240            let Some(value) = self.json_candidate_value(label, property, node_id) else {
241                continue;
242            };
243            if let Some(selected) = predicate(value) {
244                hits.push((node_id, selected));
245                if hits.len() == k {
246                    break;
247                }
248            }
249        }
250        if candidates_since_check > 0 {
251            checker.note_nodes_scanned(candidates_since_check)?;
252        }
253        Ok(hits)
254    }
255
256    fn json_candidate_value(
257        &self,
258        label: &DbString,
259        property: &DbString,
260        node_id: NodeId,
261    ) -> Option<&JsonValue> {
262        let labels = self.node_labels(node_id)?;
263        if !labels.contains(label) {
264            return None;
265        }
266        let properties = self.node_properties(node_id)?;
267        match properties.get(property) {
268            Some(Value::Json(value)) => Some(value),
269            _ => None,
270        }
271    }
272}
273
274impl SharedGraph {
275    /// Find candidate nodes whose JSON property contains `candidate`.
276    pub fn exact_json_contains_candidate_nodes(
277        &self,
278        label: &DbString,
279        property: &DbString,
280        candidate: &JsonValue,
281        candidates: &[NodeId],
282        k: usize,
283    ) -> GraphResult<Vec<JsonContainmentHit>> {
284        self.read()
285            .exact_json_contains_candidate_nodes(label, property, candidate, candidates, k)
286    }
287
288    /// Find candidate JSON containment matches with cancellation checks.
289    pub fn exact_json_contains_candidate_nodes_checked(
290        &self,
291        label: &DbString,
292        property: &DbString,
293        candidate: &JsonValue,
294        candidates: &[NodeId],
295        k: usize,
296        checker: CancellationChecker<'_>,
297    ) -> Result<Vec<JsonContainmentHit>, JsonSearchError> {
298        self.read().exact_json_contains_candidate_nodes_checked(
299            label, property, candidate, candidates, k, checker,
300        )
301    }
302
303    /// Find candidate nodes whose JSON property has `path`.
304    pub fn exact_json_path_exists_candidate_nodes(
305        &self,
306        label: &DbString,
307        property: &DbString,
308        path: &[JsonPathSelector],
309        candidates: &[NodeId],
310        k: usize,
311    ) -> GraphResult<Vec<JsonPathHit>> {
312        self.read()
313            .exact_json_path_exists_candidate_nodes(label, property, path, candidates, k)
314    }
315
316    /// Find candidate JSON path-existence matches with cancellation checks.
317    pub fn exact_json_path_exists_candidate_nodes_checked(
318        &self,
319        label: &DbString,
320        property: &DbString,
321        path: &[JsonPathSelector],
322        candidates: &[NodeId],
323        k: usize,
324        checker: CancellationChecker<'_>,
325    ) -> Result<Vec<JsonPathHit>, JsonSearchError> {
326        self.read().exact_json_path_exists_candidate_nodes_checked(
327            label, property, path, candidates, k, checker,
328        )
329    }
330
331    /// Find candidate nodes whose selected JSON path contains `candidate`.
332    pub fn exact_json_path_contains_candidate_nodes(
333        &self,
334        label: &DbString,
335        property: &DbString,
336        options: JsonPathContainmentCandidateOptions<'_>,
337    ) -> GraphResult<Vec<JsonPathContainmentHit>> {
338        self.read()
339            .exact_json_path_contains_candidate_nodes(label, property, options)
340    }
341
342    /// Find candidate JSON path-containment matches with cancellation checks.
343    pub fn exact_json_path_contains_candidate_nodes_checked(
344        &self,
345        label: &DbString,
346        property: &DbString,
347        options: JsonPathContainmentCandidateOptions<'_>,
348        checker: CancellationChecker<'_>,
349    ) -> Result<Vec<JsonPathContainmentHit>, JsonSearchError> {
350        self.read()
351            .exact_json_path_contains_candidate_nodes_checked(label, property, options, checker)
352    }
353
354    /// Return selected JSON values for matching candidate nodes.
355    pub fn exact_json_path_value_candidate_nodes(
356        &self,
357        label: &DbString,
358        property: &DbString,
359        path: &[JsonPathSelector],
360        candidates: &[NodeId],
361        k: usize,
362    ) -> GraphResult<Vec<JsonPathValueHit>> {
363        self.read()
364            .exact_json_path_value_candidate_nodes(label, property, path, candidates, k)
365    }
366
367    /// Return selected candidate JSON path values with cancellation checks.
368    pub fn exact_json_path_value_candidate_nodes_checked(
369        &self,
370        label: &DbString,
371        property: &DbString,
372        path: &[JsonPathSelector],
373        candidates: &[NodeId],
374        k: usize,
375        checker: CancellationChecker<'_>,
376    ) -> Result<Vec<JsonPathValueHit>, JsonSearchError> {
377        self.read().exact_json_path_value_candidate_nodes_checked(
378            label, property, path, candidates, k, checker,
379        )
380    }
381}
382
383fn sorted_unique_candidates(candidates: &[NodeId]) -> Vec<NodeId> {
384    let mut candidates = candidates.to_vec();
385    candidates.sort_unstable();
386    candidates.dedup();
387    candidates
388}