UNPKG

@claude-vector/core

Version:

Core vector search engine for code intelligence

932 lines (782 loc) 28.5 kB
/** * Advanced Relevance Scorer - 高度な関連性スコアリングシステム * * 機能: * - 多次元評価(セマンティック、時間的、複雑度、使用頻度、フェーズ適合) * - 開発フェーズ別重み付け * - 個人使用パターン学習 * - 動的スコア調整 */ import { EventEmitter } from 'events'; export class AdvancedRelevanceScorer extends EventEmitter { constructor(options = {}) { super(); this.options = { learningEnabled: options.learningEnabled ?? true, useTemporalDecay: options.useTemporalDecay ?? true, usagePatternWeight: options.usagePatternWeight || 0.2, complexityWeight: options.complexityWeight || 0.15, semanticWeight: options.semanticWeight || 0.4, temporalWeight: options.temporalWeight || 0.1, contextWeight: options.contextWeight || 0.15, adaptiveWeighting: options.adaptiveWeighting ?? true, ...options }; // スコアリング次元の定義 this.scoringDimensions = { semantic: { weight: this.options.semanticWeight, description: 'Semantic similarity based on content and meaning', calculator: this.calculateSemanticScore.bind(this) }, temporal: { weight: this.options.temporalWeight, description: 'Time-based relevance considering recency and update frequency', calculator: this.calculateTemporalScore.bind(this) }, complexity: { weight: this.options.complexityWeight, description: 'Complexity matching between query context and code', calculator: this.calculateComplexityScore.bind(this) }, usage: { weight: this.options.usagePatternWeight, description: 'Usage frequency and importance in codebase', calculator: this.calculateUsageScore.bind(this) }, context: { weight: this.options.contextWeight, description: 'Contextual relevance for development phase and task', calculator: this.calculateContextScore.bind(this) } }; // 開発フェーズ別重み調整 this.phaseWeightAdjustments = { exploration: { semantic: 0.3, temporal: 0.1, complexity: 0.1, usage: 0.2, context: 0.3 }, implementation: { semantic: 0.4, temporal: 0.15, complexity: 0.2, usage: 0.15, context: 0.1 }, debugging: { semantic: 0.5, temporal: 0.1, complexity: 0.3, usage: 0.05, context: 0.05 }, refactoring: { semantic: 0.3, temporal: 0.2, complexity: 0.3, usage: 0.1, context: 0.1 }, testing: { semantic: 0.4, temporal: 0.1, complexity: 0.2, usage: 0.2, context: 0.1 }, documentation: { semantic: 0.3, temporal: 0.1, complexity: 0.1, usage: 0.2, context: 0.3 } }; // 学習データ this.userFeedback = new Map(); // チャンクID -> フィードバックデータ this.queryPatterns = new Map(); // クエリパターン -> パフォーマンスデータ this.contextualPreferences = new Map(); // コンテキスト -> 優先パターン this.performanceHistory = []; // キャッシュ this.scoreCache = new Map(); this.complexityCache = new Map(); this.temporalCache = new Map(); } /** * 関連性スコアの計算 */ async calculateRelevanceScore(query, chunk, searchContext = {}) { const cacheKey = this.generateCacheKey(query, chunk, searchContext); // キャッシュチェック if (this.scoreCache.has(cacheKey)) { return this.scoreCache.get(cacheKey); } const startTime = Date.now(); // 各次元のスコア計算 const dimensionScores = {}; const weights = this.getAdjustedWeights(searchContext); for (const [dimension, config] of Object.entries(this.scoringDimensions)) { try { dimensionScores[dimension] = await config.calculator(query, chunk, searchContext); } catch (error) { console.warn(`Error calculating ${dimension} score:`, error.message); dimensionScores[dimension] = 0; } } // 重み付き総合スコア計算 let totalScore = 0; let totalWeight = 0; for (const [dimension, score] of Object.entries(dimensionScores)) { const weight = weights[dimension] || this.scoringDimensions[dimension].weight; totalScore += score * weight; totalWeight += weight; } const finalScore = totalWeight > 0 ? totalScore / totalWeight : 0; // 学習ベースの調整 const adjustedScore = await this.applyLearningAdjustments( finalScore, dimensionScores, query, chunk, searchContext ); // 結果の構築 const result = { finalScore: Math.max(0, Math.min(1, adjustedScore)), dimensionScores, weights, metadata: { calculationTime: Date.now() - startTime, cacheUsed: false, learningApplied: this.options.learningEnabled, context: searchContext.phase || 'general' } }; // キャッシュに保存 this.scoreCache.set(cacheKey, result); // パフォーマンス履歴の記録 this.recordPerformance(query, chunk, result, searchContext); return result; } /** * セマンティックスコアの計算 */ async calculateSemanticScore(query, chunk, searchContext) { // 基本的なコサイン類似度 const baseSimilarity = chunk.score || 0; // メタデータベースの意味的関連性 const metadataRelevance = this.calculateMetadataRelevance(query, chunk.metadata); // キーワードマッチング強化 const keywordBoost = this.calculateKeywordBoost(query, chunk); // セマンティックタイプマッチング const typeRelevance = this.calculateTypeRelevance(query, chunk.metadata); // パターンマッチング const patternRelevance = this.calculatePatternRelevance(query, chunk.metadata); // 組み合わせスコア let semanticScore = baseSimilarity * 0.5 + metadataRelevance * 0.2 + keywordBoost * 0.15 + typeRelevance * 0.1 + patternRelevance * 0.05; // クエリ長による調整 const queryLength = query.split(' ').length; if (queryLength > 5) { semanticScore *= 1.1; // 長いクエリには重みを上げる } return Math.max(0, Math.min(1, semanticScore)); } /** * メタデータ関連性の計算 */ calculateMetadataRelevance(query, metadata) { let relevance = 0; const queryLower = query.toLowerCase(); // 名前との関連性 if (metadata.semantic?.name && queryLower.includes(metadata.semantic.name.toLowerCase())) { relevance += 0.3; } // セマンティックタイプとの関連性 if (metadata.semantic?.type && queryLower.includes(metadata.semantic.type)) { relevance += 0.2; } // 目的との関連性 if (metadata.semantic?.purpose && queryLower.includes(metadata.semantic.purpose)) { relevance += 0.25; } // 依存関係との関連性 if (metadata.relationships?.dependencies) { const matchingDeps = metadata.relationships.dependencies.filter(dep => queryLower.includes(dep.toLowerCase()) ); relevance += matchingDeps.length * 0.05; } // タグとの関連性 if (metadata.searchOptimization?.keywords) { const matchingKeywords = metadata.searchOptimization.keywords.filter(keyword => queryLower.includes(keyword.toLowerCase()) ); relevance += matchingKeywords.length * 0.03; } return Math.min(relevance, 1); } /** * キーワードブーストの計算 */ calculateKeywordBoost(query, chunk) { const content = chunk.content.toLowerCase(); const queryWords = query.toLowerCase().split(/\s+/).filter(word => word.length > 2); let boost = 0; const totalWords = queryWords.length; for (const word of queryWords) { // 完全一致 if (content.includes(word)) { boost += 0.1; } // 部分一致(語幹) const stemmed = this.simpleStem(word); if (stemmed !== word && content.includes(stemmed)) { boost += 0.05; } } return totalWords > 0 ? boost / totalWords : 0; } /** * 簡単な語幹抽出 */ simpleStem(word) { // 英語の基本的な語尾変化を処理 return word .replace(/ing$/, '') .replace(/ed$/, '') .replace(/s$/, '') .replace(/ly$/, ''); } /** * タイプ関連性の計算 */ calculateTypeRelevance(query, metadata) { const queryLower = query.toLowerCase(); const semanticType = metadata.semantic?.type || metadata.semanticType; // クエリにタイプ関連のキーワードが含まれている場合 const typeKeywords = { 'function': ['function', 'func', 'method', 'procedure'], 'class': ['class', 'object', 'constructor', 'instance'], 'variable': ['variable', 'var', 'const', 'let', 'value'], 'export': ['export', 'module', 'public', 'api'], 'import': ['import', 'require', 'include', 'dependency'] }; const keywords = typeKeywords[semanticType] || []; const matchingKeywords = keywords.filter(keyword => queryLower.includes(keyword)); return matchingKeywords.length > 0 ? 0.3 : 0; } /** * パターン関連性の計算 */ calculatePatternRelevance(query, metadata) { if (!metadata.semantic?.patterns) return 0; const queryLower = query.toLowerCase(); let relevance = 0; const patternKeywords = { 'async_pattern': ['async', 'await', 'promise', 'asynchronous'], 'callback_pattern': ['callback', 'cb', 'handler', 'listener'], 'constructor_pattern': ['constructor', 'new', 'instance', 'init'], 'module_export_pattern': ['export', 'module', 'public'] }; for (const pattern of metadata.semantic.patterns) { const keywords = patternKeywords[pattern] || []; const matches = keywords.filter(keyword => queryLower.includes(keyword)); if (matches.length > 0) { relevance += 0.1; } } return Math.min(relevance, 0.5); } /** * 時間的関連性スコアの計算 */ async calculateTemporalScore(query, chunk, searchContext) { const metadata = chunk.metadata; let temporalScore = 0.5; // ベーススコア // 最終更新時刻による調整 if (metadata.development?.lastModified) { const lastModified = new Date(metadata.development.lastModified); const now = new Date(); const daysDiff = (now - lastModified) / (1000 * 60 * 60 * 24); // 時間的減衰関数(exponential decay) if (this.options.useTemporalDecay) { const decayFactor = Math.exp(-daysDiff / 30); // 30日で半減 temporalScore *= (0.5 + 0.5 * decayFactor); } } // 変更頻度による調整 if (metadata.development?.changeFrequency) { // 頻繁に変更されるコードは関連性が高い可能性 temporalScore += metadata.development.changeFrequency * 0.2; } // バグ履歴による調整 if (metadata.development?.bugHistory) { const recentBugs = metadata.development.bugHistory.filter(bug => { const bugDate = new Date(bug.date); const daysDiff = (new Date() - bugDate) / (1000 * 60 * 60 * 24); return daysDiff < 30 && !bug.resolved; }); if (recentBugs.length > 0 && searchContext.phase === 'debugging') { temporalScore += 0.3; // デバッグフェーズでは最近のバグは重要 } } return Math.max(0, Math.min(1, temporalScore)); } /** * 複雑度スコアの計算 */ async calculateComplexityScore(query, chunk, searchContext) { const metadata = chunk.metadata; let complexityScore = 0.5; if (!metadata.complexity) return complexityScore; const cognitive = metadata.complexity.cognitive || 0; const cyclomatic = metadata.complexity.cyclomatic || 0; // 開発フェーズに基づく複雑度適合性 switch (searchContext.phase) { case 'exploration': // 探索時は中程度の複雑度が好ましい if (cognitive >= 3 && cognitive <= 7) { complexityScore += 0.3; } break; case 'implementation': // 実装時は具体的で適度な複雑度 if (cognitive >= 2 && cognitive <= 10) { complexityScore += 0.2; } break; case 'debugging': // デバッグ時は高複雑度のコードが重要 if (cognitive > 5) { complexityScore += 0.4; } break; case 'refactoring': // リファクタリング時は高複雑度が対象 if (cognitive > 8 || cyclomatic > 10) { complexityScore += 0.5; } break; default: // 一般的には中程度の複雑度 if (cognitive >= 3 && cognitive <= 8) { complexityScore += 0.2; } } // 保守性指数による調整 if (metadata.complexity.maintainabilityIndex) { const maintainability = metadata.complexity.maintainabilityIndex / 100; if (searchContext.phase === 'refactoring') { // リファクタリング時は低保守性が重要 complexityScore += (1 - maintainability) * 0.3; } else { // その他は高保守性が好ましい complexityScore += maintainability * 0.2; } } return Math.max(0, Math.min(1, complexityScore)); } /** * 使用パターンスコアの計算 */ async calculateUsageScore(query, chunk, searchContext) { const metadata = chunk.metadata; let usageScore = 0.5; // AI重要度による調整 if (metadata.aiContext?.importance) { usageScore += metadata.aiContext.importance * 0.3; } // 検索可能性による調整 if (metadata.aiContext?.searchability) { usageScore += metadata.aiContext.searchability * 0.2; } // 依存関係の数による調整(重要度の指標) if (metadata.relationships?.dependencies) { const depCount = metadata.relationships.dependencies.length; usageScore += Math.min(depCount / 20, 0.2); } // エクスポートされているかどうか if (metadata.semantic?.type === 'export' || metadata.relationships?.exports?.length > 0) { usageScore += 0.15; } // テストカバレッジによる調整 if (metadata.quality?.testCoverage) { usageScore += metadata.quality.testCoverage * 0.1; } return Math.max(0, Math.min(1, usageScore)); } /** * コンテキストスコアの計算 */ async calculateContextScore(query, chunk, searchContext) { const metadata = chunk.metadata; let contextScore = 0.5; // コンテキスト関連性(メタデータから) if (metadata.aiContext?.contextRelevance) { contextScore += metadata.aiContext.contextRelevance * 0.3; } // 開発フェーズ適合性 if (metadata.development?.phase === searchContext.phase) { contextScore += 0.2; } // ファイルタイプとクエリの適合性 const fileType = this.inferFileType(metadata); const queryContext = this.inferQueryContext(query); if (this.isContextualMatch(fileType, queryContext)) { contextScore += 0.15; } // プロジェクト内での位置による調整 if (metadata.file) { const fileDepth = metadata.file.split('/').length; // 深い階層のファイルは特定の目的を持つ可能性が高い if (fileDepth > 3 && searchContext.phase === 'implementation') { contextScore += 0.1; } // ルート近くのファイルは設定や概要の可能性 if (fileDepth <= 2 && searchContext.phase === 'exploration') { contextScore += 0.1; } } return Math.max(0, Math.min(1, contextScore)); } /** * ファイルタイプの推論 */ inferFileType(metadata) { const fileName = metadata.file?.toLowerCase() || ''; if (fileName.includes('test') || fileName.includes('spec')) return 'test'; if (fileName.includes('config') || fileName.includes('setting')) return 'config'; if (fileName.includes('component')) return 'component'; if (fileName.includes('service') || fileName.includes('api')) return 'service'; if (fileName.includes('util') || fileName.includes('helper')) return 'utility'; if (fileName.includes('doc') || fileName.endsWith('.md')) return 'documentation'; return 'code'; } /** * クエリコンテキストの推論 */ inferQueryContext(query) { const queryLower = query.toLowerCase(); if (queryLower.includes('test') || queryLower.includes('spec')) return 'test'; if (queryLower.includes('error') || queryLower.includes('bug')) return 'debug'; if (queryLower.includes('config') || queryLower.includes('setting')) return 'config'; if (queryLower.includes('component')) return 'component'; if (queryLower.includes('api') || queryLower.includes('service')) return 'service'; if (queryLower.includes('util') || queryLower.includes('helper')) return 'utility'; return 'general'; } /** * コンテキスト適合性の判定 */ isContextualMatch(fileType, queryContext) { const contextMap = { 'test': ['test', 'debug'], 'config': ['config'], 'component': ['component', 'general'], 'service': ['service', 'general'], 'utility': ['utility', 'general'], 'documentation': ['general'] }; return contextMap[fileType]?.includes(queryContext) || false; } /** * 調整された重みの取得 */ getAdjustedWeights(searchContext) { const phase = searchContext.phase || 'implementation'; const phaseWeights = this.phaseWeightAdjustments[phase]; if (!phaseWeights || !this.options.adaptiveWeighting) { return Object.fromEntries( Object.entries(this.scoringDimensions).map(([dim, config]) => [dim, config.weight]) ); } return phaseWeights; } /** * 学習ベースの調整適用 */ async applyLearningAdjustments(baseScore, dimensionScores, query, chunk, searchContext) { if (!this.options.learningEnabled) { return baseScore; } let adjustedScore = baseScore; // ユーザーフィードバックによる調整 const chunkId = chunk.metadata.id || this.generateChunkId(chunk); const feedback = this.userFeedback.get(chunkId); if (feedback) { const feedbackAdjustment = this.calculateFeedbackAdjustment(feedback, searchContext); adjustedScore += feedbackAdjustment; } // クエリパターンによる調整 const patternAdjustment = this.calculatePatternAdjustment(query, dimensionScores, searchContext); adjustedScore += patternAdjustment; // コンテキスト学習による調整 const contextAdjustment = this.calculateContextLearningAdjustment(searchContext, chunk); adjustedScore += contextAdjustment; return adjustedScore; } /** * フィードバック調整の計算 */ calculateFeedbackAdjustment(feedback, searchContext) { let adjustment = 0; // 正のフィードバック if (feedback.positive > 0) { adjustment += Math.min(feedback.positive * 0.1, 0.2); } // 負のフィードバック if (feedback.negative > 0) { adjustment -= Math.min(feedback.negative * 0.1, 0.2); } // コンテキスト特有のフィードバック const contextFeedback = feedback.contexts?.[searchContext.phase]; if (contextFeedback) { adjustment += contextFeedback.score * 0.05; } return adjustment; } /** * パターン調整の計算 */ calculatePatternAdjustment(query, dimensionScores, searchContext) { const queryPattern = this.extractQueryPattern(query); const patternData = this.queryPatterns.get(queryPattern); if (!patternData) return 0; // パターンの成功率に基づく調整 let adjustment = 0; if (patternData.successRate > 0.7) { adjustment += 0.1; } else if (patternData.successRate < 0.3) { adjustment -= 0.1; } // 次元スコアのパターンマッチング const scoreSimilarity = this.calculateScoreSimilarity(dimensionScores, patternData.averageScores); adjustment += scoreSimilarity * 0.05; return adjustment; } /** * コンテキスト学習調整の計算 */ calculateContextLearningAdjustment(searchContext, chunk) { const contextKey = `${searchContext.phase}_${chunk.metadata.semantic?.type}`; const preferences = this.contextualPreferences.get(contextKey); if (!preferences) return 0; // 学習された優先度に基づく調整 return preferences.averageScore * 0.1 - 0.05; // -0.05から+0.05の範囲 } /** * ユーザーフィードバックの記録 */ recordUserFeedback(chunkId, feedbackType, searchContext) { if (!this.userFeedback.has(chunkId)) { this.userFeedback.set(chunkId, { positive: 0, negative: 0, neutral: 0, contexts: {} }); } const feedback = this.userFeedback.get(chunkId); // 全体フィードバック feedback[feedbackType]++; // コンテキスト特有のフィードバック const contextKey = searchContext.phase || 'general'; if (!feedback.contexts[contextKey]) { feedback.contexts[contextKey] = { count: 0, score: 0 }; } const contextFeedback = feedback.contexts[contextKey]; contextFeedback.count++; // スコア調整 switch (feedbackType) { case 'positive': contextFeedback.score = Math.min(contextFeedback.score + 0.1, 1); break; case 'negative': contextFeedback.score = Math.max(contextFeedback.score - 0.1, -1); break; case 'neutral': // 中性フィードバックは徐々に0に近づける contextFeedback.score *= 0.9; break; } this.emit('feedback-recorded', { chunkId, feedbackType, searchContext }); } /** * パフォーマンス記録 */ recordPerformance(query, chunk, scoringResult, searchContext) { this.performanceHistory.push({ query, chunkId: chunk.metadata.id || this.generateChunkId(chunk), finalScore: scoringResult.finalScore, dimensionScores: scoringResult.dimensionScores, searchContext, timestamp: Date.now() }); // 履歴サイズの制限 if (this.performanceHistory.length > 1000) { this.performanceHistory.shift(); } // クエリパターンの更新 this.updateQueryPattern(query, scoringResult, searchContext); } /** * クエリパターンの更新 */ updateQueryPattern(query, scoringResult, searchContext) { const pattern = this.extractQueryPattern(query); if (!this.queryPatterns.has(pattern)) { this.queryPatterns.set(pattern, { count: 0, successRate: 0.5, averageScores: {}, contexts: new Set() }); } const patternData = this.queryPatterns.get(pattern); patternData.count++; patternData.contexts.add(searchContext.phase || 'general'); // 平均スコアの更新 for (const [dimension, score] of Object.entries(scoringResult.dimensionScores)) { if (!patternData.averageScores[dimension]) { patternData.averageScores[dimension] = score; } else { // 移動平均 patternData.averageScores[dimension] = (patternData.averageScores[dimension] * 0.9 + score * 0.1); } } } /** * クエリパターンの抽出 */ extractQueryPattern(query) { // 簡略化されたパターン抽出 const words = query.toLowerCase().split(/\s+/).filter(word => word.length > 2); // キーワードのカテゴリ化 const categories = { 'error': ['error', 'bug', 'issue', 'problem', 'exception'], 'function': ['function', 'method', 'func', 'procedure'], 'class': ['class', 'object', 'constructor'], 'implement': ['implement', 'create', 'build', 'develop'], 'test': ['test', 'spec', 'unit', 'integration'] }; const detectedCategories = []; for (const [category, keywords] of Object.entries(categories)) { if (keywords.some(keyword => words.includes(keyword))) { detectedCategories.push(category); } } return detectedCategories.length > 0 ? detectedCategories.join('_') : 'general'; } /** * スコア類似度の計算 */ calculateScoreSimilarity(scores1, scores2) { const dimensions = Object.keys(scores1); let similarity = 0; let count = 0; for (const dim of dimensions) { if (scores2[dim] !== undefined) { const diff = Math.abs(scores1[dim] - scores2[dim]); similarity += 1 - diff; count++; } } return count > 0 ? similarity / count : 0; } /** * キャッシュキーの生成 */ generateCacheKey(query, chunk, searchContext) { const chunkId = chunk.metadata.id || this.generateChunkId(chunk); const contextKey = JSON.stringify({ phase: searchContext.phase, options: searchContext.options }); return `${query}_${chunkId}_${this.simpleHash(contextKey)}`; } /** * チャンクIDの生成 */ generateChunkId(chunk) { const metadata = chunk.metadata; const identifier = `${metadata.file}_${metadata.startLine}_${metadata.endLine}`; return this.simpleHash(identifier); } /** * 簡単なハッシュ関数 */ simpleHash(str) { let hash = 0; for (let i = 0; i < str.length; i++) { const char = str.charCodeAt(i); hash = ((hash << 5) - hash) + char; hash = hash & hash; } return Math.abs(hash).toString(36); } /** * キャッシュのクリア */ clearCache() { this.scoreCache.clear(); this.complexityCache.clear(); this.temporalCache.clear(); this.emit('cache-cleared'); } /** * 学習データのエクスポート */ exportLearningData() { return { userFeedback: Object.fromEntries(this.userFeedback), queryPatterns: Object.fromEntries(this.queryPatterns), contextualPreferences: Object.fromEntries(this.contextualPreferences), performanceHistory: this.performanceHistory.slice(-100), // 最新100件 exportedAt: Date.now() }; } /** * 学習データのインポート */ importLearningData(data) { if (data.userFeedback) { this.userFeedback = new Map(Object.entries(data.userFeedback)); } if (data.queryPatterns) { this.queryPatterns = new Map(Object.entries(data.queryPatterns)); } if (data.contextualPreferences) { this.contextualPreferences = new Map(Object.entries(data.contextualPreferences)); } if (data.performanceHistory) { this.performanceHistory = data.performanceHistory; } this.emit('learning-data-imported', data); } /** * 統計情報の取得 */ getStats() { const recentPerformance = this.performanceHistory.slice(-50); const avgScore = recentPerformance.reduce((sum, p) => sum + p.finalScore, 0) / recentPerformance.length; return { totalFeedback: this.userFeedback.size, queryPatterns: this.queryPatterns.size, performanceHistory: this.performanceHistory.length, averageScore: avgScore || 0, cacheSize: this.scoreCache.size, learningEnabled: this.options.learningEnabled, lastUpdate: this.performanceHistory.length > 0 ? this.performanceHistory[this.performanceHistory.length - 1].timestamp : null }; } }