rusty_files/search/
executor.rs

1use crate::core::config::SearchConfig;
2use crate::core::error::Result;
3use crate::core::types::{FileEntry, MatchMode, SearchResult, SearchScope};
4use crate::filters::{apply_date_filter, apply_extension_filter, apply_size_filter};
5use crate::search::fuzzy::FuzzyMatcher;
6use crate::search::matcher::create_matcher;
7use crate::search::query::Query;
8use crate::search::ranker::ResultRanker;
9use crate::storage::{Database, FileBloomFilter, LruCache};
10use std::sync::Arc;
11
12pub struct SearchExecutor {
13    database: Arc<Database>,
14    config: Arc<SearchConfig>,
15    _cache: Arc<LruCache>,
16    _bloom_filter: Arc<FileBloomFilter>,
17    ranker: ResultRanker,
18}
19
20impl SearchExecutor {
21    pub fn new(
22        database: Arc<Database>,
23        config: Arc<SearchConfig>,
24        cache: Arc<LruCache>,
25        bloom_filter: Arc<FileBloomFilter>,
26    ) -> Self {
27        let ranker = ResultRanker::new(config.fuzzy_threshold);
28
29        Self {
30            database,
31            config,
32            _cache: cache,
33            _bloom_filter: bloom_filter,
34            ranker,
35        }
36    }
37
38    pub fn execute(&self, query: &Query) -> Result<Vec<SearchResult>> {
39        if self.config.enable_fuzzy_search && query.match_mode == MatchMode::Fuzzy {
40            return self.execute_fuzzy_search(query);
41        }
42
43        let candidates = self.get_candidates(query)?;
44        let filtered = self.apply_filters(candidates, query)?;
45        let matched = self.apply_matchers(filtered, query)?;
46        let results = self.create_search_results(matched, query);
47
48        let ranked = self.ranker.rank(results, &query.pattern);
49
50        let max_results = query
51            .max_results
52            .unwrap_or(self.config.max_search_results);
53
54        Ok(ranked.into_iter().take(max_results).collect())
55    }
56
57    fn get_candidates(&self, query: &Query) -> Result<Vec<FileEntry>> {
58        match query.scope {
59            SearchScope::Name => {
60                if !query.extensions.is_empty() && query.extensions.len() == 1 {
61                    self.database.search_by_extension(
62                        &query.extensions[0],
63                        self.config.max_search_results * 2,
64                    )
65                } else {
66                    self.database.search_by_name(
67                        &query.pattern,
68                        self.config.max_search_results * 2,
69                    )
70                }
71            }
72            SearchScope::Path => self.database.search_by_name(
73                &query.pattern,
74                self.config.max_search_results * 2,
75            ),
76            SearchScope::Content => {
77                if self.config.enable_content_search {
78                    let file_ids = self.database.search_content(
79                        &query.pattern,
80                        self.config.max_search_results * 2,
81                    )?;
82
83                    let mut files = Vec::new();
84                    for id in file_ids {
85                        if let Ok(Some(file)) = self.database.find_by_id(id) {
86                            files.push(file);
87                        }
88                    }
89                    Ok(files)
90                } else {
91                    Ok(Vec::new())
92                }
93            }
94            SearchScope::All => self.database.get_all_files(
95                self.config.max_search_results * 2,
96                0,
97            ),
98        }
99    }
100
101    fn apply_filters(&self, candidates: Vec<FileEntry>, query: &Query) -> Result<Vec<FileEntry>> {
102        let filtered = candidates
103            .into_iter()
104            .filter(|entry| {
105                if !query.extensions.is_empty()
106                    && !apply_extension_filter(entry, &query.extensions)
107                {
108                    return false;
109                }
110
111                if let Some(ref size_filter) = query.size_filter {
112                    if !apply_size_filter(entry, size_filter) {
113                        return false;
114                    }
115                }
116
117                if let Some(ref date_filter) = query.date_filter {
118                    if !apply_date_filter(entry, date_filter) {
119                        return false;
120                    }
121                }
122
123                true
124            })
125            .collect();
126
127        Ok(filtered)
128    }
129
130    fn apply_matchers(&self, candidates: Vec<FileEntry>, query: &Query) -> Result<Vec<FileEntry>> {
131        let matcher = create_matcher(&query.pattern, query.match_mode)?;
132
133        let matched = candidates
134            .into_iter()
135            .filter(|entry| {
136                match query.scope {
137                    SearchScope::Name => matcher.is_match(&entry.name),
138                    SearchScope::Path => matcher.is_match(&entry.path.to_string_lossy()),
139                    SearchScope::Content => true,
140                    SearchScope::All => matcher.is_match(&entry.name),
141                }
142            })
143            .collect();
144
145        Ok(matched)
146    }
147
148    fn execute_fuzzy_search(&self, query: &Query) -> Result<Vec<SearchResult>> {
149        let fuzzy_matcher = FuzzyMatcher::new(self.config.fuzzy_threshold);
150        let mut all_files = self.database.get_all_files(10000, 0)?;
151
152        if !query.extensions.is_empty() {
153            all_files.retain(|f| apply_extension_filter(f, &query.extensions));
154        }
155
156        if let Some(ref size_filter) = query.size_filter {
157            all_files.retain(|f| apply_size_filter(f, size_filter));
158        }
159
160        if let Some(ref date_filter) = query.date_filter {
161            all_files.retain(|f| apply_date_filter(f, date_filter));
162        }
163
164        let mut scored_results: Vec<(FileEntry, i64)> = all_files
165            .into_iter()
166            .filter_map(|entry| {
167                fuzzy_matcher
168                    .fuzzy_match_with_threshold(&entry.name, &query.pattern)
169                    .map(|score| (entry, score))
170            })
171            .collect();
172
173        scored_results.sort_by(|a, b| b.1.cmp(&a.1));
174
175        let max_results = query
176            .max_results
177            .unwrap_or(self.config.max_search_results);
178
179        let results: Vec<SearchResult> = scored_results
180            .into_iter()
181            .take(max_results)
182            .map(|(file, score)| SearchResult {
183                file,
184                score: score as f64 / 100.0,
185                snippet: None,
186                matches: vec![],
187            })
188            .collect();
189
190        Ok(results)
191    }
192
193    fn create_search_results(&self, files: Vec<FileEntry>, _query: &Query) -> Vec<SearchResult> {
194        files
195            .into_iter()
196            .map(|file| SearchResult {
197                file,
198                score: 0.0,
199                snippet: None,
200                matches: vec![],
201            })
202            .collect()
203    }
204
205    pub fn search_with_cache(&self, query: &Query) -> Result<Vec<SearchResult>> {
206        self.execute(query)
207    }
208}
209
210#[cfg(test)]
211mod tests {
212    use super::*;
213    use crate::core::config::SearchConfig;
214    use crate::filters::ExclusionFilter;
215    use crate::indexer::builder::IndexBuilder;
216    use std::fs;
217    use tempfile::TempDir;
218
219    #[test]
220    fn test_search_executor() {
221        let temp_dir = TempDir::new().unwrap();
222        let root = temp_dir.path();
223
224        fs::write(root.join("test1.txt"), "content1").unwrap();
225        fs::write(root.join("test2.txt"), "content2").unwrap();
226        fs::write(root.join("other.rs"), "content3").unwrap();
227
228        let db = Arc::new(Database::in_memory(10).unwrap());
229        // Enable hidden files indexing since temp dirs often start with a dot
230        let mut config = SearchConfig::default();
231        config.index_hidden_files = true;
232        let config = Arc::new(config);
233        // Use empty exclusion filter to avoid any pattern matching issues
234        let filter = Arc::new(ExclusionFilter::from_patterns(&[]).unwrap());
235
236        let builder = IndexBuilder::new(db.clone(), config.clone(), filter);
237        builder.build(root, None).unwrap();
238
239        let cache = Arc::new(LruCache::new(100));
240        let bloom = Arc::new(FileBloomFilter::default());
241
242        let executor = SearchExecutor::new(db, config, cache, bloom);
243
244        let query = Query::new("test".to_string());
245        let results = executor.execute(&query).unwrap();
246
247        assert!(!results.is_empty(), "Expected at least one search result");
248    }
249
250    #[test]
251    fn test_search_with_extension_filter() {
252        let temp_dir = TempDir::new().unwrap();
253        let root = temp_dir.path();
254
255        fs::write(root.join("file1.txt"), "content1").unwrap();
256        fs::write(root.join("file2.rs"), "content2").unwrap();
257
258        let db = Arc::new(Database::in_memory(10).unwrap());
259        // Enable hidden files indexing since temp dirs often start with a dot
260        let mut config = SearchConfig::default();
261        config.index_hidden_files = true;
262        let config = Arc::new(config);
263        // Use empty exclusion filter to avoid any pattern matching issues
264        let filter = Arc::new(ExclusionFilter::from_patterns(&[]).unwrap());
265
266        let builder = IndexBuilder::new(db.clone(), config.clone(), filter);
267        builder.build(root, None).unwrap();
268
269        let cache = Arc::new(LruCache::new(100));
270        let bloom = Arc::new(FileBloomFilter::default());
271
272        let executor = SearchExecutor::new(db, config, cache, bloom);
273
274        let query = Query::new("file".to_string()).with_extensions(vec!["rs".to_string()]);
275        let results = executor.execute(&query).unwrap();
276
277        assert_eq!(results.len(), 1, "Expected exactly one search result");
278        assert_eq!(results[0].file.name, "file2.rs");
279    }
280}