Skip to main content

brainwires_rag/rag/types/
search.rs

1use serde::{Deserialize, Serialize};
2
3use super::index::PROJECT_NAME_MAX_LENGTH;
4use super::query::{QueryRequest, default_limit, default_min_score};
5
6/// Request to search with file type filters
7#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
8pub struct AdvancedSearchRequest {
9    /// The search query
10    pub query: String,
11    /// Optional path to filter by specific indexed codebase
12    #[serde(default)]
13    pub path: Option<String>,
14    /// Optional project name to filter by
15    #[serde(default)]
16    pub project: Option<String>,
17    /// Number of results to return
18    #[serde(default = "default_limit")]
19    pub limit: usize,
20    /// Minimum similarity score
21    #[serde(default = "default_min_score")]
22    pub min_score: f32,
23    /// Filter by file extensions (e.g., ["rs", "toml"])
24    #[serde(default)]
25    pub file_extensions: Vec<String>,
26    /// Filter by programming languages
27    #[serde(default)]
28    pub languages: Vec<String>,
29    /// Filter by file path patterns (glob)
30    #[serde(default)]
31    pub path_patterns: Vec<String>,
32}
33
34impl AdvancedSearchRequest {
35    /// Validate the advanced search request
36    pub fn validate(&self) -> Result<(), String> {
37        // Reuse QueryRequest validation logic
38        let query_req = QueryRequest {
39            query: self.query.clone(),
40            path: None,
41            project: self.project.clone(),
42            limit: self.limit,
43            min_score: self.min_score,
44            hybrid: true,
45        };
46        query_req.validate()?;
47
48        // Additional validation for file extensions
49        for ext in &self.file_extensions {
50            if ext.is_empty() {
51                return Err("file extension cannot be empty".to_string());
52            }
53            if ext.len() > 20 {
54                return Err(format!(
55                    "file extension too long: {} (max 20 characters)",
56                    ext
57                ));
58            }
59        }
60
61        // Validate languages
62        for lang in &self.languages {
63            if lang.is_empty() {
64                return Err("language name cannot be empty".to_string());
65            }
66            if lang.len() > 50 {
67                return Err(format!(
68                    "language name too long: {} (max 50 characters)",
69                    lang
70                ));
71            }
72        }
73
74        Ok(())
75    }
76}
77
78/// Default git path for search.
79pub fn default_git_path() -> String {
80    ".".to_string()
81}
82
83/// Default maximum number of commits to search.
84pub fn default_max_commits() -> usize {
85    10
86}
87
88/// Request to search git history
89#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
90pub struct SearchGitHistoryRequest {
91    /// The search query
92    pub query: String,
93    /// Path to the codebase (will discover git repo)
94    #[serde(default = "default_git_path")]
95    pub path: String,
96    /// Optional project name
97    #[serde(default)]
98    pub project: Option<String>,
99    /// Optional branch name (default: current branch)
100    #[serde(default)]
101    pub branch: Option<String>,
102    /// Maximum number of commits to index/search (default: 10)
103    #[serde(default = "default_max_commits")]
104    pub max_commits: usize,
105    /// Number of results to return (default: 10)
106    #[serde(default = "default_limit")]
107    pub limit: usize,
108    /// Minimum similarity score (0.0 to 1.0, default: 0.7)
109    #[serde(default = "default_min_score")]
110    pub min_score: f32,
111    /// Filter by commit author (optional regex pattern)
112    #[serde(default)]
113    pub author: Option<String>,
114    /// Filter by commits since this date (ISO 8601 or Unix timestamp)
115    #[serde(default)]
116    pub since: Option<String>,
117    /// Filter by commits until this date (ISO 8601 or Unix timestamp)
118    #[serde(default)]
119    pub until: Option<String>,
120    /// Filter by file path pattern (optional regex)
121    #[serde(default)]
122    pub file_pattern: Option<String>,
123}
124
125impl SearchGitHistoryRequest {
126    /// Validate the git history search request
127    pub fn validate(&self) -> Result<(), String> {
128        // Validate query
129        if self.query.trim().is_empty() {
130            return Err("query cannot be empty".to_string());
131        }
132
133        const MAX_QUERY_LENGTH: usize = 10_240; // 10KB
134        if self.query.len() > MAX_QUERY_LENGTH {
135            return Err(format!(
136                "query too long: {} bytes (max: {} bytes)",
137                self.query.len(),
138                MAX_QUERY_LENGTH
139            ));
140        }
141
142        // Validate path
143        let path = std::path::Path::new(&self.path);
144        if !path.exists() {
145            return Err(format!("Path does not exist: {}", self.path));
146        }
147
148        // Validate min_score range
149        if !(0.0..=1.0).contains(&self.min_score) {
150            return Err(format!(
151                "min_score must be between 0.0 and 1.0, got: {}",
152                self.min_score
153            ));
154        }
155
156        // Validate limit
157        const MAX_LIMIT: usize = 1000;
158        if self.limit > MAX_LIMIT {
159            return Err(format!(
160                "limit too large: {} (max: {})",
161                self.limit, MAX_LIMIT
162            ));
163        }
164
165        // Validate max_commits
166        const MAX_COMMITS_LIMIT: usize = 10000;
167        if self.max_commits > MAX_COMMITS_LIMIT {
168            return Err(format!(
169                "max_commits too large: {} (max: {})",
170                self.max_commits, MAX_COMMITS_LIMIT
171            ));
172        }
173
174        // Validate project name if provided
175        if let Some(ref project) = self.project {
176            if project.is_empty() {
177                return Err("project name cannot be empty".to_string());
178            }
179            if project.len() > PROJECT_NAME_MAX_LENGTH {
180                return Err(format!(
181                    "project name too long (max {} characters)",
182                    PROJECT_NAME_MAX_LENGTH
183                ));
184            }
185        }
186
187        Ok(())
188    }
189}
190
191/// A single git search result
192#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
193pub struct GitSearchResult {
194    /// Git commit hash (SHA)
195    pub commit_hash: String,
196    /// Commit message
197    pub commit_message: String,
198    /// Author name
199    pub author: String,
200    /// Author email
201    pub author_email: String,
202    /// Commit date (Unix timestamp)
203    pub commit_date: i64,
204    /// Combined similarity score (0.0 to 1.0)
205    pub score: f32,
206    /// Vector similarity score
207    pub vector_score: f32,
208    /// Keyword match score (if hybrid search enabled)
209    pub keyword_score: Option<f32>,
210    /// Files changed in this commit
211    pub files_changed: Vec<String>,
212    /// Diff snippet (first ~500 characters)
213    pub diff_snippet: String,
214}
215
216/// Response from git history search
217#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
218pub struct SearchGitHistoryResponse {
219    /// List of matching commits, ordered by relevance
220    pub results: Vec<GitSearchResult>,
221    /// Number of commits indexed during this search
222    pub commits_indexed: usize,
223    /// Total commits in cache for this repo
224    pub total_cached_commits: usize,
225    /// Time taken in milliseconds
226    pub duration_ms: u64,
227}