rusty_files/core/
engine.rs1use 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}