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 let mut config = SearchConfig::default();
231 config.index_hidden_files = true;
232 let config = Arc::new(config);
233 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 let mut config = SearchConfig::default();
261 config.index_hidden_files = true;
262 let config = Arc::new(config);
263 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}