Skip to main content
Glama

layout.search

Search for web layout patterns using natural language queries in English or Japanese. Filter by section types like hero, feature, or pricing, and use hybrid vision-text search to find relevant design components.

Instructions

セクションパターンを自然言語クエリでセマンティック検索します。日本語・英語の両方に対応しています。hero、feature、cta、testimonial、pricing、footer等のセクションタイプでフィルタリングできます。use_vision_search=trueでvision_embeddingを使用したハイブリッド検索(RRF: 60% vision + 40% text)が可能です。

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
queryYes検索クエリ(日本語または英語、1-500文字)
filtersNo検索フィルター
limitNo取得件数(1-50、デフォルト: 10)
offsetNoオフセット(0以上、デフォルト: 0)
include_htmlNoHTMLスニペットを含めるか(デフォルト: false)- snake_case正式形式
includeHtmlNoHTMLスニペットを含めるか(デフォルト: false)- レガシー互換、include_html推奨
include_previewNoサニタイズ済みHTMLプレビューを含めるか(デフォルト: true)
preview_max_lengthNoHTMLプレビューの最大文字数(100-1000、デフォルト: 500)
project_contextNoプロジェクトコンテキスト解析オプション。プロジェクトのデザインパターンを検出し、検索結果の適合度を評価します。
auto_detect_contextNoクエリから業界・スタイルコンテキストを自動推論し、結果をブーストします。推論されたコンテキスト(業界: technology/ecommerce/healthcare等、スタイル: minimal/bold/corporate等)にマッチする結果の類似度スコアが最大0.15ブーストされます(デフォルト: true)
use_vision_searchNoVision検索を有効化。vision_embeddingを使用したセマンティック検索を行います(デフォルト: false)
vision_search_queryNoVision検索クエリ(use_vision_search=true時に使用)
vision_search_optionsNoVision検索オプション(use_vision_search=true時に使用)
search_modeNo検索モード。text_only: text_embeddingのみを使用(デフォルト)。vision_only: vision_embeddingのみを使用。combined: 両方を使用してRRF統合検索。text_only
multimodal_optionsNoマルチモーダルオプション。search_mode='combined'時のRRF統合パラメータ。
profile_idNo嗜好プロファイルID(検索結果のリランキングに使用) / Preference profile ID (used for search result reranking)

Implementation Reference

  • LayoutSearchServiceクラスには、レイアウトセマンティック検索(ベクトル検索とハイブリッド検索)の主要なロジックが実装されています。特に `searchSectionPatterns` と `searchSectionPatternsHybrid` メソッドがコア機能を提供します。
    export class LayoutSearchService implements ILayoutSearchService {
      private embeddingService: IEmbeddingService | null = null;
      private prismaClient: IPrismaClient | null = null;
    
      /**
       * EmbeddingServiceを取得
       */
      private getEmbeddingService(): IEmbeddingService {
        if (this.embeddingService) {
          return this.embeddingService;
        }
    
        if (embeddingServiceFactory) {
          this.embeddingService = embeddingServiceFactory();
          return this.embeddingService;
        }
    
        throw new Error("EmbeddingService not initialized");
      }
    
      /**
       * PrismaClientを取得
       */
      private getPrismaClient(): IPrismaClient {
        if (this.prismaClient) {
          return this.prismaClient;
        }
    
        if (prismaClientFactory) {
          this.prismaClient = prismaClientFactory();
          return this.prismaClient;
        }
    
        throw new Error("PrismaClient not initialized");
      }
    
      /**
       * クエリテキストからEmbeddingを生成
       * EmbeddingServiceが利用できない場合はnullを返す
       */
      async generateQueryEmbedding(query: string): Promise<number[] | null> {
        if (isDevelopment()) {
          logger.info("[LayoutSearchService] Generating query embedding", {
            queryLength: query.length,
          });
        }
    
        // EmbeddingServiceが利用できない場合はnullを返す
        if (!embeddingServiceFactory) {
          if (isDevelopment()) {
            logger.warn("[LayoutSearchService] EmbeddingService not available, returning null");
          }
          return null;
        }
    
        try {
          const embeddingService = this.getEmbeddingService();
          return await embeddingService.generateEmbedding(query, "query");
        } catch (error) {
          if (isDevelopment()) {
            logger.warn("[LayoutSearchService] Embedding generation failed, returning null", {
              error: error instanceof Error ? error.message : "Unknown error",
            });
          }
          // エラー時もnullを返し、空の結果を返す
          return null;
        }
      }
    
      /**
       * セクションパターンを検索
       */
      async searchSectionPatterns(
        embedding: number[],
        options: SearchOptions
      ): Promise<SearchServiceResult | null> {
        const startTime = Date.now();
    
        if (isDevelopment()) {
          logger.info("[LayoutSearchService] Starting section pattern search", {
            embeddingDimensions: embedding.length,
            limit: options.limit,
            offset: options.offset,
            hasFilters: !!options.filters,
          });
        }
    
        // PrismaClient取得を試みる
        let prisma: IPrismaClient;
        try {
          prisma = this.getPrismaClient();
        } catch {
          // PrismaClientが利用できない場合はnullを返す
          if (isDevelopment()) {
            logger.warn("[LayoutSearchService] PrismaClient not available, returning null");
          }
          return null;
        }
    
        try {
          const { clause: filterClause, params: filterParams } = buildWhereClause(options.filters);
          const vectorString = `[${embedding.join(",")}]`;
    
          // パラメータインデックスを計算
          const vectorParamIndex = filterParams.length + 1;
          const limitParamIndex = filterParams.length + 2;
          const offsetParamIndex = filterParams.length + 3;
    
          // WHERE句を構築
          let whereClause = "";
          if (filterClause) {
            whereClause = `WHERE ${filterClause} AND se.text_embedding IS NOT NULL`;
          } else {
            whereClause = "WHERE se.text_embedding IS NOT NULL";
          }
    
          // ベクトル検索クエリ
          const query = `
            SELECT
              sp.id,
              sp.web_page_id,
              sp.section_type,
              sp.section_name,
              sp.layout_info,
              sp.visual_features,
              sp.html_snippet,
              1 - (se.text_embedding <=> $${vectorParamIndex}::vector) as similarity,
              wp.id as wp_id,
              wp.url as wp_url,
              wp.title as wp_title,
              wp.source_type as wp_source_type,
              wp.usage_scope as wp_usage_scope,
              wp.screenshot_desktop_url as wp_screenshot_desktop_url
            FROM section_patterns sp
            LEFT JOIN section_embeddings se ON se.section_pattern_id = sp.id
            INNER JOIN web_pages wp ON wp.id = sp.web_page_id
            ${whereClause}
            ORDER BY similarity DESC
            LIMIT $${limitParamIndex}
            OFFSET $${offsetParamIndex}
          `;
    
          // カウントクエリ
          const countQuery = `
            SELECT COUNT(*) as total
            FROM section_patterns sp
            LEFT JOIN section_embeddings se ON se.section_pattern_id = sp.id
            INNER JOIN web_pages wp ON wp.id = sp.web_page_id
            ${whereClause}
          `;
    
          let searchResults: VectorSearchResult[] = [];
          let total = 0;
    
          try {
            // 検索実行
            searchResults = await prisma.$queryRawUnsafe<VectorSearchResult[]>(
              query,
              ...filterParams,
              vectorString,
              options.limit,
              options.offset
            );
    
            // カウント取得
            const countResult = await prisma.$queryRawUnsafe<Array<{ total: bigint | number }>>(
              countQuery,
              ...filterParams,
              vectorString
            );
            total = Number(countResult[0]?.total || 0);
          } catch (dbError) {
            if (isDevelopment()) {
              logger.warn("[LayoutSearchService] Vector search failed, returning empty results", {
                error: dbError instanceof Error ? dbError.message : "Unknown error",
              });
            }
            // データベースエラー時は空の結果を返す
            return {
              results: [],
              total: 0,
            };
          }
    
          // 結果をマップ
          let results: SearchResult[] = searchResults.map(vectorResultToSearchResult);
    
          // visualFeaturesフィルターを適用(アプリケーション層でのフィルタリング)
          if (options.filters?.visualFeatures) {
            const visualFeaturesFilter = options.filters.visualFeatures;
            results = results.filter((r) =>
              matchesVisualFeaturesFilter(r.visualFeatures, visualFeaturesFilter)
            );
    
            if (isDevelopment()) {
              logger.debug("[LayoutSearchService] Applied visualFeatures filter", {
                originalCount: searchResults.length,
                filteredCount: results.length,
                filter: visualFeaturesFilter,
              });
            }
          }
    
          const processingTimeMs = Date.now() - startTime;
    
          if (isDevelopment()) {
            logger.info("[LayoutSearchService] Search completed", {
              resultsCount: results.length,
              total,
              processingTimeMs,
            });
          }
    
          return {
            results,
            total,
          };
        } catch (error) {
          if (isDevelopment()) {
            logger.error("[LayoutSearchService] Search error", {
              error: error instanceof Error ? error.message : "Unknown error",
            });
          }
          throw error;
        }
      }
    
      /**
       * ハイブリッド検索: ベクトル検索 + 全文検索をRRFで統合
       *
       * 両検索を並列実行し、Reciprocal Rank Fusion (60% vector + 40% fulltext) で
       * 結果をマージする。全文検索が失敗した場合はベクトル検索のみで結果を返す。
       *
       * @param queryText - 生のクエリテキスト(全文検索用)
       * @param embedding - クエリEmbedding(ベクトル検索用)
       * @param options - 検索オプション
       * @returns マージされた検索結果
       */
      async searchSectionPatternsHybrid(
        queryText: string,
        embedding: number[],
        options: SearchOptions
      ): Promise<SearchServiceResult | null> {
        const startTime = Date.now();
    
        if (isDevelopment()) {
          logger.info("[LayoutSearchService] Starting hybrid search (vector + fulltext)", {
            queryTextLength: queryText.length,
            embeddingDimensions: embedding.length,
            limit: options.limit,
            offset: options.offset,
          });
        }
    
        let prisma: IPrismaClient;
        try {
          prisma = this.getPrismaClient();
        } catch {
          if (isDevelopment()) {
            logger.warn("[LayoutSearchService] PrismaClient not available, returning null");
          }
          return null;
        }
    
        try {
          const { clause: filterClause, params: filterParams } = buildWhereClause(options.filters);
          // RRFマージ用に多めに取得(最終的にlimit/offsetで切り出す)
          const fetchLimit = Math.min(options.limit * 3, 150);
    
          // ベクトル検索関数
          const vectorSearchFn = async (): Promise<RankedItem[]> => {
            const vectorString = `[${embedding.join(",")}]`;
            const vectorParamIndex = filterParams.length + 1;
            const limitParamIndex = filterParams.length + 2;
    
            let whereClause = "";
            if (filterClause) {
              whereClause = `WHERE ${filterClause} AND se.text_embedding IS NOT NULL`;
            } else {
              whereClause = "WHERE se.text_embedding IS NOT NULL";
            }
    
            const query = `
              SELECT
                sp.id,
                sp.web_page_id,
                sp.section_type,
                sp.section_name,
                sp.layout_info,
                sp.visual_features,
                sp.html_snippet,
                1 - (se.text_embedding <=> $${vectorParamIndex}::vector) as similarity,
                wp.id as wp_id,
                wp.url as wp_url,
                wp.title as wp_title,
                wp.source_type as wp_source_type,
                wp.usage_scope as wp_usage_scope,
                wp.screenshot_desktop_url as wp_screenshot_desktop_url
              FROM section_patterns sp
              LEFT JOIN section_embeddings se ON se.section_pattern_id = sp.id
              INNER JOIN web_pages wp ON wp.id = sp.web_page_id
              ${whereClause}
              ORDER BY similarity DESC
              LIMIT $${limitParamIndex}
            `;
    
            const results = await prisma.$queryRawUnsafe<VectorSearchResult[]>(
              query,
              ...filterParams,
              vectorString,
              fetchLimit
            );
    
            return toRankedItems(results);
          };
    
          // 全文検索関数
          const fulltextSearchFn = async (): Promise<RankedItem[]> => {
            const queryParamIndex = filterParams.length + 1;
            const limitParamIndex = filterParams.length + 2;
    
            const ftConditions = buildFulltextConditions("se.search_vector", queryParamIndex);
            const ftRank = buildFulltextRankExpression("se.search_vector", queryParamIndex);
            const whereClause = filterClause
              ? `WHERE ${filterClause} AND ${ftConditions}`
              : `WHERE ${ftConditions}`;
    
            const query = `
              SELECT
                sp.id,
                sp.web_page_id,
                sp.section_type,
                sp.section_name,
                sp.layout_info,
                sp.visual_features,
                sp.html_snippet,
                ${ftRank} as similarity,
                wp.id as wp_id,
                wp.url as wp_url,
                wp.title as wp_title,
                wp.source_type as wp_source_type,
                wp.usage_scope as wp_usage_scope,
                wp.screenshot_desktop_url as wp_screenshot_desktop_url
              FROM section_patterns sp
              LEFT JOIN section_embeddings se ON se.section_pattern_id = sp.id
              INNER JOIN web_pages wp ON wp.id = sp.web_page_id
              ${whereClause}
              ORDER BY similarity DESC
              LIMIT $${limitParamIndex}
            `;
    
            try {
              const results = await prisma.$queryRawUnsafe<VectorSearchResult[]>(
                query,
                ...filterParams,
                queryText,
                fetchLimit
              );
    
              return toRankedItems(results);
            } catch (ftError) {
              // 全文検索の失敗はハイブリッド検索全体をブロックしない
              if (isDevelopment()) {
                logger.warn("[LayoutSearchService] Full-text search failed, using vector only", {
                  error: ftError instanceof Error ? ftError.message : "Unknown error",
                });
              }
              return [];
            }
          };
    
          // RRFハイブリッド検索を実行(両検索を並列実行)
          const hybridResults = await executeHybridSearch(vectorSearchFn, fulltextSearchFn);
    
          // offset/limitを適用
          const paginatedResults = hybridResults.slice(options.offset, options.offset + options.limit);
    
          // HybridSearchResult を SearchResult に変換
          // mergeWithRRF が data から id を除去するため、hr.id で復元する
          // mergeWithRRF strips id from data, so restore it from hr.id
          const results: SearchResult[] = paginatedResults.map((hr) => {
            const data = hr.data as unknown as VectorSearchResult;
            data.id = hr.id;
            const converted = vectorResultToSearchResult(data);
            converted.similarity = hr.similarity;
            return converted;
          });
    
          // visualFeaturesフィルターを適用
          let filteredResults = results;
          if (options.filters?.visualFeatures) {
            const visualFeaturesFilter = options.filters.visualFeatures;
            filteredResults = results.filter((r) =>
              matchesVisualFeaturesFilter(r.visualFeatures, visualFeaturesFilter)
            );
          }
    
          const processingTimeMs = Date.now() - startTime;
    
          if (isDevelopment()) {
            logger.info("[LayoutSearchService] Hybrid search completed", {
              totalMerged: hybridResults.length,
              resultsCount: filteredResults.length,
              processingTimeMs,
            });
          }
    
          return {
            results: filteredResults,
            total: hybridResults.length,
          };
        } catch (error) {
          if (isDevelopment()) {
            logger.error("[LayoutSearchService] Hybrid search error, falling back to vector only", {
              error: error instanceof Error ? error.message : "Unknown error",
            });
          }
          // フォールバック: ベクトル検索のみ
          return this.searchSectionPatterns(embedding, options);
        }
      }
    }
  • `toolHandlers` オブジェクトにより、"layout.search" が `layoutSearchHandler` にマッピングされ、MCPツールとして登録されています。
    export const toolHandlers: Record<string, (input: unknown) => Promise<unknown>> = {
      // style.get_palette(ブランドパレット取得)
      "style.get_palette": styleGetPaletteHandler,
      // system.health(MCPサーバーヘルスチェック)
      "system.health": systemHealthHandler,
      // layout.inspect(Phase 2-4 Webページレイアウト解析)
      "layout.inspect": layoutInspectHandler,
      // layout.ingest(Phase 2-1 Webページインジェスト)
      "layout.ingest": layoutIngestHandler,
      // layout.search(Phase 2-5 レイアウトセマンティック検索)
      "layout.search": layoutSearchHandler,
      // layout.generate_code(Phase 2-6 レイアウトコード生成)
      // v0.1.0: layout.to_code から layout.generate_code にリネーム
      "layout.generate_code": layoutGenerateCodeHandler,
      // layout.batch_ingest(Phase 2-7 バッチインジェスト)
      "layout.batch_ingest": layoutBatchIngestHandler,
      // quality.evaluate(Phase 3-3 品質評価)
      "quality.evaluate": qualityEvaluateHandler,
      // quality.batch_evaluate(Phase 3-5 一括品質評価)
      "quality.batch_evaluate": batchQualityEvaluateHandler,
      // quality.getJobStatus(Phase 3-5 一括評価ジョブステータス確認)
      "quality.getJobStatus": qualityGetJobStatusHandler,
      // motion.detect(Phase 3-6 モーション検出)
      "motion.detect": motionDetectHandler,
      // motion.search(Phase 3-6 モーション検索)
      "motion.search": motionSearchHandler,
      // brief.validate(Phase 4-3 Design Brief Validation)
      "brief.validate": briefValidateHandler,
      // project.get(Studio プロジェクト取得)
      "project.get": projectGetHandler,
      // project.list(Studio プロジェクト一覧)
      "project.list": projectListHandler,
      // page.analyze(統合Web分析)
      "page.analyze": pageAnalyzeHandler,
      // page.getJobStatus(非同期ジョブステータス確認 Phase3-2)
      "page.getJobStatus": pageGetJobStatusHandler,
      // narrative.search(世界観・レイアウト構成セマンティック検索)
      "narrative.search": narrativeSearchHandler,
      // background.search(BackgroundDesignセマンティック検索)
      "background.search": backgroundSearchHandler,
      // responsive.search(レスポンシブ分析セマンティック検索)
      "responsive.search": responsiveSearchHandler,
      // preference.hear(ユーザー嗜好ヒアリングセッション)
      "preference.hear": preferenceHearHandler,
      // preference.get(プロファイル取得)
      "preference.get": preferenceGetHandler,
      // preference.reset(プロファイルリセット)
      "preference.reset": preferenceResetHandler,
      // part.search(パーツセマンティック検索)
      "part.search": partSearchHandler,
      // part.inspect(パーツ詳細情報取得)
      "part.inspect": partInspectHandler,
      // part.compare(パーツ並列比較)
      "part.compare": partCompareHandler,
    };
  • `applyPreferenceReranking` ヘルパーは、検索結果に対してユーザーの嗜好プロファイルに基づいたリランキングを適用するために、`layout.search` ツール内で呼び出される共通ユーティリティです。
    /**
     * 検索結果にpreference rerankingを適用する統合ヘルパー
     * Integrated helper to apply preference reranking to search results
     *
     * @param results - 検索結果配列 / Search result array
     * @param profileId - プロファイルID(undefinedの場合はスキップ) / Profile ID (skip if undefined)
     * @param prismaFactory - PrismaClientファクトリ / PrismaClient factory
     * @param domain - embeddingドメイン / Embedding domain
     * @param toolName - ログ用ツール名 / Tool name for logging
     * @returns リランキング済み結果(または元の結果) / Reranked results (or original)
     */
    export async function applyPreferenceReranking<T extends { id: string; similarity: number }>(
      results: T[],
      profileId: string | undefined,
      prismaFactory: (() => IPrismaClient) | null,
      domain: EmbeddingDomain,
      toolName: string
    ): Promise<T[]> {
      if (!profileId || results.length === 0 || !prismaFactory) {
        return results;
      }
    
      try {
        const prisma = prismaFactory();
        const rerankableItems: RerankableItem[] = results.map((item) => ({
          ...item,
          id: item.id,
          similarity: item.similarity,
        }));
        const rerankResult = await rerankWithPreference(rerankableItems, profileId, prisma, { domain });
    
        if (rerankResult.reranked) {
          logger.info(`[MCP Tool] ${toolName} preference reranking applied`, {
            profileId: truncateId(profileId),
            resultCount: results.length,
          });
          return rerankResult.items as T[];
        }
    
        logger.info(`[MCP Tool] ${toolName} preference reranking skipped`, {
          profileId: truncateId(profileId),
          reason: rerankResult.reason,
        });
        return results;
      } catch (error) {
        logger.warn(`[MCP Tool] ${toolName} preference reranking failed`, {
          profileId: truncateId(profileId),
          error: error instanceof Error ? error.message : String(error),
        });
        return results;
      }
    }

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