Skip to main content
Glama

Claude MCP Server Integration

by mokemoke0821
advanced-search.ts21.8 kB
/** * 高度ファイル検索システム * Phase 3: 高度機能のインテリジェント検索・インデックスシステム */ import { promises as fs } from 'fs'; import { basename, extname, join } from 'path'; import { OperationResult, SavedSearch, SearchHighlight, SearchIndex, SearchIndexConfig, SearchQuery, SearchResult } from '../types/index.js'; import { createFailureResult, createSuccessResult, ensureDirectory, fileExists, getFileInfo, getFilesRecursively } from '../utils/file-helper.js'; import { logger } from '../utils/logger.js'; export class AdvancedSearchEngine { private searchIndex = new Map<string, SearchIndex>(); private savedSearches = new Map<string, SavedSearch>(); private fileContentCache = new Map<string, string>(); private static readonly DEFAULT_INDEX_CONFIG: SearchIndexConfig = { indexContent: true, indexMetadata: true, excludePatterns: ['.git/**', 'node_modules/**', '*.exe', '*.dll', '*.so'], maxFileSize: 10, // MB supportedTypes: ['.txt', '.md', '.js', '.ts', '.py', '.java', '.cpp', '.c', '.json', '.xml', '.html', '.css'], updateInterval: 60 // minutes }; /** * 検索インデックスを作成/更新 */ async createSearchIndex( basePath: string, indexPath?: string, config: Partial<SearchIndexConfig> = {} ): Promise<OperationResult<SearchIndex>> { try { logger.info(`検索インデックス作成開始: ${basePath}`); if (!await fileExists(basePath)) { throw new Error(`ベースパスが見つかりません: ${basePath}`); } const fullConfig = { ...AdvancedSearchEngine.DEFAULT_INDEX_CONFIG, ...config }; const finalIndexPath = indexPath || join(basePath, '.search-index'); await ensureDirectory(finalIndexPath); // ファイル一覧取得 const allFiles = await getFilesRecursively(basePath, { includeHidden: false, fileFilter: (filePath) => this.shouldIndexFile(filePath, fullConfig) }); logger.info(`インデックス対象ファイル: ${allFiles.length}個`); const searchIndex: SearchIndex = { basePath, indexPath: finalIndexPath, totalFiles: 0, lastUpdated: new Date(), version: '1.0.0', config: fullConfig }; // インデックス作成 const indexedFiles = await this.buildIndex(allFiles, searchIndex); searchIndex.totalFiles = indexedFiles; // インデックス保存 const indexFilePath = join(finalIndexPath, 'index.json'); await fs.writeFile(indexFilePath, JSON.stringify(searchIndex, null, 2), 'utf-8'); this.searchIndex.set(basePath, searchIndex); logger.info(`検索インデックス作成完了: ${indexedFiles}ファイル`); return createSuccessResult(`検索インデックスが作成されました: ${indexedFiles}ファイル`, searchIndex); } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); logger.error(`検索インデックス作成エラー: ${basePath}`, { error }); return createFailureResult(`検索インデックス作成に失敗しました: ${errorMessage}`, error instanceof Error ? error : new Error(errorMessage)); } } /** * 高度検索を実行 */ async performAdvancedSearch( query: SearchQuery, searchPaths: string[] ): Promise<OperationResult<SearchResult[]>> { try { logger.info(`高度検索実行: クエリ「${query.text || '(空)'}」`); const results: SearchResult[] = []; const maxResults = query.maxResults || 100; for (const searchPath of searchPaths) { if (!await fileExists(searchPath)) { logger.warn(`検索パスが見つかりません: ${searchPath}`); continue; } // インデックスが存在するかチェック let searchIndex = this.searchIndex.get(searchPath); if (!searchIndex) { // インデックスをロードまたは作成 const indexResult = await this.loadOrCreateIndex(searchPath); if (indexResult.success && indexResult.data) { searchIndex = indexResult.data; } } // ファイル検索実行 const pathResults = await this.searchInPath(query, searchPath, searchIndex); results.push(...pathResults); if (results.length >= maxResults) { break; } } // 結果をスコア順にソート results.sort((a, b) => b.score - a.score); // 最大結果数で切り詰め const finalResults = results.slice(0, maxResults); logger.info(`高度検索完了: ${finalResults.length}件ヒット`); return createSuccessResult(`${finalResults.length}件の検索結果が見つかりました`, finalResults); } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); logger.error(`高度検索エラー`, { error, query }); return createFailureResult(`高度検索に失敗しました: ${errorMessage}`, error instanceof Error ? error : new Error(errorMessage)); } } /** * 保存済み検索を作成 */ async createSavedSearch( name: string, description: string, query: SearchQuery ): Promise<OperationResult<SavedSearch>> { try { logger.info(`保存済み検索作成: ${name}`); const searchId = this.generateSearchId(name); if (this.savedSearches.has(searchId)) { throw new Error(`同じ名前の保存済み検索が既に存在します: ${name}`); } const savedSearch: SavedSearch = { id: searchId, name, description, query, created: new Date(), lastUsed: new Date(), useCount: 0, favorite: false }; this.savedSearches.set(searchId, savedSearch); logger.info(`保存済み検索作成完了: ${name}`); return createSuccessResult(`保存済み検索「${name}」が作成されました`, savedSearch); } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); logger.error(`保存済み検索作成エラー: ${name}`, { error }); return createFailureResult(`保存済み検索作成に失敗しました: ${errorMessage}`, error instanceof Error ? error : new Error(errorMessage)); } } /** * ファジー検索を実行 */ async performFuzzySearch( searchTerm: string, searchPaths: string[], threshold: number = 0.6 ): Promise<OperationResult<SearchResult[]>> { try { logger.info(`ファジー検索実行: 「${searchTerm}」, 閾値: ${threshold}`); const query: SearchQuery = { text: searchTerm, fuzzy: true, maxResults: 50 }; // 通常検索を実行 const searchResult = await this.performAdvancedSearch(query, searchPaths); if (!searchResult.success || !searchResult.data) { throw new Error(`検索処理に失敗: ${searchResult.message}`); } const results = searchResult.data; // ファジー検索スコア計算 const fuzzyResults = results.map(result => { const fileName = basename(result.file.path); const fuzzyScore = this.calculateFuzzyScore(searchTerm, fileName); return { ...result, score: result.score * fuzzyScore }; }).filter(result => result.score >= threshold); // スコア順にソート fuzzyResults.sort((a, b) => b.score - a.score); logger.info(`ファジー検索完了: ${fuzzyResults.length}件ヒット`); return createSuccessResult(`${fuzzyResults.length}件のファジー検索結果が見つかりました`, fuzzyResults); } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); logger.error(`ファジー検索エラー: ${searchTerm}`, { error }); return createFailureResult(`ファジー検索に失敗しました: ${errorMessage}`, error instanceof Error ? error : new Error(errorMessage)); } } /** * 内容ベース検索を実行 */ async performContentSearch( searchTerm: string, searchPaths: string[], caseSensitive: boolean = false ): Promise<OperationResult<SearchResult[]>> { try { logger.info(`内容ベース検索実行: 「${searchTerm}」`); const results: SearchResult[] = []; const searchRegex = new RegExp(searchTerm, caseSensitive ? 'g' : 'gi'); for (const searchPath of searchPaths) { const files = await getFilesRecursively(searchPath, { includeHidden: false, fileFilter: (filePath) => this.isTextFile(filePath) }); for (const filePath of files) { try { const content = await this.readFileContent(filePath); const matches = content.match(searchRegex); if (matches && matches.length > 0) { const fileInfo = await getFileInfo(filePath); const highlights = this.generateHighlights(content, searchTerm, caseSensitive); const result: SearchResult = { file: fileInfo, score: matches.length * 0.1, // マッチ数に基づくスコア highlights, metadata: { matchCount: matches.length, contentType: 'text' } }; results.push(result); } } catch (error) { logger.warn(`ファイル内容読み込みスキップ: ${filePath}`, { error }); } } } // スコア順にソート results.sort((a, b) => b.score - a.score); logger.info(`内容ベース検索完了: ${results.length}件ヒット`); return createSuccessResult(`${results.length}件の内容ベース検索結果が見つかりました`, results); } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); logger.error(`内容ベース検索エラー: ${searchTerm}`, { error }); return createFailureResult(`内容ベース検索に失敗しました: ${errorMessage}`, error instanceof Error ? error : new Error(errorMessage)); } } /** * 検索統計レポート生成 */ async generateSearchReport( searchQueries: SearchQuery[], outputPath?: string ): Promise<OperationResult<string>> { try { logger.info(`検索統計レポート生成開始: ${searchQueries.length}クエリ`); const report = this.createSearchReport(searchQueries); const finalOutputPath = outputPath || join(process.cwd(), `search_report_${Date.now()}.txt`); await fs.writeFile(finalOutputPath, report, 'utf-8'); logger.info(`検索統計レポート生成完了: ${finalOutputPath}`); return createSuccessResult(`検索統計レポートが生成されました: ${finalOutputPath}`, finalOutputPath); } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); logger.error(`検索統計レポート生成エラー`, { error }); return createFailureResult(`検索統計レポート生成に失敗しました: ${errorMessage}`, error instanceof Error ? error : new Error(errorMessage)); } } /** * インデックス構築(内部用) */ private async buildIndex(files: string[], searchIndex: SearchIndex): Promise<number> { let indexedCount = 0; for (const filePath of files) { try { // ファイル情報取得 const fileInfo = await getFileInfo(filePath); // 内容インデックス if (searchIndex.config.indexContent && this.isTextFile(filePath)) { const content = await this.readFileContent(filePath); this.fileContentCache.set(filePath, content); } indexedCount++; if (indexedCount % 1000 === 0) { logger.info(`インデックス進行状況: ${indexedCount}/${files.length}ファイル`); } } catch (error) { logger.warn(`ファイルインデックススキップ: ${filePath}`, { error }); } } return indexedCount; } /** * 指定パスでの検索実行(内部用) */ private async searchInPath( query: SearchQuery, searchPath: string, searchIndex?: SearchIndex ): Promise<SearchResult[]> { const results: SearchResult[] = []; // ファイル一覧取得 const files = await getFilesRecursively(searchPath, { includeHidden: false }); for (const filePath of files) { try { const score = await this.calculateSearchScore(filePath, query); if (score > 0) { const fileInfo = await getFileInfo(filePath); const result: SearchResult = { file: fileInfo, score, metadata: { searchPath, indexed: !!searchIndex } }; // ハイライト生成 if (query.text && this.isTextFile(filePath)) { const highlights = await this.generateFileHighlights(filePath, query.text); result.highlights = highlights; } results.push(result); } } catch (error) { logger.warn(`ファイル検索スキップ: ${filePath}`, { error }); } } return results; } /** * 検索スコア計算(内部用) */ private async calculateSearchScore(filePath: string, query: SearchQuery): Promise<number> { let score = 0; const fileName = basename(filePath); const fileExtension = extname(filePath).toLowerCase(); // ファイル名マッチング if (query.filename) { if (fileName.toLowerCase().includes(query.filename.toLowerCase())) { score += 0.8; } } // 拡張子マッチング if (query.extension && query.extension.length > 0) { if (query.extension.includes(fileExtension)) { score += 0.6; } } // テキスト検索 if (query.text && this.isTextFile(filePath)) { const content = await this.readFileContent(filePath); const regex = new RegExp(query.text, 'gi'); const matches = content.match(regex); if (matches) { score += matches.length * 0.1; } } // ファイルサイズフィルタ if (query.sizeRange) { try { const fileInfo = await getFileInfo(filePath); const sizeRange = query.sizeRange; if (sizeRange.min !== undefined && fileInfo.size < sizeRange.min) return 0; if (sizeRange.max !== undefined && fileInfo.size > sizeRange.max) return 0; score += 0.2; } catch (error) { // ファイル情報取得エラーは無視 } } // 日付範囲フィルタ if (query.dateRange) { try { const fileInfo = await getFileInfo(filePath); const dateRange = query.dateRange; if (dateRange.start && fileInfo.modified < dateRange.start) return 0; if (dateRange.end && fileInfo.modified > dateRange.end) return 0; score += 0.2; } catch (error) { // ファイル情報取得エラーは無視 } } return Math.min(score, 1.0); // 最大スコアは1.0 } /** * ファジースコア計算(内部用) */ private calculateFuzzyScore(searchTerm: string, targetText: string): number { const s1 = searchTerm.toLowerCase(); const s2 = targetText.toLowerCase(); // レーベンシュタイン距離ベースの類似度 const distance = this.levenshteinDistance(s1, s2); const maxLength = Math.max(s1.length, s2.length); if (maxLength === 0) return 1.0; return 1.0 - (distance / maxLength); } /** * レーベンシュタイン距離計算(内部用) */ private levenshteinDistance(str1: string, str2: string): number { const matrix = []; for (let i = 0; i <= str2.length; i++) { matrix[i] = [i]; } for (let j = 0; j <= str1.length; j++) { matrix[0][j] = j; } for (let i = 1; i <= str2.length; i++) { for (let j = 1; j <= str1.length; j++) { if (str2.charAt(i - 1) === str1.charAt(j - 1)) { matrix[i][j] = matrix[i - 1][j - 1]; } else { matrix[i][j] = Math.min( matrix[i - 1][j - 1] + 1, matrix[i][j - 1] + 1, matrix[i - 1][j] + 1 ); } } } return matrix[str2.length][str1.length]; } /** * ハイライト生成(内部用) */ private generateHighlights(content: string, searchTerm: string, caseSensitive: boolean): SearchHighlight[] { const highlights: SearchHighlight[] = []; const regex = new RegExp(searchTerm, caseSensitive ? 'g' : 'gi'); let match; while ((match = regex.exec(content)) !== null) { const start = Math.max(0, match.index - 50); const end = Math.min(content.length, match.index + searchTerm.length + 50); const snippet = content.substring(start, end); highlights.push({ field: 'content', snippet, positions: [{ start: match.index, end: match.index + searchTerm.length }] }); if (highlights.length >= 5) break; // 最大5個のハイライト } return highlights; } /** * ファイルハイライト生成(内部用) */ private async generateFileHighlights(filePath: string, searchTerm: string): Promise<SearchHighlight[]> { try { const content = await this.readFileContent(filePath); return this.generateHighlights(content, searchTerm, false); } catch (error) { return []; } } /** * ファイル内容読み込み(内部用) */ private async readFileContent(filePath: string): Promise<string> { // キャッシュから取得 const cached = this.fileContentCache.get(filePath); if (cached) { return cached; } // ファイル読み込み const content = await fs.readFile(filePath, 'utf-8'); // 大きすぎるファイルはキャッシュしない if (content.length < 100000) { // 100KB以下 this.fileContentCache.set(filePath, content); } return content; } /** * テキストファイル判定(内部用) */ private isTextFile(filePath: string): boolean { const textExtensions = ['.txt', '.md', '.js', '.ts', '.py', '.java', '.cpp', '.c', '.cs', '.php', '.rb', '.go', '.json', '.xml', '.html', '.css', '.yaml', '.yml']; const extension = extname(filePath).toLowerCase(); return textExtensions.includes(extension); } /** * インデックス対象ファイル判定(内部用) */ private shouldIndexFile(filePath: string, config: SearchIndexConfig): boolean { const fileName = basename(filePath); const extension = extname(filePath).toLowerCase(); // 除外パターンチェック for (const pattern of config.excludePatterns) { if (this.matchPattern(filePath, pattern) || this.matchPattern(fileName, pattern)) { return false; } } // サポート形式チェック if (config.supportedTypes.length > 0) { return config.supportedTypes.includes(extension); } return true; } /** * パターンマッチング(内部用) */ private matchPattern(text: string, pattern: string): boolean { const regexPattern = pattern .replace(/\./g, '\\.') .replace(/\*\*/g, '.*') .replace(/\*/g, '[^/]*') .replace(/\?/g, '[^/]'); return new RegExp(`^${regexPattern}$`).test(text); } /** * インデックス読み込みまたは作成(内部用) */ private async loadOrCreateIndex(searchPath: string): Promise<OperationResult<SearchIndex>> { const indexPath = join(searchPath, '.search-index', 'index.json'); if (await fileExists(indexPath)) { try { const indexData = JSON.parse(await fs.readFile(indexPath, 'utf-8')); this.searchIndex.set(searchPath, indexData); return createSuccessResult('インデックスを読み込みました', indexData); } catch (error) { logger.warn(`インデックス読み込みエラー: ${indexPath}`, { error }); } } // インデックスが存在しない場合は作成 return await this.createSearchIndex(searchPath); } /** * 検索レポート作成(内部用) */ private createSearchReport(searchQueries: SearchQuery[]): string { const lines: string[] = []; lines.push('# 検索統計レポート'); lines.push(`生成日時: ${new Date().toLocaleString('ja-JP')}`); lines.push(''); // サマリー lines.push('## サマリー'); lines.push(`- 検索クエリ数: ${searchQueries.length}`); lines.push(''); // クエリ分析 const textQueries = searchQueries.filter(q => q.text).length; const filenameQueries = searchQueries.filter(q => q.filename).length; const extensionQueries = searchQueries.filter(q => q.extension && q.extension.length > 0).length; const fuzzyQueries = searchQueries.filter(q => q.fuzzy).length; lines.push('## クエリ種別'); lines.push(`- テキスト検索: ${textQueries}件`); lines.push(`- ファイル名検索: ${filenameQueries}件`); lines.push(`- 拡張子フィルタ: ${extensionQueries}件`); lines.push(`- ファジー検索: ${fuzzyQueries}件`); lines.push(''); return lines.join('\n'); } /** * 検索ID生成(内部用) */ private generateSearchId(name: string): string { const safeName = name.replace(/[^a-zA-Z0-9]/g, '_'); return `search_${safeName}_${Date.now()}`; } }

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/mokemoke0821/claude-mcp-integration'

If you have feedback or need assistance with the MCP directory API, please join our Discord server