Skip to main content
Glama

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
NameRequiredDescriptionDefault
queryNo検索クエリ(queryまたはembeddingのいずれか必須)
embeddingNo直接Embedding指定(768次元、queryまたはembeddingのいずれか必須)
filtersNo
optionsNo
profile_idNo嗜好プロファイル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: "内部エラーが発生しました",
          },
        };
      }
    }
  • 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)",
          },
        },
      },
    };

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/TKMD/reftrix-mcp'

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