Skip to content

検索API v1

  • 利用条件: Pro プラン以上
  • 想定読者: ストアフロントを実装されるエンドエンジニアの方

まるっと検索は通常、検索結果ページの HTML をアプリ側で描画します。検索APIでは アプリは一切描画せず、検索結果の JSON だけをテーマに渡します

  • 既存テーマのデザイン・レイアウトには一切触れません
  • 日本語の形態素解析・表記ゆれ吸収・同義語・絞り込みのロジックだけを利用できます
  • 描画は御社のコードで行っていただきます

1. 検索の実行

まるっと検索は JSON を返すだけです。呼び出し・状態管理・描画はすべて御社の実装になります。 こちらから JavaScript を読み込ませることはしません。

GET /apps/marutto-search/search?type=search&contract=v1&q=ワンピース&limit=24&offset=0
  • ストア自身のドメイン上の URL です(Shopify App Proxy)。API キーも CORS 設定も不要で、 ショップの特定は Shopify の署名により自動で行われます
  • ブラウザからは、そのストアのページからのみ読み取れます。CORS ヘッダを付けていないため、 他ドメインのページからは応答を読めません
  • ただしサーバ側からの呼び出し(curl 等)は、どこからでも到達します。URL を知っていれば 誰でも叩けるため、秘密を返すエンドポイントとしては設計されていません(返るのは公開カタログの 情報で、Shopify 自身も同じストアで /products.json として公開しています)
  • contract=v1 が本仕様を要求する印です。これを付けたリクエストにだけ、本ドキュメントの 形状が保証されます。付けない場合はアプリ内部のレスポンスが返り、そちらは予告なく変わります (まるっと検索のウィジェットが読むための形で、仕様ではありません)
  • contract=v1Pro プラン以上で利用できます。プランが足りない場合は 403{"code": "plan_required"} を返します。検索が壊れたのか契約が切れたのかを 御社のコードで区別できるようにするためです
  • v1 が定めるのは確定検索(type=search)のレスポンスのみです。suggest(入力補完)に contract=v1 を付けた場合は 400{"code": "unsupported_contract_type"} を返します。 仕様として書いていない形を、契約として返すことはしません

クエリパラメータ

パラメータ既定内容
contractv1必須(本仕様の形状を要求する印)
typesearch(確定検索)。必須contract=v1search のみ対応
q検索語。必須(全件ブラウズは *
session_idセッション識別子。必須7章
limit8取得件数。上限 50
offset0開始位置。上限 2,000(超えた値は 2,000 に丸められます)
sortrelevancerelevance / price_asc / price_desc / newest
localeストア既定ja / en など。多言語ストアで解析言語を切り替える
filter_vendorベンダー名
filter_type商品タイプ
filter_tagsタグ(複数はカンマ区切り。AND 条件)
filter_price_min / filter_price_max価格帯
filter_availabletrue / false(在庫あり)
filter_on_saletrue / false(セール中)
filter_content_typeproduct / article / page
filter_collectionコレクションのハンドル
filter_option_name / filter_option_valueバリエーション(例: カラー / ベージュ
filter_metafieldメタフィールド(namespace.key:値)。繰り返し指定可

メタフィールドによる絞り込みfilter_metafield繰り返し指定します。 値の形式は namespace.key:値 で、最初の : で分割されます(キーに : は含まれないため、 値に : があってもそのまま扱えます)。項目をまたいだ指定は AND になります。

&filter_metafield=custom.material:リネン&filter_metafield=custom.origin:日本

対象になるのは、管理画面で「絞り込みに使う」を有効にしたメタフィールドのみです (テキスト型・リスト型、最大10個)。

絞り込みが効かない指定について。 管理画面で「絞り込みに使う」を有効にしていない メタフィールドを filter_metafield に渡した場合、その指定はエラーにならず無視されます (全件が返ります)。設定を切り替えたあとも古い URL が共有され続けるため、残ったまま解除できない 状態を避ける仕様です。開発中にキー名を打ち間違えると全件が返るので、まずファセットに出ている name と一致しているかご確認ください。

全件ブラウズ(q=*)でできないこと。 ブラウズは商品テーブルだけを読む別経路です。 通常の検索語では動く次の3点が、q=* のときだけ変わります。

指定q=* のときの挙動
filter_content_type=article / page常に0件(記事・ページはブラウズに含まれません)
facets.tags / content_types / variant_options常に [](キーは存在します)
上記以外のフィルタ・filter_metafield検索語のときと同じく効きます

記事・ページを一覧したい場合は検索語を伴う type=search をご利用ください。

エラーの返り方

状況ステータスcode
q が無い400query_required
q が200文字を超える400query_too_long
typesearch / suggest / preview 以外400invalid_type
contractv1 以外400unsupported_contract
contract=v1type=search 以外に付けた400unsupported_contract_type
プランが Pro 未満403plan_required
検索基盤の障害503本文は通常の形(degraded: true

リクエストの誤り(4xx)は { error, code } だけを返します。 空の検索結果として返すと、 total: 0 に見えて原因が分からなくなるためです。code で分岐してください。

検索基盤側の問題(503)は通常のレスポンス形で返します。障害の上に data.results で例外まで起こさないためです。

参照実装

そのまま動く最小構成を search-api-sample.js に 置いています。描画は御社のデザインに書き換えていただく前提ですが、書き換えないでいただきたい 箇所が3つあります。

  1. contract=v1 を必ず付ける(これが本仕様の形を要求する印)
  2. degraded を見る(新規ストアは導入初日に必ずこの経路を通ります)
  3. Shopify.analytics.publish の2つ(実装しないと検索経由売上が計上されません)

呼び出し側でお願いしたいこと

まるっと検索側の JS が無いため、以下は御社の実装に委ねられます。

  • 入力補完のデバウンス(推奨 250〜300ms)。1打鍵ごとに type=suggest を送ると レート制限に達します
  • 応答の順序保証。高速に入力すると応答が前後します。レスポンスの query / offset / limit / sort はリクエスト値のエコーバックなので、古い応答を破棄する 判定に使えます
  • 同一条件の連続リクエストの抑制

2. ページング

レスポンスは自己完結しており、次に何を要求すればよいかがレスポンス自身から分かります

フィールド用途
total全ヒット件数
offsetこのレスポンスが何件目から始まっているか
limitこのレスポンスの1ページあたり件数
js
// 「もっと読む」/無限スクロール
const MAX_OFFSET = 2000; // 1章のとおり、これを超える指定は 2000 に丸められます
const next = data.offset + data.results.length;
if (next < data.total && next < MAX_OFFSET) {
  // offset=next で再取得し、既存リストに追加する
}

next < data.total だけで判定しないでください。 offset の上限に達したあとも サーバは同じページを返すため、total がそれより大きいと「もっと読む」が消えず、 同じ商品が何度も追加され続けます。上限との比較を必ず入れてください。

ページ番号ナビ・「もっと読む」・無限スクロールのいずれも、offsetlimit の指定だけで 実現できます。

注意: offset 方式のため、ページを送っている最中に商品の追加・削除・在庫変動で インデックスが更新されると、同じ商品が2回出る/1件飛ばされることがあります。 無限スクロールで顕在化しやすい挙動です。厳密な一貫性が必要な場合はご相談ください。

3. レート制限

不正利用対策のため、10秒あたりのリクエスト数に上限があります。超過時は HTTP 429 を返します。

種別ショップ単位訪問者単位全ストア合計
type=search50 / 10秒20 / 10秒125 / 10秒
type=suggest150 / 10秒40 / 10秒150 / 10秒

通常の買い物客の操作でこの上限に当たることはありませんが、デバウンスなしで入力補完を 呼ぶと訪問者単位の上限に容易に達します。429 を受けた場合は、時間をおいて再試行するか、 前回の結果を表示したままにする実装を推奨します。

「全ストア合計」について。 まるっと検索は単一のインスタンスで全ストアを処理して いるため、ショップ単位の上限内であっても、同時刻の他ストアの負荷と合わさって合計上限に 達すると 429 になることがあります。自社のリクエスト数だけからは予測できない 429 が あり得るということです(実運用でこの合計に達したことはありませんが、仕様として明記して おきます)。負荷試験など短時間に大量のリクエストを送る予定がある場合は、事前にご連絡ください。

4. レスポンスの保証

トップレベルのキーは常に存在します

結果が0件でも、内部でエラーが起きていても、results / facets / total などのキーが 省略されることはありません(それぞれ [] / {} / 0 になります)。 data.facets.tags.forEach(...) のような記述が、状況によって例外になることはありません。

一時的に検索が縮退することがあります

インストール直後の初回同期中、多言語ストアの翻訳同期中など、まるっと検索の索引が まだ使えない期間があります。この間は degraded が立ち、結果が空または Shopify 標準検索の 結果になります。

json
{
  "schemaVersion": "v1",
  "degraded": true,
  "degraded_reason": "sync_pending",
  "total": 0,
  "results": [],
  "facets": {}
}
js
if (data.degraded) {
  // 例: 「検索を準備しています」を出す、Shopify標準の検索にフォールバックする 等
}

導入初日に必ず通る経路です。 新規ストアはインストール後しばらく sync_pending を 返すため、degraded を見ていない実装は「入れた直後は動かない」ように見えます。 必ずご対応ください。

degraded_reason は原因の識別用で、値の集合は将来増えます。分岐条件には degraded (真偽値)をお使いいただき、degraded_reason は表示やログの用途に留めてください。

5. レスポンス例

検索語「ワンピース」でアパレルストアを検索した場合の例です。値はすべてサンプルです。

json
{
  "schemaVersion": "v1",
  "degraded": false,
  "degraded_reason": null,
  "query": "ワンピース",
  "query_id": "q_8f3c1ad24b7e",
  "measurement_proof": "mp_5c19e0b7a3d84f26",
  "total": 137,
  "limit": 24,
  "offset": 0,
  "sort": "relevance",
  "redirect_url": null,

  "results": [
    {
      "content_type": "product",
      "product_id": "gid://shopify/Product/8123456789",
      "title": "リネンブレンド ロングワンピース",
      "handle": "linen-blend-long-dress",
      "url": "/products/linen-blend-long-dress",
      "image_url": "https://cdn.shopify.com/s/files/1/0001/0002/products/linen-dress.jpg",
      "price_min": 12800,
      "price_max": 12800,
      "compare_at_price": 16000,
      "available": true,
      "tags": ["春夏", "洗える", "新作"],

      "variants": [
        {
          "id": "44001",
          "title": "ベージュ / M",
          "image_url": "https://cdn.shopify.com/s/files/1/0001/0002/products/linen-dress-beige.jpg",
          "price": 12800,
          "compare_at_price": 16000,
          "available": true,
          "options": [
            { "name": "カラー", "value": "ベージュ" },
            { "name": "サイズ", "value": "M" }
          ]
        },
        {
          "id": "44002",
          "title": "ベージュ / L",
          "price": 12800,
          "available": false,
          "options": [
            { "name": "カラー", "value": "ベージュ" },
            { "name": "サイズ", "value": "L" }
          ]
        }
      ]
    },
    {
      "content_type": "article",
      "product_id": "gid://shopify/Article/5500112233",
      "title": "ワンピースの着回しコーデ 12選",
      "handle": "one-piece-coordinate",
      "url": "/blogs/journal/one-piece-coordinate",
      "image_url": "https://cdn.shopify.com/s/files/1/0001/0002/articles/coordinate.jpg",
      "tags": ["コーディネート"],
      "excerpt": "一枚で決まるワンピースを、季節ごとに着回すコツをご紹介します。",
      "published_at": "2026-04-18T09:00:00Z",
      "author": "スタッフ 田中",
      "blog_handle": "journal"
    }
  ],

  "facets": {
    "vendors": [
      { "value": "ATELIER NOA", "count": 42 },
      { "value": "LUMIÈRE", "count": 31 }
    ],
    "product_types": [{ "value": "ワンピース", "count": 118 }],
    "tags": [
      { "value": "春夏", "count": 64 },
      { "value": "洗える", "count": 38 }
    ],
    "content_types": [
      { "value": "product", "count": 131 },
      { "value": "article", "count": 5 }
    ],
    "variant_options": [
      {
        "name": "カラー",
        "values": [
          { "value": "ベージュ", "count": 57 },
          { "value": "ブラック", "count": 49 }
        ]
      }
    ],
    "metafields": [
      {
        "name": "custom.material",
        "label": "素材",
        "values": [{ "value": "リネン", "count": 42 }]
      }
    ],
    "price_range": [3900, 42000],
    "on_sale_count": 18
  },

  "suggestions": [],
  "related_keywords": ["ワンピース 春", "ロングワンピース", "シャツワンピース"]
}

6. フィールド定義

トップレベル

フィールド内容
schemaVersionstringデータ構造のバージョン。初版は "v1"
degradedboolean索引が一時的に使えない状態か(→ 4章)。常に存在します
degraded_reasonstring | null縮退の原因。degradedfalse のときは null(キーは常に存在)
querystring検索語(入力そのまま)
query_idstringこの検索の識別子。クリックをこれで紐付けます(→ 7章)
measurement_proofstring売上紐付け用の署名。検索経由売上の計測に必須(→ 7章)
totalnumberヒット総件数(ページングされる前の全件)
limitnumberこのレスポンスの1ページあたり件数
offsetnumberこのレスポンスが何件目から始まっているか
sortstringrelevance / price_asc / price_desc / newest
redirect_urlstring | null管理画面で設定したキーワードリダイレクトの遷移先。値がある場合は結果を描画せず遷移する想定
resultsarray検索結果(商品・記事・ページが混在。content_type で判別)
facetsobject絞り込み用の集計値(下記)
suggestionsstring[]0件時の候補語。該当なしは空配列(キーは常に存在)
related_keywordsstring[]他の購入者が続けて検索した語。該当なしは空配列(キーは常に存在)

ここに載っていないフィールドは返りません。 内部のスコアやレビュー照合の詳細は、 改善のたびに変わるため公開対象から外しています。results は並び替え済みで届きます。

facets

フィールド内容
vendorsarrayベンダー別の件数。常に存在(該当なしは []
product_typesarray商品タイプ別。常に存在
tagsarrayタグ別。常に存在
content_typesarray種別(product / article / page)別。常に存在
variant_optionsarrayバリエーション軸別(カラー・サイズ等)。常に存在
metafieldsarrayメタフィールド別。常に存在
price_rangenumber[] | null価格の下限・上限。該当なしは null[0, 0] は返しません)
on_sale_countnumberセール中の件数。該当なしは 0

facets.metafields[]{ name, label?, values[] } です。namenamespace.keylabel は管理画面で設定した見出しで、未設定なら省略されます。表示には label ?? name をお使いください。

並び順は件数の降順、同数のときは値の昇順です。同じ検索・同じカタログなら常に同じ順で 返ります。各グループは上位20件までです。

results[](共通)

フィールド内容
content_typestringproduct / article / page
product_idstringShopify の GID。種別を問わずこのキー名です(歴史的経緯)
titlestringタイトル
handlestringハンドル
urlstringストア内の相対 URL
image_urlstring?画像 URL(無い場合あり)
tagsstring[]Shopify のタグ。付いていない場合は空配列(キーは常に存在)

results[]content_type: "product" のみ)

フィールド内容
price_min / price_maxnumber価格帯(下記)
compare_at_pricenumber | null参考価格(セール表示用)
availableboolean購入可能な在庫があるか
variants[]array?バリエーション。id数値の ID(下記)

価格は「ショップの基準通貨」です。 税抜/税込は Shopify の設定に準拠します。

Shopify Markets で多通貨展開されている場合、買い物客が実際に見ている通貨とは 一致しません。表示通貨に合わせる必要がある場合は、御社側で換算していただくか、 価格の描画のみ Shopify の Storefront API から取得してください。 多通貨対応のご要望があればお知らせください。

variants[].id は数値の ID です。 商品の product_idgid://shopify/Product/… 形式なのに対し、バリエーションの ID は "44001" のような数値文字列を返します。これは Shopify 自身がこの形を要求するためです。

  • 商品ページでバリエーションを選ばせる: /products/xxx?variant=44001
  • カートに入れる(Cart AJAX API): { "id": 44001, "quantity": 1 }

どちらも GID を渡すとエラーにならずに無視されます(選択されないまま商品ページが 開きます)。GID が必要な場合は "gid://shopify/ProductVariant/" + variant.id で 組み立ててください。

results[]content_type: "article" / "page" のみ)

フィールド内容
excerptstring?本文の抜粋
published_atstring?公開日時(ISO 8601)
authorstring?著者(記事のみ)
blog_handlestring?所属ブログのハンドル(記事のみ)

7. 計測

まるっと検索が商品カードを描画しないため、何も実装しないと管理画面の検索分析が機能しません。 影響するのはクリック率・クリックされた商品ランキング・検索経由売上です。

計測は Shopify の Web Pixel 経由で行うため、御社の実装ではイベントを publish していただく だけです(HTTP リクエストはまるっと検索の Pixel が行います)。

検索リクエストに session_id を付けてください(必須)

このエンドポイントは URL を知っていれば誰でも到達できるため、session_id の無い リクエストは買い物客の検索ではないと判断し、検索分析に計上しません(検索結果は通常どおり 返ります)。付け忘れると、検索は動くのに管理画面の分析が空のままになります。

session_id は御社で発番していただく任意の文字列です。同一セッション内で同じ値であれば 形式は問いません。

1) 検索したとき

js
Shopify.analytics.publish("marutto_search", {
  query: data.query,
  query_id: data.query_id,
  session_id: sessionId, // 検索リクエストに付けたものと同じ値
  measurement_proof: data.measurement_proof,
  result_count: data.total,
});

2) 検索結果をクリックしたとき

js
Shopify.analytics.publish("marutto_result_clicked", {
  query_id: data.query_id,
  session_id: sessionId,
  measurement_proof: data.measurement_proof,
  product_id: item.product_id,
  position: index, // 0 始まり
});

measurement_proof について

検索経由売上の紐付けはこの値で行われます。 まるっと検索の Web Pixel が marutto_search 受信時にこれを保存し、その後の「カート追加」「購入完了」に自動で付け直す ことで、どの検索が売上につながったかを追跡します。

この値を publish していただかないと、そのストアの検索経由売上は計上されません。 検索は正常に動くため気づきにくい箇所なので、実装時にご確認ください。

8. バージョン運用

  • v1 として公開したフィールド名・型・意味は変更しません。イベント名も同様です
  • フィールドの追加は v1 のまま行います(既存実装は影響を受けません)
  • 変更が必要な場合は v2 として新規に提供し、v1 は維持します
  • 廃止する場合は事前に告知期間を設けます

9. 利用できる範囲

検索API は検索結果ページ専用ではありません。App Proxy 経由の同一オリジン通信のため、 ストア内のどのページからでも利用できます。

  • 検索結果ページ
  • コレクションページの絞り込み
  • ヘッダーのサジェスト(入力補完)
  • 商品ページの関連商品
  • 特集ページ・LP への「このキーワードの商品一覧」の埋め込み

一方、ストア外(独自ドメインの Hydrogen、ネイティブアプリ等)からの利用は App Proxy の 対象外です。現在は提供していません。ご要望があればお知らせください。

まるっと検索