rusty_files/core/
engine.rs

1use crate::core::config::{SearchConfig, SearchConfigBuilder};
2use crate::core::error::Result;
3use crate::core::types::{IndexStats, ProgressCallback, SearchResult};
4use crate::filters::ExclusionFilter;
5use crate::indexer::{IndexBuilder, IncrementalIndexer};
6use crate::search::{Query, QueryParser, SearchExecutor};
7use crate::storage::{Database, FileBloomFilter, LruCache};
8use crate::watcher::FileSystemMonitor;
9use std::path::{Path, PathBuf};
10use std::sync::Arc;
11
12pub struct SearchEngine {
13    database: Arc<Database>,
14    config: Arc<SearchConfig>,
15    exclusion_filter: Arc<ExclusionFilter>,
16    cache: Arc<LruCache>,
17    bloom_filter: Arc<FileBloomFilter>,
18    index_builder: Arc<IndexBuilder>,
19    incremental_indexer: Arc<IncrementalIndexer>,
20    search_executor: Arc<SearchExecutor>,
21    monitor: Option<FileSystemMonitor>,
22}
23
24impl SearchEngine {
25    pub fn new<P: AsRef<Path>>(index_path: P) -> Result<Self> {
26        let config = SearchConfig::default();
27        Self::with_config(index_path, config)
28    }
29
30    pub fn with_config<P: AsRef<Path>>(index_path: P, config: SearchConfig) -> Result<Self> {
31        let database = Arc::new(Database::new(index_path, config.db_pool_size)?);
32        let config = Arc::new(config);
33
34        let exclusion_rules = database.get_exclusion_rules()?;
35        let exclusion_filter = if exclusion_rules.is_empty() {
36            Arc::new(ExclusionFilter::from_patterns(&config.exclusion_patterns)?)
37        } else {
38            Arc::new(ExclusionFilter::new(exclusion_rules)?)
39        };
40
41        let cache = Arc::new(LruCache::new(config.cache_size));
42        let bloom_filter = Arc::new(FileBloomFilter::new(
43            config.bloom_filter_capacity,
44            config.bloom_filter_error_rate,
45        ));
46
47        let index_builder = Arc::new(IndexBuilder::new(
48            Arc::clone(&database),
49            Arc::clone(&config),
50            Arc::clone(&exclusion_filter),
51        ));
52
53        let incremental_indexer = Arc::new(IncrementalIndexer::new(
54            Arc::clone(&database),
55            Arc::clone(&config),
56            Arc::clone(&exclusion_filter),
57        ));
58
59        let search_executor = Arc::new(SearchExecutor::new(
60            Arc::clone(&database),
61            Arc::clone(&config),
62            Arc::clone(&cache),
63            Arc::clone(&bloom_filter),
64        ));
65
66        Ok(Self {
67            database,
68            config,
69            exclusion_filter,
70            cache,
71            bloom_filter,
72            index_builder,
73            incremental_indexer,
74            search_executor,
75            monitor: None,
76        })
77    }
78
79    pub fn builder() -> SearchEngineBuilder {
80        SearchEngineBuilder::new()
81    }
82
83    pub fn index_directory<P: AsRef<Path>>(
84        &self,
85        root: P,
86        progress_callback: Option<ProgressCallback>,
87    ) -> Result<usize> {
88        self.index_builder.build(root, progress_callback)
89    }
90
91    pub fn update_index<P: AsRef<Path>>(
92        &self,
93        root: P,
94        progress_callback: Option<ProgressCallback>,
95    ) -> Result<crate::indexer::UpdateStats> {
96        self.incremental_indexer.update(root, progress_callback)
97    }
98
99    pub fn search(&self, query_str: &str) -> Result<Vec<SearchResult>> {
100        let query = QueryParser::parse(query_str)?;
101        self.search_executor.execute(&query)
102    }
103
104    pub fn search_with_query(&self, query: &Query) -> Result<Vec<SearchResult>> {
105        self.search_executor.execute(query)
106    }
107
108    pub fn start_watching<P: AsRef<Path>>(&mut self, root: P) -> Result<()> {
109        if self.monitor.is_none() {
110            let mut monitor = FileSystemMonitor::new(
111                Arc::clone(&self.database),
112                Arc::clone(&self.config),
113                Arc::clone(&self.exclusion_filter),
114            );
115
116            monitor.start(root)?;
117            self.monitor = Some(monitor);
118        }
119
120        Ok(())
121    }
122
123    pub fn stop_watching(&mut self) -> Result<()> {
124        if let Some(mut monitor) = self.monitor.take() {
125            monitor.stop()?;
126        }
127        Ok(())
128    }
129
130    pub fn is_watching(&self) -> bool {
131        self.monitor.as_ref().map(|m| m.is_running()).unwrap_or(false)
132    }
133
134    pub fn get_stats(&self) -> Result<IndexStats> {
135        self.database.get_stats()
136    }
137
138    pub fn clear_index(&self) -> Result<()> {
139        self.database.clear_all()?;
140        self.cache.clear();
141        self.bloom_filter.clear();
142        Ok(())
143    }
144
145    pub fn vacuum(&self) -> Result<()> {
146        self.database.vacuum()
147    }
148
149    pub fn verify_index<P: AsRef<Path>>(
150        &self,
151        root: P,
152    ) -> Result<crate::indexer::VerificationStats> {
153        self.incremental_indexer.verify_index(root)
154    }
155
156    pub fn add_exclusion_pattern(&self, pattern: String) -> Result<()> {
157        use crate::core::types::{ExclusionRule, ExclusionRuleType};
158
159        let rule = ExclusionRule {
160            pattern,
161            rule_type: ExclusionRuleType::Glob,
162        };
163
164        self.database.add_exclusion_rule(&rule)?;
165        Ok(())
166    }
167
168    pub fn get_config(&self) -> &SearchConfig {
169        &self.config
170    }
171
172    pub fn cache_stats(&self) -> (usize, bool) {
173        (self.cache.len(), self.cache.is_empty())
174    }
175}
176
177pub struct SearchEngineBuilder {
178    config_builder: SearchConfigBuilder,
179    index_path: Option<PathBuf>,
180}
181
182impl SearchEngineBuilder {
183    pub fn new() -> Self {
184        Self {
185            config_builder: SearchConfigBuilder::new(),
186            index_path: None,
187        }
188    }
189
190    pub fn index_path<P: Into<PathBuf>>(mut self, path: P) -> Self {
191        self.index_path = Some(path.into());
192        self.config_builder = self.config_builder.index_path(
193            self.index_path.as_ref().unwrap().clone()
194        );
195        self
196    }
197
198    pub fn thread_count(mut self, count: usize) -> Self {
199        self.config_builder = self.config_builder.thread_count(count);
200        self
201    }
202
203    pub fn enable_content_search(mut self, enable: bool) -> Self {
204        self.config_builder = self.config_builder.enable_content_search(enable);
205        self
206    }
207
208    pub fn enable_fuzzy_search(mut self, enable: bool) -> Self {
209        self.config_builder = self.config_builder.enable_fuzzy_search(enable);
210        self
211    }
212
213    pub fn cache_size(mut self, size: usize) -> Self {
214        self.config_builder = self.config_builder.cache_size(size);
215        self
216    }
217
218    pub fn max_search_results(mut self, max: usize) -> Self {
219        self.config_builder = self.config_builder.max_search_results(max);
220        self
221    }
222
223    pub fn exclusion_patterns(mut self, patterns: Vec<String>) -> Self {
224        self.config_builder = self.config_builder.exclusion_patterns(patterns);
225        self
226    }
227
228    pub fn build(self) -> Result<SearchEngine> {
229        let config = self.config_builder.build();
230        let index_path = self.index_path.unwrap_or_else(|| config.index_path.clone());
231
232        SearchEngine::with_config(index_path, config)
233    }
234}
235
236impl Default for SearchEngineBuilder {
237    fn default() -> Self {
238        Self::new()
239    }
240}
241
242#[cfg(test)]
243mod tests {
244    use super::*;
245    use std::fs;
246    use tempfile::TempDir;
247
248    #[test]
249    fn test_search_engine_creation() {
250        let temp_dir = TempDir::new().unwrap();
251        let index_path = temp_dir.path().join("index.db");
252
253        let engine = SearchEngine::new(&index_path).unwrap();
254        assert!(!engine.is_watching());
255    }
256
257    #[test]
258    fn test_search_engine_builder() {
259        let temp_dir = TempDir::new().unwrap();
260        let index_path = temp_dir.path().join("index.db");
261
262        let engine = SearchEngine::builder()
263            .index_path(index_path)
264            .thread_count(4)
265            .enable_content_search(false)
266            .build()
267            .unwrap();
268
269        assert_eq!(engine.get_config().thread_count, 4);
270    }
271
272    #[test]
273    fn test_indexing_and_search() {
274        let temp_dir = TempDir::new().unwrap();
275        let root = temp_dir.path().join("data");
276        fs::create_dir(&root).unwrap();
277
278        fs::write(root.join("test1.txt"), "content1").unwrap();
279        fs::write(root.join("test2.txt"), "content2").unwrap();
280
281        let index_path = temp_dir.path().join("index.db");
282        let engine = SearchEngine::new(&index_path).unwrap();
283
284        let count = engine.index_directory(&root, None).unwrap();
285        assert!(count > 0);
286
287        let results = engine.search("test").unwrap();
288        assert!(!results.is_empty());
289    }
290
291    #[test]
292    fn test_stats() {
293        let temp_dir = TempDir::new().unwrap();
294        let root = temp_dir.path().join("data");
295        fs::create_dir(&root).unwrap();
296
297        fs::write(root.join("file.txt"), "content").unwrap();
298
299        let index_path = temp_dir.path().join("index.db");
300        let engine = SearchEngine::new(&index_path).unwrap();
301
302        engine.index_directory(&root, None).unwrap();
303
304        let stats = engine.get_stats().unwrap();
305        assert!(stats.total_files > 0);
306    }
307}