Skip to main content

parsnip_core/
query.rs

1//! Query types for searching the knowledge graph
2
3use crate::project::ProjectId;
4use serde::{Deserialize, Serialize};
5
6/// Search mode
7#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
8#[serde(rename_all = "lowercase")]
9pub enum SearchMode {
10    /// Exact substring matching
11    #[default]
12    Exact,
13    /// Fuzzy matching with Levenshtein distance
14    Fuzzy,
15    /// Full-text search with ranking
16    FullText,
17    /// Hybrid: combines fuzzy and full-text
18    Hybrid,
19    /// Vector/semantic search using embeddings
20    Vector,
21}
22
23/// Tag matching mode
24#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
25#[serde(rename_all = "lowercase")]
26pub enum TagMatchMode {
27    /// Match any of the specified tags
28    #[default]
29    Any,
30    /// Match all of the specified tags
31    All,
32}
33
34/// Project scope for search
35#[derive(Debug, Clone, Serialize, Deserialize)]
36#[serde(rename_all = "lowercase")]
37pub enum ProjectScope {
38    /// Search in a single project
39    Single(ProjectId),
40    /// Search in multiple specific projects
41    Multiple(Vec<ProjectId>),
42    /// Search across all projects
43    All,
44}
45
46impl Default for ProjectScope {
47    fn default() -> Self {
48        Self::All
49    }
50}
51
52/// Pagination options
53#[derive(Debug, Clone, Serialize, Deserialize)]
54pub struct Pagination {
55    /// Page number (0-indexed)
56    #[serde(default)]
57    pub page: usize,
58
59    /// Number of results per page
60    #[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), // Max 1000 per page
82        }
83    }
84
85    pub fn offset(&self) -> usize {
86        self.page * self.page_size
87    }
88}
89
90/// Search query builder
91#[derive(Debug, Clone, Default, Serialize, Deserialize)]
92pub struct SearchQuery {
93    /// Text to search for
94    #[serde(skip_serializing_if = "Option::is_none")]
95    pub text: Option<String>,
96
97    /// Search mode
98    #[serde(default)]
99    pub mode: SearchMode,
100
101    /// Fuzzy search threshold (0.0-1.0, lower = more results)
102    #[serde(default = "default_fuzzy_threshold")]
103    pub fuzzy_threshold: f32,
104
105    /// Query embedding for vector search
106    #[serde(skip_serializing_if = "Option::is_none")]
107    pub query_embedding: Option<Vec<f32>>,
108
109    /// Similarity threshold for vector search (0.0-1.0, higher = stricter)
110    #[serde(default = "default_similarity_threshold")]
111    pub similarity_threshold: f32,
112
113    /// Filter by entity types
114    #[serde(default)]
115    pub entity_types: Vec<String>,
116
117    /// Filter by tags
118    #[serde(default)]
119    pub tags: Vec<String>,
120
121    /// Tag matching mode
122    #[serde(default)]
123    pub tag_match_mode: TagMatchMode,
124
125    /// Project scope
126    #[serde(default)]
127    pub projects: ProjectScope,
128
129    /// Pagination
130    #[serde(default)]
131    pub pagination: Pagination,
132
133    /// Include relations in results
134    #[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    /// Create a new search query with text
152    pub fn new(text: impl Into<String>) -> Self {
153        Self {
154            text: Some(text.into()),
155            ..Default::default()
156        }
157    }
158
159    /// Alias for new() - create search query with text
160    pub fn text(text: impl Into<String>) -> Self {
161        Self::new(text)
162    }
163
164    /// Create an empty search query (for tag-only search)
165    pub fn empty() -> Self {
166        Self::default()
167    }
168
169    /// Set search mode
170    pub fn with_mode(mut self, mode: SearchMode) -> Self {
171        self.mode = mode;
172        self
173    }
174
175    /// Set fuzzy threshold
176    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    /// Set query embedding for vector search
182    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    /// Set similarity threshold for vector search
189    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    /// Add entity type filter
195    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    /// Add tag filter
201    pub fn with_tag(mut self, tag: impl Into<String>) -> Self {
202        self.tags.push(tag.into());
203        self
204    }
205
206    /// Set tag match mode
207    pub fn with_tag_match_mode(mut self, mode: TagMatchMode) -> Self {
208        self.tag_match_mode = mode;
209        self
210    }
211
212    /// Search in a specific project
213    pub fn in_project(mut self, project_id: ProjectId) -> Self {
214        self.projects = ProjectScope::Single(project_id);
215        self
216    }
217
218    /// Search across all projects
219    pub fn in_all_projects(mut self) -> Self {
220        self.projects = ProjectScope::All;
221        self
222    }
223
224    /// Set pagination
225    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/// Paginated search results
232#[derive(Debug, Clone, Serialize, Deserialize)]
233pub struct PaginatedResults<T> {
234    /// The data for this page
235    pub data: Vec<T>,
236
237    /// Pagination metadata
238    pub pagination: PaginationInfo,
239}
240
241/// Pagination metadata
242#[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}