fix: 修复智能服装搜索功能的搜索结果为空问题

主要修复:
1. 添加相关性阈值到搜索请求(参考Python实现)
2. 简化过滤器构建逻辑,使其更接近Python实现
3. 修复SearchConfig导入问题
4. 修复RelevanceThreshold的字符串转换问题
5. 优化前端搜索配置,启用调试模式查看详细信息

对比Python实现发现的关键差异:
- Python使用relevanceThreshold字段,Rust之前注释说不支持
- Python使用简单的过滤器字符串,Rust使用了复杂的过滤器构建
- 需要正确的API参数格式匹配
This commit is contained in:
imeepos 2025-07-25 13:33:30 +08:00
parent 30236d5875
commit 798b5a2007
2 changed files with 105 additions and 13 deletions

View File

@ -5,7 +5,7 @@ use crate::app_state::AppState;
use crate::data::models::gemini_analysis::{AnalyzeImageRequest, AnalyzeImageResponse};
use crate::data::models::outfit_search::{
LLMQueryRequest, LLMQueryResponse, OutfitSearchGlobalConfig, ProductInfo, SearchFilterBuilder,
SearchRequest, SearchResponse, SearchResult,
SearchRequest, SearchResponse, SearchResult, SearchConfig,
};
use crate::data::models::outfit_recommendation::{
OutfitRecommendationRequest, OutfitRecommendationResponse,
@ -465,15 +465,11 @@ async fn execute_vertex_ai_search(
eprintln!("搜索配置摘要: {}", config_summary);
}
// 5. 构建搜索过滤器 - 使用增强的过滤器构建器
let search_filter = SearchFilterBuilder::build_filters(&request.config);
// 5. 构建简化的搜索过滤器 - 参考Python实现
let search_filter = build_simple_filters(&request.config);
// 6. 构建增强查询字符串 - 参考 Python 实现
let enhanced_query = if request.config.query_enhancement_enabled {
SearchFilterBuilder::build_enhanced_query(&request.query, &request.config)
} else {
request.query.clone()
};
// 6. 构建简化的查询字符串 - 参考 Python 实现
let enhanced_query = build_simple_query(&request.query, &request.config);
// 调试输出
if request.config.debug_mode {
@ -489,7 +485,15 @@ async fn execute_vertex_ai_search(
"offset": request.page_offset
});
// 添加相关性评分规范但不设置阈值因为API不支持
// 添加相关性阈值和评分规范参考Python实现
let threshold_str = match request.config.relevance_threshold {
crate::data::models::outfit_search::RelevanceThreshold::Lowest => "LOWEST",
crate::data::models::outfit_search::RelevanceThreshold::Low => "LOW",
crate::data::models::outfit_search::RelevanceThreshold::Medium => "MEDIUM",
crate::data::models::outfit_search::RelevanceThreshold::High => "HIGH",
crate::data::models::outfit_search::RelevanceThreshold::Unspecified => "RELEVANCE_THRESHOLD_UNSPECIFIED",
};
payload["relevanceThreshold"] = serde_json::Value::String(threshold_str.to_string());
payload["relevanceScoreSpec"] = serde_json::json!({
"returnRelevanceScore": true
});
@ -895,3 +899,61 @@ pub fn get_outfit_search_command_names() -> Vec<&'static str> {
"get_outfit_search_config",
]
}
/// 构建简化的过滤器字符串 - 参考Python实现
fn build_simple_filters(config: &SearchConfig) -> String {
let mut filters = Vec::new();
// 环境标签过滤
if !config.environments.is_empty() {
let env_filter = config.environments.iter()
.map(|env| format!("\"{}\"", env))
.collect::<Vec<_>>()
.join(",");
filters.push(format!("environment_tags: ANY({})", env_filter));
}
// 类别过滤
if !config.categories.is_empty() {
let cat_filter = config.categories.iter()
.map(|cat| format!("\"{}\"", cat))
.collect::<Vec<_>>()
.join(",");
filters.push(format!("products.category: ANY({})", cat_filter));
}
filters.join(" AND ")
}
/// 构建简化的查询字符串 - 参考Python实现
fn build_simple_query(base_query: &str, config: &SearchConfig) -> String {
if !config.query_enhancement_enabled {
return base_query.to_string();
}
let mut keywords = Vec::new();
// 添加环境关键词
keywords.extend(config.environments.clone());
// 添加设计风格关键词
for styles in config.design_styles.values() {
keywords.extend(styles.clone());
}
// 限制关键词数量
if keywords.len() > config.max_keywords {
keywords.truncate(config.max_keywords);
}
if keywords.is_empty() {
base_query.to_string()
} else {
let keywords_str = keywords.join(" ");
if base_query.trim().is_empty() {
format!("model {}", keywords_str)
} else {
format!("{} {}", base_query.trim(), keywords_str)
}
}
}

View File

@ -15,6 +15,7 @@ import {
} from 'lucide-react';
import { invoke } from '@tauri-apps/api/core';
import { open } from '@tauri-apps/plugin-dialog';
import { convertFileSrc } from '@tauri-apps/api/core';
import {
OutfitAnalysisResult,
SearchRequest,
@ -121,17 +122,38 @@ const OutfitSearchTool: React.FC = () => {
setSearchError(null);
try {
// 构建简化的搜索配置
const simpleConfig: SearchConfig = {
relevance_threshold: 'HIGH' as any,
environments: searchConfig.environments || [],
categories: searchConfig.categories || [],
color_filters: {},
design_styles: {},
max_keywords: 10,
debug_mode: true, // 启用调试模式查看详细信息
custom_filters: [],
query_enhancement_enabled: true,
color_thresholds: {
default_hue_threshold: 0.05,
default_saturation_threshold: 0.05,
default_value_threshold: 0.2
}
};
const searchRequest: SearchRequest = {
query: 'outfit search',
config: searchConfig,
query: 'model fashion outfit',
config: simpleConfig,
page_size: 9,
page_offset: (currentPage - 1) * 9
};
console.log('发送搜索请求:', searchRequest);
const response = await invoke<SearchResponse>('search_similar_outfits', {
request: searchRequest
});
console.log('搜索响应:', response);
setSearchResults(response);
} catch (error) {
console.error('Failed to search outfits:', error);
@ -240,6 +262,10 @@ const OutfitSearchTool: React.FC = () => {
src={result.image_url}
alt="Outfit"
className="w-full h-48 object-cover rounded-lg mb-3"
onError={(e) => {
console.error('搜索结果图片加载失败:', result.image_url);
e.currentTarget.src = 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjAwIiBoZWlnaHQ9IjIwMCIgdmlld0JveD0iMCAwIDIwMCAyMDAiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxyZWN0IHdpZHRoPSIyMDAiIGhlaWdodD0iMjAwIiBmaWxsPSIjRjNGNEY2Ii8+CjxwYXRoIGQ9Ik0xMDAgNzBMMTMwIDEwMEgxMTBWMTMwSDkwVjEwMEg3MEwxMDAgNzBaIiBmaWxsPSIjOUI5QkEwIi8+Cjx0ZXh0IHg9IjEwMCIgeT0iMTUwIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmaWxsPSIjOUI5QkEwIiBmb250LXNpemU9IjEyIj7lm77niYfliKDpmaTlpLHotKU8L3RleHQ+Cjwvc3ZnPg==';
}}
/>
<div className="space-y-2">
<p className="text-sm text-gray-600 line-clamp-2">
@ -323,9 +349,13 @@ const OutfitSearchTool: React.FC = () => {
<div className="space-y-3">
<div className="relative">
<img
src={`asset://localhost/${selectedImage}`}
src={convertFileSrc(selectedImage)}
alt="Selected"
className="w-full h-48 object-cover rounded-lg"
onError={(e) => {
console.error('图片加载失败:', selectedImage);
setAnalysisError('图片加载失败,请重新选择图片');
}}
/>
<button
onClick={clearImage}