Skip to main content

sift_core/query/
planner.rs

1use std::collections::HashSet;
2use std::path::PathBuf;
3
4use crate::Candidate;
5use crate::CandidateFilter;
6use crate::StoreMeta;
7use crate::index::Indexes;
8use crate::query::QuerySpec;
9
10/// Whether search needs all candidate paths or only potential matches.
11#[derive(Debug, Clone, Copy, PartialEq, Eq)]
12pub enum CandidateRequirement {
13    Complete,
14    PotentialMatches,
15}
16
17/// Plans candidate selection by combining indexes and a lazy base provider.
18pub struct QueryPlanner<'a> {
19    spec: QuerySpec<'a>,
20}
21
22impl<'a> QueryPlanner<'a> {
23    #[must_use]
24    pub const fn new(spec: QuerySpec<'a>) -> Self {
25        Self { spec }
26    }
27
28    /// Resolve candidates using indexes or the lazy base provider.
29    ///
30    /// When `walk_unindexed` is true and the index narrows candidates, also walk
31    /// corpus paths that are not yet present in the current snapshot.
32    ///
33    /// # Errors
34    ///
35    /// Delegates to `base` when fallback is triggered; returns `base` errors unchanged.
36    pub fn candidates(
37        &self,
38        indexes: &Indexes,
39        requirement: CandidateRequirement,
40        filter: &CandidateFilter,
41        store_meta: Option<&StoreMeta>,
42        walk_unindexed: bool,
43        base: impl FnOnce() -> crate::Result<Vec<Candidate>>,
44    ) -> crate::Result<Vec<Candidate>> {
45        match requirement {
46            CandidateRequirement::Complete => {
47                if indexes.is_empty() {
48                    return base();
49                }
50                if store_meta.is_some_and(|meta| !meta.matches_search_filter(filter)) {
51                    return base();
52                }
53                Ok(indexes.complete_candidates())
54            }
55            CandidateRequirement::PotentialMatches => {
56                if indexes.is_empty() {
57                    return base();
58                }
59                match indexes.candidates(&self.spec) {
60                    None => base(),
61                    Some(snapshot_hits) if !walk_unindexed => Ok(snapshot_hits),
62                    Some(mut snapshot_hits) => {
63                        let indexed_paths = indexes.indexed_rel_paths();
64                        let walked = base()?;
65                        let mut seen: HashSet<PathBuf> = snapshot_hits
66                            .iter()
67                            .map(|c| c.rel_path().to_path_buf())
68                            .collect();
69                        for candidate in walked {
70                            if indexed_paths.contains(candidate.rel_path()) {
71                                continue;
72                            }
73                            if seen.insert(candidate.rel_path().to_path_buf()) {
74                                snapshot_hits.push(candidate);
75                            }
76                        }
77                        Ok(snapshot_hits)
78                    }
79                }
80            }
81        }
82    }
83}