1use crate::project::ProjectId;
4use serde::{Deserialize, Serialize};
5
6#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
8#[serde(rename_all = "lowercase")]
9pub enum SearchMode {
10 #[default]
12 Exact,
13 Fuzzy,
15 FullText,
17 Hybrid,
19 Vector,
21}
22
23#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
25#[serde(rename_all = "lowercase")]
26pub enum TagMatchMode {
27 #[default]
29 Any,
30 All,
32}
33
34#[derive(Debug, Clone, Serialize, Deserialize)]
36#[serde(rename_all = "lowercase")]
37pub enum ProjectScope {
38 Single(ProjectId),
40 Multiple(Vec<ProjectId>),
42 All,
44}
45
46impl Default for ProjectScope {
47 fn default() -> Self {
48 Self::All
49 }
50}
51
52#[derive(Debug, Clone, Serialize, Deserialize)]
54pub struct Pagination {
55 #[serde(default)]
57 pub page: usize,
58
59 #[serde(default = "default_page_size")]
61 pub page_size: usize,
62}
63
64impl Default for Pagination {
65 fn default() -> Self {
66 Self {
67 page: 0,
68 page_size: default_page_size(),
69 }
70 }
71}
72
73fn default_page_size() -> usize {
74 100
75}
76
77impl Pagination {
78 pub fn new(page: usize, page_size: usize) -> Self {
79 Self {
80 page,
81 page_size: page_size.min(1000), }
83 }
84
85 pub fn offset(&self) -> usize {
86 self.page * self.page_size
87 }
88}
89
90#[derive(Debug, Clone, Default, Serialize, Deserialize)]
92pub struct SearchQuery {
93 #[serde(skip_serializing_if = "Option::is_none")]
95 pub text: Option<String>,
96
97 #[serde(default)]
99 pub mode: SearchMode,
100
101 #[serde(default = "default_fuzzy_threshold")]
103 pub fuzzy_threshold: f32,
104
105 #[serde(skip_serializing_if = "Option::is_none")]
107 pub query_embedding: Option<Vec<f32>>,
108
109 #[serde(default = "default_similarity_threshold")]
111 pub similarity_threshold: f32,
112
113 #[serde(default)]
115 pub entity_types: Vec<String>,
116
117 #[serde(default)]
119 pub tags: Vec<String>,
120
121 #[serde(default)]
123 pub tag_match_mode: TagMatchMode,
124
125 #[serde(default)]
127 pub projects: ProjectScope,
128
129 #[serde(default)]
131 pub pagination: Pagination,
132
133 #[serde(default = "default_true")]
135 pub include_relations: bool,
136}
137
138fn default_fuzzy_threshold() -> f32 {
139 0.3
140}
141
142fn default_similarity_threshold() -> f32 {
143 0.7
144}
145
146fn default_true() -> bool {
147 true
148}
149
150impl SearchQuery {
151 pub fn new(text: impl Into<String>) -> Self {
153 Self {
154 text: Some(text.into()),
155 ..Default::default()
156 }
157 }
158
159 pub fn text(text: impl Into<String>) -> Self {
161 Self::new(text)
162 }
163
164 pub fn empty() -> Self {
166 Self::default()
167 }
168
169 pub fn with_mode(mut self, mode: SearchMode) -> Self {
171 self.mode = mode;
172 self
173 }
174
175 pub fn with_fuzzy_threshold(mut self, threshold: f32) -> Self {
177 self.fuzzy_threshold = threshold.clamp(0.0, 1.0);
178 self
179 }
180
181 pub fn with_embedding(mut self, embedding: Vec<f32>) -> Self {
183 self.query_embedding = Some(embedding);
184 self.mode = SearchMode::Vector;
185 self
186 }
187
188 pub fn with_similarity_threshold(mut self, threshold: f32) -> Self {
190 self.similarity_threshold = threshold.clamp(0.0, 1.0);
191 self
192 }
193
194 pub fn with_entity_type(mut self, entity_type: impl Into<String>) -> Self {
196 self.entity_types.push(entity_type.into());
197 self
198 }
199
200 pub fn with_tag(mut self, tag: impl Into<String>) -> Self {
202 self.tags.push(tag.into());
203 self
204 }
205
206 pub fn with_tag_match_mode(mut self, mode: TagMatchMode) -> Self {
208 self.tag_match_mode = mode;
209 self
210 }
211
212 pub fn in_project(mut self, project_id: ProjectId) -> Self {
214 self.projects = ProjectScope::Single(project_id);
215 self
216 }
217
218 pub fn in_all_projects(mut self) -> Self {
220 self.projects = ProjectScope::All;
221 self
222 }
223
224 pub fn with_pagination(mut self, page: usize, page_size: usize) -> Self {
226 self.pagination = Pagination::new(page, page_size);
227 self
228 }
229}
230
231#[derive(Debug, Clone, Serialize, Deserialize)]
233pub struct PaginatedResults<T> {
234 pub data: Vec<T>,
236
237 pub pagination: PaginationInfo,
239}
240
241#[derive(Debug, Clone, Serialize, Deserialize)]
243pub struct PaginationInfo {
244 pub current_page: usize,
245 pub page_size: usize,
246 pub total_count: usize,
247 pub total_pages: usize,
248 pub has_next_page: bool,
249 pub has_previous_page: bool,
250}
251
252impl PaginationInfo {
253 pub fn new(current_page: usize, page_size: usize, total_count: usize) -> Self {
254 let total_pages = (total_count + page_size - 1) / page_size;
255 Self {
256 current_page,
257 page_size,
258 total_count,
259 total_pages,
260 has_next_page: current_page + 1 < total_pages,
261 has_previous_page: current_page > 0,
262 }
263 }
264}
265
266#[cfg(test)]
267mod tests {
268 use super::*;
269
270 #[test]
271 fn test_search_query_builder() {
272 let query = SearchQuery::new("rust programming")
273 .with_mode(SearchMode::Fuzzy)
274 .with_fuzzy_threshold(0.4)
275 .with_entity_type("person")
276 .with_tag("technical")
277 .in_all_projects();
278
279 assert_eq!(query.text, Some("rust programming".to_string()));
280 assert_eq!(query.mode, SearchMode::Fuzzy);
281 assert_eq!(query.fuzzy_threshold, 0.4);
282 assert!(query.entity_types.contains(&"person".to_string()));
283 assert!(query.tags.contains(&"technical".to_string()));
284 assert!(matches!(query.projects, ProjectScope::All));
285 }
286
287 #[test]
288 fn test_pagination() {
289 let pagination = Pagination::new(2, 50);
290 assert_eq!(pagination.offset(), 100);
291 }
292
293 #[test]
294 fn test_pagination_info() {
295 let info = PaginationInfo::new(1, 10, 35);
296 assert_eq!(info.total_pages, 4);
297 assert!(info.has_next_page);
298 assert!(info.has_previous_page);
299 }
300}