Skip to main content
Glama
by 8b-is
unified_search.rs18.7 kB
//! 🔍 Unified Search - Natural Language Query Engine //! //! This module provides a unified search interface that accepts natural //! language queries and intelligently combines multiple search tools //! to provide comprehensive, context-aware results. use super::context::ContextAnalyzer; use super::nlp::{ParsedQuery, QueryParser, SearchIntent}; use super::smart_ls::SmartLS; use super::smart_read::SmartReader; use super::{SmartResponse, TaskContext, TokenSavings}; use crate::scanner::{FileNode, Scanner, ScannerConfig}; use anyhow::Result; use serde::{Deserialize, Serialize}; use std::path::Path; /// 🔍 Unified search engine pub struct UnifiedSearch { query_parser: QueryParser, context_analyzer: ContextAnalyzer, _smart_read: SmartReader, _smart_ls: SmartLS, } /// 🎯 Search result with multiple result types #[derive(Debug, Clone, Serialize, Deserialize)] pub struct SearchResult { /// File or directory node pub node: FileNode, /// Relevance score pub relevance: super::RelevanceScore, /// Result type pub result_type: SearchResultType, /// Content snippet (for file content matches) pub snippet: Option<String>, /// Line number (for content matches) pub line_number: Option<usize>, /// Suggested actions pub suggested_actions: Vec<String>, } /// 🏷️ Types of search results #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub enum SearchResultType { FileMatch, DirectoryMatch, ContentMatch, StructureMatch, ConfigMatch, TestMatch, DocumentationMatch, } /// 📊 Unified search response pub type UnifiedSearchResponse = SmartResponse<SearchResult>; impl UnifiedSearch { /// Create new unified search engine pub fn new() -> Self { Self { query_parser: QueryParser::new(), context_analyzer: ContextAnalyzer::new(), _smart_read: SmartReader::new(), _smart_ls: SmartLS::new(), } } /// 🔍 Execute unified search with natural language query pub fn search( &self, path: &Path, query: &str, max_results: Option<usize>, ) -> Result<UnifiedSearchResponse> { // Parse natural language query let parsed_query = self.query_parser.parse(query); // Create task context from parsed query let task_context = self.create_task_context(&parsed_query, max_results); // Execute search based on intent let results = match parsed_query.intent { SearchIntent::FindCode => self.search_code(path, &parsed_query, &task_context)?, SearchIntent::FindConfig => self.search_config(path, &parsed_query, &task_context)?, SearchIntent::FindTests => self.search_tests(path, &parsed_query, &task_context)?, SearchIntent::FindDocs => self.search_docs(path, &parsed_query, &task_context)?, SearchIntent::FindAPI => self.search_api(path, &parsed_query, &task_context)?, SearchIntent::FindAuth => self.search_auth(path, &parsed_query, &task_context)?, SearchIntent::FindSecurity => { self.search_security(path, &parsed_query, &task_context)? } SearchIntent::FindPerformance => { self.search_performance(path, &parsed_query, &task_context)? } SearchIntent::Debug => self.search_debug(path, &parsed_query, &task_context)?, _ => self.search_general(path, &parsed_query, &task_context)?, }; // Split results by relevance let (primary, secondary) = self.split_results_by_relevance(&results, &task_context); // Calculate token savings let token_savings = self.calculate_token_savings(&results, &parsed_query); // Generate context summary and suggestions let context_summary = self.generate_search_summary(&parsed_query, &primary, &secondary); let suggestions = self.generate_search_suggestions(&parsed_query, &primary, &secondary); Ok(UnifiedSearchResponse { primary, secondary, context_summary, token_savings, suggestions, }) } /// Create task context from parsed query fn create_task_context( &self, parsed_query: &ParsedQuery, max_results: Option<usize>, ) -> TaskContext { TaskContext { task: parsed_query.original_query.clone(), focus_areas: parsed_query.focus_areas.clone(), relevance_threshold: if parsed_query.confidence > 0.8 { 0.7 } else { 0.5 }, max_results, } } /// 💻 Search for code files and content fn search_code( &self, path: &Path, parsed_query: &ParsedQuery, context: &TaskContext, ) -> Result<Vec<SearchResult>> { let mut results = Vec::new(); // Find code files let config = ScannerConfig { max_depth: 10, show_hidden: false, ..Default::default() }; let scanner = Scanner::new(path, config)?; let (nodes, _stats) = scanner.scan()?; for node in nodes { if self.is_code_file(&node) { let relevance = self.context_analyzer.score_file_relevance(&node, context); if relevance.score >= context.relevance_threshold { let result = SearchResult { node: node.clone(), relevance, result_type: SearchResultType::FileMatch, snippet: None, line_number: None, suggested_actions: vec![ "Smart read".to_string(), "Analyze code".to_string(), ], }; results.push(result); } } } // Search within code files for keywords if !parsed_query.keywords.is_empty() { results.extend(self.search_file_contents(path, &parsed_query.keywords, context)?); } Ok(results) } /// ⚙️ Search for configuration files fn search_config( &self, path: &Path, _parsed_query: &ParsedQuery, context: &TaskContext, ) -> Result<Vec<SearchResult>> { let mut results = Vec::new(); let config = ScannerConfig { max_depth: 3, show_hidden: true, // Config files might be hidden follow_symlinks: false, ..Default::default() }; let scanner = Scanner::new(path, config)?; let (nodes, _) = scanner.scan()?; for node in nodes { if self.is_config_file(&node) { let relevance = self.context_analyzer.score_file_relevance(&node, context); if relevance.score >= context.relevance_threshold { let result = SearchResult { node: node.clone(), relevance, result_type: SearchResultType::ConfigMatch, snippet: None, line_number: None, suggested_actions: vec![ "Edit config".to_string(), "Validate syntax".to_string(), ], }; results.push(result); } } } Ok(results) } /// 🧪 Search for test files fn search_tests( &self, path: &Path, _parsed_query: &ParsedQuery, context: &TaskContext, ) -> Result<Vec<SearchResult>> { let mut results = Vec::new(); let config = ScannerConfig { max_depth: 4, show_hidden: false, follow_symlinks: false, ..Default::default() }; let scanner = Scanner::new(path, config)?; let (nodes, _) = scanner.scan()?; for node in nodes { if self.is_test_file(&node) { let relevance = self.context_analyzer.score_file_relevance(&node, context); if relevance.score >= context.relevance_threshold { let result = SearchResult { node: node.clone(), relevance, result_type: SearchResultType::TestMatch, snippet: None, line_number: None, suggested_actions: vec![ "Run tests".to_string(), "Analyze coverage".to_string(), ], }; results.push(result); } } } Ok(results) } /// 📚 Search for documentation fn search_docs( &self, path: &Path, _parsed_query: &ParsedQuery, context: &TaskContext, ) -> Result<Vec<SearchResult>> { let mut results = Vec::new(); let config = ScannerConfig { max_depth: 3, show_hidden: false, follow_symlinks: false, ..Default::default() }; let scanner = Scanner::new(path, config)?; let (nodes, _) = scanner.scan()?; for node in nodes { if self.is_doc_file(&node) { let relevance = self.context_analyzer.score_file_relevance(&node, context); if relevance.score >= context.relevance_threshold { let result = SearchResult { node: node.clone(), relevance, result_type: SearchResultType::DocumentationMatch, snippet: None, line_number: None, suggested_actions: vec!["Read docs".to_string(), "Update docs".to_string()], }; results.push(result); } } } Ok(results) } /// 🌐 Search for API-related files fn search_api( &self, path: &Path, parsed_query: &ParsedQuery, context: &TaskContext, ) -> Result<Vec<SearchResult>> { // Similar implementation to search_code but with API-specific filtering self.search_code(path, parsed_query, context) } /// 🔐 Search for authentication-related files fn search_auth( &self, path: &Path, parsed_query: &ParsedQuery, context: &TaskContext, ) -> Result<Vec<SearchResult>> { // Similar implementation to search_code but with auth-specific filtering self.search_code(path, parsed_query, context) } /// 🛡️ Search for security-related files fn search_security( &self, path: &Path, parsed_query: &ParsedQuery, context: &TaskContext, ) -> Result<Vec<SearchResult>> { // Similar implementation to search_code but with security-specific filtering self.search_code(path, parsed_query, context) } /// ⚡ Search for performance-related files fn search_performance( &self, path: &Path, parsed_query: &ParsedQuery, context: &TaskContext, ) -> Result<Vec<SearchResult>> { // Similar implementation to search_code but with performance-specific filtering self.search_code(path, parsed_query, context) } /// 🐛 Search for debugging-related files fn search_debug( &self, path: &Path, parsed_query: &ParsedQuery, context: &TaskContext, ) -> Result<Vec<SearchResult>> { // Similar implementation to search_code but with debug-specific filtering self.search_code(path, parsed_query, context) } /// 🔍 General search combining multiple strategies fn search_general( &self, path: &Path, parsed_query: &ParsedQuery, context: &TaskContext, ) -> Result<Vec<SearchResult>> { let mut results = Vec::new(); // Combine results from different search types results.extend(self.search_code(path, parsed_query, context)?); results.extend(self.search_config(path, parsed_query, context)?); results.extend(self.search_docs(path, parsed_query, context)?); // Remove duplicates and sort by relevance results.sort_by(|a, b| b.relevance.score.partial_cmp(&a.relevance.score).unwrap()); results.dedup_by(|a, b| a.node.path == b.node.path); Ok(results) } /// Search within file contents for keywords fn search_file_contents( &self, _path: &Path, _keywords: &[String], _context: &TaskContext, ) -> Result<Vec<SearchResult>> { // This would integrate with ripgrep or similar for content search // For now, return empty results Ok(Vec::new()) } /// Check if file is a code file fn is_code_file(&self, node: &FileNode) -> bool { matches!( node.category, crate::scanner::FileCategory::Rust | crate::scanner::FileCategory::Python | crate::scanner::FileCategory::JavaScript | crate::scanner::FileCategory::TypeScript | crate::scanner::FileCategory::Go | crate::scanner::FileCategory::Java | crate::scanner::FileCategory::Cpp | crate::scanner::FileCategory::C ) } /// Check if file is a configuration file fn is_config_file(&self, node: &FileNode) -> bool { matches!( node.category, crate::scanner::FileCategory::Json | crate::scanner::FileCategory::Yaml | crate::scanner::FileCategory::Toml ) || { let name = node.path.file_name().and_then(|n| n.to_str()).unwrap_or(""); name.starts_with('.') && (name.contains("config") || name.contains("env") || name == ".gitignore" || name == ".dockerignore") } } /// Check if file is a test file fn is_test_file(&self, node: &FileNode) -> bool { let name = node.path.file_name().and_then(|n| n.to_str()).unwrap_or(""); let name_lower = name.to_lowercase(); name_lower.contains("test") || name_lower.contains("spec") || node.path.to_string_lossy().contains("/test/") || node.path.to_string_lossy().contains("/tests/") } /// Check if file is documentation fn is_doc_file(&self, node: &FileNode) -> bool { matches!(node.category, crate::scanner::FileCategory::Markdown) || { let name = node.path.file_name().and_then(|n| n.to_str()).unwrap_or(""); let name_lower = name.to_lowercase(); name_lower.starts_with("readme") || name_lower.contains("doc") } || node.path.to_string_lossy().contains("/docs/") } /// Split results by relevance threshold fn split_results_by_relevance( &self, results: &[SearchResult], context: &TaskContext, ) -> (Vec<SearchResult>, Vec<SearchResult>) { let mut primary = Vec::new(); let mut secondary = Vec::new(); for result in results { if result.relevance.score >= context.relevance_threshold { primary.push(result.clone()); } else if result.relevance.score >= context.relevance_threshold * 0.6 { secondary.push(result.clone()); } } (primary, secondary) } /// Calculate token savings from unified search fn calculate_token_savings( &self, results: &[SearchResult], _parsed_query: &ParsedQuery, ) -> TokenSavings { let original_tokens = 1000; // Estimated tokens for multiple separate tool calls let compressed_tokens = results.len() * 20; // Estimated tokens per result TokenSavings::new(original_tokens, compressed_tokens, "unified-search") } /// Generate search summary fn generate_search_summary( &self, parsed_query: &ParsedQuery, primary: &[SearchResult], secondary: &[SearchResult], ) -> String { format!( "Unified search for '{}' (intent: {:?}, confidence: {:.1}%) found {} high-priority and {} medium-priority results", parsed_query.original_query, parsed_query.intent, parsed_query.confidence * 100.0, primary.len(), secondary.len() ) } /// Generate search suggestions fn generate_search_suggestions( &self, parsed_query: &ParsedQuery, primary: &[SearchResult], secondary: &[SearchResult], ) -> Vec<String> { let mut suggestions = Vec::new(); if primary.is_empty() && secondary.is_empty() { suggestions.push("No results found. Try broadening your search terms.".to_string()); suggestions.push("Consider using different keywords or checking spelling.".to_string()); } else if primary.is_empty() { suggestions.push( "No high-priority results. Consider lowering relevance threshold.".to_string(), ); } // Intent-specific suggestions match parsed_query.intent { SearchIntent::FindCode => { suggestions.push("Use SmartRead to analyze code files in detail.".to_string()); } SearchIntent::FindConfig => { suggestions.push( "Use find_config_files for comprehensive configuration discovery.".to_string(), ); } SearchIntent::FindTests => { suggestions .push("Use find_tests to discover all test files and patterns.".to_string()); } _ => {} } suggestions } } impl Default for UnifiedSearch { fn default() -> Self { Self::new() } } #[cfg(test)] mod tests { use super::*; // use std::path::PathBuf; // Commented out as unused #[test] fn test_unified_search_creation() { let _search = UnifiedSearch::new(); // Basic creation test - verify it was created properly // UnifiedSearch structure verified by successful creation } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/8b-is/smart-tree'

If you have feedback or need assistance with the MCP directory API, please join our Discord server