use serde::{Deserialize, Serialize}; use std::collections::HashMap; use chrono::{DateTime, Utc}; use crate::data::models::gemini_analysis::ColorHSV; /// 搜索相关性阈值 #[derive(Debug, Clone, Serialize, Deserialize)] pub enum RelevanceThreshold { #[serde(rename = "LOWEST")] Lowest, #[serde(rename = "LOW")] Low, #[serde(rename = "MEDIUM")] Medium, #[serde(rename = "HIGH")] High, } impl RelevanceThreshold { /// 获取阈值对应的数值 pub fn to_value(&self) -> f64 { match self { RelevanceThreshold::Lowest => 0.3, RelevanceThreshold::Low => 0.5, RelevanceThreshold::Medium => 0.7, RelevanceThreshold::High => 0.9, } } } impl Default for RelevanceThreshold { fn default() -> Self { RelevanceThreshold::High } } /// 颜色过滤器 #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ColorFilter { /// 是否启用颜色过滤 pub enabled: bool, /// 目标颜色 pub color: ColorHSV, /// 色相阈值 pub hue_threshold: f64, /// 饱和度阈值 pub saturation_threshold: f64, /// 明度阈值 pub value_threshold: f64, } impl Default for ColorFilter { fn default() -> Self { Self { enabled: false, color: ColorHSV::new(0.0, 0.0, 0.0), hue_threshold: 0.05, saturation_threshold: 0.05, value_threshold: 0.20, } } } /// 搜索配置 #[derive(Debug, Clone, Serialize, Deserialize)] pub struct SearchConfig { /// 相关性阈值 pub relevance_threshold: RelevanceThreshold, /// 环境标签过滤 pub environments: Vec, /// 类别过滤 pub categories: Vec, /// 颜色过滤器(按类别) pub color_filters: HashMap, /// 设计风格过滤(按类别) pub design_styles: HashMap>, /// 最大关键词数量 pub max_keywords: usize, } impl Default for SearchConfig { fn default() -> Self { Self { relevance_threshold: RelevanceThreshold::default(), environments: Vec::new(), categories: Vec::new(), color_filters: HashMap::new(), design_styles: HashMap::new(), max_keywords: 10, } } } /// 搜索请求 #[derive(Debug, Clone, Serialize, Deserialize)] pub struct SearchRequest { /// 搜索查询字符串 pub query: String, /// 搜索配置 pub config: SearchConfig, /// 页面大小 pub page_size: usize, /// 页面偏移量 pub page_offset: usize, } impl Default for SearchRequest { fn default() -> Self { Self { query: "model".to_string(), config: SearchConfig::default(), page_size: 9, page_offset: 0, } } } /// 搜索结果中的产品信息 #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ProductInfo { /// 产品类别 pub category: String, /// 产品描述 pub description: String, /// 主要颜色 pub color_pattern: ColorHSV, /// 设计风格 pub design_styles: Vec, } /// 单个搜索结果 #[derive(Debug, Clone, Serialize, Deserialize)] pub struct SearchResult { /// 结果ID pub id: String, /// 图片URL pub image_url: String, /// 风格描述 pub style_description: String, /// 环境标签 pub environment_tags: Vec, /// 产品信息列表 pub products: Vec, /// 相关性评分 pub relevance_score: f64, } /// 搜索响应 #[derive(Debug, Clone, Serialize, Deserialize)] pub struct SearchResponse { /// 搜索结果列表 pub results: Vec, /// 总结果数量 pub total_size: usize, /// 下一页令牌 pub next_page_token: Option, /// 搜索耗时(毫秒) pub search_time_ms: u64, /// 搜索时间戳 pub searched_at: DateTime, } /// 搜索历史记录 #[derive(Debug, Clone, Serialize, Deserialize)] pub struct SearchHistory { /// 历史记录ID pub id: String, /// 搜索查询 pub query: String, /// 搜索配置 pub config: SearchConfig, /// 结果数量 pub results_count: usize, /// 搜索耗时(毫秒) pub search_time_ms: u64, /// 搜索时间 pub created_at: DateTime, } /// LLM问答请求 #[derive(Debug, Clone, Serialize, Deserialize)] pub struct LLMQueryRequest { /// 用户输入的情景描述 pub user_input: String, /// 会话ID(可选) pub session_id: Option, } /// LLM问答响应 #[derive(Debug, Clone, Serialize, Deserialize)] pub struct LLMQueryResponse { /// LLM回答内容 pub answer: String, /// 相关的搜索结果 pub related_results: Vec, /// 响应时间(毫秒) pub response_time_ms: u64, /// 响应时间戳 pub responded_at: DateTime, } /// 全局配置 #[derive(Debug, Clone)] pub struct OutfitSearchGlobalConfig { /// Google Cloud项目ID pub google_project_id: String, /// Vertex AI应用ID pub vertex_ai_app_id: String, /// 存储桶名称 pub storage_bucket_name: String, /// 数据存储ID pub data_store_id: String, /// Cloudflare项目ID pub cloudflare_project_id: String, /// Cloudflare网关ID pub cloudflare_gateway_id: String, } impl Default for OutfitSearchGlobalConfig { fn default() -> Self { Self { google_project_id: "gen-lang-client-0413414134".to_string(), vertex_ai_app_id: "jeans-search_1751353769585".to_string(), storage_bucket_name: "fashion_image_block".to_string(), data_store_id: "jeans_pattern_data_store".to_string(), cloudflare_project_id: "67720b647ff2b55cf37ba3ef9e677083".to_string(), cloudflare_gateway_id: "bowong-dev".to_string(), } } } /// 搜索过滤器构建器 pub struct SearchFilterBuilder; impl SearchFilterBuilder { /// 构建搜索过滤器字符串 pub fn build_filters(config: &SearchConfig) -> String { let mut filters = Vec::new(); // 类别过滤 if !config.categories.is_empty() { for category in &config.categories { let mut inner_filters = vec![ format!("products.category: ANY(\"{}\")", category) ]; // 颜色过滤 if let Some(color_filter) = config.color_filters.get(category) { if color_filter.enabled { inner_filters.extend(Self::build_color_filters(color_filter)); } } // 设计风格过滤 if let Some(styles) = config.design_styles.get(category) { if !styles.is_empty() { let styles_str = styles.iter() .map(|s| format!("\"{}\"", s)) .collect::>() .join(","); inner_filters.push(format!("products.design_styles: ANY({})", styles_str)); } } filters.push(format!("({})", inner_filters.join(" AND "))); } } // 环境标签过滤 if !config.environments.is_empty() { let env_str = config.environments.iter() .map(|e| format!("\"{}\"", e)) .collect::>() .join(","); filters.push(format!("environment_tags: ANY({})", env_str)); } filters.join(" AND ") } /// 构建颜色过滤器 fn build_color_filters(color_filter: &ColorFilter) -> Vec { let hsv = &color_filter.color; vec![ format!( "products.color_pattern.Hue: IN({}, {})", (hsv.hue - color_filter.hue_threshold).max(0.0), (hsv.hue + color_filter.hue_threshold).min(1.0) ), format!( "products.color_pattern.Saturation: IN({}, {})", (hsv.saturation - color_filter.saturation_threshold).max(0.0), (hsv.saturation + color_filter.saturation_threshold).min(1.0) ), format!( "products.color_pattern.Value: IN({}, {})", (hsv.value - color_filter.value_threshold).max(0.0), (hsv.value + color_filter.value_threshold).min(1.0) ), ] } /// 构建查询关键词 pub fn build_query_keywords(config: &SearchConfig) -> Vec { let mut keywords = Vec::new(); // 添加设计风格关键词 for styles in config.design_styles.values() { keywords.extend(styles.clone()); } // 添加环境关键词 keywords.extend(config.environments.clone()); // 限制关键词数量 if keywords.len() > config.max_keywords { keywords.truncate(config.max_keywords); } keywords } } #[cfg(test)] mod tests { use super::*; use crate::data::models::gemini_analysis::ColorHSV; #[test] fn test_relevance_threshold_values() { assert_eq!(RelevanceThreshold::Lowest.to_value(), 0.3); assert_eq!(RelevanceThreshold::Low.to_value(), 0.5); assert_eq!(RelevanceThreshold::Medium.to_value(), 0.7); assert_eq!(RelevanceThreshold::High.to_value(), 0.9); } #[test] fn test_color_filter_default() { let filter = ColorFilter::default(); assert!(!filter.enabled); assert_eq!(filter.hue_threshold, 0.05); assert_eq!(filter.saturation_threshold, 0.05); assert_eq!(filter.value_threshold, 0.20); } #[test] fn test_search_config_default() { let config = SearchConfig::default(); assert!(matches!(config.relevance_threshold, RelevanceThreshold::High)); assert!(config.categories.is_empty()); assert!(config.environments.is_empty()); assert!(config.color_filters.is_empty()); assert!(config.design_styles.is_empty()); assert_eq!(config.max_keywords, 10); } #[test] fn test_search_request_default() { let request = SearchRequest::default(); assert_eq!(request.query, "model"); assert_eq!(request.page_size, 9); assert_eq!(request.page_offset, 0); } #[test] fn test_search_filter_builder_empty_config() { let config = SearchConfig::default(); let filters = SearchFilterBuilder::build_filters(&config); assert!(filters.is_empty()); } #[test] fn test_search_filter_builder_with_categories() { let mut config = SearchConfig::default(); config.categories = vec!["上装".to_string(), "下装".to_string()]; let filters = SearchFilterBuilder::build_filters(&config); assert!(filters.contains("products.category: ANY(\"上装\")")); assert!(filters.contains("products.category: ANY(\"下装\")")); } #[test] fn test_search_filter_builder_with_environments() { let mut config = SearchConfig::default(); config.environments = vec!["Outdoor".to_string(), "Indoor".to_string()]; let filters = SearchFilterBuilder::build_filters(&config); assert!(filters.contains("environment_tags: ANY(\"Outdoor\",\"Indoor\")")); } #[test] fn test_search_filter_builder_with_color_filters() { let mut config = SearchConfig::default(); config.categories = vec!["上装".to_string()]; let color_filter = ColorFilter { enabled: true, color: ColorHSV::new(0.5, 0.8, 0.9), hue_threshold: 0.05, saturation_threshold: 0.05, value_threshold: 0.20, }; config.color_filters.insert("上装".to_string(), color_filter); let filters = SearchFilterBuilder::build_filters(&config); assert!(filters.contains("products.color_pattern.Hue: IN(")); assert!(filters.contains("products.color_pattern.Saturation: IN(")); assert!(filters.contains("products.color_pattern.Value: IN(")); } #[test] fn test_search_filter_builder_with_design_styles() { let mut config = SearchConfig::default(); config.categories = vec!["上装".to_string()]; config.design_styles.insert("上装".to_string(), vec!["休闲".to_string(), "正式".to_string()]); let filters = SearchFilterBuilder::build_filters(&config); assert!(filters.contains("products.design_styles: ANY(\"休闲\",\"正式\")")); } #[test] fn test_query_keywords_builder() { let mut config = SearchConfig::default(); config.design_styles.insert("上装".to_string(), vec!["休闲".to_string(), "正式".to_string()]); config.environments = vec!["Outdoor".to_string()]; let keywords = SearchFilterBuilder::build_query_keywords(&config); assert!(keywords.contains(&"休闲".to_string())); assert!(keywords.contains(&"正式".to_string())); assert!(keywords.contains(&"Outdoor".to_string())); } #[test] fn test_query_keywords_builder_max_limit() { let mut config = SearchConfig::default(); config.max_keywords = 3; // 添加超过限制的关键词 config.design_styles.insert("上装".to_string(), vec!["休闲".to_string(), "正式".to_string()]); config.design_styles.insert("下装".to_string(), vec!["运动".to_string(), "街头".to_string()]); config.environments = vec!["Outdoor".to_string(), "Indoor".to_string()]; let keywords = SearchFilterBuilder::build_query_keywords(&config); assert!(keywords.len() <= 3); } #[test] fn test_product_info_creation() { let color = ColorHSV::new(0.6, 0.5, 0.7); let product = ProductInfo { category: "牛仔裤".to_string(), description: "蓝色牛仔裤".to_string(), color_pattern: color.clone(), design_styles: vec!["休闲".to_string()], }; assert_eq!(product.category, "牛仔裤"); assert_eq!(product.description, "蓝色牛仔裤"); assert_eq!(product.color_pattern, color); assert_eq!(product.design_styles.len(), 1); } #[test] fn test_search_result_creation() { let color = ColorHSV::new(0.6, 0.5, 0.7); let product = ProductInfo { category: "上装".to_string(), description: "白色衬衫".to_string(), color_pattern: color, design_styles: vec!["正式".to_string()], }; let result = SearchResult { id: "test-id".to_string(), image_url: "https://example.com/image.jpg".to_string(), style_description: "商务风格".to_string(), environment_tags: vec!["Office".to_string()], products: vec![product], relevance_score: 0.85, }; assert_eq!(result.id, "test-id"); assert_eq!(result.style_description, "商务风格"); assert_eq!(result.relevance_score, 0.85); assert_eq!(result.products.len(), 1); assert_eq!(result.environment_tags.len(), 1); } #[test] fn test_llm_query_request() { let request = LLMQueryRequest { user_input: "如何搭配牛仔裤?".to_string(), session_id: Some("session-123".to_string()), }; assert_eq!(request.user_input, "如何搭配牛仔裤?"); assert_eq!(request.session_id, Some("session-123".to_string())); } #[test] fn test_outfit_search_global_config_default() { let config = OutfitSearchGlobalConfig::default(); assert_eq!(config.google_project_id, "gen-lang-client-0413414134"); assert_eq!(config.vertex_ai_app_id, "jeans-search_1751353769585"); assert_eq!(config.storage_bucket_name, "fashion_image_block"); assert_eq!(config.data_store_id, "jeans_pattern_data_store"); assert_eq!(config.cloudflare_project_id, "67720b647ff2b55cf37ba3ef9e677083"); assert_eq!(config.cloudflare_gateway_id, "bowong-dev"); } }