Skip to main content

brainwires_rag/rag/types/
query.rs

1use serde::{Deserialize, Serialize};
2
3// Re-export shared types from core
4pub use brainwires_core::SearchResult;
5
6use super::index::PROJECT_NAME_MAX_LENGTH;
7
8/// Default value for hybrid search (enabled).
9pub fn default_hybrid() -> bool {
10    true
11}
12
13/// Default result limit.
14pub fn default_limit() -> usize {
15    10
16}
17
18/// Default minimum similarity score.
19pub fn default_min_score() -> f32 {
20    0.7
21}
22
23/// Request to query the codebase
24#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
25pub struct QueryRequest {
26    /// The question or search query
27    pub query: String,
28    /// Optional path to filter by specific indexed codebase
29    #[serde(default)]
30    pub path: Option<String>,
31    /// Optional project name to filter by
32    #[serde(default)]
33    pub project: Option<String>,
34    /// Number of results to return (default: 10)
35    #[serde(default = "default_limit")]
36    pub limit: usize,
37    /// Minimum similarity score (0.0 to 1.0, default: 0.7)
38    #[serde(default = "default_min_score")]
39    pub min_score: f32,
40    /// Enable hybrid search (vector + keyword) - default: true
41    #[serde(default = "default_hybrid")]
42    pub hybrid: bool,
43}
44
45/// Response from query operation
46#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
47pub struct QueryResponse {
48    /// List of search results, ordered by relevance
49    pub results: Vec<SearchResult>,
50    /// Time taken in milliseconds
51    pub duration_ms: u64,
52    /// The actual threshold used (may be lower than requested if adaptive search kicked in)
53    #[serde(default)]
54    pub threshold_used: f32,
55    /// Whether the threshold was automatically lowered to find results
56    #[serde(default)]
57    pub threshold_lowered: bool,
58}
59
60impl QueryRequest {
61    /// Validate the query request
62    pub fn validate(&self) -> Result<(), String> {
63        // Validate query is not empty
64        if self.query.trim().is_empty() {
65            return Err("query cannot be empty".to_string());
66        }
67
68        // Validate query length is reasonable (max 10KB)
69        const MAX_QUERY_LENGTH: usize = 10_240; // 10KB
70        if self.query.len() > MAX_QUERY_LENGTH {
71            return Err(format!(
72                "query too long: {} bytes (max: {} bytes)",
73                self.query.len(),
74                MAX_QUERY_LENGTH
75            ));
76        }
77
78        // Validate min_score is in valid range [0.0, 1.0]
79        if !(0.0..=1.0).contains(&self.min_score) {
80            return Err(format!(
81                "min_score must be between 0.0 and 1.0, got: {}",
82                self.min_score
83            ));
84        }
85
86        // Validate limit is reasonable (max 1000)
87        const MAX_LIMIT: usize = 1000;
88        if self.limit > MAX_LIMIT {
89            return Err(format!(
90                "limit too large: {} (max: {})",
91                self.limit, MAX_LIMIT
92            ));
93        }
94
95        // Validate project name if provided
96        if let Some(ref project) = self.project {
97            if project.is_empty() {
98                return Err("project name cannot be empty".to_string());
99            }
100            if project.len() > PROJECT_NAME_MAX_LENGTH {
101                return Err(format!(
102                    "project name too long (max {} characters)",
103                    PROJECT_NAME_MAX_LENGTH
104                ));
105            }
106        }
107
108        Ok(())
109    }
110}