新功能: - 完整的服装搭配智能搜索系统 - AI图像分析和服装识别 - 智能搜索和过滤功能 - LLM搭配顾问聊天功能 - HSV颜色匹配算法 - 响应式UI界面 技术实现: - 统一的GeminiService架构 - 完整的数据模型和类型定义 - Tauri命令接口层 - React前端组件库 - Zustand状态管理 - 数据库扩展支持 UI/UX: - 现代化的搭配搜索界面 - 直观的颜色选择器 - 多级筛选面板 - 图像上传和分析 - 搜索结果展示 - AI聊天界面 测试: - 核心功能单元测试 - 颜色匹配算法测试 - API集成测试 文档: - 完整的系统设计文档 - API接口文档 - 开发指南
511 lines
16 KiB
Rust
511 lines
16 KiB
Rust
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<String>,
|
||
/// 类别过滤
|
||
pub categories: Vec<String>,
|
||
/// 颜色过滤器(按类别)
|
||
pub color_filters: HashMap<String, ColorFilter>,
|
||
/// 设计风格过滤(按类别)
|
||
pub design_styles: HashMap<String, Vec<String>>,
|
||
/// 最大关键词数量
|
||
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<String>,
|
||
}
|
||
|
||
/// 单个搜索结果
|
||
#[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<String>,
|
||
/// 产品信息列表
|
||
pub products: Vec<ProductInfo>,
|
||
/// 相关性评分
|
||
pub relevance_score: f64,
|
||
}
|
||
|
||
/// 搜索响应
|
||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||
pub struct SearchResponse {
|
||
/// 搜索结果列表
|
||
pub results: Vec<SearchResult>,
|
||
/// 总结果数量
|
||
pub total_size: usize,
|
||
/// 下一页令牌
|
||
pub next_page_token: Option<String>,
|
||
/// 搜索耗时(毫秒)
|
||
pub search_time_ms: u64,
|
||
/// 搜索时间戳
|
||
pub searched_at: DateTime<Utc>,
|
||
}
|
||
|
||
/// 搜索历史记录
|
||
#[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<Utc>,
|
||
}
|
||
|
||
/// LLM问答请求
|
||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||
pub struct LLMQueryRequest {
|
||
/// 用户输入的情景描述
|
||
pub user_input: String,
|
||
/// 会话ID(可选)
|
||
pub session_id: Option<String>,
|
||
}
|
||
|
||
/// LLM问答响应
|
||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||
pub struct LLMQueryResponse {
|
||
/// LLM回答内容
|
||
pub answer: String,
|
||
/// 相关的搜索结果
|
||
pub related_results: Vec<SearchResult>,
|
||
/// 响应时间(毫秒)
|
||
pub response_time_ms: u64,
|
||
/// 响应时间戳
|
||
pub responded_at: DateTime<Utc>,
|
||
}
|
||
|
||
/// 全局配置
|
||
#[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::<Vec<_>>()
|
||
.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::<Vec<_>>()
|
||
.join(",");
|
||
filters.push(format!("environment_tags: ANY({})", env_str));
|
||
}
|
||
|
||
filters.join(" AND ")
|
||
}
|
||
|
||
/// 构建颜色过滤器
|
||
fn build_color_filters(color_filter: &ColorFilter) -> Vec<String> {
|
||
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<String> {
|
||
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");
|
||
}
|
||
}
|