1pub fn is_cjk_char(c: char) -> bool {
17 let code = c as u32;
18 matches!(code,
19 0x4E00..=0x9FFF |
21 0x3400..=0x4DBF |
23 0x20000..=0x2A6DF |
25 0x2A700..=0x2B73F |
26 0x2B740..=0x2B81F |
27 0x2B820..=0x2CEAF |
28 0x2CEB0..=0x2EBEF |
29 0x3040..=0x309F |
31 0x30A0..=0x30FF |
33 0xAC00..=0xD7AF
35 )
36}
37
38pub fn needs_like_fallback(query: &str) -> bool {
47 let chars: Vec<char> = query.chars().collect();
48
49 if chars.len() == 1 && is_cjk_char(chars[0]) {
51 return true;
52 }
53
54 if chars.len() == 2 && chars.iter().all(|c| is_cjk_char(*c)) {
58 return true;
59 }
60
61 false
62}
63
64#[cfg(test)]
65mod tests {
66 use super::*;
67
68 #[test]
69 fn test_is_cjk_char() {
70 assert!(is_cjk_char('中'));
72 assert!(is_cjk_char('文'));
73 assert!(is_cjk_char('认'));
74 assert!(is_cjk_char('证'));
75
76 assert!(is_cjk_char('あ'));
78 assert!(is_cjk_char('い'));
79
80 assert!(is_cjk_char('ア'));
82 assert!(is_cjk_char('イ'));
83
84 assert!(is_cjk_char('가'));
86 assert!(is_cjk_char('나'));
87
88 assert!(!is_cjk_char('a'));
90 assert!(!is_cjk_char('A'));
91 assert!(!is_cjk_char('1'));
92 assert!(!is_cjk_char(' '));
93 assert!(!is_cjk_char('.'));
94 }
95
96 #[test]
97 fn test_needs_like_fallback() {
98 assert!(needs_like_fallback("中"));
100 assert!(needs_like_fallback("认"));
101 assert!(needs_like_fallback("あ"));
102 assert!(needs_like_fallback("가"));
103
104 assert!(needs_like_fallback("中文"));
106 assert!(needs_like_fallback("认证"));
107 assert!(needs_like_fallback("用户"));
108
109 assert!(!needs_like_fallback("用户认"));
111 assert!(!needs_like_fallback("用户认证"));
112
113 assert!(!needs_like_fallback("JWT"));
115 assert!(!needs_like_fallback("auth"));
116 assert!(!needs_like_fallback("a")); assert!(!needs_like_fallback("JWT认证"));
120 assert!(!needs_like_fallback("API接口"));
121 }
122}
123
124use crate::db::models::UnifiedSearchResult;
129use crate::error::Result;
130use crate::events::EventManager;
131use crate::tasks::TaskManager;
132use sqlx::SqlitePool;
133
134pub struct SearchManager<'a> {
135 pool: &'a SqlitePool,
136}
137
138impl<'a> SearchManager<'a> {
139 pub fn new(pool: &'a SqlitePool) -> Self {
140 Self { pool }
141 }
142
143 pub async fn unified_search(
154 &self,
155 query: &str,
156 include_tasks: bool,
157 include_events: bool,
158 limit: Option<i64>,
159 ) -> Result<Vec<UnifiedSearchResult>> {
160 let total_limit = limit.unwrap_or(20);
161 let mut results = Vec::new();
162
163 let (task_limit, event_limit) = match (include_tasks, include_events) {
165 (true, true) => (total_limit / 2, total_limit / 2),
166 (true, false) => (total_limit, 0),
167 (false, true) => (0, total_limit),
168 (false, false) => return Ok(results), };
170
171 if include_tasks && task_limit > 0 {
173 let task_mgr = TaskManager::new(self.pool);
174 let mut task_results = task_mgr.search_tasks(query).await?;
175
176 task_results.truncate(task_limit as usize);
178
179 for task_result in task_results {
180 let match_field = if task_result
182 .match_snippet
183 .to_lowercase()
184 .contains(&task_result.task.name.to_lowercase())
185 {
186 "name".to_string()
187 } else {
188 "spec".to_string()
189 };
190
191 results.push(UnifiedSearchResult::Task {
192 task: task_result.task,
193 match_snippet: task_result.match_snippet,
194 match_field,
195 });
196 }
197 }
198
199 if include_events && event_limit > 0 {
201 let event_mgr = EventManager::new(self.pool);
202 let event_results = event_mgr
203 .search_events_fts5(query, Some(event_limit))
204 .await?;
205
206 let task_mgr = TaskManager::new(self.pool);
207 for event_result in event_results {
208 let task_chain = task_mgr
210 .get_task_ancestry(event_result.event.task_id)
211 .await?;
212
213 results.push(UnifiedSearchResult::Event {
214 event: event_result.event,
215 task_chain,
216 match_snippet: event_result.match_snippet,
217 });
218 }
219 }
220
221 results.truncate(total_limit as usize);
223
224 Ok(results)
225 }
226}
227
228#[cfg(test)]
229mod unified_search_tests {
230 use super::*;
231 use crate::test_utils::test_helpers::TestContext;
232
233 #[tokio::test]
234 async fn test_unified_search_basic() {
235 let ctx = TestContext::new().await;
236 let task_mgr = TaskManager::new(ctx.pool());
237 let event_mgr = EventManager::new(ctx.pool());
238 let search_mgr = SearchManager::new(ctx.pool());
239
240 let task = task_mgr
242 .add_task("JWT Authentication", Some("Implement JWT auth"), None)
243 .await
244 .unwrap();
245
246 event_mgr
248 .add_event(task.id, "decision", "Chose JWT over OAuth")
249 .await
250 .unwrap();
251
252 let results = search_mgr
254 .unified_search("JWT", true, true, None)
255 .await
256 .unwrap();
257
258 assert!(results.len() >= 2);
259
260 let has_task = results
262 .iter()
263 .any(|r| matches!(r, UnifiedSearchResult::Task { .. }));
264 let has_event = results
265 .iter()
266 .any(|r| matches!(r, UnifiedSearchResult::Event { .. }));
267
268 assert!(has_task);
269 assert!(has_event);
270 }
271
272 #[tokio::test]
273 async fn test_unified_search_tasks_only() {
274 let ctx = TestContext::new().await;
275 let task_mgr = TaskManager::new(ctx.pool());
276 let search_mgr = SearchManager::new(ctx.pool());
277
278 task_mgr
280 .add_task("OAuth Implementation", None, None)
281 .await
282 .unwrap();
283
284 let results = search_mgr
286 .unified_search("OAuth", true, false, None)
287 .await
288 .unwrap();
289
290 assert!(!results.is_empty());
291
292 for result in results {
294 assert!(matches!(result, UnifiedSearchResult::Task { .. }));
295 }
296 }
297
298 #[tokio::test]
299 async fn test_unified_search_events_only() {
300 let ctx = TestContext::new().await;
301 let task_mgr = TaskManager::new(ctx.pool());
302 let event_mgr = EventManager::new(ctx.pool());
303 let search_mgr = SearchManager::new(ctx.pool());
304
305 let task = task_mgr.add_task("Test task", None, None).await.unwrap();
307
308 event_mgr
309 .add_event(task.id, "blocker", "OAuth library missing")
310 .await
311 .unwrap();
312
313 let results = search_mgr
315 .unified_search("OAuth", false, true, None)
316 .await
317 .unwrap();
318
319 assert!(!results.is_empty());
320
321 for result in results {
323 assert!(matches!(result, UnifiedSearchResult::Event { .. }));
324 }
325 }
326
327 #[tokio::test]
328 async fn test_unified_search_with_limit() {
329 let ctx = TestContext::new().await;
330 let task_mgr = TaskManager::new(ctx.pool());
331 let search_mgr = SearchManager::new(ctx.pool());
332
333 for i in 0..10 {
335 task_mgr
336 .add_task(&format!("Test task {}", i), None, None)
337 .await
338 .unwrap();
339 }
340
341 let results = search_mgr
343 .unified_search("Test", true, true, Some(3))
344 .await
345 .unwrap();
346
347 assert!(results.len() <= 3);
348 }
349}