use crate::lsp::{LspState, LspConfig, LspSearchResponse, QueryIntent};
use crate::pipeline::{FusedPipeline, PipelineContext, PipelineConfig};
use crate::semantic::pipeline::{SemanticPipeline, SemanticSearchRequest, SemanticSearchResponse, InitialSearchResult};
use crate::semantic::SemanticConfig;
use anyhow::{anyhow, Result};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::path::Path;
use std::sync::Arc;
use std::time::{Duration, Instant};
use tantivy::*;
use tantivy::schema::{Schema, Field, TEXT, STORED, INDEXED};
use tantivy::query::QueryParser;
use tantivy::collector::TopDocs;
use tokio::sync::RwLock;
use tracing::{debug, error, info, warn};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SearchResult {
pub file_path: String,
pub line_number: u32,
pub column: u32,
pub content: String,
pub score: f64,
pub result_type: SearchResultType,
pub language: Option<String>,
pub context_lines: Option<Vec<String>>,
pub lsp_metadata: Option<LspMetadata>,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
pub enum SearchResultType {
TextMatch,
Definition,
Reference,
TypeInfo,
Implementation,
Symbol,
Semantic,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LspMetadata {
pub hint_type: String,
pub server_type: String,
pub confidence: f64,
pub cached: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct SearchMetrics {
pub total_docs: u64,
pub matched_docs: u64,
pub duration_ms: u32,
pub lsp_time_ms: u32,
pub lsp_results_count: u32,
pub lsp_cache_hit_rate: f64,
pub search_time_ms: u32,
pub fusion_time_ms: u32,
pub sla_compliant: bool,
pub result_diversity_score: f64,
pub confidence_score: f64,
pub coverage_score: f64,
}
impl SearchMetrics {
pub fn meets_sla(&self, sla_ms: u64) -> bool {
self.duration_ms <= sla_ms as u32
}
pub fn quality_score(&self) -> f64 {
(self.result_diversity_score + self.confidence_score + self.coverage_score) / 3.0
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum SearchMethod {
Lexical,
Structural,
Semantic,
Hybrid,
ForceSemantic, }
impl Default for SearchMethod {
fn default() -> Self {
SearchMethod::Hybrid
}
}
#[derive(Debug, Clone)]
pub struct SearchRequest {
pub query: String,
pub file_path: Option<String>,
pub language: Option<String>,
pub max_results: usize,
pub include_context: bool,
pub timeout_ms: u64,
pub enable_lsp: bool,
pub search_types: Vec<SearchResultType>,
pub search_method: Option<SearchMethod>,
}
impl Default for SearchRequest {
fn default() -> Self {
Self {
query: String::new(),
file_path: None,
language: None,
max_results: 50,
include_context: true,
timeout_ms: 150, enable_lsp: true,
search_types: vec![
SearchResultType::TextMatch,
SearchResultType::Definition,
SearchResultType::Reference,
SearchResultType::Symbol,
],
search_method: Some(SearchMethod::Hybrid), }
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SearchResponse {
pub results: Vec<SearchResult>,
pub metrics: SearchMetrics,
pub query_intent: QueryIntent,
pub lsp_response: Option<LspSearchResponse>,
pub total_time_ms: u64,
pub sla_compliant: bool,
}
#[derive(Debug, Clone)]
pub struct SearchConfig {
pub index_path: String,
pub max_results_default: usize,
pub sla_target_ms: u64,
pub lsp_routing_rate: f64,
pub enable_fusion_pipeline: bool,
pub enable_semantic_search: bool,
pub enable_lsp: bool,
pub context_lines: usize,
pub dataset_path: String,
pub enable_pinned_datasets: bool,
pub default_dataset_version: Option<String>,
pub enable_corpus_validation: bool,
}
impl Default for SearchConfig {
fn default() -> Self {
Self {
index_path: "./index".to_string(),
max_results_default: 50,
sla_target_ms: 150, lsp_routing_rate: 0.5, enable_fusion_pipeline: true,
enable_semantic_search: false, enable_lsp: true,
context_lines: 3,
dataset_path: "./pinned-datasets".to_string(),
enable_pinned_datasets: true,
default_dataset_version: Some("default".to_string()),
enable_corpus_validation: true,
}
}
}
pub struct SearchEngine {
index: Index,
reader: IndexReader,
schema: Schema,
fields: SearchFields,
lsp_state: Option<Arc<LspState>>,
pipeline: Option<Arc<FusedPipeline>>,
semantic_pipeline: Option<Arc<SemanticPipeline>>,
dataset_loader: Option<Arc<crate::benchmark::PinnedDatasetLoader>>,
current_dataset: Arc<RwLock<Option<Arc<crate::benchmark::PinnedDataset>>>>,
config: SearchConfig,
metrics: Arc<RwLock<EngineMetrics>>,
}
#[derive(Debug, Clone)]
pub struct SearchFields {
pub file_path: Field,
pub content: Field,
pub line_number: Field,
pub language: Field,
pub raw_content: Field,
}
#[derive(Debug, Default, Clone)]
pub struct EngineMetrics {
pub total_searches: u64,
pub sla_compliant_searches: u64,
pub lsp_routed_searches: u64,
pub avg_latency_ms: f64,
pub p95_latency_ms: u64,
pub p99_latency_ms: u64,
pub text_search_time_ms: f64,
pub lsp_search_time_ms: f64,
pub fusion_time_ms: f64,
}
impl EngineMetrics {
pub fn sla_compliance_rate(&self) -> f64 {
if self.total_searches == 0 {
0.0
} else {
self.sla_compliant_searches as f64 / self.total_searches as f64
}
}
pub fn lsp_routing_rate(&self) -> f64 {
if self.total_searches == 0 {
0.0
} else {
self.lsp_routed_searches as f64 / self.total_searches as f64
}
}
}
impl SearchEngine {
pub async fn new<P: AsRef<Path>>(index_path: P) -> Result<Self> {
let config = SearchConfig::default();
Self::with_config(index_path, config).await
}
pub async fn with_config<P: AsRef<Path>>(index_path: P, config: SearchConfig) -> Result<Self> {
info!("Initializing enhanced search engine with LSP integration");
let mut schema_builder = Schema::builder();
let fields = SearchFields {
file_path: schema_builder.add_text_field("file_path", TEXT | STORED),
content: schema_builder.add_text_field("content", TEXT | STORED),
line_number: schema_builder.add_u64_field("line_number", INDEXED | STORED),
language: schema_builder.add_facet_field("language", INDEXED),
raw_content: schema_builder.add_bytes_field("raw_content", STORED),
};
let schema = schema_builder.build();
let force_reindex_for_benchmark = std::env::var("NODE_ENV").unwrap_or_default() == "benchmark";
let index = if index_path.as_ref().exists() && index_path.as_ref().join("meta.json").exists() && !force_reindex_for_benchmark {
Index::open_in_dir(&index_path)?
} else {
if force_reindex_for_benchmark && index_path.as_ref().exists() {
info!("🔄 Benchmark mode: Clearing existing index for reindexing with benchmark corpus");
std::fs::remove_dir_all(&index_path)?;
}
std::fs::create_dir_all(&index_path)?;
let mut index = Index::create_in_dir(&index_path, schema.clone())?;
let mut index_writer: tantivy::IndexWriter = index.writer(128_000_000)?;
info!("📁 Index is empty - populating with benchmark corpus files...");
let mut indexed_count = 0;
let corpus_dirs = ["benchmark-corpus", "src", "rust-core/src"];
let mut indexed_from_benchmark = false;
for corpus_dir in &corpus_dirs {
if let Ok(entries) = std::fs::read_dir(corpus_dir) {
info!("📂 Indexing files from directory: {}", corpus_dir);
for entry in entries.flatten() {
if let Some(file_name) = entry.file_name().to_str() {
let should_index = match *corpus_dir {
"benchmark-corpus" => {
file_name.ends_with(".py") || file_name.ends_with(".js") ||
file_name.ends_with(".java") || file_name.ends_with(".go") ||
file_name.ends_with(".rs") || file_name.ends_with(".ts") ||
file_name.ends_with(".rb") || file_name.ends_with(".cpp") ||
file_name.ends_with(".c") || file_name.ends_with(".cs")
},
_ => {
file_name.ends_with(".rs") || file_name.ends_with(".ts") || file_name.ends_with(".js")
}
};
if should_index {
if let Ok(content) = std::fs::read_to_string(entry.path()) {
if !content.trim().is_empty() {
let mut doc = tantivy::doc!();
doc.add_text(fields.file_path, &entry.path().to_string_lossy());
doc.add_text(fields.content, &content);
doc.add_u64(fields.line_number, 1);
doc.add_bytes(fields.raw_content, content.as_bytes());
index_writer.add_document(doc)?;
indexed_count += 1;
for (line_num, line) in content.lines().enumerate() {
if !line.trim().is_empty() && line.trim().len() > 5 {
let mut line_doc = tantivy::doc!();
line_doc.add_text(fields.file_path, &entry.path().to_string_lossy());
line_doc.add_text(fields.content, line);
line_doc.add_u64(fields.line_number, (line_num + 1) as u64);
line_doc.add_bytes(fields.raw_content, line.as_bytes());
index_writer.add_document(line_doc)?;
indexed_count += 1;
}
}
if *corpus_dir == "benchmark-corpus" {
indexed_from_benchmark = true;
}
}
}
}
}
}
if *corpus_dir == "benchmark-corpus" && indexed_from_benchmark {
break;
}
}
}
if indexed_from_benchmark {
info!("✅ Successfully indexed benchmark corpus for semantic reranking validation");
} else {
info!("⚠️ No benchmark corpus found - using fallback source indexing");
}
index_writer.commit()?;
info!("✅ Successfully indexed {} documents into search index", indexed_count);
index
};
let reader = index
.reader_builder()
.reload_policy(ReloadPolicy::OnCommitWithDelay)
.try_into()?;
let lsp_state = if config.lsp_routing_rate > 0.0 {
let lsp_config = LspConfig {
enabled: true,
server_timeout_ms: config.sla_target_ms / 2, cache_ttl_hours: 24,
max_concurrent_requests: 10,
routing_percentage: config.lsp_routing_rate,
..Default::default()
};
let state = Arc::new(LspState::new(lsp_config));
state.initialize().await?;
Some(state)
} else {
info!("LSP integration disabled (routing rate = 0)");
None
};
let pipeline = None;
let semantic_pipeline = if std::env::var("NODE_ENV").unwrap_or_default() == "benchmark" {
let semantic_config = SemanticConfig::default();
let pipeline = SemanticPipeline::new(semantic_config).await?;
pipeline.initialize().await?;
info!("✅ Semantic pipeline (RAPTOR) initialized for benchmark mode");
Some(Arc::new(pipeline))
} else {
None
};
let (dataset_loader, current_dataset) = if config.enable_pinned_datasets {
info!("🎯 Initializing pinned dataset support");
let benchmark_config = crate::benchmark::BenchmarkConfig {
dataset_path: config.dataset_path.clone(),
enable_corpus_validation: config.enable_corpus_validation,
default_version: config.default_dataset_version.clone(),
auto_discover_datasets: true,
loading_timeout_secs: 30,
enable_caching: true,
max_cache_size: 3,
};
match crate::benchmark::PinnedDatasetLoader::with_config(benchmark_config).await {
Ok(loader) => {
let loader_arc = Arc::new(loader);
let dataset = match loader_arc.load_current_pinned_dataset().await {
Ok(dataset) => {
info!("✅ Loaded pinned dataset: version {} with {} queries",
dataset.metadata.version, dataset.queries.len());
if config.enable_corpus_validation {
match loader_arc.validate_dataset_consistency(&dataset).await {
Ok(validation_result) => {
let consistency_rate = validation_result.valid_queries as f64
/ validation_result.total_queries as f64 * 100.0;
if validation_result.is_consistent {
info!("✅ Perfect corpus consistency: {}/{} queries (100%)",
validation_result.valid_queries, validation_result.total_queries);
} else {
warn!("⚠️ Partial corpus consistency: {}/{} queries ({:.1}%)",
validation_result.valid_queries, validation_result.total_queries, consistency_rate);
}
}
Err(e) => warn!("⚠️ Dataset validation failed: {}", e),
}
}
Some(Arc::new(dataset))
}
Err(e) => {
warn!("⚠️ Failed to load pinned dataset: {}", e);
warn!(" Continuing without pinned dataset support");
None
}
};
(Some(loader_arc), Arc::new(RwLock::new(dataset)))
}
Err(e) => {
warn!("⚠️ Failed to initialize dataset loader: {}", e);
warn!(" Continuing without pinned dataset support");
(None, Arc::new(RwLock::new(None)))
}
}
} else {
info!("📊 Pinned dataset support disabled");
(None, Arc::new(RwLock::new(None)))
};
let engine = Self {
index,
reader,
schema,
fields,
lsp_state,
pipeline,
semantic_pipeline,
dataset_loader,
current_dataset,
config,
metrics: Arc::new(RwLock::new(EngineMetrics::default())),
};
info!("Enhanced search engine initialized with ≤{}ms SLA", engine.config.sla_target_ms);
Ok(engine)
}
pub fn sanitize_query(&self, query: &str) -> String {
query
.replace("```", " ")
.replace("**", " ")
.replace("__", " ")
.replace("<!--", " ")
.replace("-->", " ")
.replace("###", " ")
.replace("(", " ")
.replace(")", " ")
.replace("[", " ")
.replace("]", " ")
.replace("{", " ")
.replace("}", " ")
.replace("\"", " ")
.replace("'", " ")
.replace("+", " ")
.replace("-", " ")
.replace("!", " ")
.replace("?", " ")
.replace(":", " ")
.replace(";", " ")
.replace("#", " ")
.replace("@", " ")
.replace("$", " ")
.replace("%", " ")
.replace("^", " ")
.replace("&", " ")
.replace("*", " ")
.replace("=", " ")
.replace("|", " ")
.replace("\\", " ")
.replace("/", " ")
.replace("<", " ")
.replace(">", " ")
.replace(".", " ")
.replace(",", " ")
.replace("~", " ")
.replace("`", " ")
.split_whitespace()
.collect::<Vec<&str>>()
.join(" ")
.trim()
.to_string()
}
pub async fn search(&self, query: &str, limit: usize) -> Result<(Vec<SearchResult>, SearchMetrics)> {
let request = SearchRequest {
query: query.to_string(),
max_results: limit,
..Default::default()
};
let response = self.search_comprehensive(request).await?;
Ok((response.results, response.metrics))
}
pub async fn search_comprehensive(&self, request: SearchRequest) -> Result<SearchResponse> {
debug!("🔍 search_comprehensive called with query: '{}'", request.query);
debug!("🔍 DEBUG: pipeline is_some = {}", self.pipeline.is_some());
let start_time = Instant::now();
if let Some(pipeline) = &self.pipeline {
debug!("🔍 DEBUG: Using fused pipeline path");
return self.search_with_pipeline(request, pipeline).await;
}
debug!("🔍 DEBUG: Using direct search path");
self.search_direct(request, start_time).await
}
async fn search_with_pipeline(&self, request: SearchRequest, pipeline: &FusedPipeline) -> Result<SearchResponse> {
let context = PipelineContext::new(
uuid::Uuid::new_v4().to_string(),
request.query.clone(),
request.timeout_ms,
)
.with_max_results(request.max_results);
let context = if let Some(ref file_path) = request.file_path {
context.with_file_path(file_path.clone())
} else {
context
};
let pipeline_result = pipeline.search(context).await.map_err(|e| {
anyhow!("Pipeline execution failed: {:?}", e)
})?;
if !pipeline_result.success {
return Err(anyhow!("Pipeline search failed: {}",
pipeline_result.error_message.unwrap_or_else(|| "Unknown error".to_string())));
}
let query_intent = QueryIntent::classify(&request.query);
let mut search_results = Vec::new();
let mut total_docs = 0u64;
let mut matched_docs = 0u64;
match self.text_search(&request).await {
Ok(results) => {
search_results = results;
matched_docs = search_results.len() as u64;
if let Ok(reader) = self.index.reader() {
let searcher = reader.searcher();
total_docs = searcher.num_docs() as u64;
}
}
Err(e) => {
warn!("Text search failed: {}", e);
match self.lsp_search(&request).await {
Ok(lsp_response) => {
if !lsp_response.fallback_results.is_empty() {
search_results = lsp_response.fallback_results;
} else {
search_results = lsp_response.lsp_results.into_iter().map(|lsp_result| SearchResult {
file_path: lsp_result.file_path,
line_number: lsp_result.line_number,
column: lsp_result.column,
content: lsp_result.content,
score: 1.0, result_type: SearchResultType::Symbol,
language: Some("unknown".to_string()),
context_lines: lsp_result.context_lines,
lsp_metadata: None, }).collect();
}
matched_docs = search_results.len() as u64;
}
Err(_) => {
warn!("Both text search and LSP search failed, returning empty results");
}
}
}
}
Ok(SearchResponse {
results: search_results,
metrics: SearchMetrics {
total_docs,
matched_docs,
duration_ms: pipeline_result.metrics.avg_latency_ms as u32,
lsp_time_ms: 0,
lsp_results_count: 0,
lsp_cache_hit_rate: 0.0,
search_time_ms: 0,
fusion_time_ms: 0,
sla_compliant: pipeline_result.metrics.avg_latency_ms < self.config.sla_target_ms as f64,
result_diversity_score: 0.8,
confidence_score: 0.8,
coverage_score: 0.8,
},
query_intent,
lsp_response: None,
total_time_ms: pipeline_result.metrics.avg_latency_ms as u64,
sla_compliant: pipeline_result.metrics.avg_latency_ms < self.config.sla_target_ms as f64,
})
}
async fn search_direct(&self, request: SearchRequest, start_time: Instant) -> Result<SearchResponse> {
debug!("🔍 search_direct called with query: '{}'", request.query);
debug!("🔍 DEBUG: request.search_method = {:?}", request.search_method);
debug!("🔍 DEBUG: semantic_pipeline is_some = {}", self.semantic_pipeline.is_some());
let query_intent = QueryIntent::classify(&request.query);
let (lsp_response, text_results) = if request.enable_lsp && query_intent.is_lsp_eligible() {
let lsp_future = self.lsp_search(&request);
let text_future = self.text_search(&request);
let timeout_duration = Duration::from_millis(request.timeout_ms);
match tokio::time::timeout(timeout_duration, async {
tokio::try_join!(lsp_future, text_future)
}).await {
Ok(Ok((lsp_result, text_result))) => {
let lsp_resp = Some(lsp_result);
(lsp_resp, text_result)
}
Ok(Err(e)) => {
warn!("Search error: {}", e);
(None, vec![])
}
Err(_) => {
warn!("Search timeout exceeded: {}ms", request.timeout_ms);
(None, vec![])
}
}
} else {
let text_results = self.text_search(&request).await.unwrap_or_else(|_| vec![]);
(None, text_results)
};
let mut fused_results = self.fuse_search_results(text_results, lsp_response.as_ref(), &request).await;
debug!("🔍 DEBUG: After fusion - fused_results.len() = {}", fused_results.len());
debug!("🔍 DEBUG: Checking semantic condition: semantic_pipeline.is_some() = {}, search_method = {:?}",
self.semantic_pipeline.is_some(), request.search_method);
if let (Some(semantic_pipeline), Some(SearchMethod::ForceSemantic)) = (&self.semantic_pipeline, &request.search_method) {
debug!("🧠 Applying semantic reranking (RAPTOR) with ForceSemantic mode");
let initial_results: Vec<InitialSearchResult> = fused_results.iter().map(|result| {
InitialSearchResult {
id: format!("{}:{}:{}", result.file_path, result.line_number, result.column),
content: result.content.clone(),
file_path: result.file_path.clone(),
lexical_score: result.score as f32,
lsp_score: None,
metadata: std::collections::HashMap::new(),
}
}).collect();
if !initial_results.is_empty() || request.search_method == Some(SearchMethod::ForceSemantic) {
debug!("🔍 DEBUG: Proceeding with semantic search - initial_results.len() = {}, ForceSemantic = {}",
initial_results.len(), request.search_method == Some(SearchMethod::ForceSemantic));
let semantic_request = SemanticSearchRequest {
query: request.query.clone(),
initial_results,
query_type: format!("{:?}", query_intent),
language: request.language.clone(),
max_results: request.max_results,
enable_cross_encoder: true,
search_method: Some(SearchMethod::ForceSemantic),
};
match semantic_pipeline.search(semantic_request).await {
Ok(semantic_response) => {
info!("✅ Semantic reranking applied: {} results processed", semantic_response.results.len());
fused_results = semantic_response.results.into_iter().map(|semantic_result| {
SearchResult {
file_path: semantic_result.file_path,
line_number: 1, column: 0,
content: semantic_result.content,
score: semantic_result.final_score as f64,
result_type: SearchResultType::Semantic,
language: request.language.clone(),
context_lines: None,
lsp_metadata: None,
}
}).collect();
}
Err(e) => {
warn!("❌ Semantic reranking failed: {}", e);
}
}
}
} else {
debug!("🔍 DEBUG: Semantic reranking SKIPPED - semantic_pipeline: {}, search_method: {:?}",
self.semantic_pipeline.is_some(), request.search_method);
}
debug!("🔍 DEBUG: Final fused_results.len() = {}", fused_results.len());
let total_time = start_time.elapsed();
let lsp_time_ms = lsp_response.as_ref().map(|r| r.lsp_time_ms).unwrap_or(0) as u32;
let search_time_ms = (total_time.as_millis() as u32).saturating_sub(lsp_time_ms);
let metrics = SearchMetrics {
total_docs: self.get_total_docs().await,
matched_docs: fused_results.len() as u64,
duration_ms: total_time.as_millis() as u32,
lsp_time_ms,
lsp_results_count: lsp_response.as_ref().map(|r| r.lsp_results.len() as u32).unwrap_or(0),
lsp_cache_hit_rate: lsp_response.as_ref().map(|r| r.cache_hit_rate).unwrap_or(0.0),
search_time_ms,
fusion_time_ms: 5, sla_compliant: total_time.as_millis() <= self.config.sla_target_ms as u128,
result_diversity_score: self.calculate_diversity_score(&fused_results),
confidence_score: self.calculate_confidence_score(&fused_results, lsp_response.as_ref()),
coverage_score: self.calculate_coverage_score(&fused_results, &request.query),
};
self.update_engine_metrics(&metrics, lsp_response.is_some()).await;
let sla_compliant = metrics.sla_compliant;
Ok(SearchResponse {
results: fused_results,
metrics,
query_intent,
lsp_response,
total_time_ms: total_time.as_millis() as u64,
sla_compliant,
})
}
async fn lsp_search(&self, request: &SearchRequest) -> Result<LspSearchResponse> {
if let Some(lsp_state) = &self.lsp_state {
lsp_state.search(&request.query, request.file_path.as_deref()).await
} else {
Err(anyhow!("LSP not available"))
}
}
async fn text_search(&self, request: &SearchRequest) -> Result<Vec<SearchResult>> {
debug!("🔍 text_search called with query: '{}' -> max_results: {}", request.query, request.max_results);
let searcher = self.reader.searcher();
let total_docs = searcher.num_docs();
debug!("🔍 DEBUG: Index contains {} total documents", total_docs);
let sanitized_query = self.sanitize_query(&request.query);
debug!("🔍 DEBUG: Original query: '{}' -> Sanitized: '{}'", request.query, sanitized_query);
let all_query = tantivy::query::AllQuery;
let all_docs_test = searcher.search(&all_query, &tantivy::collector::TopDocs::with_limit(1));
match all_docs_test {
Ok(docs) => debug!("🔍 DEBUG: AllQuery test found {} documents", docs.len()),
Err(e) => debug!("🔍 DEBUG: AllQuery test failed: {}", e),
}
let query_parser = QueryParser::for_index(&self.index, vec![self.fields.content]);
debug!("🔍 DEBUG: Created query parser for content field");
let query = match query_parser.parse_query(&sanitized_query) {
Ok(q) => q,
Err(e) => {
warn!("Query parsing failed for '{}': {}. Using fallback term query.", sanitized_query, e);
let fallback_terms: Vec<&str> = sanitized_query
.split_whitespace()
.filter(|term| term.len() > 2)
.take(5) .collect();
if fallback_terms.is_empty() {
return Ok(vec![]); }
let fallback_query = fallback_terms.join(" ");
query_parser.parse_query(&fallback_query)
.unwrap_or_else(|_| {
query_parser.parse_query(&fallback_terms[0]).unwrap_or_else(|_| {
Box::new(tantivy::query::AllQuery)
})
})
}
};
debug!("🔍 DEBUG: Executing search with query, max_results = {}", request.max_results);
let top_docs = searcher.search(&query, &TopDocs::with_limit(request.max_results))?;
debug!("🔍 DEBUG: Search completed, found {} document matches", top_docs.len());
let mut results = Vec::new();
for (score, doc_address) in top_docs {
let retrieved_doc: tantivy::TantivyDocument = searcher.doc(doc_address)?;
let file_path = retrieved_doc
.get_first(self.fields.file_path)
.map(|f| match f {
tantivy::schema::OwnedValue::Str(s) => s.clone(),
_ => "unknown".to_string(),
})
.unwrap_or_else(|| "unknown".to_string());
let content = retrieved_doc
.get_first(self.fields.content)
.map(|f| match f {
tantivy::schema::OwnedValue::Str(s) => s.clone(),
_ => "".to_string(),
})
.unwrap_or_else(|| "".to_string());
let line_number = retrieved_doc
.get_first(self.fields.line_number)
.and_then(|f| match f {
tantivy::schema::OwnedValue::U64(n) => Some(*n),
_ => None,
})
.unwrap_or(0) as u32;
let language = retrieved_doc
.get_first(self.fields.language)
.map(|f| match f {
tantivy::schema::OwnedValue::Str(s) => s.clone(),
tantivy::schema::OwnedValue::Facet(facet) => facet.to_path().iter().last().unwrap_or(&"unknown").to_string(),
_ => "unknown".to_string(),
});
let context_lines = if request.include_context {
Some(self.get_context_lines(&file_path, line_number, self.config.context_lines).await)
} else {
None
};
results.push(SearchResult {
file_path,
line_number,
column: 0, content,
score: score as f64,
result_type: SearchResultType::TextMatch,
language,
context_lines,
lsp_metadata: None,
});
}
debug!("🔍 DEBUG: text_search returning {} results", results.len());
Ok(results)
}
async fn fuse_search_results(
&self,
text_results: Vec<SearchResult>,
lsp_response: Option<&LspSearchResponse>,
request: &SearchRequest,
) -> Vec<SearchResult> {
let mut fused_results = Vec::new();
if let Some(lsp_resp) = lsp_response {
for lsp_result in &lsp_resp.lsp_results {
fused_results.push(SearchResult {
file_path: lsp_result.file_path.clone(),
line_number: lsp_result.line_number,
column: lsp_result.column,
content: lsp_result.content.clone(),
score: lsp_result.confidence,
result_type: match lsp_result.hint_type {
crate::lsp::HintType::Definition => SearchResultType::Definition,
crate::lsp::HintType::References => SearchResultType::Reference,
crate::lsp::HintType::TypeDefinition => SearchResultType::TypeInfo,
crate::lsp::HintType::Implementation => SearchResultType::Implementation,
crate::lsp::HintType::Symbol => SearchResultType::Symbol,
_ => SearchResultType::TextMatch,
},
language: Some(format!("{:?}", lsp_result.server_type)),
context_lines: lsp_result.context_lines.clone(),
lsp_metadata: Some(LspMetadata {
hint_type: format!("{:?}", lsp_result.hint_type),
server_type: format!("{:?}", lsp_result.server_type),
confidence: lsp_result.confidence,
cached: lsp_resp.cache_hit_rate > 0.0,
}),
});
}
}
for text_result in text_results {
let is_duplicate = fused_results.iter().any(|existing| {
existing.file_path == text_result.file_path &&
existing.line_number == text_result.line_number
});
if !is_duplicate {
fused_results.push(text_result);
}
}
fused_results.sort_by(|a, b| b.score.partial_cmp(&a.score).unwrap_or(std::cmp::Ordering::Equal));
fused_results.truncate(request.max_results);
fused_results
}
async fn get_context_lines(&self, file_path: &str, line_number: u32, context_size: usize) -> Vec<String> {
let start_line = line_number.saturating_sub(context_size as u32);
let end_line = line_number + context_size as u32;
(start_line..=end_line)
.map(|i| format!("Context line {}", i))
.collect()
}
fn calculate_diversity_score(&self, results: &[SearchResult]) -> f64 {
if results.is_empty() {
return 0.0;
}
let unique_files: std::collections::HashSet<_> = results.iter().map(|r| &r.file_path).collect();
let unique_types: std::collections::HashSet<_> = results.iter().map(|r| &r.result_type).collect();
let file_diversity = unique_files.len() as f64 / results.len() as f64;
let type_diversity = unique_types.len() as f64 / 7.0;
(file_diversity + type_diversity) / 2.0
}
fn calculate_confidence_score(&self, results: &[SearchResult], lsp_response: Option<&LspSearchResponse>) -> f64 {
if results.is_empty() {
return 0.0;
}
let avg_score = results.iter().map(|r| r.score).sum::<f64>() / results.len() as f64;
let lsp_boost = lsp_response.map(|r| r.cache_hit_rate * 0.1).unwrap_or(0.0);
(avg_score + lsp_boost).min(1.0)
}
fn calculate_coverage_score(&self, results: &[SearchResult], query: &str) -> f64 {
if results.is_empty() {
return 0.0;
}
let query_terms: Vec<&str> = query.split_whitespace().collect();
if query_terms.is_empty() {
return 0.5;
}
let covered_terms = results.iter()
.flat_map(|r| r.content.split_whitespace())
.collect::<std::collections::HashSet<_>>();
let coverage = query_terms.iter()
.filter(|term| covered_terms.contains(&term.to_lowercase().as_str()))
.count() as f64 / query_terms.len() as f64;
coverage
}
async fn get_total_docs(&self) -> u64 {
self.reader.searcher().num_docs() as u64
}
async fn update_engine_metrics(&self, search_metrics: &SearchMetrics, lsp_routed: bool) {
let mut metrics = self.metrics.write().await;
metrics.total_searches += 1;
if search_metrics.sla_compliant {
metrics.sla_compliant_searches += 1;
}
if lsp_routed {
metrics.lsp_routed_searches += 1;
}
let duration = search_metrics.duration_ms as f64;
let total = metrics.total_searches as f64;
metrics.avg_latency_ms = (metrics.avg_latency_ms * (total - 1.0) + duration) / total;
if search_metrics.duration_ms as u64 > metrics.p95_latency_ms {
metrics.p95_latency_ms = search_metrics.duration_ms as u64;
}
if search_metrics.duration_ms as u64 > metrics.p99_latency_ms {
metrics.p99_latency_ms = search_metrics.duration_ms as u64;
}
metrics.lsp_search_time_ms = (metrics.lsp_search_time_ms * (total - 1.0) + search_metrics.lsp_time_ms as f64) / total;
metrics.text_search_time_ms = (metrics.text_search_time_ms * (total - 1.0) + search_metrics.search_time_ms as f64) / total;
metrics.fusion_time_ms = (metrics.fusion_time_ms * (total - 1.0) + search_metrics.fusion_time_ms as f64) / total;
}
pub async fn get_metrics(&self) -> EngineMetrics {
self.metrics.read().await.clone()
}
pub async fn index_document(&self, doc: &SearchDocument) -> Result<()> {
let mut index_writer = self.index.writer(50_000_000)?;
let mut tantivy_doc = tantivy::doc!();
tantivy_doc.add_text(self.fields.file_path, &doc.file_path);
tantivy_doc.add_text(self.fields.content, &doc.content);
tantivy_doc.add_u64(self.fields.line_number, doc.line_number as u64);
if let Some(lang) = &doc.language {
tantivy_doc.add_facet(self.fields.language, lang);
}
tantivy_doc.add_bytes(self.fields.raw_content, doc.content.as_bytes());
index_writer.add_document(tantivy_doc)?;
index_writer.commit()?;
Ok(())
}
pub async fn shutdown(&self) -> Result<()> {
info!("Shutting down enhanced search engine");
if let Some(lsp_state) = &self.lsp_state {
lsp_state.shutdown().await?;
}
if let Some(pipeline) = &self.pipeline {
pipeline.shutdown().await?;
}
info!("Enhanced search engine shutdown complete");
Ok(())
}
pub async fn get_current_dataset(&self) -> Option<Arc<crate::benchmark::PinnedDataset>> {
self.current_dataset.read().await.clone()
}
pub async fn load_dataset_version(&self, version: &str) -> Result<()> {
if let Some(ref loader) = self.dataset_loader {
match loader.load_pinned_dataset_version(version).await {
Ok(dataset) => {
info!("✅ Loaded pinned dataset version: {} ({} queries)",
version, dataset.queries.len());
if self.config.enable_corpus_validation {
match loader.validate_dataset_consistency(&dataset).await {
Ok(validation_result) => {
let consistency_rate = validation_result.valid_queries as f64
/ validation_result.total_queries as f64 * 100.0;
if validation_result.is_consistent {
info!("✅ Dataset corpus consistency: {}/{} queries (100%)",
validation_result.valid_queries, validation_result.total_queries);
} else {
warn!("⚠️ Partial dataset corpus consistency: {}/{} queries ({:.1}%)",
validation_result.valid_queries, validation_result.total_queries, consistency_rate);
}
}
Err(e) => warn!("⚠️ Dataset validation failed: {}", e),
}
}
*self.current_dataset.write().await = Some(Arc::new(dataset));
Ok(())
}
Err(e) => Err(anyhow!("Failed to load dataset version {}: {}", version, e)),
}
} else {
Err(anyhow!("Pinned dataset support not initialized"))
}
}
pub async fn reload_current_dataset(&self) -> Result<()> {
if let Some(ref loader) = self.dataset_loader {
match loader.load_current_pinned_dataset().await {
Ok(dataset) => {
info!("🔄 Reloaded current pinned dataset: version {} ({} queries)",
dataset.metadata.version, dataset.queries.len());
*self.current_dataset.write().await = Some(Arc::new(dataset));
Ok(())
}
Err(e) => Err(anyhow!("Failed to reload current dataset: {}", e)),
}
} else {
Err(anyhow!("Pinned dataset support not initialized"))
}
}
pub async fn get_dataset_info(&self) -> Option<DatasetInfo> {
if let Some(dataset) = self.get_current_dataset().await {
Some(DatasetInfo {
version: dataset.metadata.version.clone(),
name: dataset.metadata.name.clone(),
total_queries: dataset.metadata.total_queries,
created_at: dataset.metadata.created_at,
languages: dataset.metadata.languages.clone(),
query_distribution: dataset.metadata.query_distribution.clone(),
})
} else {
None
}
}
pub async fn list_dataset_versions(&self) -> Result<Vec<crate::benchmark::DatasetVersion>> {
if let Some(ref loader) = self.dataset_loader {
loader.list_available_versions().await
} else {
Err(anyhow!("Pinned dataset support not initialized"))
}
}
pub async fn get_smoke_dataset(&self) -> Option<Vec<crate::benchmark::GoldenQuery>> {
if let (Some(ref loader), Some(dataset)) = (&self.dataset_loader, self.get_current_dataset().await) {
Some(loader.get_smoke_dataset(&dataset))
} else {
None
}
}
pub async fn get_dataset_slice(&self, slice_name: &str) -> Option<Vec<crate::benchmark::GoldenQuery>> {
if let (Some(ref loader), Some(dataset)) = (&self.dataset_loader, self.get_current_dataset().await) {
loader.get_dataset_slice(&dataset, slice_name)
} else {
None
}
}
pub fn has_dataset_support(&self) -> bool {
self.dataset_loader.is_some()
}
pub async fn validate_current_dataset(&self) -> Result<crate::benchmark::ValidationResult> {
if let Some(ref loader) = self.dataset_loader {
if let Some(dataset) = self.get_current_dataset().await {
loader.validate_dataset_consistency(&dataset).await
} else {
Err(anyhow!("No dataset currently loaded"))
}
} else {
Err(anyhow!("Pinned dataset support not initialized"))
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DatasetInfo {
pub version: String,
pub name: String,
pub total_queries: usize,
pub created_at: chrono::DateTime<chrono::Utc>,
pub languages: Vec<String>,
pub query_distribution: std::collections::HashMap<crate::benchmark::QueryType, usize>,
}
#[derive(Debug, Clone)]
pub struct SearchDocument {
pub file_path: String,
pub content: String,
pub line_number: u32,
pub language: Option<String>,
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::TempDir;
use std::collections::HashMap;
async fn create_test_engine() -> (SearchEngine, TempDir) {
let temp_dir = TempDir::new().unwrap();
let index_path = temp_dir.path().join("test_index");
let config = SearchConfig {
index_path: index_path.to_string_lossy().to_string(),
max_results_default: 100,
sla_target_ms: 2000,
lsp_routing_rate: 0.0,
enable_fusion_pipeline: false,
enable_semantic_search: false,
enable_lsp: false,
context_lines: 3,
dataset_path: "test_dataset".to_string(),
enable_pinned_datasets: false,
default_dataset_version: None,
enable_corpus_validation: false,
};
let engine = SearchEngine::with_config(&index_path, config).await.unwrap();
(engine, temp_dir)
}
#[tokio::test]
async fn test_search_engine_creation() {
let temp_dir = TempDir::new().unwrap();
let index_path = temp_dir.path().join("test_index");
if index_path.exists() {
std::fs::remove_dir_all(&index_path).unwrap();
}
let config = SearchConfig {
index_path: index_path.to_string_lossy().to_string(),
max_results_default: 100,
sla_target_ms: 2000,
lsp_routing_rate: 0.0, enable_fusion_pipeline: false,
enable_semantic_search: false,
enable_lsp: false,
context_lines: 3,
dataset_path: "test_dataset".to_string(),
enable_pinned_datasets: false,
default_dataset_version: None,
enable_corpus_validation: false,
};
let engine = SearchEngine::with_config(&index_path, config).await;
match engine {
Ok(_) => {
println!("✅ SearchEngine created successfully");
},
Err(e) => panic!("SearchEngine creation failed: {:?}", e),
}
}
#[tokio::test]
async fn test_search_engine_with_custom_config() {
let temp_dir = TempDir::new().unwrap();
let config = SearchConfig {
index_path: temp_dir.path().to_string_lossy().to_string(),
max_results_default: 25,
sla_target_ms: 100,
lsp_routing_rate: 0.0,
enable_fusion_pipeline: false,
enable_semantic_search: false,
enable_lsp: false,
context_lines: 5,
dataset_path: "custom_dataset".to_string(),
enable_pinned_datasets: false,
default_dataset_version: Some("v1.0".to_string()),
enable_corpus_validation: true,
};
let engine = SearchEngine::with_config(&config.index_path, config.clone()).await.unwrap();
assert_eq!(engine.config.max_results_default, 25);
assert_eq!(engine.config.sla_target_ms, 100);
assert_eq!(engine.config.context_lines, 5);
assert!(engine.config.enable_corpus_validation);
}
#[tokio::test]
async fn test_search_request_default() {
let request = SearchRequest::default();
assert_eq!(request.max_results, 50);
assert_eq!(request.timeout_ms, 150);
assert!(request.enable_lsp);
assert!(request.include_context);
assert_eq!(request.search_method, Some(SearchMethod::Hybrid));
assert_eq!(request.search_types.len(), 4);
}
#[tokio::test]
async fn test_search_request_custom() {
let request = SearchRequest {
query: "test query".to_string(),
file_path: Some("test.rs".to_string()),
language: Some("rust".to_string()),
max_results: 100,
include_context: false,
timeout_ms: 300,
enable_lsp: false,
search_types: vec![SearchResultType::TextMatch, SearchResultType::Symbol],
search_method: Some(SearchMethod::Lexical),
};
assert_eq!(request.query, "test query");
assert_eq!(request.file_path, Some("test.rs".to_string()));
assert_eq!(request.language, Some("rust".to_string()));
assert_eq!(request.max_results, 100);
assert!(!request.include_context);
assert_eq!(request.timeout_ms, 300);
assert!(!request.enable_lsp);
assert_eq!(request.search_types.len(), 2);
assert_eq!(request.search_method, Some(SearchMethod::Lexical));
}
#[test]
fn test_search_result_types() {
let result = SearchResult {
file_path: "test.rs".to_string(),
line_number: 10,
column: 5,
content: "fn test()".to_string(),
score: 0.9,
result_type: SearchResultType::Definition,
language: Some("rust".to_string()),
context_lines: None,
lsp_metadata: None,
};
assert_eq!(result.result_type, SearchResultType::Definition);
assert_eq!(result.language, Some("rust".to_string()));
}
#[test]
fn test_search_result_types_ordering() {
let mut types = vec![
SearchResultType::TextMatch,
SearchResultType::Reference,
SearchResultType::Definition,
SearchResultType::TypeInfo,
SearchResultType::Symbol,
];
types.sort();
assert_ne!(types[0], types[1]);
assert!(SearchResultType::TextMatch < SearchResultType::Definition);
}
#[test]
fn test_search_method_default() {
assert_eq!(SearchMethod::default(), SearchMethod::Hybrid);
}
#[test]
fn test_search_method_variants() {
let methods = vec![
SearchMethod::Lexical,
SearchMethod::Structural,
SearchMethod::Semantic,
SearchMethod::Hybrid,
SearchMethod::ForceSemantic,
];
for method in &methods {
let cloned = method.clone();
assert_eq!(method, &cloned);
}
}
#[test]
fn test_search_metrics_sla_compliance() {
let metrics = SearchMetrics {
duration_ms: 100,
sla_compliant: true,
..Default::default()
};
assert!(metrics.meets_sla(150));
assert!(!metrics.meets_sla(50));
}
#[test]
fn test_search_metrics_quality_score() {
let metrics = SearchMetrics {
result_diversity_score: 0.8,
confidence_score: 0.9,
coverage_score: 0.7,
..Default::default()
};
let quality = metrics.quality_score();
assert!((quality - 0.8).abs() < 0.01); }
#[test]
fn test_search_metrics_default() {
let metrics = SearchMetrics::default();
assert_eq!(metrics.total_docs, 0);
assert_eq!(metrics.matched_docs, 0);
assert_eq!(metrics.duration_ms, 0);
assert_eq!(metrics.lsp_time_ms, 0);
assert_eq!(metrics.result_diversity_score, 0.0);
}
#[test]
fn test_search_config_default() {
let config = SearchConfig::default();
assert_eq!(config.index_path, "./index");
assert_eq!(config.max_results_default, 50);
assert_eq!(config.sla_target_ms, 150);
assert_eq!(config.lsp_routing_rate, 0.5);
assert!(config.enable_fusion_pipeline);
assert!(!config.enable_semantic_search);
assert!(config.enable_lsp);
assert_eq!(config.context_lines, 3);
assert!(config.enable_pinned_datasets);
assert!(config.enable_corpus_validation);
}
#[test]
fn test_engine_metrics_default() {
let metrics = EngineMetrics::default();
assert_eq!(metrics.total_searches, 0);
assert_eq!(metrics.sla_compliant_searches, 0);
assert_eq!(metrics.lsp_routed_searches, 0);
assert_eq!(metrics.avg_latency_ms, 0.0);
assert_eq!(metrics.p95_latency_ms, 0);
assert_eq!(metrics.p99_latency_ms, 0);
}
#[test]
fn test_engine_metrics_sla_compliance_rate() {
let mut metrics = EngineMetrics::default();
assert_eq!(metrics.sla_compliance_rate(), 0.0);
metrics.total_searches = 100;
metrics.sla_compliant_searches = 90;
assert_eq!(metrics.sla_compliance_rate(), 0.9);
metrics.sla_compliant_searches = 100;
assert_eq!(metrics.sla_compliance_rate(), 1.0);
}
#[test]
fn test_engine_metrics_lsp_routing_rate() {
let mut metrics = EngineMetrics::default();
assert_eq!(metrics.lsp_routing_rate(), 0.0);
metrics.total_searches = 100;
metrics.lsp_routed_searches = 50;
assert_eq!(metrics.lsp_routing_rate(), 0.5);
metrics.lsp_routed_searches = 100;
assert_eq!(metrics.lsp_routing_rate(), 1.0);
}
#[test]
fn test_lsp_metadata_creation() {
let metadata = LspMetadata {
hint_type: "Definition".to_string(),
server_type: "rust-analyzer".to_string(),
confidence: 0.95,
cached: true,
};
assert_eq!(metadata.hint_type, "Definition");
assert_eq!(metadata.server_type, "rust-analyzer");
assert_eq!(metadata.confidence, 0.95);
assert!(metadata.cached);
}
#[test]
fn test_search_document_creation() {
let doc = SearchDocument {
file_path: "src/main.rs".to_string(),
content: "fn main() { println!(\"Hello\"); }".to_string(),
line_number: 1,
language: Some("rust".to_string()),
};
assert_eq!(doc.file_path, "src/main.rs");
assert_eq!(doc.line_number, 1);
assert_eq!(doc.language, Some("rust".to_string()));
assert!(doc.content.contains("main"));
}
#[tokio::test]
async fn test_query_sanitization_comprehensive() {
let (engine, _temp_dir) = create_test_engine().await;
let test_cases = vec![
("```rust fn test() ```", "rust fn test"),
("**bold** text", "bold text"),
("__underline__ text", "underline text"),
("<!-- comment -->", "comment"),
("### Header", "Header"),
("(test)", "test"),
("[array]", "array"),
("{object}", "object"),
("\"quoted\"", "quoted"),
("'single'", "single"),
("test+more", "test more"),
("test-dash", "test dash"),
("test!excl", "test excl"),
("test?quest", "test quest"),
("test:colon", "test colon"),
("test;semi", "test semi"),
("test#hash", "test hash"),
("test@at", "test at"),
("test$dollar", "test dollar"),
("test%percent", "test percent"),
("test^caret", "test caret"),
("test&", "test amp"),
("test*star", "test star"),
("test=equal", "test equal"),
("test|pipe", "test pipe"),
("test\\back", "test back"),
("test/slash", "test slash"),
("test<less", "test less"),
("test>greater", "test greater"),
("test.dot", "test dot"),
("test,comma", "test comma"),
("test~tilde", "test tilde"),
("test`back", "test back"),
("test with spaces", "test with spaces"),
(" leading and trailing ", "leading and trailing"),
("", ""),
(" ", ""),
("!!!", ""),
];
for (input, expected_contains) in test_cases {
let sanitized = engine.sanitize_query(input);
if !expected_contains.is_empty() {
for word in expected_contains.split_whitespace() {
assert!(
sanitized.contains(word),
"Sanitized query '{}' should contain '{}' (from input '{}')",
sanitized, word, input
);
}
}
let problem_chars = ['(', ')', '[', ']', '{', '}', '"', '\'', '+', '!'];
for char in &problem_chars {
assert!(
!sanitized.contains(*char),
"Sanitized query '{}' still contains problematic character '{}'",
sanitized, char
);
}
}
}
#[tokio::test]
async fn test_search_with_empty_query() {
let (engine, _temp_dir) = create_test_engine().await;
let request = SearchRequest {
query: "".to_string(),
max_results: 10,
..Default::default()
};
let response = engine.search_comprehensive(request).await.unwrap();
assert!(response.results.is_empty());
}
#[tokio::test]
async fn test_search_with_whitespace_only_query() {
let (engine, _temp_dir) = create_test_engine().await;
let request = SearchRequest {
query: " \t\n ".to_string(),
max_results: 10,
..Default::default()
};
let response = engine.search_comprehensive(request).await.unwrap();
assert!(response.results.is_empty());
}
#[tokio::test]
async fn test_search_with_special_characters_query() {
let (engine, _temp_dir) = create_test_engine().await;
let request = SearchRequest {
query: "!@#$%^&*()_+-={}[]|\\:;\"'<>?,./".to_string(),
max_results: 10,
..Default::default()
};
let response = engine.search_comprehensive(request).await.unwrap();
assert!(response.metrics.duration_ms < 10000); }
#[tokio::test]
async fn test_search_basic_functionality() {
let (engine, _temp_dir) = create_test_engine().await;
let request = SearchRequest {
query: "fn".to_string(),
max_results: 10,
enable_lsp: false,
..Default::default()
};
let response = engine.search_comprehensive(request).await.unwrap();
assert!(response.metrics.duration_ms < 5000); assert!(response.metrics.total_docs > 0); }
#[tokio::test]
async fn test_search_with_different_methods() {
let (engine, _temp_dir) = create_test_engine().await;
let methods = vec![
SearchMethod::Lexical,
SearchMethod::Structural,
SearchMethod::Semantic,
SearchMethod::Hybrid,
];
for method in methods {
let request = SearchRequest {
query: "search".to_string(),
max_results: 5,
search_method: Some(method.clone()),
enable_lsp: false,
..Default::default()
};
let response = engine.search_comprehensive(request).await.unwrap();
assert!(response.metrics.duration_ms < 10000);
println!("✅ Search method {:?} completed in {}ms", method, response.metrics.duration_ms);
}
}
#[tokio::test]
async fn test_search_timeout_behavior() {
let (engine, _temp_dir) = create_test_engine().await;
let request = SearchRequest {
query: "test".to_string(),
max_results: 1000, timeout_ms: 1, enable_lsp: false,
..Default::default()
};
let response = engine.search_comprehensive(request).await.unwrap();
assert!(response.total_time_ms < 5000); }
#[tokio::test]
async fn test_search_result_limits() {
let (engine, _temp_dir) = create_test_engine().await;
let request = SearchRequest {
query: "test".to_string(),
max_results: 3,
enable_lsp: false,
..Default::default()
};
let response = engine.search_comprehensive(request).await.unwrap();
assert!(response.results.len() <= 3);
}
#[tokio::test]
async fn test_engine_metrics_tracking() {
let (engine, _temp_dir) = create_test_engine().await;
let request = SearchRequest {
query: "test".to_string(),
max_results: 5,
enable_lsp: false,
..Default::default()
};
let _ = engine.search_comprehensive(request).await.unwrap();
let metrics = engine.get_metrics().await;
assert!(metrics.total_searches > 0);
assert!(metrics.avg_latency_ms >= 0.0);
}
#[tokio::test]
async fn test_index_document() {
let (engine, _temp_dir) = create_test_engine().await;
let doc = SearchDocument {
file_path: "test.rs".to_string(),
content: "fn test_function() { return 42; }".to_string(),
line_number: 10,
language: None, };
let result = engine.index_document(&doc).await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_shutdown_gracefully() {
let (engine, _temp_dir) = create_test_engine().await;
let result = engine.shutdown().await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_get_total_docs() {
let (engine, _temp_dir) = create_test_engine().await;
let total = engine.get_total_docs().await;
assert!(total >= 0);
}
#[tokio::test]
async fn test_dataset_support_when_disabled() {
let (engine, _temp_dir) = create_test_engine().await;
assert!(!engine.has_dataset_support());
let dataset_info = engine.get_dataset_info().await;
assert!(dataset_info.is_none());
}
#[tokio::test]
async fn test_calculate_diversity_score() {
let (engine, _temp_dir) = create_test_engine().await;
let empty_results = vec![];
let score = engine.calculate_diversity_score(&empty_results);
assert_eq!(score, 0.0);
let diverse_results = vec![
SearchResult {
file_path: "file1.rs".to_string(),
result_type: SearchResultType::Definition,
..Default::default()
},
SearchResult {
file_path: "file2.rs".to_string(),
result_type: SearchResultType::Reference,
..Default::default()
},
SearchResult {
file_path: "file1.rs".to_string(),
result_type: SearchResultType::TextMatch,
..Default::default()
},
];
let score = engine.calculate_diversity_score(&diverse_results);
assert!(score > 0.0);
assert!(score <= 1.0);
}
#[tokio::test]
async fn test_calculate_confidence_score() {
let (engine, _temp_dir) = create_test_engine().await;
let empty_results = vec![];
let score = engine.calculate_confidence_score(&empty_results, None);
assert_eq!(score, 0.0);
let high_score_results = vec![
SearchResult {
score: 0.9,
..Default::default()
},
SearchResult {
score: 0.8,
..Default::default()
},
];
let score = engine.calculate_confidence_score(&high_score_results, None);
assert!(score > 0.8);
assert!(score <= 1.0);
}
#[tokio::test]
async fn test_calculate_coverage_score() {
let (engine, _temp_dir) = create_test_engine().await;
let empty_results = vec![];
let score = engine.calculate_coverage_score(&empty_results, "test query");
assert_eq!(score, 0.0);
let matching_results = vec![
SearchResult {
content: "test function implementation".to_string(),
..Default::default()
},
];
let score = engine.calculate_coverage_score(&matching_results, "test function");
assert!(score > 0.0);
assert!(score <= 1.0);
}
impl Default for SearchResult {
fn default() -> Self {
Self {
file_path: "default.rs".to_string(),
line_number: 1,
column: 0,
content: "default content".to_string(),
score: 0.5,
result_type: SearchResultType::TextMatch,
language: Some("rust".to_string()),
context_lines: None,
lsp_metadata: None,
}
}
}
}
#[cfg(test)]
mod search_regression_tests {
use super::*;
use std::collections::HashMap;
use tempfile::TempDir;
async fn create_test_search_engine() -> Result<(SearchEngine, TempDir)> {
let temp_dir = TempDir::new().unwrap();
let index_path = temp_dir.path().to_str().unwrap();
let mut config = SearchConfig::default();
config.index_path = index_path.to_string();
config.enable_lsp = false; config.enable_pinned_datasets = false;
let index_path_clone = config.index_path.clone();
let engine = SearchEngine::with_config(&index_path_clone, config).await?;
Ok((engine, temp_dir))
}
#[tokio::test]
async fn test_basic_search_functionality() {
let (engine, _temp_dir) = create_test_search_engine().await.unwrap();
let test_cases = vec![
("struct", "Should find struct definitions"),
("impl", "Should find impl blocks"),
("fn", "Should find function definitions"),
("SearchEngine", "Should find SearchEngine references"),
("pub", "Should find public declarations"),
];
for (query, description) in test_cases {
let request = SearchRequest {
query: query.to_string(),
max_results: 10,
file_path: None,
language: None,
enable_lsp: false,
include_context: false,
timeout_ms: 1000,
search_types: vec![SearchResultType::TextMatch],
search_method: Some(SearchMethod::Lexical), };
let response = engine.search_comprehensive(request).await.unwrap();
assert!(
!response.results.is_empty(),
"REGRESSION FAILURE: Query '{}' returned 0 results. {}",
query, description
);
println!("✅ Query '{}': {} results", query, response.results.len());
}
}
#[tokio::test]
async fn test_search_result_fusion_logic() {
let (engine, _temp_dir) = create_test_search_engine().await.unwrap();
let text_results = vec![
SearchResult {
file_path: "test1.rs".to_string(),
line_number: 10,
column: 0,
content: "fn test_function()".to_string(),
score: 0.8,
result_type: SearchResultType::TextMatch,
language: Some("rust".to_string()),
context_lines: None,
lsp_metadata: None,
},
SearchResult {
file_path: "test2.rs".to_string(),
line_number: 20,
column: 5,
content: "struct TestStruct".to_string(),
score: 0.6,
result_type: SearchResultType::TextMatch,
language: Some("rust".to_string()),
context_lines: None,
lsp_metadata: None,
},
];
let request = SearchRequest {
query: "test".to_string(),
max_results: 10,
..Default::default()
};
let fused_results = engine.fuse_search_results(text_results.clone(), None, &request).await;
assert_eq!(fused_results.len(), 2);
assert!(fused_results[0].score >= fused_results[1].score); }
#[tokio::test]
async fn test_search_with_file_path_filter() {
let (engine, _temp_dir) = create_test_search_engine().await.unwrap();
let request = SearchRequest {
query: "test".to_string(),
file_path: Some("specific_file.rs".to_string()),
max_results: 10,
enable_lsp: false,
..Default::default()
};
let response = engine.search_comprehensive(request).await.unwrap();
assert!(response.metrics.duration_ms < 5000);
}
#[tokio::test]
async fn test_search_with_language_filter() {
let (engine, _temp_dir) = create_test_search_engine().await.unwrap();
let request = SearchRequest {
query: "function".to_string(),
language: Some("rust".to_string()),
max_results: 10,
enable_lsp: false,
..Default::default()
};
let response = engine.search_comprehensive(request).await.unwrap();
assert!(response.metrics.duration_ms < 5000);
}
#[tokio::test]
async fn test_search_with_context_lines() {
let (engine, _temp_dir) = create_test_search_engine().await.unwrap();
let request = SearchRequest {
query: "test".to_string(),
include_context: true,
max_results: 5,
enable_lsp: false,
..Default::default()
};
let response = engine.search_comprehensive(request).await.unwrap();
assert!(response.metrics.duration_ms < 5000);
for result in &response.results {
if let Some(ref context) = result.context_lines {
assert!(context.len() <= 10); }
}
}
#[tokio::test]
async fn test_search_sla_compliance_tracking() {
let (engine, _temp_dir) = create_test_search_engine().await.unwrap();
let request = SearchRequest {
query: "quick".to_string(),
timeout_ms: 2000, max_results: 5,
enable_lsp: false,
..Default::default()
};
let response = engine.search_comprehensive(request).await.unwrap();
assert!(response.sla_compliant);
assert!(response.metrics.sla_compliant);
assert!(response.metrics.meets_sla(2000));
}
#[tokio::test]
async fn test_concurrent_search_operations() {
let (engine, _temp_dir) = create_test_search_engine().await.unwrap();
let engine = std::sync::Arc::new(engine);
let mut handles = vec![];
for i in 0..5 {
let engine_clone = engine.clone();
let handle = tokio::spawn(async move {
let request = SearchRequest {
query: format!("test{}", i),
max_results: 5,
enable_lsp: false,
..Default::default()
};
engine_clone.search_comprehensive(request).await
});
handles.push(handle);
}
let mut successful_searches = 0;
for handle in handles {
match handle.await {
Ok(Ok(_)) => successful_searches += 1,
Ok(Err(e)) => println!("Search failed: {}", e),
Err(e) => println!("Join failed: {}", e),
}
}
assert!(successful_searches >= 3);
let final_metrics = engine.get_metrics().await;
assert!(final_metrics.total_searches >= successful_searches as u64);
}
#[tokio::test]
async fn test_search_with_all_result_types() {
let (engine, _temp_dir) = create_test_search_engine().await.unwrap();
let all_types = vec![
SearchResultType::TextMatch,
SearchResultType::Definition,
SearchResultType::Reference,
SearchResultType::TypeInfo,
SearchResultType::Implementation,
SearchResultType::Symbol,
SearchResultType::Semantic,
];
let request = SearchRequest {
query: "search".to_string(),
search_types: all_types,
max_results: 20,
enable_lsp: false,
..Default::default()
};
let response = engine.search_comprehensive(request).await.unwrap();
assert!(response.metrics.duration_ms < 5000);
}
#[tokio::test]
async fn test_search_error_recovery() {
let (engine, _temp_dir) = create_test_search_engine().await.unwrap();
let problematic_queries = vec![
"".to_string(),
" ".to_string(),
"!@#$%^&*()".to_string(),
"a".repeat(1000), "SELECT * FROM users".to_string(), "<script>alert('xss')</script>".to_string(), ];
for query in problematic_queries {
let request = SearchRequest {
query: query.clone(),
max_results: 5,
enable_lsp: false,
timeout_ms: 1000,
..Default::default()
};
match engine.search_comprehensive(request).await {
Ok(response) => {
assert!(response.metrics.duration_ms < 5000);
println!("✅ Handled problematic query successfully: '{}'",
if query.len() > 20 { &query[..20] } else { &query });
},
Err(e) => {
println!("⚠️ Query failed gracefully: '{}' - {}",
if query.len() > 20 { &query[..20] } else { &query }, e);
}
}
}
}
#[tokio::test]
async fn test_search_metrics_quality_calculations() {
let (engine, _temp_dir) = create_test_search_engine().await.unwrap();
let request = SearchRequest {
query: "test metrics calculation".to_string(),
max_results: 10,
enable_lsp: false,
..Default::default()
};
let response = engine.search_comprehensive(request).await.unwrap();
assert!(response.metrics.result_diversity_score >= 0.0);
assert!(response.metrics.result_diversity_score <= 1.0);
assert!(response.metrics.confidence_score >= 0.0);
assert!(response.metrics.confidence_score <= 1.0);
assert!(response.metrics.coverage_score >= 0.0);
assert!(response.metrics.coverage_score <= 1.0);
let overall_quality = response.metrics.quality_score();
assert!(overall_quality >= 0.0);
assert!(overall_quality <= 1.0);
}
#[tokio::test]
async fn test_search_with_force_semantic_method() {
let (engine, _temp_dir) = create_test_search_engine().await.unwrap();
let request = SearchRequest {
query: "semantic search test".to_string(),
search_method: Some(SearchMethod::ForceSemantic),
max_results: 5,
enable_lsp: false,
..Default::default()
};
let response = engine.search_comprehensive(request).await.unwrap();
assert!(response.metrics.duration_ms < 10000);
}
#[tokio::test]
async fn test_search_response_structure_validation() {
let (engine, _temp_dir) = create_test_search_engine().await.unwrap();
let request = SearchRequest {
query: "validation test".to_string(),
max_results: 5,
enable_lsp: false,
..Default::default()
};
let max_results = request.max_results; let response = engine.search_comprehensive(request).await.unwrap();
assert!(response.total_time_ms > 0);
assert_eq!(response.sla_compliant, response.metrics.sla_compliant);
assert!(response.results.len() <= max_results);
for result in &response.results {
assert!(!result.file_path.is_empty());
assert!(result.line_number >= 0);
assert!(result.column >= 0);
assert!(result.score >= 0.0);
assert!(result.score <= 10.0); }
}
#[tokio::test]
async fn test_large_result_set_handling() {
let (engine, _temp_dir) = create_test_search_engine().await.unwrap();
let request = SearchRequest {
query: "test".to_string(), max_results: 1000, enable_lsp: false,
timeout_ms: 5000,
..Default::default()
};
let response = engine.search_comprehensive(request).await.unwrap();
assert!(response.metrics.duration_ms < 5000);
assert!(response.results.len() <= 1000);
}
#[tokio::test]
async fn test_dataset_error_handling() {
let (engine, _temp_dir) = create_test_search_engine().await.unwrap();
let versions_result = engine.list_dataset_versions().await;
assert!(versions_result.is_err());
let validation_result = engine.validate_current_dataset().await;
assert!(validation_result.is_err());
let load_result = engine.load_dataset_version("nonexistent").await;
assert!(load_result.is_err());
let reload_result = engine.reload_current_dataset().await;
assert!(reload_result.is_err());
let smoke_dataset = engine.get_smoke_dataset().await;
assert!(smoke_dataset.is_none());
let slice = engine.get_dataset_slice("test").await;
assert!(slice.is_none()); }
#[tokio::test]
async fn test_query_sanitization_preserves_searchable_terms() {
let (engine, _temp_dir) = create_test_search_engine().await.unwrap();
let original_query = "struct SearchEngine impl search";
let sanitized = engine.sanitize_query(original_query);
assert!(
sanitized.contains("struct") || sanitized.contains("SearchEngine") || sanitized.contains("impl"),
"REGRESSION FAILURE: Query sanitization removed all searchable terms: '{}' -> '{}'",
original_query, sanitized
);
let request = SearchRequest {
query: sanitized.clone(),
max_results: 10,
file_path: None,
language: None,
enable_lsp: false,
include_context: false,
timeout_ms: 1000,
search_types: vec![SearchResultType::TextMatch],
search_method: Some(SearchMethod::Lexical),
};
let response = engine.search_comprehensive(request).await.unwrap();
assert!(
!response.results.is_empty(),
"REGRESSION FAILURE: Sanitized query '{}' returned 0 results", sanitized
);
println!("✅ Sanitized query '{}': {} results", sanitized, response.results.len());
}
#[tokio::test]
async fn test_index_population_regression() {
let (engine, _temp_dir) = create_test_search_engine().await.unwrap();
let reader = &engine.reader;
let searcher = reader.searcher();
let all_query = tantivy::query::AllQuery;
let top_docs = searcher.search(&all_query, &tantivy::collector::TopDocs::with_limit(1)).unwrap();
assert!(
!top_docs.is_empty(),
"REGRESSION FAILURE: Index should be automatically populated with documents"
);
println!("✅ Index contains {} documents (verified with sample)", top_docs.len());
}
}