UNPKG

@xiaohui-wang/mcpadvisor

Version:

MCP Advisor & Installation - Find the right MCP server for your needs

336 lines (335 loc) 13.8 kB
/** * 增强型离线搜索提供者 * 结合文本匹配和向量搜索,确保即使在向量相似度较低的情况下,也能返回相关的服务器 */ import { EnhancedMemoryVectorEngine } from '../database/memory/enhancedMemoryVectorEngine.js'; import { getTextEmbedding } from '../../utils/embedding.js'; import logger from '../../utils/logger.js'; import fs from 'fs'; import path from 'path'; import { fileURLToPath } from 'url'; // 获取当前文件的目录 const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); /** * 默认兜底数据路径 */ const DEFAULT_FALLBACK_DATA_PATH = path.resolve(__dirname, '../../../data/mcp_server_list.json'); /** * 默认关键词列表 */ const DEFAULT_KEYWORDS = [ '小红书', 'xiaohongshu', 'rednote', '热点', '社交媒体', 'social media', '新闻', 'news', '微博', 'weibo', '知乎', 'zhihu', 'twitter', '抖音', 'tiktok', '快手', 'kuaishou' ]; /** * 增强型离线搜索提供者 * 结合文本匹配和向量搜索,确保即使在向量相似度较低的情况下,也能返回相关的服务器 */ export class EnhancedOfflineSearchProvider { vectorEngine; config; fallbackData = []; dataLoaded = false; /** * 构造函数 * @param config 配置选项 */ constructor(config = {}) { this.config = { minSimilarity: 0.1, textMatchWeight: 0.5, vectorSearchWeight: 0.5, keywords: DEFAULT_KEYWORDS, ...config }; this.vectorEngine = new EnhancedMemoryVectorEngine(config.fallbackDataPath || DEFAULT_FALLBACK_DATA_PATH); logger.info('Enhanced offline search provider initialized', { provider: 'EnhancedOfflineSearchProvider', config: this.config }); } /** * 加载兜底数据 */ async loadFallbackData() { if (this.dataLoaded) { return; } try { const dataPath = this.config.fallbackDataPath || DEFAULT_FALLBACK_DATA_PATH; logger.info(`Loading fallback data from: ${dataPath}`); // 检查文件是否存在 if (!fs.existsSync(dataPath)) { logger.warn(`Fallback data file not found at: ${dataPath}`); this.dataLoaded = true; return; } // 读取并解析JSON文件 const rawData = fs.readFileSync(dataPath, 'utf8'); this.fallbackData = JSON.parse(rawData); logger.info(`Loaded ${this.fallbackData.length} fallback MCP servers`); this.dataLoaded = true; } catch (error) { const message = error instanceof Error ? error.message : String(error); logger.error(`Error loading fallback data: ${message}`, { error }); this.dataLoaded = true; } } /** * 搜索 MCP 服务器 * @param query 搜索查询 * @returns 搜索结果 */ async search(query) { try { logger.info(`Searching for MCP servers with query: ${query}`); // 确保数据已加载 await this.loadFallbackData(); // 检查查询中是否包含关键词 const containsKeywords = this.checkQueryForKeywords(query); // 如果查询包含关键词,先进行文本匹配 if (containsKeywords) { logger.info(`Query contains keywords, prioritizing text matching`); // 先执行文本匹配 const textMatchResults = await this.textSearch(query); // 如果文本匹配有结果,则优先返回 if (textMatchResults.length > 0) { // 再执行向量搜索(不阻塞返回) this.vectorSearch(query).then(vectorResults => { logger.debug(`Background vector search found ${vectorResults.length} results`); }).catch(error => { logger.error(`Background vector search error: ${error instanceof Error ? error.message : String(error)}`); }); logger.debug(`Prioritized text match found ${textMatchResults.length} results`); // 标记结果为离线来源 return textMatchResults.map(result => ({ ...result, source: 'offline' })); } } // 并行执行向量搜索和文本匹配 const [vectorResults, textMatchResults] = await Promise.all([ this.vectorSearch(query), this.textSearch(query) ]); // 合并结果 const mergedResults = this.mergeResults(vectorResults, textMatchResults); logger.debug(`Found ${mergedResults.length} results from enhanced offline search`); // 标记结果为离线来源 return mergedResults.map(result => ({ ...result, source: 'offline' })); } catch (error) { const message = error instanceof Error ? error.message : String(error); logger.error(`Error in enhanced offline search: ${message}`, { error, query, provider: 'EnhancedOfflineSearchProvider' }); // 即使出错,也返回空结果而不是抛出异常 return []; } } /** * 执行向量搜索 * @param query 搜索查询 * @returns 向量搜索结果 */ async vectorSearch(query) { try { // 获取查询的嵌入向量 const queryEmbedding = await getTextEmbedding(query); // 执行向量搜索 const results = await this.vectorEngine.search(queryEmbedding, 20, // 增加结果数量,以便后续合并 { minSimilarity: this.config.minSimilarity, textQuery: query }); logger.debug(`Found ${results.length} results from vector search`); return results; } catch (error) { logger.error(`Error in vector search: ${error instanceof Error ? error.message : String(error)}`); return []; } } /** * 检查查询中是否包含关键词 * @param query 搜索查询 * @returns 是否包含关键词 */ checkQueryForKeywords(query) { const queryLower = query.toLowerCase(); const priorityKeywords = [ '小红书', 'xiaohongshu', 'rednote', '热点', '社交媒体', '微博', 'weibo', '知乎', 'zhihu' ]; return priorityKeywords.some(keyword => queryLower.includes(keyword.toLowerCase())); } /** * 执行文本匹配搜索 * @param query 搜索查询 * @returns 文本匹配结果 */ async textSearch(query) { try { if (!this.dataLoaded || this.fallbackData.length === 0) { logger.warn('Fallback data not loaded for text search'); return []; } // 准备关键词列表 const keywords = [...(this.config.keywords || [])]; // 从查询中提取额外关键词 const queryWords = query.split(/\s+/).filter(word => word.length > 1); keywords.push(...queryWords); // 执行文本匹配 const matchedServers = this.fallbackData.filter(server => { const searchText = `${server.name} ${server.display_name} ${server.description} ${JSON.stringify(server.categories || [])} ${JSON.stringify(server.tags || [])}`.toLowerCase(); return keywords.some(keyword => searchText.includes(keyword.toLowerCase())); }); // 转换为 MCPServerResponse 格式 const results = matchedServers.map(server => ({ title: server.display_name || server.name, description: server.description || '', github_url: server.repository?.url || server.homepage || '', categories: server.categories || [], tags: server.tags || [], similarity: 0.5 // 默认相似度 })); // 计算文本相似度分数 for (const result of results) { // 检查是否包含小红书相关关键词 const isRedNoteRelated = this.isRedNoteRelated(result); // 计算查询与标题、描述的匹配程度 const titleMatch = this.calculateTextSimilarity(query, result.title); const descMatch = this.calculateTextSimilarity(query, result.description); const categoryMatch = Array.isArray(result.categories) ? Math.max(...result.categories.map(cat => this.calculateTextSimilarity(query, cat))) : 0; const tagMatch = Array.isArray(result.tags) ? Math.max(...result.tags.map(tag => this.calculateTextSimilarity(query, tag))) : 0; // 综合分数 (标题权重高) let similarity = titleMatch * 0.5 + descMatch * 0.3 + Math.max(categoryMatch, tagMatch) * 0.2; // 如果是小红书相关,则提高分数 if (isRedNoteRelated) { similarity = Math.max(similarity, 0.8); // 确保小红书相关结果有足够高的分数 } result.similarity = Math.max(similarity, result.similarity || 0); } // 按相似度排序 const sortedResults = results.sort((a, b) => (b.similarity || 0) - (a.similarity || 0)); logger.debug(`Found ${sortedResults.length} results from text search`); return sortedResults; } catch (error) { logger.error(`Error in text search: ${error instanceof Error ? error.message : String(error)}`); return []; } } /** * 检查是否与小红书相关 */ isRedNoteRelated(result) { const redNoteKeywords = [ '小红书', 'xiaohongshu', 'rednote', 'redbook', 'red note' ]; // 检查标题 if (redNoteKeywords.some(keyword => result.title.toLowerCase().includes(keyword.toLowerCase()))) { return true; } // 检查描述 if (redNoteKeywords.some(keyword => result.description.toLowerCase().includes(keyword.toLowerCase()))) { return true; } // 检查分类 if (Array.isArray(result.categories) && result.categories.some(category => redNoteKeywords.some(keyword => category.toLowerCase().includes(keyword.toLowerCase())))) { return true; } // 检查标签 if (Array.isArray(result.tags) && result.tags.some(tag => redNoteKeywords.some(keyword => tag.toLowerCase().includes(keyword.toLowerCase())))) { return true; } return false; } /** * 计算文本相似度 * 简单实现,基于关键词匹配 */ calculateTextSimilarity(query, text) { if (!text) return 0; const queryLower = query.toLowerCase(); const textLower = text.toLowerCase(); // 完全匹配 if (textLower === queryLower) return 1.0; // 包含整个查询 if (textLower.includes(queryLower)) return 0.8; // 查询词分割匹配 const queryWords = queryLower.split(/\s+/).filter(word => word.length > 1); let matchCount = 0; for (const word of queryWords) { if (textLower.includes(word)) { matchCount++; } } if (queryWords.length === 0) return 0; // 返回匹配词的比例 return matchCount / queryWords.length * 0.6; } /** * 合并向量搜索和文本搜索结果 */ mergeResults(vectorResults, textResults) { // 创建结果映射,避免重复 const resultMap = new Map(); // 处理向量结果 vectorResults.forEach(result => { const key = result.github_url || result.title; resultMap.set(key, { ...result, similarity: (result.similarity || 0) * (this.config.vectorSearchWeight || 0.5) }); }); // 合并文本结果 textResults.forEach(result => { const key = result.github_url || result.title; if (resultMap.has(key)) { // 如果已存在,合并相似度分数 const existing = resultMap.get(key); resultMap.set(key, { ...existing, similarity: (existing.similarity || 0) + (result.similarity || 0) * (this.config.textMatchWeight || 0.5) }); } else { // 如果不存在,添加新条目 resultMap.set(key, { ...result, similarity: (result.similarity || 0) * (this.config.textMatchWeight || 0.5) }); } }); // 转换回数组并排序 return Array.from(resultMap.values()) .sort((a, b) => (b.similarity || 0) - (a.similarity || 0)); } /** * 设置自定义兜底数据路径 * @param path 数据路径 */ setFallbackDataPath(path) { this.config.fallbackDataPath = path; this.vectorEngine.setFallbackDataPath(path); this.dataLoaded = false; logger.info(`Updated fallback data path to: ${path}`); } }