narrative.search
Search web design layouts and worldviews using natural language queries or embeddings. Find designs matching specific moods, styles, and semantic concepts through hybrid vector and full-text search.
Instructions
世界観・レイアウト構成でセマンティック検索します。自然言語クエリ(例: "サイバーセキュリティ感のあるダークなデザイン")または768次元Embeddingで検索可能。Hybrid Search(Vector + Full-text)でRRF統合。
Input Schema
TableJSON Schema
| Name | Required | Description | Default |
|---|---|---|---|
| query | No | 検索クエリ(queryまたはembeddingのいずれか必須) | |
| embedding | No | 直接Embedding指定(768次元、queryまたはembeddingのいずれか必須) | |
| filters | No | ||
| options | No | ||
| profile_id | No | 嗜好プロファイルID(検索結果のリランキングに使用) / Preference profile ID (used for search result reranking) |
Implementation Reference
- The core handler function for 'narrative.search', which executes semantic search, applies filters, performs preference-based reranking, and returns the formatted response.
export async function narrativeSearchHandler(input: unknown): Promise<NarrativeSearchOutput> { const startTime = Date.now(); if (isDevelopment()) { logger.info("[narrative.search] Handler called", { inputType: typeof input, }); } try { // 1. 入力バリデーション const validatedInput = narrativeSearchInputSchema.parse(input); if (isDevelopment()) { logger.info("[narrative.search] Input validated", { hasQuery: !!validatedInput.query, hasEmbedding: !!validatedInput.embedding, filters: validatedInput.filters, options: validatedInput.options, }); } // 2. サービス取得 const narrativeService = await getNarrativeAnalysisService(); // 3. クエリ文字列の決定 let queryText: string; if (validatedInput.query) { queryText = validatedInput.query; // Embedding生成(サービス内で行う場合はスキップ) // NOTE: サービスのsearch()がクエリ文字列を受け取る想定 } else if (validatedInput.embedding) { queryText = "[embedding]"; // NOTE: embedding直接検索はNarrativeAnalysisService.searchWithEmbeddingで実装予定 // 現在はサービスがクエリ文字列からembeddingを生成する設計 } else { // バリデーションでチェック済みなのでここには来ないはず throw new Error("queryまたはembeddingが必要です"); } // 4. 検索オプションの準備 // フィルターオブジェクトを構築(exactOptionalPropertyTypes対応) const baseSearchOptions: ServiceSearchOptions = { query: queryText, limit: validatedInput.options?.limit ?? 10, vectorWeight: validatedInput.options?.vectorWeight ?? 0.6, fulltextWeight: validatedInput.options?.fulltextWeight ?? 0.4, }; // フィルターを条件付きで追加 if (validatedInput.filters) { const moodCategory = validatedInput.filters.moodCategory; const minConfidence = validatedInput.filters.minConfidence; const filtersObj: NonNullable<ServiceSearchOptions["filters"]> = {}; if (moodCategory !== undefined) { filtersObj.moodCategory = [moodCategory]; } if (minConfidence !== undefined) { filtersObj.minConfidence = minConfidence; } // フィールドが存在する場合のみ設定 if (Object.keys(filtersObj).length > 0) { baseSearchOptions.filters = filtersObj; } } const searchOptions = baseSearchOptions; // 5. 検索実行(searchHybridが利用可能な場合はHybrid Search、なければvector-only) const searchMode = validatedInput.options?.searchMode ?? "hybrid"; let results: NarrativeSearchResult[]; if (searchMode === "hybrid" && narrativeService.searchHybrid != null) { results = await narrativeService.searchHybrid(searchOptions); } else { results = await narrativeService.search(searchOptions); } // 6. 最小類似度フィルター適用 const minSimilarity = validatedInput.options?.minSimilarity ?? 0.6; let filteredResults = results.filter((r) => r.score >= minSimilarity); // 6.5. 嗜好プロファイルによるリランキング / Preference profile reranking // narrative結果はscoreフィールドを使用するため、similarity にマッピングして戻す // Narrative results use 'score' field, so map to 'similarity' and back const narrativeItems = filteredResults.map((r) => ({ ...r, similarity: r.score })); filteredResults = ( await applyPreferenceReranking( narrativeItems, validatedInput.profile_id, prismaClientFactory, "narrative", "narrative.search" ) ).map((r) => ({ ...r, score: r.similarity })) as NarrativeSearchResult[]; const searchTimeMs = Date.now() - startTime; // 7. レスポンス生成 const data = convertSearchResultsToMcpResponse( filteredResults, queryText, searchMode, searchTimeMs ); if (isDevelopment()) { logger.info("[narrative.search] Search completed", { query: queryText.substring(0, 50), totalResults: data.searchInfo.totalResults, searchTimeMs, searchMode, }); } return { success: true, data, }; } catch (error) { // エラーハンドリング if (error instanceof ZodError) { const details = error.errors.map((e) => ({ path: e.path.join("."), message: e.message, })); if (isDevelopment()) { logger.warn("[narrative.search] Validation error", { details }); } return { success: false, error: { code: NARRATIVE_MCP_ERROR_CODES.VALIDATION_ERROR, message: "バリデーションエラー", }, }; } // 特定エラータイプのハンドリング if (error instanceof Error) { const errorCode = mapErrorToCode(error); if (isDevelopment()) { logger.error("[narrative.search] Error", { code: errorCode, message: error.message, stack: error.stack, }); } return { success: false, error: { code: errorCode, message: error.message, }, }; } // 未知のエラー logger.error("[narrative.search] Unknown error", { error }); return { success: false, error: { code: NARRATIVE_MCP_ERROR_CODES.INTERNAL_ERROR, message: "内部エラーが発生しました", }, }; } } - apps/mcp-server/src/tools/index.ts:631-631 (registration)Tool registration map linking 'narrative.search' to its handler.
"narrative.search": narrativeSearchHandler, - The tool definition schema for 'narrative.search', defining the expected inputs and tool metadata.
export const narrativeSearchToolDefinition = { name: "narrative.search", description: "世界観・レイアウト構成でセマンティック検索します。" + '自然言語クエリ(例: "サイバーセキュリティ感のあるダークなデザイン")または768次元Embeddingで検索可能。' + "Hybrid Search(Vector + Full-text)でRRF統合。", inputSchema: { type: "object", properties: { query: { type: "string", description: "検索クエリ(queryまたはembeddingのいずれか必須)", minLength: 1, maxLength: 500, }, embedding: { type: "array", items: { type: "number" }, description: "直接Embedding指定(768次元、queryまたはembeddingのいずれか必須)", minItems: 768, maxItems: 768, }, filters: { type: "object", properties: { moodCategory: { type: "string", enum: [ "professional", "playful", "premium", "tech", "organic", "minimal", "bold", "elegant", "friendly", "artistic", "trustworthy", "energetic", ], description: "ムードカテゴリでフィルター", }, minConfidence: { type: "number", minimum: 0, maximum: 1, description: "最小信頼度フィルター(0-1)", }, }, }, options: { type: "object", properties: { limit: { type: "number", default: 10, minimum: 1, maximum: 50, description: "結果数", }, minSimilarity: { type: "number", default: 0.6, minimum: 0, maximum: 1, description: "最小類似度", }, searchMode: { type: "string", enum: ["vector", "hybrid"], default: "hybrid", description: "検索モード", }, vectorWeight: { type: "number", default: 0.6, minimum: 0, maximum: 1, description: "Vector検索の重み(hybridモード時)", }, fulltextWeight: { type: "number", default: 0.4, minimum: 0, maximum: 1, description: "Full-text検索の重み(hybridモード時)", }, }, }, // Preference reranking profile_id: { type: "string", format: "uuid", description: "嗜好プロファイルID(検索結果のリランキングに使用) / Preference profile ID (used for search result reranking)", }, }, }, };