UNPKG

@xiaohui-wang/mcpadvisor

Version:

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

408 lines (407 loc) 16.6 kB
/** * 离线搜索提供者 * 使用增强型内存向量引擎提供离线搜索功能 */ import { EnhancedMemoryVectorEngine } from '../../providers/offline/enhancedMemoryVectorEngine.js'; import logger from '../../../utils/logger.js'; import { getTextEmbedding } from '../../../utils/embedding.js'; export class OfflineSearchProvider { vectorEngine; config; dataLoaded = false; /** * 构造函数 * @param config 配置 */ constructor(config = {}) { this.config = { fallbackDataPath: config.fallbackDataPath, minSimilarity: config.minSimilarity || 0.1, // 降低默认阈值,提高召回率 textMatchWeight: config.textMatchWeight || 0.7, vectorSearchWeight: config.vectorSearchWeight || 0.3, }; this.vectorEngine = new EnhancedMemoryVectorEngine(this.config.fallbackDataPath); logger.info('Offline search provider initialized', { provider: 'OfflineSearchProvider', config: this.config, }); } async search(arg) { // 解析参数 let taskDescription; let keywords; let capabilities; if (typeof arg === 'string') { // legacy path taskDescription = arg; } else { ({ taskDescription, keywords, capabilities } = arg); } // 将关键词与能力拼接到查询文本中以复用现有逻辑 const combinedQueryParts = [taskDescription]; if (keywords && keywords.length) combinedQueryParts.push(keywords.join(' ')); if (capabilities && capabilities.length) combinedQueryParts.push(capabilities.join(' ')); const combinedQuery = combinedQueryParts.join(' ').trim(); return this.internalSearch(combinedQuery); } /** * 原有搜索实现搬迁至此,供重载方法调用 */ async internalSearch(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 offline search`); // 标记结果为离线来源 return mergedResults.map(result => ({ ...result, source: 'offline', })); } catch (error) { logger.error(`Offline search error: ${error instanceof Error ? error.message : String(error)}`); return []; } } /** * 加载备用数据 */ async loadFallbackData() { if (!this.dataLoaded) { // 确保向量引擎已初始化 if (this.vectorEngine) { // EnhancedMemoryVectorEngine 会在构造时自动加载数据 this.dataLoaded = true; } } } /** * 执行向量搜索 * @param query 搜索查询 * @returns 向量搜索结果 */ // --- legacy private helpers remain untouched below --- async vectorSearch(query) { try { // 获取查询的向量嵌入 const queryEmbedding = await getTextEmbedding(query); // 执行向量搜索 const results = await this.vectorEngine.search(queryEmbedding, 10, { minSimilarity: this.config.minSimilarity, textQuery: query, }); return results; } catch (error) { logger.error(`Vector search error: ${error instanceof Error ? error.message : String(error)}`); return []; } } /** * 检查查询中是否包含关键词或相关语义 * @param query 搜索查询 * @returns 是否包含关键词或相关语义 */ checkQueryForKeywords(query) { const queryLower = query.toLowerCase(); // 精确匹配关键词 const exactKeywords = [ '小红书', 'xiaohongshu', 'rednote', '热点', '社交媒体', '微博', 'weibo', '知乎', 'zhihu', ]; // 相关语义词组 const relatedPhrases = [ '今日热门', '热门话题', '网络热点', '今天的热点', '流行趋势', '网红', '博主', '种草', '网络舆论', '社交平台', '短视频', '评论', '锐评', '点评', ]; // 检查精确匹配 const hasExactKeyword = exactKeywords.some(keyword => queryLower.includes(keyword.toLowerCase())); if (hasExactKeyword) { return true; } // 检查相关语义 const hasRelatedPhrase = relatedPhrases.some(phrase => queryLower.includes(phrase.toLowerCase())); // 如果包含相关语义词组,也认为是相关查询 return hasRelatedPhrase; } /** * 执行文本匹配搜索 * @param query 搜索查询 * @returns 文本匹配结果 */ async textSearch(query) { try { // 获取所有服务器数据 // 通过执行一个空查询来获取所有服务器 const allServers = await this.vectorEngine.search([], 1000, { minSimilarity: 0, // 设置为0以获取所有服务器 }); // 调试信息:检查加载的服务器数据 console.log(`[DEBUG] 文本搜索 - 加载了 ${allServers.length} 个服务器`); // 检查是否包含小红书相关服务器 const redNoteServers = allServers.filter(server => server.id === 'rednote-mcp' || server.id === 'mcp-hotnews-server'); console.log(`[DEBUG] 文本搜索 - 找到 ${redNoteServers.length} 个小红书相关服务器:`, redNoteServers.map(s => ({ id: s.id, title: s.title }))); // 将查询分解为关键词 const keywords = query .toLowerCase() .split(/\s+/) .filter(k => k.length > 1); console.log(`[DEBUG] 文本搜索 - 查询关键词:`, keywords); // 如果没有有效关键词,返回空结果 if (keywords.length === 0) { console.log(`[DEBUG] 文本搜索 - 没有有效关键词,返回空结果`); return []; } // 复制服务器数据以避免修改原始数据 const results = [...allServers]; // 计算文本相似度分数 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); } // 按相似度排序 results.sort((a, b) => (b.similarity || 0) - (a.similarity || 0)); // 过滤相似度低于阈值的结果 const filteredResults = results.filter(result => (result.similarity || 0) >= this.config.minSimilarity); logger.debug(`[DEBUG] 文本搜索 - 过滤后剩余 ${filteredResults.length} 个结果`); return filteredResults; } catch (error) { logger.error(`Text search error: ${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; if (!query) return 0; // 将查询和文本转为小写 const queryLower = query.toLowerCase(); const textLower = text.toLowerCase(); // 如果文本完全包含查询,给予较高分数 if (textLower.includes(queryLower)) { return 0.9; } // 将查询分解为词项 const queryTerms = queryLower .split(/[\s,.!?;:"'()\[\]{}\-_+=<>~|/\\]+/) // 更全面的分隔符 .filter(term => term.length > 1); if (queryTerms.length === 0) return 0; // 计算匹配分数 let totalScore = 0; let matchedTerms = 0; for (const term of queryTerms) { // 完全匹配 if (textLower.includes(term)) { totalScore += 1.0; matchedTerms++; continue; } // 部分匹配(针对较长的词项) if (term.length >= 3) { // 检查是否有部分匹配 for (let i = 0; i < term.length - 2; i++) { const subTerm = term.substring(i, i + 3); // 至少3个字符的子串 if (textLower.includes(subTerm)) { totalScore += 0.5; // 部分匹配给予较低分数 matchedTerms++; break; } } } // 中文字符匹配(单字匹配) // 对于中文查询,即使单个字符也可能有意义 const chineseChars = term.match(/[\u4e00-\u9fa5]/g); if (chineseChars && chineseChars.length > 0) { let chineseMatched = false; for (const char of chineseChars) { if (textLower.includes(char)) { chineseMatched = true; break; } } if (chineseMatched) { totalScore += 0.3; // 中文单字匹配给予较低分数 matchedTerms++; } } } // 如果没有任何匹配,返回0 if (matchedTerms === 0) return 0; // 计算最终分数,考虑匹配质量和覆盖率 const coverage = matchedTerms / queryTerms.length; const avgScore = totalScore / matchedTerms; // 综合考虑匹配质量和覆盖率 return (avgScore * 0.7 + coverage * 0.3); } /** * 合并向量搜索和文本匹配结果 * @param vectorResults 向量搜索结果 * @param textResults 文本匹配结果 * @returns 合并后的结果 */ mergeResults(vectorResults, textResults) { // 创建一个映射来存储合并结果 const mergedMap = new Map(); // 先处理向量搜索结果 for (const result of vectorResults) { if (result.id) { // 应用向量搜索权重 const weightedSimilarity = (result.similarity || 0) * (this.config.vectorSearchWeight || 0.3); mergedMap.set(result.id, { ...result, similarity: weightedSimilarity, }); } } // 然后处理文本匹配结果 for (const result of textResults) { if (result.id) { const existing = mergedMap.get(result.id); if (existing) { // 如果已存在,合并相似度分数 const textWeightedSimilarity = (result.similarity || 0) * (this.config.textMatchWeight || 0.7); existing.similarity = Math.max(existing.similarity || 0, textWeightedSimilarity); } else { // 如果不存在,添加新结果 const textWeightedSimilarity = (result.similarity || 0) * (this.config.textMatchWeight || 0.7); mergedMap.set(result.id, { ...result, similarity: textWeightedSimilarity, }); } } } // 转换为数组并按相似度排序 const results = Array.from(mergedMap.values()); // 确保每个结果都有 score 属性,用于排序功能测试 results.forEach(result => { // 将 similarity 值复制到 score 属性 result.score = result.similarity || 0; }); // 按相似度排序 results.sort((a, b) => (b.similarity || 0) - (a.similarity || 0)); return results; } /** * 设置自定义兜底数据路径 * @param path 数据路径 */ setFallbackDataPath(path) { this.config.fallbackDataPath = path; this.vectorEngine.setFallbackDataPath(path); logger.info(`Updated fallback data path to: ${path}`); } }