350 lines
10 KiB
TypeScript
350 lines
10 KiB
TypeScript
import OutfitSearchService from './outfitSearchService';
|
||
import {
|
||
SearchResponse,
|
||
OutfitAnalysisResult,
|
||
LLMQueryResponse,
|
||
SearchConfig,
|
||
DEFAULT_SEARCH_CONFIG,
|
||
} from '../types/outfitSearch';
|
||
|
||
/**
|
||
* 智能检索服务
|
||
* 整合图像分析、LLM问答和搜索功能,提供统一的智能检索体验
|
||
* 遵循 Tauri 开发规范的服务层设计原则
|
||
*/
|
||
export class IntelligentSearchService {
|
||
/**
|
||
* 智能搜索模式枚举
|
||
*/
|
||
static readonly SearchMode = {
|
||
TEXT: 'text', // 文本搜索
|
||
IMAGE: 'image', // 图像分析搜索
|
||
LLM: 'llm', // LLM智能搜索
|
||
HYBRID: 'hybrid', // 混合搜索
|
||
} as const;
|
||
|
||
/**
|
||
* 执行智能搜索
|
||
* 根据输入类型自动选择最佳搜索策略
|
||
*/
|
||
static async executeIntelligentSearch(params: {
|
||
query?: string;
|
||
imagePath?: string;
|
||
llmQuery?: string;
|
||
config?: SearchConfig;
|
||
mode?: keyof typeof IntelligentSearchService.SearchMode;
|
||
}): Promise<{
|
||
searchResponse: SearchResponse;
|
||
analysisResult?: OutfitAnalysisResult;
|
||
llmResponse?: LLMQueryResponse;
|
||
searchMode: string;
|
||
suggestions: string[];
|
||
}> {
|
||
const { query, imagePath, llmQuery, config = DEFAULT_SEARCH_CONFIG, mode } = params;
|
||
|
||
// 自动检测搜索模式
|
||
const detectedMode = mode || this.detectSearchMode({ query, imagePath, llmQuery });
|
||
|
||
let searchResponse: SearchResponse;
|
||
let analysisResult: OutfitAnalysisResult | undefined;
|
||
let llmResponse: LLMQueryResponse | undefined;
|
||
let finalConfig = { ...config };
|
||
let suggestions: string[] = [];
|
||
|
||
switch (detectedMode) {
|
||
case this.SearchMode.IMAGE:
|
||
if (!imagePath) throw new Error('Image path is required for image search');
|
||
|
||
// 1. 分析图像
|
||
const imageAnalysis = await OutfitSearchService.analyzeOutfitImage({
|
||
image_path: imagePath,
|
||
image_name: imagePath.split('/').pop() || 'image',
|
||
});
|
||
|
||
analysisResult = imageAnalysis.result as OutfitAnalysisResult;
|
||
|
||
// 2. 基于分析结果生成搜索配置
|
||
finalConfig = await OutfitSearchService.generateSearchConfigFromAnalysis(analysisResult);
|
||
|
||
// 3. 执行搜索
|
||
searchResponse = await OutfitSearchService.searchSimilarOutfits({
|
||
query: analysisResult.style_description || 'model',
|
||
config: finalConfig,
|
||
page_size: 9,
|
||
page_offset: 0,
|
||
});
|
||
|
||
// 4. 生成搜索建议
|
||
suggestions = this.generateImageSearchSuggestions(analysisResult);
|
||
break;
|
||
|
||
case this.SearchMode.LLM:
|
||
if (!llmQuery) throw new Error('LLM query is required for LLM search');
|
||
|
||
// 1. 获取LLM回答
|
||
llmResponse = await OutfitSearchService.askLLMOutfitAdvice({
|
||
user_input: llmQuery,
|
||
});
|
||
|
||
// 2. 从LLM回答中提取搜索关键词
|
||
const extractedKeywords = this.extractKeywordsFromLLMResponse(llmResponse.answer);
|
||
|
||
// 3. 执行基于关键词的搜索
|
||
searchResponse = await OutfitSearchService.searchSimilarOutfits({
|
||
query: extractedKeywords.join(' ') || llmQuery,
|
||
config: finalConfig,
|
||
page_size: 9,
|
||
page_offset: 0,
|
||
});
|
||
|
||
// 4. 生成搜索建议
|
||
suggestions = extractedKeywords;
|
||
break;
|
||
|
||
case this.SearchMode.HYBRID:
|
||
// 混合搜索:结合多种输入
|
||
const hybridResults = await this.executeHybridSearch({
|
||
query,
|
||
imagePath,
|
||
llmQuery,
|
||
config: finalConfig,
|
||
});
|
||
|
||
searchResponse = hybridResults.searchResponse;
|
||
analysisResult = hybridResults.analysisResult;
|
||
llmResponse = hybridResults.llmResponse;
|
||
suggestions = hybridResults.suggestions;
|
||
break;
|
||
|
||
case this.SearchMode.TEXT:
|
||
default:
|
||
if (!query) throw new Error('Query is required for text search');
|
||
|
||
// 1. 生成搜索建议
|
||
suggestions = await OutfitSearchService.getSearchSuggestions(query);
|
||
|
||
// 2. 执行文本搜索
|
||
searchResponse = await OutfitSearchService.searchSimilarOutfits({
|
||
query,
|
||
config: finalConfig,
|
||
page_size: 9,
|
||
page_offset: 0,
|
||
});
|
||
break;
|
||
}
|
||
|
||
return {
|
||
searchResponse,
|
||
analysisResult,
|
||
llmResponse,
|
||
searchMode: detectedMode,
|
||
suggestions,
|
||
};
|
||
}
|
||
|
||
/**
|
||
* 自动检测搜索模式
|
||
*/
|
||
private static detectSearchMode(params: {
|
||
query?: string;
|
||
imagePath?: string;
|
||
llmQuery?: string;
|
||
}): string {
|
||
const { query, imagePath, llmQuery } = params;
|
||
|
||
// 如果有多个输入,使用混合模式
|
||
const inputCount = [query, imagePath, llmQuery].filter(Boolean).length;
|
||
if (inputCount > 1) {
|
||
return this.SearchMode.HYBRID;
|
||
}
|
||
|
||
// 单一输入模式检测
|
||
if (imagePath) return this.SearchMode.IMAGE;
|
||
if (llmQuery) return this.SearchMode.LLM;
|
||
if (query) return this.SearchMode.TEXT;
|
||
|
||
// 默认文本模式
|
||
return this.SearchMode.TEXT;
|
||
}
|
||
|
||
/**
|
||
* 执行混合搜索
|
||
*/
|
||
private static async executeHybridSearch(params: {
|
||
query?: string;
|
||
imagePath?: string;
|
||
llmQuery?: string;
|
||
config: SearchConfig;
|
||
}): Promise<{
|
||
searchResponse: SearchResponse;
|
||
analysisResult?: OutfitAnalysisResult;
|
||
llmResponse?: LLMQueryResponse;
|
||
suggestions: string[];
|
||
}> {
|
||
const { query, imagePath, llmQuery, config } = params;
|
||
const results: any = {};
|
||
const allSuggestions: string[] = [];
|
||
|
||
// 并行执行各种分析
|
||
const promises: Promise<any>[] = [];
|
||
|
||
if (imagePath) {
|
||
promises.push(
|
||
OutfitSearchService.analyzeOutfitImage({
|
||
image_path: imagePath,
|
||
image_name: imagePath.split('/').pop() || 'image',
|
||
}).then(result => {
|
||
results.analysisResult = result.result as OutfitAnalysisResult;
|
||
allSuggestions.push(...this.generateImageSearchSuggestions(results.analysisResult));
|
||
})
|
||
);
|
||
}
|
||
|
||
if (llmQuery) {
|
||
promises.push(
|
||
OutfitSearchService.askLLMOutfitAdvice({
|
||
user_input: llmQuery,
|
||
}).then(result => {
|
||
results.llmResponse = result;
|
||
const keywords = this.extractKeywordsFromLLMResponse(result.answer);
|
||
allSuggestions.push(...keywords);
|
||
})
|
||
);
|
||
}
|
||
|
||
if (query) {
|
||
promises.push(
|
||
OutfitSearchService.getSearchSuggestions(query).then(suggestions => {
|
||
allSuggestions.push(...suggestions);
|
||
})
|
||
);
|
||
}
|
||
|
||
// 等待所有分析完成
|
||
await Promise.all(promises);
|
||
|
||
// 合并搜索配置
|
||
let finalConfig = { ...config };
|
||
if (results.analysisResult) {
|
||
const imageConfig = await OutfitSearchService.generateSearchConfigFromAnalysis(results.analysisResult);
|
||
finalConfig = this.mergeSearchConfigs(finalConfig, imageConfig);
|
||
}
|
||
|
||
// 构建综合搜索查询
|
||
const searchQueries = [
|
||
query,
|
||
results.analysisResult?.style_description,
|
||
...allSuggestions.slice(0, 3), // 限制关键词数量
|
||
].filter(Boolean);
|
||
|
||
const finalQuery = searchQueries.join(' ').substring(0, 200); // 限制查询长度
|
||
|
||
// 执行最终搜索
|
||
const searchResponse = await OutfitSearchService.searchSimilarOutfits({
|
||
query: finalQuery || 'model',
|
||
config: finalConfig,
|
||
page_size: 9,
|
||
page_offset: 0,
|
||
});
|
||
|
||
return {
|
||
searchResponse,
|
||
analysisResult: results.analysisResult,
|
||
llmResponse: results.llmResponse,
|
||
suggestions: [...new Set(allSuggestions)].slice(0, 5), // 去重并限制数量
|
||
};
|
||
}
|
||
|
||
/**
|
||
* 从图像分析结果生成搜索建议
|
||
*/
|
||
private static generateImageSearchSuggestions(analysisResult: OutfitAnalysisResult): string[] {
|
||
const suggestions: string[] = [];
|
||
|
||
// 添加环境标签
|
||
if (analysisResult.environment_tags) {
|
||
suggestions.push(...analysisResult.environment_tags);
|
||
}
|
||
|
||
// 添加产品类别和设计风格
|
||
if (analysisResult.products) {
|
||
analysisResult.products.forEach(product => {
|
||
suggestions.push(product.category);
|
||
if (product.design_styles) {
|
||
suggestions.push(...product.design_styles);
|
||
}
|
||
});
|
||
}
|
||
|
||
return [...new Set(suggestions)].slice(0, 5);
|
||
}
|
||
|
||
/**
|
||
* 从LLM回答中提取关键词
|
||
*/
|
||
private static extractKeywordsFromLLMResponse(response: string): string[] {
|
||
const keywords: string[] = [];
|
||
const text = response.toLowerCase();
|
||
|
||
// 服装相关关键词模式
|
||
const patterns = [
|
||
/(?:推荐|建议|选择).*?([\u4e00-\u9fa5]+(?:装|衣|裤|鞋|包|帽))/g,
|
||
/(?:休闲|正式|运动|街头|优雅|可爱|性感|简约|复古|时尚)/g,
|
||
/(?:春|夏|秋|冬)(?:季|天)/g,
|
||
/(?:上班|约会|聚会|旅行|运动|居家)/g,
|
||
];
|
||
|
||
patterns.forEach(pattern => {
|
||
let match;
|
||
while ((match = pattern.exec(text)) !== null) {
|
||
if (match[1]) {
|
||
keywords.push(match[1]);
|
||
} else {
|
||
keywords.push(match[0]);
|
||
}
|
||
}
|
||
});
|
||
|
||
return [...new Set(keywords)].slice(0, 3);
|
||
}
|
||
|
||
/**
|
||
* 合并搜索配置
|
||
*/
|
||
private static mergeSearchConfigs(config1: SearchConfig, config2: SearchConfig): SearchConfig {
|
||
return {
|
||
relevance_threshold: config1.relevance_threshold,
|
||
environments: [...new Set([...config1.environments, ...config2.environments])],
|
||
categories: [...new Set([...config1.categories, ...config2.categories])],
|
||
color_filters: { ...config1.color_filters, ...config2.color_filters },
|
||
design_styles: {
|
||
...config1.design_styles,
|
||
...config2.design_styles,
|
||
},
|
||
max_keywords: Math.max(config1.max_keywords, config2.max_keywords),
|
||
debug_mode: config1.debug_mode || config2.debug_mode,
|
||
custom_filters: [...new Set([...config1.custom_filters, ...config2.custom_filters])],
|
||
query_enhancement_enabled: config1.query_enhancement_enabled && config2.query_enhancement_enabled,
|
||
color_thresholds: config1.color_thresholds,
|
||
};
|
||
}
|
||
|
||
/**
|
||
* 获取搜索历史和建议
|
||
*/
|
||
static async getSearchRecommendations(): Promise<{
|
||
popularKeywords: string[];
|
||
recentSearches: string[];
|
||
trendingStyles: string[];
|
||
}> {
|
||
// 这里可以从本地存储或服务器获取数据
|
||
return {
|
||
popularKeywords: ['休闲搭配', '正式风格', '运动装', '街头风', '优雅风格'],
|
||
recentSearches: [], // 从本地存储获取
|
||
trendingStyles: ['简约风', '复古风', '韩式风格', '日系风格', '欧美风'],
|
||
};
|
||
}
|
||
}
|
||
|
||
export default IntelligentSearchService;
|