Skip to main content
Glama
02-search-features.md23.6 kB
# 検索機能設計 ## 概要 Search MCP Serverの最も重要な機能の一つが、ツールの効率的な検索です。このドキュメントでは、以下の検索機能の詳細設計を記述します: 1. テキスト検索(ツール名・説明) 2. タグベースの検索 3. カテゴリフィルタリング 4. 検索結果のランキング 5. セマンティック検索(Phase 5) ## 1. テキスト検索 ### 1.1 概要 ツール名と説明文に対するテキスト検索機能を提供します。部分一致、前方一致、完全一致をサポートします。 ### 1.2 実装設計 #### 1.2.1 検索インターフェース ```typescript // src/search/text-search.ts export interface SearchOptions { query: string; matchType?: 'partial' | 'prefix' | 'exact'; fields?: ('name' | 'description')[]; caseSensitive?: boolean; limit?: number; } export interface SearchResult { tool: ToolMetadata; score: number; // 関連度スコア (0-1) matches: { field: string; positions: number[]; // マッチした位置 }[]; } export class TextSearch { search(tools: ToolMetadata[], options: SearchOptions): SearchResult[] { const { query, matchType = 'partial', fields = ['name', 'description'], caseSensitive = false, limit = 20 } = options; if (!query || query.trim() === '') { return []; } const normalizedQuery = caseSensitive ? query : query.toLowerCase(); const results: SearchResult[] = []; for (const tool of tools) { const result = this.matchTool(tool, normalizedQuery, matchType, fields, caseSensitive); if (result) { results.push(result); } } // スコアでソート results.sort((a, b) => b.score - a.score); return results.slice(0, limit); } private matchTool( tool: ToolMetadata, query: string, matchType: string, fields: string[], caseSensitive: boolean ): SearchResult | null { const matches: SearchResult['matches'] = []; let totalScore = 0; for (const field of fields) { const fieldValue = tool[field as keyof ToolMetadata] as string; if (!fieldValue) continue; const normalizedValue = caseSensitive ? fieldValue : fieldValue.toLowerCase(); const match = this.matchField(normalizedValue, query, matchType); if (match) { matches.push({ field, positions: match.positions }); totalScore += match.score * this.getFieldWeight(field); } } if (matches.length === 0) { return null; } return { tool, score: Math.min(totalScore, 1.0), matches }; } private matchField( value: string, query: string, matchType: string ): { positions: number[]; score: number } | null { let positions: number[] = []; let score = 0; switch (matchType) { case 'exact': if (value === query) { positions = [0]; score = 1.0; } break; case 'prefix': if (value.startsWith(query)) { positions = [0]; score = 0.9; } break; case 'partial': default: const index = value.indexOf(query); if (index !== -1) { positions = [index]; // スコアは位置とマッチ長に基づく score = this.calculatePartialScore(value, query, index); } break; } return positions.length > 0 ? { positions, score } : null; } private calculatePartialScore(value: string, query: string, position: number): number { // 前方一致ほど高スコア const positionScore = 1 - (position / value.length); // マッチ長が長いほど高スコア const lengthScore = query.length / value.length; // 重み付き平均 return positionScore * 0.7 + lengthScore * 0.3; } private getFieldWeight(field: string): number { const weights: Record<string, number> = { name: 1.0, // ツール名が最も重要 description: 0.7, category: 0.5, tags: 0.6 }; return weights[field] || 0.5; } } ``` #### 1.2.2 APIエンドポイント ```typescript // src/index.ts import { TextSearch } from './search/text-search.js'; const textSearch = new TextSearch(); server.post<{ Body: SearchToolsRequest }>('/v1/tools/search', { schema: { body: { type: 'object', required: ['query'], properties: { query: { type: 'string', minLength: 1 }, matchType: { type: 'string', enum: ['partial', 'prefix', 'exact'] }, fields: { type: 'array', items: { type: 'string', enum: ['name', 'description', 'category'] } }, caseSensitive: { type: 'boolean' }, limit: { type: 'integer', minimum: 1, maximum: 100 } } } } }, async (request, reply) => { const { query, matchType, fields, caseSensitive, limit } = request.body; const allTools = toolRegistry.list(); const results = textSearch.search(allTools, { query, matchType, fields, caseSensitive, limit }); return { query, matchType: matchType || 'partial', results: results.map(r => ({ tool: r.tool, score: r.score, matches: r.matches })), total: results.length }; }); ``` ## 2. タグベースの検索 ### 2.1 概要 ツールにタグを付与し、タグで検索・フィルタリングできる機能を提供します。 ### 2.2 実装設計 #### 2.2.1 タグ管理 ```typescript // src/search/tag-search.ts export class TagSearch { /** * タグで完全一致検索 */ searchByTag(tools: ToolMetadata[], tag: string): ToolMetadata[] { return tools.filter(tool => tool.tags && tool.tags.includes(tag) ); } /** * 複数タグでAND検索 */ searchByTagsAnd(tools: ToolMetadata[], tags: string[]): ToolMetadata[] { return tools.filter(tool => tool.tags && tags.every(tag => tool.tags!.includes(tag)) ); } /** * 複数タグでOR検索 */ searchByTagsOr(tools: ToolMetadata[], tags: string[]): ToolMetadata[] { return tools.filter(tool => tool.tags && tags.some(tag => tool.tags!.includes(tag)) ); } /** * すべてのタグを取得(統計情報付き) */ getAllTags(tools: ToolMetadata[]): Array<{ tag: string; count: number }> { const tagCounts = new Map<string, number>(); for (const tool of tools) { if (!tool.tags) continue; for (const tag of tool.tags) { tagCounts.set(tag, (tagCounts.get(tag) || 0) + 1); } } return Array.from(tagCounts.entries()) .map(([tag, count]) => ({ tag, count })) .sort((a, b) => b.count - a.count); } /** * 関連タグの提案 */ suggestRelatedTags(tools: ToolMetadata[], selectedTags: string[]): string[] { // 選択されたタグを持つツールを取得 const relevantTools = this.searchByTagsOr(tools, selectedTags); // それらのツールが持つ他のタグを集計 const tagCounts = new Map<string, number>(); for (const tool of relevantTools) { if (!tool.tags) continue; for (const tag of tool.tags) { if (!selectedTags.includes(tag)) { tagCounts.set(tag, (tagCounts.get(tag) || 0) + 1); } } } // 出現頻度でソートして返す return Array.from(tagCounts.entries()) .sort((a, b) => b[1] - a[1]) .map(([tag]) => tag) .slice(0, 10); } } ``` #### 2.2.2 APIエンドポイント ```typescript // タグ一覧取得 server.get('/v1/tags', async (request, reply) => { const tagSearch = new TagSearch(); const allTools = toolRegistry.list(); const tags = tagSearch.getAllTags(allTools); return { tags, total: tags.length }; }); // タグで検索 server.post<{ Body: { tags: string[]; operator?: 'and' | 'or' } }>( '/v1/tools/search/by-tags', { schema: { body: { type: 'object', required: ['tags'], properties: { tags: { type: 'array', items: { type: 'string' }, minItems: 1 }, operator: { type: 'string', enum: ['and', 'or'] } } } } }, async (request, reply) => { const { tags, operator = 'or' } = request.body; const tagSearch = new TagSearch(); const allTools = toolRegistry.list(); const results = operator === 'and' ? tagSearch.searchByTagsAnd(allTools, tags) : tagSearch.searchByTagsOr(allTools, tags); return { tags, operator, results, total: results.length }; } ); // 関連タグの提案 server.post<{ Body: { tags: string[] } }>( '/v1/tags/suggest', async (request, reply) => { const { tags } = request.body; const tagSearch = new TagSearch(); const allTools = toolRegistry.list(); const suggestions = tagSearch.suggestRelatedTags(allTools, tags); return { selectedTags: tags, suggestions }; } ); ``` ## 3. カテゴリフィルタリング ### 3.1 概要 ツールをカテゴリで分類し、カテゴリでフィルタリングする機能を提供します。 ### 3.2 実装設計 ```typescript // src/search/category-search.ts export class CategorySearch { /** * カテゴリで検索 */ searchByCategory(tools: ToolMetadata[], category: string): ToolMetadata[] { return tools.filter(tool => tool.category === category); } /** * すべてのカテゴリを取得 */ getAllCategories(tools: ToolMetadata[]): Array<{ category: string; count: number }> { const categoryCounts = new Map<string, number>(); for (const tool of tools) { if (!tool.category) continue; categoryCounts.set( tool.category, (categoryCounts.get(tool.category) || 0) + 1 ); } return Array.from(categoryCounts.entries()) .map(([category, count]) => ({ category, count })) .sort((a, b) => a.category.localeCompare(b.category)); } /** * カテゴリの階層構造をサポート(将来の拡張) */ searchByCategoryPath(tools: ToolMetadata[], categoryPath: string[]): ToolMetadata[] { // 例: ["data", "processing", "transform"] -> "data/processing/transform" const fullPath = categoryPath.join('/'); return tools.filter(tool => tool.category && tool.category.startsWith(fullPath) ); } } ``` #### 3.2.1 APIエンドポイント ```typescript // カテゴリ一覧取得 server.get('/v1/categories', async (request, reply) => { const categorySearch = new CategorySearch(); const allTools = toolRegistry.list(); const categories = categorySearch.getAllCategories(allTools); return { categories, total: categories.length }; }); // カテゴリで検索 server.get<{ Querystring: { category: string } }>( '/v1/tools/by-category/:category', async (request, reply) => { const { category } = request.params; const categorySearch = new CategorySearch(); const allTools = toolRegistry.list(); const results = categorySearch.searchByCategory(allTools, category); return { category, results, total: results.length }; } ); ``` ## 4. 複合検索 ### 4.1 概要 テキスト検索、タグ、カテゴリを組み合わせた複合検索を提供します。 ### 4.2 実装設計 ```typescript // src/search/composite-search.ts export interface CompositeSearchOptions { query?: string; tags?: string[]; tagOperator?: 'and' | 'or'; category?: string; matchType?: 'partial' | 'prefix' | 'exact'; fields?: ('name' | 'description')[]; caseSensitive?: boolean; sortBy?: 'relevance' | 'name' | 'category'; sortOrder?: 'asc' | 'desc'; limit?: number; offset?: number; } export class CompositeSearch { constructor( private textSearch: TextSearch, private tagSearch: TagSearch, private categorySearch: CategorySearch ) {} search(tools: ToolMetadata[], options: CompositeSearchOptions): { results: SearchResult[]; total: number; hasMore: boolean; } { let filteredTools = [...tools]; // 1. カテゴリでフィルタ if (options.category) { filteredTools = this.categorySearch.searchByCategory( filteredTools, options.category ); } // 2. タグでフィルタ if (options.tags && options.tags.length > 0) { filteredTools = options.tagOperator === 'and' ? this.tagSearch.searchByTagsAnd(filteredTools, options.tags) : this.tagSearch.searchByTagsOr(filteredTools, options.tags); } // 3. テキスト検索 let results: SearchResult[]; if (options.query) { results = this.textSearch.search(filteredTools, { query: options.query, matchType: options.matchType, fields: options.fields, caseSensitive: options.caseSensitive, limit: 9999 // 後でページネーション }); } else { // クエリがない場合は全ツールを返す(スコアは1.0) results = filteredTools.map(tool => ({ tool, score: 1.0, matches: [] })); } // 4. ソート results = this.sortResults(results, options.sortBy, options.sortOrder); // 5. ページネーション const offset = options.offset || 0; const limit = options.limit || 20; const total = results.length; const paginatedResults = results.slice(offset, offset + limit); const hasMore = offset + limit < total; return { results: paginatedResults, total, hasMore }; } private sortResults( results: SearchResult[], sortBy: string = 'relevance', sortOrder: string = 'desc' ): SearchResult[] { const sorted = [...results]; switch (sortBy) { case 'name': sorted.sort((a, b) => a.tool.name.localeCompare(b.tool.name)); break; case 'category': sorted.sort((a, b) => (a.tool.category || '').localeCompare(b.tool.category || '') ); break; case 'relevance': default: sorted.sort((a, b) => b.score - a.score); break; } if (sortOrder === 'asc') { sorted.reverse(); } return sorted; } } ``` #### 4.2.1 APIエンドポイント ```typescript // 複合検索エンドポイント server.post<{ Body: CompositeSearchOptions }>( '/v1/search', { schema: { body: { type: 'object', properties: { query: { type: 'string' }, tags: { type: 'array', items: { type: 'string' } }, tagOperator: { type: 'string', enum: ['and', 'or'] }, category: { type: 'string' }, matchType: { type: 'string', enum: ['partial', 'prefix', 'exact'] }, fields: { type: 'array', items: { type: 'string', enum: ['name', 'description'] } }, caseSensitive: { type: 'boolean' }, sortBy: { type: 'string', enum: ['relevance', 'name', 'category'] }, sortOrder: { type: 'string', enum: ['asc', 'desc'] }, limit: { type: 'integer', minimum: 1, maximum: 100 }, offset: { type: 'integer', minimum: 0 } } } } }, async (request, reply) => { const compositeSearch = new CompositeSearch( new TextSearch(), new TagSearch(), new CategorySearch() ); const allTools = toolRegistry.list(); const searchResult = compositeSearch.search(allTools, request.body); return { query: request.body.query, filters: { tags: request.body.tags, category: request.body.category }, results: searchResult.results.map(r => ({ tool: r.tool, score: r.score, matches: r.matches })), pagination: { offset: request.body.offset || 0, limit: request.body.limit || 20, total: searchResult.total, hasMore: searchResult.hasMore } }; } ); ``` ## 5. セマンティック検索(Phase 5) ### 5.1 概要 ベクトル埋め込みを使用した意味的な検索を提供します。ツールの説明文をベクトル化し、クエリとの類似度で検索します。 ### 5.2 実装設計 ```typescript // src/search/semantic-search.ts export interface EmbeddingProvider { embed(text: string): Promise<number[]>; embedBatch(texts: string[]): Promise<number[][]>; } export class SemanticSearch { private vectorStore: Map<string, number[]>; constructor(private embeddingProvider: EmbeddingProvider) { this.vectorStore = new Map(); } /** * ツールのインデックス作成 */ async indexTools(tools: ToolMetadata[]): Promise<void> { const texts = tools.map(tool => `${tool.name} ${tool.description}` ); const embeddings = await this.embeddingProvider.embedBatch(texts); tools.forEach((tool, index) => { this.vectorStore.set(tool.name, embeddings[index]); }); } /** * セマンティック検索 */ async search( query: string, tools: ToolMetadata[], limit: number = 20 ): Promise<SearchResult[]> { // クエリのベクトル化 const queryVector = await this.embeddingProvider.embed(query); // 各ツールとの類似度を計算 const results: SearchResult[] = []; for (const tool of tools) { const toolVector = this.vectorStore.get(tool.name); if (!toolVector) continue; const similarity = this.cosineSimilarity(queryVector, toolVector); if (similarity > 0.5) { // 閾値 results.push({ tool, score: similarity, matches: [] }); } } // スコアでソート results.sort((a, b) => b.score - a.score); return results.slice(0, limit); } /** * コサイン類似度の計算 */ private cosineSimilarity(a: number[], b: number[]): number { let dotProduct = 0; let normA = 0; let normB = 0; for (let i = 0; i < a.length; i++) { dotProduct += a[i] * b[i]; normA += a[i] * a[i]; normB += b[i] * b[i]; } return dotProduct / (Math.sqrt(normA) * Math.sqrt(normB)); } } /** * OpenAI Embeddings実装例 */ export class OpenAIEmbeddings implements EmbeddingProvider { constructor(private apiKey: string) {} async embed(text: string): Promise<number[]> { // OpenAI API呼び出し // 実装省略 throw new Error('Not implemented'); } async embedBatch(texts: string[]): Promise<number[][]> { // OpenAI API呼び出し(バッチ) // 実装省略 throw new Error('Not implemented'); } } ``` #### 5.2.1 APIエンドポイント ```typescript // セマンティック検索エンドポイント(Phase 5) server.post<{ Body: { query: string; limit?: number } }>( '/v1/search/semantic', async (request, reply) => { const { query, limit = 20 } = request.body; if (!query || query.trim() === '') { return reply.code(400).send({ error: 'Query is required' }); } const allTools = toolRegistry.list(); const results = await semanticSearch.search(query, allTools, limit); return { query, type: 'semantic', results: results.map(r => ({ tool: r.tool, score: r.score })), total: results.length }; } ); ``` ## 6. 検索パフォーマンスの最適化 ### 6.1 インデックス構築 ```typescript // src/search/search-index.ts export class SearchIndex { private nameIndex: Map<string, ToolMetadata>; private descriptionTokens: Map<string, Set<string>>; private tagIndex: Map<string, Set<string>>; private categoryIndex: Map<string, Set<string>>; constructor() { this.nameIndex = new Map(); this.descriptionTokens = new Map(); this.tagIndex = new Map(); this.categoryIndex = new Map(); } /** * ツールのインデックス作成 */ buildIndex(tools: ToolMetadata[]): void { for (const tool of tools) { // 名前インデックス this.nameIndex.set(tool.name, tool); // 説明文のトークン化 const tokens = this.tokenize(tool.description); tokens.forEach(token => { if (!this.descriptionTokens.has(token)) { this.descriptionTokens.set(token, new Set()); } this.descriptionTokens.get(token)!.add(tool.name); }); // タグインデックス if (tool.tags) { tool.tags.forEach(tag => { if (!this.tagIndex.has(tag)) { this.tagIndex.set(tag, new Set()); } this.tagIndex.get(tag)!.add(tool.name); }); } // カテゴリインデックス if (tool.category) { if (!this.categoryIndex.has(tool.category)) { this.categoryIndex.set(tool.category, new Set()); } this.categoryIndex.get(tool.category)!.add(tool.name); } } } /** * インデックスを使った高速検索 */ search(query: string): Set<string> { const tokens = this.tokenize(query); const results = new Set<string>(); for (const token of tokens) { const matches = this.descriptionTokens.get(token); if (matches) { matches.forEach(name => results.add(name)); } } return results; } /** * テキストをトークン化 */ private tokenize(text: string): string[] { return text .toLowerCase() .split(/\s+/) .filter(token => token.length > 2); // 3文字以上 } } ``` ## 7. テスト戦略 ```typescript // src/search/text-search.test.ts describe('TextSearch', () => { let textSearch: TextSearch; let testTools: ToolMetadata[]; beforeEach(() => { textSearch = new TextSearch(); testTools = [ { name: 'echo', description: 'Echo a message', parameters: [] }, { name: 'search', description: 'Search for data', parameters: [] }, { name: 'calculator', description: 'Calculate expressions', parameters: [] } ]; }); describe('search', () => { it('should find tools by name partial match', () => { const results = textSearch.search(testTools, { query: 'echo', matchType: 'partial' }); expect(results.length).toBe(1); expect(results[0].tool.name).toBe('echo'); }); it('should find tools by description', () => { const results = textSearch.search(testTools, { query: 'message', fields: ['description'] }); expect(results.length).toBe(1); expect(results[0].tool.name).toBe('echo'); }); it('should return results sorted by score', () => { const results = textSearch.search(testTools, { query: 'search' }); for (let i = 1; i < results.length; i++) { expect(results[i - 1].score).toBeGreaterThanOrEqual(results[i].score); } }); }); }); ``` ## 8. 実装優先順位 ### Phase 2: 基本検索機能 - [ ] テキスト検索の実装 - [ ] タグベースの検索 - [ ] カテゴリフィルタリング - [ ] 複合検索API ### Phase 3: 検索最適化 - [ ] 検索インデックスの構築 - [ ] キャッシング ### Phase 5: 高度な検索 - [ ] セマンティック検索 - [ ] ファジー検索 - [ ] オートコンプリート ## 次のステップ 1. TextSearchクラスの実装 2. TagSearchクラスの実装 3. CategorySearchクラスの実装 4. CompositeSearchクラスの実装 5. APIエンドポイントの追加 6. テストの作成 [次へ: セキュリティ機能設計](./03-security-features.md)

Latest Blog Posts

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/krtw00/search-mcp'

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