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    /// Return all indexed candidates across all registered indexes.
90    #[must_use]
91    pub(crate) fn complete_candidates(&self) -> Vec<crate::Candidate> {
92        let indexes = self.snapshot.indexes();
93        let mut iter = indexes.iter();
94        let Some(first) = iter.next() else {
95            return Vec::new();
96        };
97
98        let mut files = first.all_files();
99
100        for index in iter {
101            let next: HashSet<PathBuf> = index
102                .all_files()
103                .into_iter()
104                .map(|c| c.rel_path().to_path_buf())
105                .collect();
106            files.retain(|c| next.contains(c.rel_path()));
107            if files.is_empty() {
108                break;
109            }
110        }
111
112        files
113    }
114
115    /// Intersect candidates from multiple indexes.
116    fn candidates_multi(
117        indexes: &[Index],
118        query: &crate::query::QuerySpec<'_>,
119    ) -> Option<Vec<crate::Candidate>> {
120        use rayon::prelude::*;
121
122        let sets: Vec<Vec<crate::Candidate>> = indexes
123            .par_iter()
124            .filter_map(|idx| idx.candidates(query))
125            .collect();
126
127        if sets.is_empty() {
128            return None;
129        }
130
131        let mut result = sets.into_iter();
132        let mut current = result.next()?;
133
134        for next in result {
135            let lookup: HashSet<&Path> = next.iter().map(crate::Candidate::rel_path).collect();
136            current.retain(|c| lookup.contains(c.rel_path()));
137            if current.is_empty() {
138                break;
139            }
140        }
141
142        Some(current)
143    }
144
145    #[must_use]
146    pub fn first(&self) -> Option<&Index> {
147        self.snapshot.indexes().first()
148    }
149
150    #[must_use]
151    pub fn corpus_kind(&self) -> Option<CorpusKind> {
152        let indexes = self.snapshot.indexes();
153        let kind = indexes.first()?.corpus_kind();
154        if indexes.iter().any(|idx| idx.corpus_kind() != kind) {
155            return None;
156        }
157        Some(kind)
158    }
159}