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