use crate::project::ProjectId;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum SearchMode {
#[default]
Exact,
Fuzzy,
FullText,
Hybrid,
Vector,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum TagMatchMode {
#[default]
Any,
All,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum ProjectScope {
Single(ProjectId),
Multiple(Vec<ProjectId>),
All,
}
impl Default for ProjectScope {
fn default() -> Self {
Self::All
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Pagination {
#[serde(default)]
pub page: usize,
#[serde(default = "default_page_size")]
pub page_size: usize,
}
impl Default for Pagination {
fn default() -> Self {
Self {
page: 0,
page_size: default_page_size(),
}
}
}
fn default_page_size() -> usize {
100
}
impl Pagination {
pub fn new(page: usize, page_size: usize) -> Self {
Self {
page,
page_size: page_size.min(1000), }
}
pub fn offset(&self) -> usize {
self.page * self.page_size
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct SearchQuery {
#[serde(skip_serializing_if = "Option::is_none")]
pub text: Option<String>,
#[serde(default)]
pub mode: SearchMode,
#[serde(default = "default_fuzzy_threshold")]
pub fuzzy_threshold: f32,
#[serde(skip_serializing_if = "Option::is_none")]
pub query_embedding: Option<Vec<f32>>,
#[serde(default = "default_similarity_threshold")]
pub similarity_threshold: f32,
#[serde(default)]
pub entity_types: Vec<String>,
#[serde(default)]
pub tags: Vec<String>,
#[serde(default)]
pub tag_match_mode: TagMatchMode,
#[serde(default)]
pub projects: ProjectScope,
#[serde(default)]
pub pagination: Pagination,
#[serde(default = "default_true")]
pub include_relations: bool,
}
fn default_fuzzy_threshold() -> f32 {
0.3
}
fn default_similarity_threshold() -> f32 {
0.7
}
fn default_true() -> bool {
true
}
impl SearchQuery {
pub fn new(text: impl Into<String>) -> Self {
Self {
text: Some(text.into()),
..Default::default()
}
}
pub fn text(text: impl Into<String>) -> Self {
Self::new(text)
}
pub fn empty() -> Self {
Self::default()
}
pub fn with_mode(mut self, mode: SearchMode) -> Self {
self.mode = mode;
self
}
pub fn with_fuzzy_threshold(mut self, threshold: f32) -> Self {
self.fuzzy_threshold = threshold.clamp(0.0, 1.0);
self
}
pub fn with_embedding(mut self, embedding: Vec<f32>) -> Self {
self.query_embedding = Some(embedding);
self.mode = SearchMode::Vector;
self
}
pub fn with_similarity_threshold(mut self, threshold: f32) -> Self {
self.similarity_threshold = threshold.clamp(0.0, 1.0);
self
}
pub fn with_entity_type(mut self, entity_type: impl Into<String>) -> Self {
self.entity_types.push(entity_type.into());
self
}
pub fn with_tag(mut self, tag: impl Into<String>) -> Self {
self.tags.push(tag.into());
self
}
pub fn with_tag_match_mode(mut self, mode: TagMatchMode) -> Self {
self.tag_match_mode = mode;
self
}
pub fn in_project(mut self, project_id: ProjectId) -> Self {
self.projects = ProjectScope::Single(project_id);
self
}
pub fn in_all_projects(mut self) -> Self {
self.projects = ProjectScope::All;
self
}
pub fn with_pagination(mut self, page: usize, page_size: usize) -> Self {
self.pagination = Pagination::new(page, page_size);
self
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PaginatedResults<T> {
pub data: Vec<T>,
pub pagination: PaginationInfo,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PaginationInfo {
pub current_page: usize,
pub page_size: usize,
pub total_count: usize,
pub total_pages: usize,
pub has_next_page: bool,
pub has_previous_page: bool,
}
impl PaginationInfo {
pub fn new(current_page: usize, page_size: usize, total_count: usize) -> Self {
let total_pages = (total_count + page_size - 1) / page_size;
Self {
current_page,
page_size,
total_count,
total_pages,
has_next_page: current_page + 1 < total_pages,
has_previous_page: current_page > 0,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_search_query_builder() {
let query = SearchQuery::new("rust programming")
.with_mode(SearchMode::Fuzzy)
.with_fuzzy_threshold(0.4)
.with_entity_type("person")
.with_tag("technical")
.in_all_projects();
assert_eq!(query.text, Some("rust programming".to_string()));
assert_eq!(query.mode, SearchMode::Fuzzy);
assert_eq!(query.fuzzy_threshold, 0.4);
assert!(query.entity_types.contains(&"person".to_string()));
assert!(query.tags.contains(&"technical".to_string()));
assert!(matches!(query.projects, ProjectScope::All));
}
#[test]
fn test_pagination() {
let pagination = Pagination::new(2, 50);
assert_eq!(pagination.offset(), 100);
}
#[test]
fn test_pagination_info() {
let info = PaginationInfo::new(1, 10, 35);
assert_eq!(info.total_pages, 4);
assert!(info.has_next_page);
assert!(info.has_previous_page);
}
}