Skip to main content

sift_core/index/
registry.rs

1use std::collections::HashSet;
2use std::path::{Path, PathBuf};
3
4use super::config::CorpusKind;
5use super::error::IndexError;
6use super::kinds::Index;
7use super::snapshot::Snapshot;
8use super::store;
9
10/// Registry of opened indexes read from a snapshot store.
11///
12/// Opens all index kinds found in the current snapshot and provides
13/// query-time candidate narrowing by intersecting results from each index.
14/// Multiple indexes together produce tighter narrowing than any single
15/// index alone.
16///
17/// Owns a [`Snapshot`] that holds a reader lease, preventing the snapshot
18/// from being garbage-collected while searches are active.
19pub struct Indexes {
20    snapshot: Snapshot,
21}
22
23impl Indexes {
24    /// Create an Indexes registry from a single index and its root.
25    ///
26    /// Useful for testing and benchmarking.
27    #[must_use]
28    pub fn from_single(index: Index, root: PathBuf) -> Self {
29        Self {
30            snapshot: Snapshot::from_indexes(root, vec![index]),
31        }
32    }
33
34    /// Open all indexes found under `sift_dir`.
35    ///
36    /// # Errors
37    ///
38    /// Returns [`IndexError::InvalidManifest`] if a snapshot manifest is
39    /// malformed, or [`IndexError::Trigram`] if a trigram index is malformed.
40    ///
41    /// Returns an empty registry if no current snapshot exists (walk fallback).
42    pub fn open(sift_dir: &Path) -> Result<Self, IndexError> {
43        let store = store::IndexStore::open(sift_dir).map_err(|e| match e {
44            crate::Error::Index(ie) => ie,
45            crate::Error::Io(io) => IndexError::Io {
46                path: sift_dir.to_path_buf(),
47                source: io,
48            },
49            _ => IndexError::Io {
50                path: sift_dir.to_path_buf(),
51                source: std::io::Error::other(e.to_string()),
52            },
53        })?;
54
55        let snapshot = store.open_current().map_err(|e| match e {
56            crate::Error::Index(ie) => ie,
57            crate::Error::Io(io) => IndexError::Io {
58                path: sift_dir.to_path_buf(),
59                source: io,
60            },
61            _ => IndexError::Io {
62                path: sift_dir.to_path_buf(),
63                source: std::io::Error::other(e.to_string()),
64            },
65        })?;
66
67        Ok(Self { snapshot })
68    }
69
70    #[must_use]
71    pub const fn is_empty(&self) -> bool {
72        self.snapshot.is_empty()
73    }
74
75    #[must_use]
76    pub fn root(&self) -> &Path {
77        self.snapshot.root()
78    }
79
80    /// Produce narrowed candidates from all indexes that can narrow the query.
81    ///
82    /// Returns `None` if no index could narrow. When at least one index
83    /// narrows, all narrowed candidate sets are intersected.
84    #[must_use]
85    pub fn candidates(&self, query: &crate::query::QuerySpec<'_>) -> Option<Vec<crate::Candidate>> {
86        let indexes = self.snapshot.indexes();
87        match indexes.len() {
88            0 => None,
89            1 => indexes[0].candidates(query),
90            _ => Self::candidates_multi(indexes, query),
91        }
92    }
93
94    /// Corpus-relative paths present in the current snapshot.
95    #[must_use]
96    pub fn indexed_rel_paths(&self) -> HashSet<PathBuf> {
97        let indexes = self.snapshot.indexes();
98        let Some(first) = indexes.first() else {
99            return HashSet::new();
100        };
101
102        let mut paths: HashSet<PathBuf> = first
103            .all_files()
104            .into_iter()
105            .map(|c| c.rel_path().to_path_buf())
106            .collect();
107
108        for index in indexes.iter().skip(1) {
109            let next: HashSet<PathBuf> = index
110                .all_files()
111                .into_iter()
112                .map(|c| c.rel_path().to_path_buf())
113                .collect();
114            paths.retain(|p| next.contains(p));
115            if paths.is_empty() {
116                break;
117            }
118        }
119
120        paths
121    }
122
123    /// Corpus-relative search hits not yet present in the current snapshot.
124    #[must_use]
125    pub fn unindexed_hits(&self, hits: impl IntoIterator<Item = PathBuf>) -> Vec<PathBuf> {
126        let indexed = self.indexed_rel_paths();
127        hits.into_iter()
128            .filter(|path| !indexed.contains(path))
129            .collect()
130    }
131
132    /// Return all indexed candidates across all registered indexes.
133    #[must_use]
134    pub(crate) fn complete_candidates(&self) -> Vec<crate::Candidate> {
135        let indexes = self.snapshot.indexes();
136        let mut iter = indexes.iter();
137        let Some(first) = iter.next() else {
138            return Vec::new();
139        };
140
141        let mut files = first.all_files();
142
143        for index in iter {
144            let next: HashSet<PathBuf> = index
145                .all_files()
146                .into_iter()
147                .map(|c| c.rel_path().to_path_buf())
148                .collect();
149            files.retain(|c| next.contains(c.rel_path()));
150            if files.is_empty() {
151                break;
152            }
153        }
154
155        files
156    }
157
158    /// Intersect candidates from multiple indexes.
159    fn candidates_multi(
160        indexes: &[Index],
161        query: &crate::query::QuerySpec<'_>,
162    ) -> Option<Vec<crate::Candidate>> {
163        use rayon::prelude::*;
164
165        let sets: Vec<Vec<crate::Candidate>> = indexes
166            .par_iter()
167            .filter_map(|idx| idx.candidates(query))
168            .collect();
169
170        if sets.is_empty() {
171            return None;
172        }
173
174        let mut result = sets.into_iter();
175        let mut current = result.next()?;
176
177        for next in result {
178            let lookup: HashSet<&Path> = next.iter().map(crate::Candidate::rel_path).collect();
179            current.retain(|c| lookup.contains(c.rel_path()));
180            if current.is_empty() {
181                break;
182            }
183        }
184
185        Some(current)
186    }
187
188    #[must_use]
189    pub fn first(&self) -> Option<&Index> {
190        self.snapshot.indexes().first()
191    }
192
193    #[must_use]
194    pub fn corpus_kind(&self) -> Option<CorpusKind> {
195        let indexes = self.snapshot.indexes();
196        let kind = indexes.first()?.corpus_kind();
197        if indexes.iter().any(|idx| idx.corpus_kind() != kind) {
198            return None;
199        }
200        Some(kind)
201    }
202}