1use crate::lsp::{LspState, LspConfig, LspSearchResponse, QueryIntent};
12use crate::pipeline::{FusedPipeline, PipelineContext, PipelineConfig};
13use crate::semantic::pipeline::{SemanticPipeline, SemanticSearchRequest, SemanticSearchResponse, InitialSearchResult};
14use crate::semantic::SemanticConfig;
15use anyhow::{anyhow, Result};
16use serde::{Deserialize, Serialize};
17use std::collections::HashMap;
18use std::path::Path;
19use std::sync::Arc;
20use std::time::{Duration, Instant};
21use tantivy::*;
22use tantivy::schema::{Schema, Field, TEXT, STORED, INDEXED};
23use tantivy::query::QueryParser;
24use tantivy::collector::TopDocs;
25use tokio::sync::RwLock;
26use tracing::{debug, error, info, warn};
27
28#[derive(Debug, Clone, Serialize, Deserialize)]
30pub struct SearchResult {
31 pub file_path: String,
32 pub line_number: u32,
33 pub column: u32,
34 pub content: String,
35 pub score: f64,
36 pub result_type: SearchResultType,
37 pub language: Option<String>,
38 pub context_lines: Option<Vec<String>>,
39 pub lsp_metadata: Option<LspMetadata>,
40}
41
42#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
44pub enum SearchResultType {
45 TextMatch,
47 Definition,
49 Reference,
51 TypeInfo,
53 Implementation,
55 Symbol,
57 Semantic,
59}
60
61#[derive(Debug, Clone, Serialize, Deserialize)]
63pub struct LspMetadata {
64 pub hint_type: String,
65 pub server_type: String,
66 pub confidence: f64,
67 pub cached: bool,
68}
69
70#[derive(Debug, Clone, Serialize, Deserialize, Default)]
72pub struct SearchMetrics {
73 pub total_docs: u64,
75 pub matched_docs: u64,
76 pub duration_ms: u32,
77
78 pub lsp_time_ms: u32,
80 pub lsp_results_count: u32,
81 pub lsp_cache_hit_rate: f64,
82
83 pub search_time_ms: u32,
85 pub fusion_time_ms: u32,
86 pub sla_compliant: bool,
87
88 pub result_diversity_score: f64,
90 pub confidence_score: f64,
91 pub coverage_score: f64,
92}
93
94impl SearchMetrics {
95 pub fn meets_sla(&self, sla_ms: u64) -> bool {
97 self.duration_ms <= sla_ms as u32
98 }
99
100 pub fn quality_score(&self) -> f64 {
102 (self.result_diversity_score + self.confidence_score + self.coverage_score) / 3.0
103 }
104}
105
106#[derive(Debug, Clone, PartialEq)]
108pub enum SearchMethod {
109 Lexical,
110 Structural,
111 Semantic,
112 Hybrid,
113 ForceSemantic, }
115
116impl Default for SearchMethod {
117 fn default() -> Self {
118 SearchMethod::Hybrid
119 }
120}
121
122#[derive(Debug, Clone)]
124pub struct SearchRequest {
125 pub query: String,
126 pub file_path: Option<String>,
127 pub language: Option<String>,
128 pub max_results: usize,
129 pub include_context: bool,
130 pub timeout_ms: u64,
131 pub enable_lsp: bool,
132 pub search_types: Vec<SearchResultType>,
133 pub search_method: Option<SearchMethod>,
134}
135
136impl Default for SearchRequest {
137 fn default() -> Self {
138 Self {
139 query: String::new(),
140 file_path: None,
141 language: None,
142 max_results: 50,
143 include_context: true,
144 timeout_ms: 150, enable_lsp: true,
146 search_types: vec![
147 SearchResultType::TextMatch,
148 SearchResultType::Definition,
149 SearchResultType::Reference,
150 SearchResultType::Symbol,
151 ],
152 search_method: Some(SearchMethod::Hybrid), }
154 }
155}
156
157#[derive(Debug, Clone, Serialize, Deserialize)]
159pub struct SearchResponse {
160 pub results: Vec<SearchResult>,
161 pub metrics: SearchMetrics,
162 pub query_intent: QueryIntent,
163 pub lsp_response: Option<LspSearchResponse>,
164 pub total_time_ms: u64,
165 pub sla_compliant: bool,
166}
167
168#[derive(Debug, Clone)]
170pub struct SearchConfig {
171 pub index_path: String,
172 pub max_results_default: usize,
173 pub sla_target_ms: u64,
174 pub lsp_routing_rate: f64,
175 pub enable_fusion_pipeline: bool,
176 pub enable_semantic_search: bool,
177 pub enable_lsp: bool,
178 pub context_lines: usize,
179
180 pub dataset_path: String,
182 pub enable_pinned_datasets: bool,
183 pub default_dataset_version: Option<String>,
184 pub enable_corpus_validation: bool,
185}
186
187impl Default for SearchConfig {
188 fn default() -> Self {
189 Self {
190 index_path: "./index".to_string(),
191 max_results_default: 50,
192 sla_target_ms: 150, lsp_routing_rate: 0.5, enable_fusion_pipeline: true,
195 enable_semantic_search: false, enable_lsp: true,
197 context_lines: 3,
198
199 dataset_path: "./pinned-datasets".to_string(),
201 enable_pinned_datasets: true,
202 default_dataset_version: Some("default".to_string()),
203 enable_corpus_validation: true,
204 }
205 }
206}
207
208pub struct SearchEngine {
210 index: Index,
212 reader: IndexReader,
213 schema: Schema,
214 fields: SearchFields,
215
216 lsp_state: Option<Arc<LspState>>,
218
219 pipeline: Option<Arc<FusedPipeline>>,
221
222 semantic_pipeline: Option<Arc<SemanticPipeline>>,
224
225 dataset_loader: Option<Arc<crate::benchmark::PinnedDatasetLoader>>,
227 current_dataset: Arc<RwLock<Option<Arc<crate::benchmark::PinnedDataset>>>>,
228
229 config: SearchConfig,
231
232 metrics: Arc<RwLock<EngineMetrics>>,
234}
235
236#[derive(Debug, Clone)]
238pub struct SearchFields {
239 pub file_path: Field,
240 pub content: Field,
241 pub line_number: Field,
242 pub language: Field,
243 pub raw_content: Field,
244}
245
246#[derive(Debug, Default, Clone)]
248pub struct EngineMetrics {
249 pub total_searches: u64,
250 pub sla_compliant_searches: u64,
251 pub lsp_routed_searches: u64,
252 pub avg_latency_ms: f64,
253 pub p95_latency_ms: u64,
254 pub p99_latency_ms: u64,
255 pub text_search_time_ms: f64,
256 pub lsp_search_time_ms: f64,
257 pub fusion_time_ms: f64,
258}
259
260impl EngineMetrics {
261 pub fn sla_compliance_rate(&self) -> f64 {
262 if self.total_searches == 0 {
263 0.0
264 } else {
265 self.sla_compliant_searches as f64 / self.total_searches as f64
266 }
267 }
268
269 pub fn lsp_routing_rate(&self) -> f64 {
270 if self.total_searches == 0 {
271 0.0
272 } else {
273 self.lsp_routed_searches as f64 / self.total_searches as f64
274 }
275 }
276}
277
278impl SearchEngine {
279 pub async fn new<P: AsRef<Path>>(index_path: P) -> Result<Self> {
281 let config = SearchConfig::default();
282 Self::with_config(index_path, config).await
283 }
284
285 pub async fn with_config<P: AsRef<Path>>(index_path: P, config: SearchConfig) -> Result<Self> {
287 info!("Initializing enhanced search engine with LSP integration");
288
289 let mut schema_builder = Schema::builder();
291
292 let fields = SearchFields {
293 file_path: schema_builder.add_text_field("file_path", TEXT | STORED),
294 content: schema_builder.add_text_field("content", TEXT | STORED),
295 line_number: schema_builder.add_u64_field("line_number", INDEXED | STORED),
296 language: schema_builder.add_facet_field("language", INDEXED),
297 raw_content: schema_builder.add_bytes_field("raw_content", STORED),
298 };
299
300 let schema = schema_builder.build();
301
302 let force_reindex_for_benchmark = std::env::var("NODE_ENV").unwrap_or_default() == "benchmark";
304
305 let index = if index_path.as_ref().exists() && index_path.as_ref().join("meta.json").exists() && !force_reindex_for_benchmark {
306 Index::open_in_dir(&index_path)?
308 } else {
309 if force_reindex_for_benchmark && index_path.as_ref().exists() {
311 info!("🔄 Benchmark mode: Clearing existing index for reindexing with benchmark corpus");
312 std::fs::remove_dir_all(&index_path)?;
313 }
314 std::fs::create_dir_all(&index_path)?;
315 let mut index = Index::create_in_dir(&index_path, schema.clone())?;
316
317 let mut index_writer: tantivy::IndexWriter = index.writer(128_000_000)?; info!("📁 Index is empty - populating with benchmark corpus files...");
322 let mut indexed_count = 0;
323
324 let corpus_dirs = ["benchmark-corpus", "src", "rust-core/src"];
326 let mut indexed_from_benchmark = false;
327
328 for corpus_dir in &corpus_dirs {
329 if let Ok(entries) = std::fs::read_dir(corpus_dir) {
330 info!("📂 Indexing files from directory: {}", corpus_dir);
331
332 for entry in entries.flatten() {
333 if let Some(file_name) = entry.file_name().to_str() {
334 let should_index = match *corpus_dir {
335 "benchmark-corpus" => {
336 file_name.ends_with(".py") || file_name.ends_with(".js") ||
338 file_name.ends_with(".java") || file_name.ends_with(".go") ||
339 file_name.ends_with(".rs") || file_name.ends_with(".ts") ||
340 file_name.ends_with(".rb") || file_name.ends_with(".cpp") ||
341 file_name.ends_with(".c") || file_name.ends_with(".cs")
342 },
343 _ => {
344 file_name.ends_with(".rs") || file_name.ends_with(".ts") || file_name.ends_with(".js")
346 }
347 };
348
349 if should_index {
350 if let Ok(content) = std::fs::read_to_string(entry.path()) {
351 if !content.trim().is_empty() {
352 let mut doc = tantivy::doc!();
354 doc.add_text(fields.file_path, &entry.path().to_string_lossy());
355 doc.add_text(fields.content, &content);
356 doc.add_u64(fields.line_number, 1);
357 doc.add_bytes(fields.raw_content, content.as_bytes());
358
359 index_writer.add_document(doc)?;
360 indexed_count += 1;
361
362 for (line_num, line) in content.lines().enumerate() {
364 if !line.trim().is_empty() && line.trim().len() > 5 {
365 let mut line_doc = tantivy::doc!();
366 line_doc.add_text(fields.file_path, &entry.path().to_string_lossy());
367 line_doc.add_text(fields.content, line);
368 line_doc.add_u64(fields.line_number, (line_num + 1) as u64);
369 line_doc.add_bytes(fields.raw_content, line.as_bytes());
370
371 index_writer.add_document(line_doc)?;
372 indexed_count += 1;
373 }
374 }
375
376 if *corpus_dir == "benchmark-corpus" {
377 indexed_from_benchmark = true;
378 }
379 }
380 }
381 }
382 }
383 }
384
385 if *corpus_dir == "benchmark-corpus" && indexed_from_benchmark {
387 break;
388 }
389 }
390 }
391
392 if indexed_from_benchmark {
393 info!("✅ Successfully indexed benchmark corpus for semantic reranking validation");
394 } else {
395 info!("⚠️ No benchmark corpus found - using fallback source indexing");
396 }
397
398 index_writer.commit()?;
399 info!("✅ Successfully indexed {} documents into search index", indexed_count);
400
401 index
402 };
403
404 let reader = index
405 .reader_builder()
406 .reload_policy(ReloadPolicy::OnCommitWithDelay)
407 .try_into()?;
408
409 let lsp_state = if config.lsp_routing_rate > 0.0 {
411 let lsp_config = LspConfig {
412 enabled: true,
413 server_timeout_ms: config.sla_target_ms / 2, cache_ttl_hours: 24,
415 max_concurrent_requests: 10,
416 routing_percentage: config.lsp_routing_rate,
417 ..Default::default()
418 };
419
420 let state = Arc::new(LspState::new(lsp_config));
421 state.initialize().await?;
422 Some(state)
423 } else {
424 info!("LSP integration disabled (routing rate = 0)");
425 None
426 };
427
428 let pipeline = None;
431
432 let semantic_pipeline = if std::env::var("NODE_ENV").unwrap_or_default() == "benchmark" {
434 let semantic_config = SemanticConfig::default();
435 let pipeline = SemanticPipeline::new(semantic_config).await?;
436 pipeline.initialize().await?;
437 info!("✅ Semantic pipeline (RAPTOR) initialized for benchmark mode");
438 Some(Arc::new(pipeline))
439 } else {
440 None
441 };
442
443 let (dataset_loader, current_dataset) = if config.enable_pinned_datasets {
445 info!("🎯 Initializing pinned dataset support");
446
447 let benchmark_config = crate::benchmark::BenchmarkConfig {
448 dataset_path: config.dataset_path.clone(),
449 enable_corpus_validation: config.enable_corpus_validation,
450 default_version: config.default_dataset_version.clone(),
451 auto_discover_datasets: true,
452 loading_timeout_secs: 30,
453 enable_caching: true,
454 max_cache_size: 3,
455 };
456
457 match crate::benchmark::PinnedDatasetLoader::with_config(benchmark_config).await {
458 Ok(loader) => {
459 let loader_arc = Arc::new(loader);
460
461 let dataset = match loader_arc.load_current_pinned_dataset().await {
463 Ok(dataset) => {
464 info!("✅ Loaded pinned dataset: version {} with {} queries",
465 dataset.metadata.version, dataset.queries.len());
466
467 if config.enable_corpus_validation {
469 match loader_arc.validate_dataset_consistency(&dataset).await {
470 Ok(validation_result) => {
471 let consistency_rate = validation_result.valid_queries as f64
472 / validation_result.total_queries as f64 * 100.0;
473
474 if validation_result.is_consistent {
475 info!("✅ Perfect corpus consistency: {}/{} queries (100%)",
476 validation_result.valid_queries, validation_result.total_queries);
477 } else {
478 warn!("⚠️ Partial corpus consistency: {}/{} queries ({:.1}%)",
479 validation_result.valid_queries, validation_result.total_queries, consistency_rate);
480 }
481 }
482 Err(e) => warn!("⚠️ Dataset validation failed: {}", e),
483 }
484 }
485
486 Some(Arc::new(dataset))
487 }
488 Err(e) => {
489 warn!("⚠️ Failed to load pinned dataset: {}", e);
490 warn!(" Continuing without pinned dataset support");
491 None
492 }
493 };
494
495 (Some(loader_arc), Arc::new(RwLock::new(dataset)))
496 }
497 Err(e) => {
498 warn!("⚠️ Failed to initialize dataset loader: {}", e);
499 warn!(" Continuing without pinned dataset support");
500 (None, Arc::new(RwLock::new(None)))
501 }
502 }
503 } else {
504 info!("📊 Pinned dataset support disabled");
505 (None, Arc::new(RwLock::new(None)))
506 };
507
508 let engine = Self {
509 index,
510 reader,
511 schema,
512 fields,
513 lsp_state,
514 pipeline,
515 semantic_pipeline,
516 dataset_loader,
517 current_dataset,
518 config,
519 metrics: Arc::new(RwLock::new(EngineMetrics::default())),
520 };
521
522 info!("Enhanced search engine initialized with ≤{}ms SLA", engine.config.sla_target_ms);
523 Ok(engine)
524 }
525
526 pub fn sanitize_query(&self, query: &str) -> String {
528 query
529 .replace("```", " ")
531 .replace("**", " ")
532 .replace("__", " ")
533 .replace("<!--", " ")
534 .replace("-->", " ")
535 .replace("###", " ")
536 .replace("(", " ")
538 .replace(")", " ")
539 .replace("[", " ")
540 .replace("]", " ")
541 .replace("{", " ")
542 .replace("}", " ")
543 .replace("\"", " ")
544 .replace("'", " ")
545 .replace("+", " ")
546 .replace("-", " ")
547 .replace("!", " ")
548 .replace("?", " ")
549 .replace(":", " ")
550 .replace(";", " ")
551 .replace("#", " ")
552 .replace("@", " ")
553 .replace("$", " ")
554 .replace("%", " ")
555 .replace("^", " ")
556 .replace("&", " ")
557 .replace("*", " ")
558 .replace("=", " ")
559 .replace("|", " ")
560 .replace("\\", " ")
561 .replace("/", " ")
562 .replace("<", " ")
563 .replace(">", " ")
564 .replace(".", " ")
565 .replace(",", " ")
566 .replace("~", " ")
567 .replace("`", " ")
568 .split_whitespace()
570 .collect::<Vec<&str>>()
571 .join(" ")
572 .trim()
573 .to_string()
574 }
575
576 pub async fn search(&self, query: &str, limit: usize) -> Result<(Vec<SearchResult>, SearchMetrics)> {
578 let request = SearchRequest {
579 query: query.to_string(),
580 max_results: limit,
581 ..Default::default()
582 };
583
584 let response = self.search_comprehensive(request).await?;
585 Ok((response.results, response.metrics))
586 }
587
588 pub async fn search_comprehensive(&self, request: SearchRequest) -> Result<SearchResponse> {
590 debug!("🔍 search_comprehensive called with query: '{}'", request.query);
591 debug!("🔍 DEBUG: pipeline is_some = {}", self.pipeline.is_some());
592
593 let start_time = Instant::now();
594
595 if let Some(pipeline) = &self.pipeline {
597 debug!("🔍 DEBUG: Using fused pipeline path");
598 return self.search_with_pipeline(request, pipeline).await;
599 }
600
601 debug!("🔍 DEBUG: Using direct search path");
603 self.search_direct(request, start_time).await
604 }
605
606 async fn search_with_pipeline(&self, request: SearchRequest, pipeline: &FusedPipeline) -> Result<SearchResponse> {
607 let context = PipelineContext::new(
608 uuid::Uuid::new_v4().to_string(),
609 request.query.clone(),
610 request.timeout_ms,
611 )
612 .with_max_results(request.max_results);
613
614 let context = if let Some(ref file_path) = request.file_path {
615 context.with_file_path(file_path.clone())
616 } else {
617 context
618 };
619
620 let pipeline_result = pipeline.search(context).await.map_err(|e| {
621 anyhow!("Pipeline execution failed: {:?}", e)
622 })?;
623
624 if !pipeline_result.success {
625 return Err(anyhow!("Pipeline search failed: {}",
626 pipeline_result.error_message.unwrap_or_else(|| "Unknown error".to_string())));
627 }
628
629 let query_intent = QueryIntent::classify(&request.query);
631
632 let mut search_results = Vec::new();
634 let mut total_docs = 0u64;
635 let mut matched_docs = 0u64;
636
637 match self.text_search(&request).await {
640 Ok(results) => {
641 search_results = results;
642 matched_docs = search_results.len() as u64;
643 if let Ok(reader) = self.index.reader() {
645 let searcher = reader.searcher();
646 total_docs = searcher.num_docs() as u64;
647 }
648 }
649 Err(e) => {
650 warn!("Text search failed: {}", e);
651 match self.lsp_search(&request).await {
653 Ok(lsp_response) => {
654 if !lsp_response.fallback_results.is_empty() {
656 search_results = lsp_response.fallback_results;
657 } else {
658 search_results = lsp_response.lsp_results.into_iter().map(|lsp_result| SearchResult {
660 file_path: lsp_result.file_path,
661 line_number: lsp_result.line_number,
662 column: lsp_result.column,
663 content: lsp_result.content,
664 score: 1.0, result_type: SearchResultType::Symbol,
666 language: Some("unknown".to_string()),
667 context_lines: lsp_result.context_lines,
668 lsp_metadata: None, }).collect();
670 }
671 matched_docs = search_results.len() as u64;
672 }
673 Err(_) => {
674 warn!("Both text search and LSP search failed, returning empty results");
676 }
677 }
678 }
679 }
680
681 Ok(SearchResponse {
682 results: search_results,
683 metrics: SearchMetrics {
684 total_docs,
685 matched_docs,
686 duration_ms: pipeline_result.metrics.avg_latency_ms as u32,
687 lsp_time_ms: 0,
688 lsp_results_count: 0,
689 lsp_cache_hit_rate: 0.0,
690 search_time_ms: 0,
691 fusion_time_ms: 0,
692 sla_compliant: pipeline_result.metrics.avg_latency_ms < self.config.sla_target_ms as f64,
693 result_diversity_score: 0.8,
694 confidence_score: 0.8,
695 coverage_score: 0.8,
696 },
697 query_intent,
698 lsp_response: None,
699 total_time_ms: pipeline_result.metrics.avg_latency_ms as u64,
700 sla_compliant: pipeline_result.metrics.avg_latency_ms < self.config.sla_target_ms as f64,
701 })
702 }
703
704 async fn search_direct(&self, request: SearchRequest, start_time: Instant) -> Result<SearchResponse> {
705 debug!("🔍 search_direct called with query: '{}'", request.query);
706 debug!("🔍 DEBUG: request.search_method = {:?}", request.search_method);
707 debug!("🔍 DEBUG: semantic_pipeline is_some = {}", self.semantic_pipeline.is_some());
708
709 let query_intent = QueryIntent::classify(&request.query);
710
711 let (lsp_response, text_results) = if request.enable_lsp && query_intent.is_lsp_eligible() {
713 let lsp_future = self.lsp_search(&request);
714 let text_future = self.text_search(&request);
715
716 let timeout_duration = Duration::from_millis(request.timeout_ms);
718
719 match tokio::time::timeout(timeout_duration, async {
720 tokio::try_join!(lsp_future, text_future)
721 }).await {
722 Ok(Ok((lsp_result, text_result))) => {
723 let lsp_resp = Some(lsp_result);
724 (lsp_resp, text_result)
725 }
726 Ok(Err(e)) => {
727 warn!("Search error: {}", e);
728 (None, vec![])
729 }
730 Err(_) => {
731 warn!("Search timeout exceeded: {}ms", request.timeout_ms);
732 (None, vec![])
733 }
734 }
735 } else {
736 let text_results = self.text_search(&request).await.unwrap_or_else(|_| vec![]);
738 (None, text_results)
739 };
740
741 let mut fused_results = self.fuse_search_results(text_results, lsp_response.as_ref(), &request).await;
743
744 debug!("🔍 DEBUG: After fusion - fused_results.len() = {}", fused_results.len());
745 debug!("🔍 DEBUG: Checking semantic condition: semantic_pipeline.is_some() = {}, search_method = {:?}",
746 self.semantic_pipeline.is_some(), request.search_method);
747
748 if let (Some(semantic_pipeline), Some(SearchMethod::ForceSemantic)) = (&self.semantic_pipeline, &request.search_method) {
750 debug!("🧠 Applying semantic reranking (RAPTOR) with ForceSemantic mode");
751
752 let initial_results: Vec<InitialSearchResult> = fused_results.iter().map(|result| {
754 InitialSearchResult {
755 id: format!("{}:{}:{}", result.file_path, result.line_number, result.column),
756 content: result.content.clone(),
757 file_path: result.file_path.clone(),
758 lexical_score: result.score as f32,
759 lsp_score: None,
760 metadata: std::collections::HashMap::new(),
761 }
762 }).collect();
763
764 if !initial_results.is_empty() || request.search_method == Some(SearchMethod::ForceSemantic) {
767 debug!("🔍 DEBUG: Proceeding with semantic search - initial_results.len() = {}, ForceSemantic = {}",
768 initial_results.len(), request.search_method == Some(SearchMethod::ForceSemantic));
769
770 let semantic_request = SemanticSearchRequest {
771 query: request.query.clone(),
772 initial_results,
773 query_type: format!("{:?}", query_intent),
774 language: request.language.clone(),
775 max_results: request.max_results,
776 enable_cross_encoder: true,
777 search_method: Some(SearchMethod::ForceSemantic),
778 };
779
780 match semantic_pipeline.search(semantic_request).await {
781 Ok(semantic_response) => {
782 info!("✅ Semantic reranking applied: {} results processed", semantic_response.results.len());
783
784 fused_results = semantic_response.results.into_iter().map(|semantic_result| {
786 SearchResult {
787 file_path: semantic_result.file_path,
788 line_number: 1, column: 0,
790 content: semantic_result.content,
791 score: semantic_result.final_score as f64,
792 result_type: SearchResultType::Semantic,
793 language: request.language.clone(),
794 context_lines: None,
795 lsp_metadata: None,
796 }
797 }).collect();
798 }
799 Err(e) => {
800 warn!("❌ Semantic reranking failed: {}", e);
801 }
803 }
804 }
805 } else {
806 debug!("🔍 DEBUG: Semantic reranking SKIPPED - semantic_pipeline: {}, search_method: {:?}",
807 self.semantic_pipeline.is_some(), request.search_method);
808 }
809
810 debug!("🔍 DEBUG: Final fused_results.len() = {}", fused_results.len());
811
812 let total_time = start_time.elapsed();
814 let lsp_time_ms = lsp_response.as_ref().map(|r| r.lsp_time_ms).unwrap_or(0) as u32;
815 let search_time_ms = (total_time.as_millis() as u32).saturating_sub(lsp_time_ms);
816
817 let metrics = SearchMetrics {
818 total_docs: self.get_total_docs().await,
819 matched_docs: fused_results.len() as u64,
820 duration_ms: total_time.as_millis() as u32,
821 lsp_time_ms,
822 lsp_results_count: lsp_response.as_ref().map(|r| r.lsp_results.len() as u32).unwrap_or(0),
823 lsp_cache_hit_rate: lsp_response.as_ref().map(|r| r.cache_hit_rate).unwrap_or(0.0),
824 search_time_ms,
825 fusion_time_ms: 5, sla_compliant: total_time.as_millis() <= self.config.sla_target_ms as u128,
827 result_diversity_score: self.calculate_diversity_score(&fused_results),
828 confidence_score: self.calculate_confidence_score(&fused_results, lsp_response.as_ref()),
829 coverage_score: self.calculate_coverage_score(&fused_results, &request.query),
830 };
831
832 self.update_engine_metrics(&metrics, lsp_response.is_some()).await;
834
835 let sla_compliant = metrics.sla_compliant;
836
837 Ok(SearchResponse {
838 results: fused_results,
839 metrics,
840 query_intent,
841 lsp_response,
842 total_time_ms: total_time.as_millis() as u64,
843 sla_compliant,
844 })
845 }
846
847 async fn lsp_search(&self, request: &SearchRequest) -> Result<LspSearchResponse> {
848 if let Some(lsp_state) = &self.lsp_state {
849 lsp_state.search(&request.query, request.file_path.as_deref()).await
850 } else {
851 Err(anyhow!("LSP not available"))
852 }
853 }
854
855 async fn text_search(&self, request: &SearchRequest) -> Result<Vec<SearchResult>> {
856 debug!("🔍 text_search called with query: '{}' -> max_results: {}", request.query, request.max_results);
857 let searcher = self.reader.searcher();
858
859 let total_docs = searcher.num_docs();
861 debug!("🔍 DEBUG: Index contains {} total documents", total_docs);
862
863 let sanitized_query = self.sanitize_query(&request.query);
865 debug!("🔍 DEBUG: Original query: '{}' -> Sanitized: '{}'", request.query, sanitized_query);
866
867 let all_query = tantivy::query::AllQuery;
869 let all_docs_test = searcher.search(&all_query, &tantivy::collector::TopDocs::with_limit(1));
870 match all_docs_test {
871 Ok(docs) => debug!("🔍 DEBUG: AllQuery test found {} documents", docs.len()),
872 Err(e) => debug!("🔍 DEBUG: AllQuery test failed: {}", e),
873 }
874
875 let query_parser = QueryParser::for_index(&self.index, vec![self.fields.content]);
877 debug!("🔍 DEBUG: Created query parser for content field");
878
879 let query = match query_parser.parse_query(&sanitized_query) {
880 Ok(q) => q,
881 Err(e) => {
882 warn!("Query parsing failed for '{}': {}. Using fallback term query.", sanitized_query, e);
884 let fallback_terms: Vec<&str> = sanitized_query
885 .split_whitespace()
886 .filter(|term| term.len() > 2)
887 .take(5) .collect();
889
890 if fallback_terms.is_empty() {
891 return Ok(vec![]); }
893
894 let fallback_query = fallback_terms.join(" ");
895 query_parser.parse_query(&fallback_query)
896 .unwrap_or_else(|_| {
897 query_parser.parse_query(&fallback_terms[0]).unwrap_or_else(|_| {
899 Box::new(tantivy::query::AllQuery)
901 })
902 })
903 }
904 };
905
906 debug!("🔍 DEBUG: Executing search with query, max_results = {}", request.max_results);
908 let top_docs = searcher.search(&query, &TopDocs::with_limit(request.max_results))?;
909 debug!("🔍 DEBUG: Search completed, found {} document matches", top_docs.len());
910
911 let mut results = Vec::new();
912 for (score, doc_address) in top_docs {
913 let retrieved_doc: tantivy::TantivyDocument = searcher.doc(doc_address)?;
914
915 let file_path = retrieved_doc
916 .get_first(self.fields.file_path)
917 .map(|f| match f {
918 tantivy::schema::OwnedValue::Str(s) => s.clone(),
919 _ => "unknown".to_string(),
920 })
921 .unwrap_or_else(|| "unknown".to_string());
922
923 let content = retrieved_doc
924 .get_first(self.fields.content)
925 .map(|f| match f {
926 tantivy::schema::OwnedValue::Str(s) => s.clone(),
927 _ => "".to_string(),
928 })
929 .unwrap_or_else(|| "".to_string());
930
931 let line_number = retrieved_doc
932 .get_first(self.fields.line_number)
933 .and_then(|f| match f {
934 tantivy::schema::OwnedValue::U64(n) => Some(*n),
935 _ => None,
936 })
937 .unwrap_or(0) as u32;
938
939 let language = retrieved_doc
941 .get_first(self.fields.language)
942 .map(|f| match f {
943 tantivy::schema::OwnedValue::Str(s) => s.clone(),
944 tantivy::schema::OwnedValue::Facet(facet) => facet.to_path().iter().last().unwrap_or(&"unknown").to_string(),
945 _ => "unknown".to_string(),
946 });
947
948 let context_lines = if request.include_context {
950 Some(self.get_context_lines(&file_path, line_number, self.config.context_lines).await)
951 } else {
952 None
953 };
954
955 results.push(SearchResult {
956 file_path,
957 line_number,
958 column: 0, content,
960 score: score as f64,
961 result_type: SearchResultType::TextMatch,
962 language,
963 context_lines,
964 lsp_metadata: None,
965 });
966 }
967
968 debug!("🔍 DEBUG: text_search returning {} results", results.len());
969 Ok(results)
970 }
971
972 async fn fuse_search_results(
973 &self,
974 text_results: Vec<SearchResult>,
975 lsp_response: Option<&LspSearchResponse>,
976 request: &SearchRequest,
977 ) -> Vec<SearchResult> {
978 let mut fused_results = Vec::new();
979
980 if let Some(lsp_resp) = lsp_response {
982 for lsp_result in &lsp_resp.lsp_results {
983 fused_results.push(SearchResult {
984 file_path: lsp_result.file_path.clone(),
985 line_number: lsp_result.line_number,
986 column: lsp_result.column,
987 content: lsp_result.content.clone(),
988 score: lsp_result.confidence,
989 result_type: match lsp_result.hint_type {
990 crate::lsp::HintType::Definition => SearchResultType::Definition,
991 crate::lsp::HintType::References => SearchResultType::Reference,
992 crate::lsp::HintType::TypeDefinition => SearchResultType::TypeInfo,
993 crate::lsp::HintType::Implementation => SearchResultType::Implementation,
994 crate::lsp::HintType::Symbol => SearchResultType::Symbol,
995 _ => SearchResultType::TextMatch,
996 },
997 language: Some(format!("{:?}", lsp_result.server_type)),
998 context_lines: lsp_result.context_lines.clone(),
999 lsp_metadata: Some(LspMetadata {
1000 hint_type: format!("{:?}", lsp_result.hint_type),
1001 server_type: format!("{:?}", lsp_result.server_type),
1002 confidence: lsp_result.confidence,
1003 cached: lsp_resp.cache_hit_rate > 0.0,
1004 }),
1005 });
1006 }
1007 }
1008
1009 for text_result in text_results {
1011 let is_duplicate = fused_results.iter().any(|existing| {
1013 existing.file_path == text_result.file_path &&
1014 existing.line_number == text_result.line_number
1015 });
1016
1017 if !is_duplicate {
1018 fused_results.push(text_result);
1019 }
1020 }
1021
1022 fused_results.sort_by(|a, b| b.score.partial_cmp(&a.score).unwrap_or(std::cmp::Ordering::Equal));
1024
1025 fused_results.truncate(request.max_results);
1027
1028 fused_results
1029 }
1030
1031 async fn get_context_lines(&self, file_path: &str, line_number: u32, context_size: usize) -> Vec<String> {
1032 let start_line = line_number.saturating_sub(context_size as u32);
1034 let end_line = line_number + context_size as u32;
1035
1036 (start_line..=end_line)
1038 .map(|i| format!("Context line {}", i))
1039 .collect()
1040 }
1041
1042 fn calculate_diversity_score(&self, results: &[SearchResult]) -> f64 {
1043 if results.is_empty() {
1044 return 0.0;
1045 }
1046
1047 let unique_files: std::collections::HashSet<_> = results.iter().map(|r| &r.file_path).collect();
1049 let unique_types: std::collections::HashSet<_> = results.iter().map(|r| &r.result_type).collect();
1050
1051 let file_diversity = unique_files.len() as f64 / results.len() as f64;
1052 let type_diversity = unique_types.len() as f64 / 7.0; (file_diversity + type_diversity) / 2.0
1055 }
1056
1057 fn calculate_confidence_score(&self, results: &[SearchResult], lsp_response: Option<&LspSearchResponse>) -> f64 {
1058 if results.is_empty() {
1059 return 0.0;
1060 }
1061
1062 let avg_score = results.iter().map(|r| r.score).sum::<f64>() / results.len() as f64;
1063 let lsp_boost = lsp_response.map(|r| r.cache_hit_rate * 0.1).unwrap_or(0.0);
1064
1065 (avg_score + lsp_boost).min(1.0)
1066 }
1067
1068 fn calculate_coverage_score(&self, results: &[SearchResult], query: &str) -> f64 {
1069 if results.is_empty() {
1070 return 0.0;
1071 }
1072
1073 let query_terms: Vec<&str> = query.split_whitespace().collect();
1075 if query_terms.is_empty() {
1076 return 0.5;
1077 }
1078
1079 let covered_terms = results.iter()
1080 .flat_map(|r| r.content.split_whitespace())
1081 .collect::<std::collections::HashSet<_>>();
1082
1083 let coverage = query_terms.iter()
1084 .filter(|term| covered_terms.contains(&term.to_lowercase().as_str()))
1085 .count() as f64 / query_terms.len() as f64;
1086
1087 coverage
1088 }
1089
1090 async fn get_total_docs(&self) -> u64 {
1091 self.reader.searcher().num_docs() as u64
1092 }
1093
1094 async fn update_engine_metrics(&self, search_metrics: &SearchMetrics, lsp_routed: bool) {
1095 let mut metrics = self.metrics.write().await;
1096
1097 metrics.total_searches += 1;
1098
1099 if search_metrics.sla_compliant {
1100 metrics.sla_compliant_searches += 1;
1101 }
1102
1103 if lsp_routed {
1104 metrics.lsp_routed_searches += 1;
1105 }
1106
1107 let duration = search_metrics.duration_ms as f64;
1109 let total = metrics.total_searches as f64;
1110
1111 metrics.avg_latency_ms = (metrics.avg_latency_ms * (total - 1.0) + duration) / total;
1112
1113 if search_metrics.duration_ms as u64 > metrics.p95_latency_ms {
1115 metrics.p95_latency_ms = search_metrics.duration_ms as u64;
1116 }
1117 if search_metrics.duration_ms as u64 > metrics.p99_latency_ms {
1118 metrics.p99_latency_ms = search_metrics.duration_ms as u64;
1119 }
1120
1121 metrics.lsp_search_time_ms = (metrics.lsp_search_time_ms * (total - 1.0) + search_metrics.lsp_time_ms as f64) / total;
1123 metrics.text_search_time_ms = (metrics.text_search_time_ms * (total - 1.0) + search_metrics.search_time_ms as f64) / total;
1124 metrics.fusion_time_ms = (metrics.fusion_time_ms * (total - 1.0) + search_metrics.fusion_time_ms as f64) / total;
1125 }
1126
1127 pub async fn get_metrics(&self) -> EngineMetrics {
1129 self.metrics.read().await.clone()
1130 }
1131
1132 pub async fn index_document(&self, doc: &SearchDocument) -> Result<()> {
1134 let mut index_writer = self.index.writer(50_000_000)?; let mut tantivy_doc = tantivy::doc!();
1137 tantivy_doc.add_text(self.fields.file_path, &doc.file_path);
1138 tantivy_doc.add_text(self.fields.content, &doc.content);
1139 tantivy_doc.add_u64(self.fields.line_number, doc.line_number as u64);
1140
1141 if let Some(lang) = &doc.language {
1142 tantivy_doc.add_facet(self.fields.language, lang);
1143 }
1144
1145 tantivy_doc.add_bytes(self.fields.raw_content, doc.content.as_bytes());
1146
1147 index_writer.add_document(tantivy_doc)?;
1148 index_writer.commit()?;
1149
1150 Ok(())
1151 }
1152
1153 pub async fn shutdown(&self) -> Result<()> {
1155 info!("Shutting down enhanced search engine");
1156
1157 if let Some(lsp_state) = &self.lsp_state {
1159 lsp_state.shutdown().await?;
1160 }
1161
1162 if let Some(pipeline) = &self.pipeline {
1164 pipeline.shutdown().await?;
1165 }
1166
1167 info!("Enhanced search engine shutdown complete");
1168 Ok(())
1169 }
1170
1171 pub async fn get_current_dataset(&self) -> Option<Arc<crate::benchmark::PinnedDataset>> {
1175 self.current_dataset.read().await.clone()
1176 }
1177
1178 pub async fn load_dataset_version(&self, version: &str) -> Result<()> {
1180 if let Some(ref loader) = self.dataset_loader {
1181 match loader.load_pinned_dataset_version(version).await {
1182 Ok(dataset) => {
1183 info!("✅ Loaded pinned dataset version: {} ({} queries)",
1184 version, dataset.queries.len());
1185
1186 if self.config.enable_corpus_validation {
1188 match loader.validate_dataset_consistency(&dataset).await {
1189 Ok(validation_result) => {
1190 let consistency_rate = validation_result.valid_queries as f64
1191 / validation_result.total_queries as f64 * 100.0;
1192
1193 if validation_result.is_consistent {
1194 info!("✅ Dataset corpus consistency: {}/{} queries (100%)",
1195 validation_result.valid_queries, validation_result.total_queries);
1196 } else {
1197 warn!("⚠️ Partial dataset corpus consistency: {}/{} queries ({:.1}%)",
1198 validation_result.valid_queries, validation_result.total_queries, consistency_rate);
1199 }
1200 }
1201 Err(e) => warn!("⚠️ Dataset validation failed: {}", e),
1202 }
1203 }
1204
1205 *self.current_dataset.write().await = Some(Arc::new(dataset));
1207 Ok(())
1208 }
1209 Err(e) => Err(anyhow!("Failed to load dataset version {}: {}", version, e)),
1210 }
1211 } else {
1212 Err(anyhow!("Pinned dataset support not initialized"))
1213 }
1214 }
1215
1216 pub async fn reload_current_dataset(&self) -> Result<()> {
1218 if let Some(ref loader) = self.dataset_loader {
1219 match loader.load_current_pinned_dataset().await {
1220 Ok(dataset) => {
1221 info!("🔄 Reloaded current pinned dataset: version {} ({} queries)",
1222 dataset.metadata.version, dataset.queries.len());
1223
1224 *self.current_dataset.write().await = Some(Arc::new(dataset));
1225 Ok(())
1226 }
1227 Err(e) => Err(anyhow!("Failed to reload current dataset: {}", e)),
1228 }
1229 } else {
1230 Err(anyhow!("Pinned dataset support not initialized"))
1231 }
1232 }
1233
1234 pub async fn get_dataset_info(&self) -> Option<DatasetInfo> {
1236 if let Some(dataset) = self.get_current_dataset().await {
1237 Some(DatasetInfo {
1238 version: dataset.metadata.version.clone(),
1239 name: dataset.metadata.name.clone(),
1240 total_queries: dataset.metadata.total_queries,
1241 created_at: dataset.metadata.created_at,
1242 languages: dataset.metadata.languages.clone(),
1243 query_distribution: dataset.metadata.query_distribution.clone(),
1244 })
1245 } else {
1246 None
1247 }
1248 }
1249
1250 pub async fn list_dataset_versions(&self) -> Result<Vec<crate::benchmark::DatasetVersion>> {
1252 if let Some(ref loader) = self.dataset_loader {
1253 loader.list_available_versions().await
1254 } else {
1255 Err(anyhow!("Pinned dataset support not initialized"))
1256 }
1257 }
1258
1259 pub async fn get_smoke_dataset(&self) -> Option<Vec<crate::benchmark::GoldenQuery>> {
1261 if let (Some(ref loader), Some(dataset)) = (&self.dataset_loader, self.get_current_dataset().await) {
1262 Some(loader.get_smoke_dataset(&dataset))
1263 } else {
1264 None
1265 }
1266 }
1267
1268 pub async fn get_dataset_slice(&self, slice_name: &str) -> Option<Vec<crate::benchmark::GoldenQuery>> {
1270 if let (Some(ref loader), Some(dataset)) = (&self.dataset_loader, self.get_current_dataset().await) {
1271 loader.get_dataset_slice(&dataset, slice_name)
1272 } else {
1273 None
1274 }
1275 }
1276
1277 pub fn has_dataset_support(&self) -> bool {
1279 self.dataset_loader.is_some()
1280 }
1281
1282 pub async fn validate_current_dataset(&self) -> Result<crate::benchmark::ValidationResult> {
1284 if let Some(ref loader) = self.dataset_loader {
1285 if let Some(dataset) = self.get_current_dataset().await {
1286 loader.validate_dataset_consistency(&dataset).await
1287 } else {
1288 Err(anyhow!("No dataset currently loaded"))
1289 }
1290 } else {
1291 Err(anyhow!("Pinned dataset support not initialized"))
1292 }
1293 }
1294}
1295
1296#[derive(Debug, Clone, Serialize, Deserialize)]
1298pub struct DatasetInfo {
1299 pub version: String,
1300 pub name: String,
1301 pub total_queries: usize,
1302 pub created_at: chrono::DateTime<chrono::Utc>,
1303 pub languages: Vec<String>,
1304 pub query_distribution: std::collections::HashMap<crate::benchmark::QueryType, usize>,
1305}
1306
1307#[derive(Debug, Clone)]
1309pub struct SearchDocument {
1310 pub file_path: String,
1311 pub content: String,
1312 pub line_number: u32,
1313 pub language: Option<String>,
1314}
1315
1316#[cfg(test)]
1317mod tests {
1318 use super::*;
1319 use tempfile::TempDir;
1320 use std::collections::HashMap;
1321
1322 async fn create_test_engine() -> (SearchEngine, TempDir) {
1323 let temp_dir = TempDir::new().unwrap();
1324 let index_path = temp_dir.path().join("test_index");
1325
1326 let config = SearchConfig {
1327 index_path: index_path.to_string_lossy().to_string(),
1328 max_results_default: 100,
1329 sla_target_ms: 2000,
1330 lsp_routing_rate: 0.0,
1331 enable_fusion_pipeline: false,
1332 enable_semantic_search: false,
1333 enable_lsp: false,
1334 context_lines: 3,
1335 dataset_path: "test_dataset".to_string(),
1336 enable_pinned_datasets: false,
1337 default_dataset_version: None,
1338 enable_corpus_validation: false,
1339 };
1340
1341 let engine = SearchEngine::with_config(&index_path, config).await.unwrap();
1342 (engine, temp_dir)
1343 }
1344
1345 #[tokio::test]
1346 async fn test_search_engine_creation() {
1347 let temp_dir = TempDir::new().unwrap();
1348 let index_path = temp_dir.path().join("test_index");
1349
1350 if index_path.exists() {
1352 std::fs::remove_dir_all(&index_path).unwrap();
1353 }
1354
1355 let config = SearchConfig {
1357 index_path: index_path.to_string_lossy().to_string(),
1358 max_results_default: 100,
1359 sla_target_ms: 2000,
1360 lsp_routing_rate: 0.0, enable_fusion_pipeline: false,
1362 enable_semantic_search: false,
1363 enable_lsp: false,
1364 context_lines: 3,
1365 dataset_path: "test_dataset".to_string(),
1367 enable_pinned_datasets: false,
1368 default_dataset_version: None,
1369 enable_corpus_validation: false,
1370 };
1371
1372 let engine = SearchEngine::with_config(&index_path, config).await;
1373 match engine {
1374 Ok(_) => {
1375 println!("✅ SearchEngine created successfully");
1376 },
1377 Err(e) => panic!("SearchEngine creation failed: {:?}", e),
1378 }
1379 }
1380
1381 #[tokio::test]
1382 async fn test_search_engine_with_custom_config() {
1383 let temp_dir = TempDir::new().unwrap();
1384 let config = SearchConfig {
1385 index_path: temp_dir.path().to_string_lossy().to_string(),
1386 max_results_default: 25,
1387 sla_target_ms: 100,
1388 lsp_routing_rate: 0.0,
1389 enable_fusion_pipeline: false,
1390 enable_semantic_search: false,
1391 enable_lsp: false,
1392 context_lines: 5,
1393 dataset_path: "custom_dataset".to_string(),
1394 enable_pinned_datasets: false,
1395 default_dataset_version: Some("v1.0".to_string()),
1396 enable_corpus_validation: true,
1397 };
1398
1399 let engine = SearchEngine::with_config(&config.index_path, config.clone()).await.unwrap();
1400 assert_eq!(engine.config.max_results_default, 25);
1401 assert_eq!(engine.config.sla_target_ms, 100);
1402 assert_eq!(engine.config.context_lines, 5);
1403 assert!(engine.config.enable_corpus_validation);
1404 }
1405
1406 #[tokio::test]
1407 async fn test_search_request_default() {
1408 let request = SearchRequest::default();
1409 assert_eq!(request.max_results, 50);
1410 assert_eq!(request.timeout_ms, 150);
1411 assert!(request.enable_lsp);
1412 assert!(request.include_context);
1413 assert_eq!(request.search_method, Some(SearchMethod::Hybrid));
1414 assert_eq!(request.search_types.len(), 4);
1415 }
1416
1417 #[tokio::test]
1418 async fn test_search_request_custom() {
1419 let request = SearchRequest {
1420 query: "test query".to_string(),
1421 file_path: Some("test.rs".to_string()),
1422 language: Some("rust".to_string()),
1423 max_results: 100,
1424 include_context: false,
1425 timeout_ms: 300,
1426 enable_lsp: false,
1427 search_types: vec![SearchResultType::TextMatch, SearchResultType::Symbol],
1428 search_method: Some(SearchMethod::Lexical),
1429 };
1430
1431 assert_eq!(request.query, "test query");
1432 assert_eq!(request.file_path, Some("test.rs".to_string()));
1433 assert_eq!(request.language, Some("rust".to_string()));
1434 assert_eq!(request.max_results, 100);
1435 assert!(!request.include_context);
1436 assert_eq!(request.timeout_ms, 300);
1437 assert!(!request.enable_lsp);
1438 assert_eq!(request.search_types.len(), 2);
1439 assert_eq!(request.search_method, Some(SearchMethod::Lexical));
1440 }
1441
1442 #[test]
1443 fn test_search_result_types() {
1444 let result = SearchResult {
1445 file_path: "test.rs".to_string(),
1446 line_number: 10,
1447 column: 5,
1448 content: "fn test()".to_string(),
1449 score: 0.9,
1450 result_type: SearchResultType::Definition,
1451 language: Some("rust".to_string()),
1452 context_lines: None,
1453 lsp_metadata: None,
1454 };
1455
1456 assert_eq!(result.result_type, SearchResultType::Definition);
1457 assert_eq!(result.language, Some("rust".to_string()));
1458 }
1459
1460 #[test]
1461 fn test_search_result_types_ordering() {
1462 let mut types = vec![
1463 SearchResultType::TextMatch,
1464 SearchResultType::Reference,
1465 SearchResultType::Definition,
1466 SearchResultType::TypeInfo,
1467 SearchResultType::Symbol,
1468 ];
1469
1470 types.sort();
1471
1472 assert_ne!(types[0], types[1]);
1474 assert!(SearchResultType::TextMatch < SearchResultType::Definition);
1475 }
1476
1477 #[test]
1478 fn test_search_method_default() {
1479 assert_eq!(SearchMethod::default(), SearchMethod::Hybrid);
1480 }
1481
1482 #[test]
1483 fn test_search_method_variants() {
1484 let methods = vec![
1485 SearchMethod::Lexical,
1486 SearchMethod::Structural,
1487 SearchMethod::Semantic,
1488 SearchMethod::Hybrid,
1489 SearchMethod::ForceSemantic,
1490 ];
1491
1492 for method in &methods {
1493 let cloned = method.clone();
1495 assert_eq!(method, &cloned);
1496 }
1497 }
1498
1499 #[test]
1500 fn test_search_metrics_sla_compliance() {
1501 let metrics = SearchMetrics {
1502 duration_ms: 100,
1503 sla_compliant: true,
1504 ..Default::default()
1505 };
1506
1507 assert!(metrics.meets_sla(150));
1508 assert!(!metrics.meets_sla(50));
1509 }
1510
1511 #[test]
1512 fn test_search_metrics_quality_score() {
1513 let metrics = SearchMetrics {
1514 result_diversity_score: 0.8,
1515 confidence_score: 0.9,
1516 coverage_score: 0.7,
1517 ..Default::default()
1518 };
1519
1520 let quality = metrics.quality_score();
1521 assert!((quality - 0.8).abs() < 0.01); }
1523
1524 #[test]
1525 fn test_search_metrics_default() {
1526 let metrics = SearchMetrics::default();
1527 assert_eq!(metrics.total_docs, 0);
1528 assert_eq!(metrics.matched_docs, 0);
1529 assert_eq!(metrics.duration_ms, 0);
1530 assert_eq!(metrics.lsp_time_ms, 0);
1531 assert_eq!(metrics.result_diversity_score, 0.0);
1532 }
1533
1534 #[test]
1535 fn test_search_config_default() {
1536 let config = SearchConfig::default();
1537 assert_eq!(config.index_path, "./index");
1538 assert_eq!(config.max_results_default, 50);
1539 assert_eq!(config.sla_target_ms, 150);
1540 assert_eq!(config.lsp_routing_rate, 0.5);
1541 assert!(config.enable_fusion_pipeline);
1542 assert!(!config.enable_semantic_search);
1543 assert!(config.enable_lsp);
1544 assert_eq!(config.context_lines, 3);
1545 assert!(config.enable_pinned_datasets);
1546 assert!(config.enable_corpus_validation);
1547 }
1548
1549 #[test]
1550 fn test_engine_metrics_default() {
1551 let metrics = EngineMetrics::default();
1552 assert_eq!(metrics.total_searches, 0);
1553 assert_eq!(metrics.sla_compliant_searches, 0);
1554 assert_eq!(metrics.lsp_routed_searches, 0);
1555 assert_eq!(metrics.avg_latency_ms, 0.0);
1556 assert_eq!(metrics.p95_latency_ms, 0);
1557 assert_eq!(metrics.p99_latency_ms, 0);
1558 }
1559
1560 #[test]
1561 fn test_engine_metrics_sla_compliance_rate() {
1562 let mut metrics = EngineMetrics::default();
1563
1564 assert_eq!(metrics.sla_compliance_rate(), 0.0);
1566
1567 metrics.total_searches = 100;
1569 metrics.sla_compliant_searches = 90;
1570 assert_eq!(metrics.sla_compliance_rate(), 0.9);
1571
1572 metrics.sla_compliant_searches = 100;
1574 assert_eq!(metrics.sla_compliance_rate(), 1.0);
1575 }
1576
1577 #[test]
1578 fn test_engine_metrics_lsp_routing_rate() {
1579 let mut metrics = EngineMetrics::default();
1580
1581 assert_eq!(metrics.lsp_routing_rate(), 0.0);
1583
1584 metrics.total_searches = 100;
1586 metrics.lsp_routed_searches = 50;
1587 assert_eq!(metrics.lsp_routing_rate(), 0.5);
1588
1589 metrics.lsp_routed_searches = 100;
1591 assert_eq!(metrics.lsp_routing_rate(), 1.0);
1592 }
1593
1594 #[test]
1595 fn test_lsp_metadata_creation() {
1596 let metadata = LspMetadata {
1597 hint_type: "Definition".to_string(),
1598 server_type: "rust-analyzer".to_string(),
1599 confidence: 0.95,
1600 cached: true,
1601 };
1602
1603 assert_eq!(metadata.hint_type, "Definition");
1604 assert_eq!(metadata.server_type, "rust-analyzer");
1605 assert_eq!(metadata.confidence, 0.95);
1606 assert!(metadata.cached);
1607 }
1608
1609 #[test]
1610 fn test_search_document_creation() {
1611 let doc = SearchDocument {
1612 file_path: "src/main.rs".to_string(),
1613 content: "fn main() { println!(\"Hello\"); }".to_string(),
1614 line_number: 1,
1615 language: Some("rust".to_string()),
1616 };
1617
1618 assert_eq!(doc.file_path, "src/main.rs");
1619 assert_eq!(doc.line_number, 1);
1620 assert_eq!(doc.language, Some("rust".to_string()));
1621 assert!(doc.content.contains("main"));
1622 }
1623
1624 #[tokio::test]
1625 async fn test_query_sanitization_comprehensive() {
1626 let (engine, _temp_dir) = create_test_engine().await;
1627
1628 let test_cases = vec![
1630 ("```rust fn test() ```", "rust fn test"),
1632 ("**bold** text", "bold text"),
1633 ("__underline__ text", "underline text"),
1634 ("<!-- comment -->", "comment"),
1635 ("### Header", "Header"),
1636
1637 ("(test)", "test"),
1639 ("[array]", "array"),
1640 ("{object}", "object"),
1641 ("\"quoted\"", "quoted"),
1642 ("'single'", "single"),
1643 ("test+more", "test more"),
1644 ("test-dash", "test dash"),
1645 ("test!excl", "test excl"),
1646 ("test?quest", "test quest"),
1647 ("test:colon", "test colon"),
1648 ("test;semi", "test semi"),
1649 ("test#hash", "test hash"),
1650 ("test@at", "test at"),
1651 ("test$dollar", "test dollar"),
1652 ("test%percent", "test percent"),
1653 ("test^caret", "test caret"),
1654 ("test&", "test amp"),
1655 ("test*star", "test star"),
1656 ("test=equal", "test equal"),
1657 ("test|pipe", "test pipe"),
1658 ("test\\back", "test back"),
1659 ("test/slash", "test slash"),
1660 ("test<less", "test less"),
1661 ("test>greater", "test greater"),
1662 ("test.dot", "test dot"),
1663 ("test,comma", "test comma"),
1664 ("test~tilde", "test tilde"),
1665 ("test`back", "test back"),
1666
1667 ("test with spaces", "test with spaces"),
1669 (" leading and trailing ", "leading and trailing"),
1670
1671 ("", ""),
1673 (" ", ""),
1674 ("!!!", ""),
1675 ];
1676
1677 for (input, expected_contains) in test_cases {
1678 let sanitized = engine.sanitize_query(input);
1679
1680 if !expected_contains.is_empty() {
1681 for word in expected_contains.split_whitespace() {
1682 assert!(
1683 sanitized.contains(word),
1684 "Sanitized query '{}' should contain '{}' (from input '{}')",
1685 sanitized, word, input
1686 );
1687 }
1688 }
1689
1690 let problem_chars = ['(', ')', '[', ']', '{', '}', '"', '\'', '+', '!'];
1692 for char in &problem_chars {
1693 assert!(
1694 !sanitized.contains(*char),
1695 "Sanitized query '{}' still contains problematic character '{}'",
1696 sanitized, char
1697 );
1698 }
1699 }
1700 }
1701
1702 #[tokio::test]
1703 async fn test_search_with_empty_query() {
1704 let (engine, _temp_dir) = create_test_engine().await;
1705
1706 let request = SearchRequest {
1707 query: "".to_string(),
1708 max_results: 10,
1709 ..Default::default()
1710 };
1711
1712 let response = engine.search_comprehensive(request).await.unwrap();
1714 assert!(response.results.is_empty());
1715 }
1716
1717 #[tokio::test]
1718 async fn test_search_with_whitespace_only_query() {
1719 let (engine, _temp_dir) = create_test_engine().await;
1720
1721 let request = SearchRequest {
1722 query: " \t\n ".to_string(),
1723 max_results: 10,
1724 ..Default::default()
1725 };
1726
1727 let response = engine.search_comprehensive(request).await.unwrap();
1729 assert!(response.results.is_empty());
1730 }
1731
1732 #[tokio::test]
1733 async fn test_search_with_special_characters_query() {
1734 let (engine, _temp_dir) = create_test_engine().await;
1735
1736 let request = SearchRequest {
1737 query: "!@#$%^&*()_+-={}[]|\\:;\"'<>?,./".to_string(),
1738 max_results: 10,
1739 ..Default::default()
1740 };
1741
1742 let response = engine.search_comprehensive(request).await.unwrap();
1744 assert!(response.metrics.duration_ms < 10000); }
1747
1748 #[tokio::test]
1749 async fn test_search_basic_functionality() {
1750 let (engine, _temp_dir) = create_test_engine().await;
1751
1752 let request = SearchRequest {
1753 query: "fn".to_string(),
1754 max_results: 10,
1755 enable_lsp: false,
1756 ..Default::default()
1757 };
1758
1759 let response = engine.search_comprehensive(request).await.unwrap();
1760
1761 assert!(response.metrics.duration_ms < 5000); assert!(response.metrics.total_docs > 0); }
1765
1766 #[tokio::test]
1767 async fn test_search_with_different_methods() {
1768 let (engine, _temp_dir) = create_test_engine().await;
1769
1770 let methods = vec![
1771 SearchMethod::Lexical,
1772 SearchMethod::Structural,
1773 SearchMethod::Semantic,
1774 SearchMethod::Hybrid,
1775 ];
1776
1777 for method in methods {
1778 let request = SearchRequest {
1779 query: "search".to_string(),
1780 max_results: 5,
1781 search_method: Some(method.clone()),
1782 enable_lsp: false,
1783 ..Default::default()
1784 };
1785
1786 let response = engine.search_comprehensive(request).await.unwrap();
1787
1788 assert!(response.metrics.duration_ms < 10000);
1790 println!("✅ Search method {:?} completed in {}ms", method, response.metrics.duration_ms);
1791 }
1792 }
1793
1794 #[tokio::test]
1795 async fn test_search_timeout_behavior() {
1796 let (engine, _temp_dir) = create_test_engine().await;
1797
1798 let request = SearchRequest {
1799 query: "test".to_string(),
1800 max_results: 1000, timeout_ms: 1, enable_lsp: false,
1803 ..Default::default()
1804 };
1805
1806 let response = engine.search_comprehensive(request).await.unwrap();
1807
1808 assert!(response.total_time_ms < 5000); }
1811
1812 #[tokio::test]
1813 async fn test_search_result_limits() {
1814 let (engine, _temp_dir) = create_test_engine().await;
1815
1816 let request = SearchRequest {
1817 query: "test".to_string(),
1818 max_results: 3,
1819 enable_lsp: false,
1820 ..Default::default()
1821 };
1822
1823 let response = engine.search_comprehensive(request).await.unwrap();
1824
1825 assert!(response.results.len() <= 3);
1827 }
1828
1829 #[tokio::test]
1830 async fn test_engine_metrics_tracking() {
1831 let (engine, _temp_dir) = create_test_engine().await;
1832
1833 let request = SearchRequest {
1835 query: "test".to_string(),
1836 max_results: 5,
1837 enable_lsp: false,
1838 ..Default::default()
1839 };
1840
1841 let _ = engine.search_comprehensive(request).await.unwrap();
1842
1843 let metrics = engine.get_metrics().await;
1844
1845 assert!(metrics.total_searches > 0);
1847 assert!(metrics.avg_latency_ms >= 0.0);
1848 }
1849
1850 #[tokio::test]
1851 async fn test_index_document() {
1852 let (engine, _temp_dir) = create_test_engine().await;
1853
1854 let doc = SearchDocument {
1855 file_path: "test.rs".to_string(),
1856 content: "fn test_function() { return 42; }".to_string(),
1857 line_number: 10,
1858 language: None, };
1860
1861 let result = engine.index_document(&doc).await;
1863 assert!(result.is_ok());
1864 }
1865
1866 #[tokio::test]
1867 async fn test_shutdown_gracefully() {
1868 let (engine, _temp_dir) = create_test_engine().await;
1869
1870 let result = engine.shutdown().await;
1872 assert!(result.is_ok());
1873 }
1874
1875 #[tokio::test]
1876 async fn test_get_total_docs() {
1877 let (engine, _temp_dir) = create_test_engine().await;
1878
1879 let total = engine.get_total_docs().await;
1880
1881 assert!(total >= 0);
1883 }
1884
1885 #[tokio::test]
1886 async fn test_dataset_support_when_disabled() {
1887 let (engine, _temp_dir) = create_test_engine().await;
1888
1889 assert!(!engine.has_dataset_support());
1891
1892 let dataset_info = engine.get_dataset_info().await;
1893 assert!(dataset_info.is_none());
1894 }
1895
1896 #[tokio::test]
1897 async fn test_calculate_diversity_score() {
1898 let (engine, _temp_dir) = create_test_engine().await;
1899
1900 let empty_results = vec![];
1902 let score = engine.calculate_diversity_score(&empty_results);
1903 assert_eq!(score, 0.0);
1904
1905 let diverse_results = vec![
1907 SearchResult {
1908 file_path: "file1.rs".to_string(),
1909 result_type: SearchResultType::Definition,
1910 ..Default::default()
1911 },
1912 SearchResult {
1913 file_path: "file2.rs".to_string(),
1914 result_type: SearchResultType::Reference,
1915 ..Default::default()
1916 },
1917 SearchResult {
1918 file_path: "file1.rs".to_string(),
1919 result_type: SearchResultType::TextMatch,
1920 ..Default::default()
1921 },
1922 ];
1923
1924 let score = engine.calculate_diversity_score(&diverse_results);
1925 assert!(score > 0.0);
1926 assert!(score <= 1.0);
1927 }
1928
1929 #[tokio::test]
1930 async fn test_calculate_confidence_score() {
1931 let (engine, _temp_dir) = create_test_engine().await;
1932
1933 let empty_results = vec![];
1935 let score = engine.calculate_confidence_score(&empty_results, None);
1936 assert_eq!(score, 0.0);
1937
1938 let high_score_results = vec![
1940 SearchResult {
1941 score: 0.9,
1942 ..Default::default()
1943 },
1944 SearchResult {
1945 score: 0.8,
1946 ..Default::default()
1947 },
1948 ];
1949
1950 let score = engine.calculate_confidence_score(&high_score_results, None);
1951 assert!(score > 0.8);
1952 assert!(score <= 1.0);
1953 }
1954
1955 #[tokio::test]
1956 async fn test_calculate_coverage_score() {
1957 let (engine, _temp_dir) = create_test_engine().await;
1958
1959 let empty_results = vec![];
1961 let score = engine.calculate_coverage_score(&empty_results, "test query");
1962 assert_eq!(score, 0.0);
1963
1964 let matching_results = vec![
1966 SearchResult {
1967 content: "test function implementation".to_string(),
1968 ..Default::default()
1969 },
1970 ];
1971
1972 let score = engine.calculate_coverage_score(&matching_results, "test function");
1973 assert!(score > 0.0);
1974 assert!(score <= 1.0);
1975 }
1976
1977 impl Default for SearchResult {
1978 fn default() -> Self {
1979 Self {
1980 file_path: "default.rs".to_string(),
1981 line_number: 1,
1982 column: 0,
1983 content: "default content".to_string(),
1984 score: 0.5,
1985 result_type: SearchResultType::TextMatch,
1986 language: Some("rust".to_string()),
1987 context_lines: None,
1988 lsp_metadata: None,
1989 }
1990 }
1991 }
1992}
1993
1994#[cfg(test)]
1996mod search_regression_tests {
1997 use super::*;
1998 use std::collections::HashMap;
1999 use tempfile::TempDir;
2000
2001 async fn create_test_search_engine() -> Result<(SearchEngine, TempDir)> {
2002 let temp_dir = TempDir::new().unwrap();
2003 let index_path = temp_dir.path().to_str().unwrap();
2004
2005 let mut config = SearchConfig::default();
2006 config.index_path = index_path.to_string();
2007 config.enable_lsp = false; config.enable_pinned_datasets = false; let index_path_clone = config.index_path.clone();
2011 let engine = SearchEngine::with_config(&index_path_clone, config).await?;
2012 Ok((engine, temp_dir))
2013 }
2014
2015 #[tokio::test]
2016 async fn test_basic_search_functionality() {
2017 let (engine, _temp_dir) = create_test_search_engine().await.unwrap();
2018
2019 let test_cases = vec![
2021 ("struct", "Should find struct definitions"),
2022 ("impl", "Should find impl blocks"),
2023 ("fn", "Should find function definitions"),
2024 ("SearchEngine", "Should find SearchEngine references"),
2025 ("pub", "Should find public declarations"),
2026 ];
2027
2028 for (query, description) in test_cases {
2029 let request = SearchRequest {
2030 query: query.to_string(),
2031 max_results: 10,
2032 file_path: None,
2033 language: None,
2034 enable_lsp: false,
2035 include_context: false,
2036 timeout_ms: 1000,
2037 search_types: vec![SearchResultType::TextMatch],
2038 search_method: Some(SearchMethod::Lexical), };
2040
2041 let response = engine.search_comprehensive(request).await.unwrap();
2042
2043 assert!(
2045 !response.results.is_empty(),
2046 "REGRESSION FAILURE: Query '{}' returned 0 results. {}",
2047 query, description
2048 );
2049
2050 println!("✅ Query '{}': {} results", query, response.results.len());
2051 }
2052 }
2053
2054 #[tokio::test]
2055 async fn test_search_result_fusion_logic() {
2056 let (engine, _temp_dir) = create_test_search_engine().await.unwrap();
2057
2058 let text_results = vec![
2060 SearchResult {
2061 file_path: "test1.rs".to_string(),
2062 line_number: 10,
2063 column: 0,
2064 content: "fn test_function()".to_string(),
2065 score: 0.8,
2066 result_type: SearchResultType::TextMatch,
2067 language: Some("rust".to_string()),
2068 context_lines: None,
2069 lsp_metadata: None,
2070 },
2071 SearchResult {
2072 file_path: "test2.rs".to_string(),
2073 line_number: 20,
2074 column: 5,
2075 content: "struct TestStruct".to_string(),
2076 score: 0.6,
2077 result_type: SearchResultType::TextMatch,
2078 language: Some("rust".to_string()),
2079 context_lines: None,
2080 lsp_metadata: None,
2081 },
2082 ];
2083
2084 let request = SearchRequest {
2086 query: "test".to_string(),
2087 max_results: 10,
2088 ..Default::default()
2089 };
2090
2091 let fused_results = engine.fuse_search_results(text_results.clone(), None, &request).await;
2092
2093 assert_eq!(fused_results.len(), 2);
2095 assert!(fused_results[0].score >= fused_results[1].score); }
2097
2098 #[tokio::test]
2099 async fn test_search_with_file_path_filter() {
2100 let (engine, _temp_dir) = create_test_search_engine().await.unwrap();
2101
2102 let request = SearchRequest {
2103 query: "test".to_string(),
2104 file_path: Some("specific_file.rs".to_string()),
2105 max_results: 10,
2106 enable_lsp: false,
2107 ..Default::default()
2108 };
2109
2110 let response = engine.search_comprehensive(request).await.unwrap();
2111
2112 assert!(response.metrics.duration_ms < 5000);
2114 }
2115
2116 #[tokio::test]
2117 async fn test_search_with_language_filter() {
2118 let (engine, _temp_dir) = create_test_search_engine().await.unwrap();
2119
2120 let request = SearchRequest {
2121 query: "function".to_string(),
2122 language: Some("rust".to_string()),
2123 max_results: 10,
2124 enable_lsp: false,
2125 ..Default::default()
2126 };
2127
2128 let response = engine.search_comprehensive(request).await.unwrap();
2129
2130 assert!(response.metrics.duration_ms < 5000);
2132 }
2133
2134 #[tokio::test]
2135 async fn test_search_with_context_lines() {
2136 let (engine, _temp_dir) = create_test_search_engine().await.unwrap();
2137
2138 let request = SearchRequest {
2139 query: "test".to_string(),
2140 include_context: true,
2141 max_results: 5,
2142 enable_lsp: false,
2143 ..Default::default()
2144 };
2145
2146 let response = engine.search_comprehensive(request).await.unwrap();
2147
2148 assert!(response.metrics.duration_ms < 5000);
2150
2151 for result in &response.results {
2153 if let Some(ref context) = result.context_lines {
2155 assert!(context.len() <= 10); }
2157 }
2158 }
2159
2160 #[tokio::test]
2161 async fn test_search_sla_compliance_tracking() {
2162 let (engine, _temp_dir) = create_test_search_engine().await.unwrap();
2163
2164 let request = SearchRequest {
2165 query: "quick".to_string(),
2166 timeout_ms: 2000, max_results: 5,
2168 enable_lsp: false,
2169 ..Default::default()
2170 };
2171
2172 let response = engine.search_comprehensive(request).await.unwrap();
2173
2174 assert!(response.sla_compliant);
2176 assert!(response.metrics.sla_compliant);
2177 assert!(response.metrics.meets_sla(2000));
2178 }
2179
2180 #[tokio::test]
2181 async fn test_concurrent_search_operations() {
2182 let (engine, _temp_dir) = create_test_search_engine().await.unwrap();
2183 let engine = std::sync::Arc::new(engine);
2184
2185 let mut handles = vec![];
2186
2187 for i in 0..5 {
2189 let engine_clone = engine.clone();
2190 let handle = tokio::spawn(async move {
2191 let request = SearchRequest {
2192 query: format!("test{}", i),
2193 max_results: 5,
2194 enable_lsp: false,
2195 ..Default::default()
2196 };
2197
2198 engine_clone.search_comprehensive(request).await
2199 });
2200 handles.push(handle);
2201 }
2202
2203 let mut successful_searches = 0;
2205 for handle in handles {
2206 match handle.await {
2207 Ok(Ok(_)) => successful_searches += 1,
2208 Ok(Err(e)) => println!("Search failed: {}", e),
2209 Err(e) => println!("Join failed: {}", e),
2210 }
2211 }
2212
2213 assert!(successful_searches >= 3);
2215
2216 let final_metrics = engine.get_metrics().await;
2218 assert!(final_metrics.total_searches >= successful_searches as u64);
2219 }
2220
2221 #[tokio::test]
2222 async fn test_search_with_all_result_types() {
2223 let (engine, _temp_dir) = create_test_search_engine().await.unwrap();
2224
2225 let all_types = vec![
2226 SearchResultType::TextMatch,
2227 SearchResultType::Definition,
2228 SearchResultType::Reference,
2229 SearchResultType::TypeInfo,
2230 SearchResultType::Implementation,
2231 SearchResultType::Symbol,
2232 SearchResultType::Semantic,
2233 ];
2234
2235 let request = SearchRequest {
2236 query: "search".to_string(),
2237 search_types: all_types,
2238 max_results: 20,
2239 enable_lsp: false,
2240 ..Default::default()
2241 };
2242
2243 let response = engine.search_comprehensive(request).await.unwrap();
2244
2245 assert!(response.metrics.duration_ms < 5000);
2247 }
2248
2249 #[tokio::test]
2250 async fn test_search_error_recovery() {
2251 let (engine, _temp_dir) = create_test_search_engine().await.unwrap();
2252
2253 let problematic_queries = vec![
2255 "".to_string(),
2256 " ".to_string(),
2257 "!@#$%^&*()".to_string(),
2258 "a".repeat(1000), "SELECT * FROM users".to_string(), "<script>alert('xss')</script>".to_string(), ];
2262
2263 for query in problematic_queries {
2264 let request = SearchRequest {
2265 query: query.clone(),
2266 max_results: 5,
2267 enable_lsp: false,
2268 timeout_ms: 1000,
2269 ..Default::default()
2270 };
2271
2272 match engine.search_comprehensive(request).await {
2274 Ok(response) => {
2275 assert!(response.metrics.duration_ms < 5000);
2276 println!("✅ Handled problematic query successfully: '{}'",
2277 if query.len() > 20 { &query[..20] } else { &query });
2278 },
2279 Err(e) => {
2280 println!("⚠️ Query failed gracefully: '{}' - {}",
2281 if query.len() > 20 { &query[..20] } else { &query }, e);
2282 }
2284 }
2285 }
2286 }
2287
2288 #[tokio::test]
2289 async fn test_search_metrics_quality_calculations() {
2290 let (engine, _temp_dir) = create_test_search_engine().await.unwrap();
2291
2292 let request = SearchRequest {
2293 query: "test metrics calculation".to_string(),
2294 max_results: 10,
2295 enable_lsp: false,
2296 ..Default::default()
2297 };
2298
2299 let response = engine.search_comprehensive(request).await.unwrap();
2300
2301 assert!(response.metrics.result_diversity_score >= 0.0);
2303 assert!(response.metrics.result_diversity_score <= 1.0);
2304 assert!(response.metrics.confidence_score >= 0.0);
2305 assert!(response.metrics.confidence_score <= 1.0);
2306 assert!(response.metrics.coverage_score >= 0.0);
2307 assert!(response.metrics.coverage_score <= 1.0);
2308
2309 let overall_quality = response.metrics.quality_score();
2310 assert!(overall_quality >= 0.0);
2311 assert!(overall_quality <= 1.0);
2312 }
2313
2314 #[tokio::test]
2315 async fn test_search_with_force_semantic_method() {
2316 let (engine, _temp_dir) = create_test_search_engine().await.unwrap();
2317
2318 let request = SearchRequest {
2319 query: "semantic search test".to_string(),
2320 search_method: Some(SearchMethod::ForceSemantic),
2321 max_results: 5,
2322 enable_lsp: false,
2323 ..Default::default()
2324 };
2325
2326 let response = engine.search_comprehensive(request).await.unwrap();
2328 assert!(response.metrics.duration_ms < 10000);
2329 }
2330
2331 #[tokio::test]
2332 async fn test_search_response_structure_validation() {
2333 let (engine, _temp_dir) = create_test_search_engine().await.unwrap();
2334
2335 let request = SearchRequest {
2336 query: "validation test".to_string(),
2337 max_results: 5,
2338 enable_lsp: false,
2339 ..Default::default()
2340 };
2341
2342 let max_results = request.max_results; let response = engine.search_comprehensive(request).await.unwrap();
2344
2345 assert!(response.total_time_ms > 0);
2347 assert_eq!(response.sla_compliant, response.metrics.sla_compliant);
2348 assert!(response.results.len() <= max_results);
2349
2350 for result in &response.results {
2352 assert!(!result.file_path.is_empty());
2353 assert!(result.line_number >= 0);
2354 assert!(result.column >= 0);
2355 assert!(result.score >= 0.0);
2356 assert!(result.score <= 10.0); }
2358 }
2359
2360 #[tokio::test]
2361 async fn test_large_result_set_handling() {
2362 let (engine, _temp_dir) = create_test_search_engine().await.unwrap();
2363
2364 let request = SearchRequest {
2365 query: "test".to_string(), max_results: 1000, enable_lsp: false,
2368 timeout_ms: 5000,
2369 ..Default::default()
2370 };
2371
2372 let response = engine.search_comprehensive(request).await.unwrap();
2373
2374 assert!(response.metrics.duration_ms < 5000);
2376 assert!(response.results.len() <= 1000);
2377 }
2378
2379 #[tokio::test]
2380 async fn test_dataset_error_handling() {
2381 let (engine, _temp_dir) = create_test_search_engine().await.unwrap();
2382
2383 let versions_result = engine.list_dataset_versions().await;
2385 assert!(versions_result.is_err()); let validation_result = engine.validate_current_dataset().await;
2388 assert!(validation_result.is_err()); let load_result = engine.load_dataset_version("nonexistent").await;
2391 assert!(load_result.is_err()); let reload_result = engine.reload_current_dataset().await;
2394 assert!(reload_result.is_err()); let smoke_dataset = engine.get_smoke_dataset().await;
2398 assert!(smoke_dataset.is_none()); let slice = engine.get_dataset_slice("test").await;
2401 assert!(slice.is_none()); }
2403
2404 #[tokio::test]
2405 async fn test_query_sanitization_preserves_searchable_terms() {
2406 let (engine, _temp_dir) = create_test_search_engine().await.unwrap();
2407
2408 let original_query = "struct SearchEngine impl search";
2410 let sanitized = engine.sanitize_query(original_query);
2411
2412 assert!(
2414 sanitized.contains("struct") || sanitized.contains("SearchEngine") || sanitized.contains("impl"),
2415 "REGRESSION FAILURE: Query sanitization removed all searchable terms: '{}' -> '{}'",
2416 original_query, sanitized
2417 );
2418
2419 let request = SearchRequest {
2421 query: sanitized.clone(),
2422 max_results: 10,
2423 file_path: None,
2424 language: None,
2425 enable_lsp: false,
2426 include_context: false,
2427 timeout_ms: 1000,
2428 search_types: vec![SearchResultType::TextMatch],
2429 search_method: Some(SearchMethod::Lexical),
2430 };
2431
2432 let response = engine.search_comprehensive(request).await.unwrap();
2433
2434 assert!(
2436 !response.results.is_empty(),
2437 "REGRESSION FAILURE: Sanitized query '{}' returned 0 results", sanitized
2438 );
2439
2440 println!("✅ Sanitized query '{}': {} results", sanitized, response.results.len());
2441 }
2442
2443 #[tokio::test]
2444 async fn test_index_population_regression() {
2445 let (engine, _temp_dir) = create_test_search_engine().await.unwrap();
2446
2447 let reader = &engine.reader;
2449 let searcher = reader.searcher();
2450
2451 let all_query = tantivy::query::AllQuery;
2453 let top_docs = searcher.search(&all_query, &tantivy::collector::TopDocs::with_limit(1)).unwrap();
2454
2455 assert!(
2456 !top_docs.is_empty(),
2457 "REGRESSION FAILURE: Index should be automatically populated with documents"
2458 );
2459
2460 println!("✅ Index contains {} documents (verified with sample)", top_docs.len());
2461 }
2462}