Article

商品フィード データとは?初心者にもわかりやすく解説【2025年版】

高田晃太郎
商品フィード データとは?初心者にもわかりやすく解説【2025年版】

主要な集客チャネル(Google Merchant Center、Meta Shop、Criteo、Yahoo!広告など)は、配信審査や入札最適化の多くを商品フィードに依存する¹²³⁴⁵。仕様不一致や属性欠落は即座に掲載保留・拒否につながり¹、在庫同期の遅延はコンバージョン損失を生む。2025年も仕様更新は継続し、構造化データや配送・返品属性がより厳格化する方向にある⁶⁷。本稿では、商品フィードの技術仕様からスケーラブルな実装、検証・監視、パフォーマンス最適化、さらにROIの観点まで、CTO/技術リーダーが導入・刷新を判断できる水準で整理する。

商品フィードの定義と課題整理

商品フィードとは何か(2025年版の位置づけ)

商品フィードは、EC事業者の商品カタログを広告・販売チャネルへ機械可読な形式で提供するデータセットである。代表的な形式はXML(Google Merchant Centerが採用)、CSV/TSV(広告プラットフォームのインジェストで一般的)、JSON(API/GraphQL連携や内部中間表現で増加)で、スキーマはチャネルごとに差分がある²。2025年は以下の傾向が明確だ。

  • より厳格な在庫・価格の鮮度要件(ランディングページとの整合性チェック強化)¹²
  • 返品・配送・税の属性の詳細化(構造化データでの明示が推奨)⁷
  • モバイル先行での画像品質・アスペクト比の明確な基準化(最小サイズ・推奨サイズの順守)⁸

代表スキーマ(Google Merchant Center)²

属性必須制約/例
idstring必須一意・安定ID(再利用不可)¹²
titlestring必須150文字以内推奨²
descriptionstring推奨5000文字以内²
linkurl必須http/httpsを含むURL(https推奨)²
image_linkurl必須URL長4096文字以内、画像は非アパレル100×100px以上・アパレル250×250px以上、推奨800×800px以上⁸
pricestring必須"1234 JPY" のように数値+通貨コード²
availabilityenum必須in_stock / out_of_stock / preorder⁹
brandstring推奨/一部必須自社ブランド時も明記²
gtin / mpnstringカテゴリにより必須GTINは8/12/13/14桁¹⁰
conditionenum必須new / refurbished / used¹¹
shipping / tax複合地域により必須国・地域、金額、税率の明示²

実運用ではチャネル差分(例:Metaの"availability"表現、Criteoのカスタムラベルなど)を中間モデルで吸収し、多出力へマッピングするのが定石である³⁴。

実装パターンと完全なコード例

前提条件と環境

  • Node.js 20.x / TypeScript 5.x / Next.js 14
  • Python 3.11(検証・バリデーション)
  • BigQuery(ETL・正規化)
  • ストレージ(GCSまたはS3)、CDN(Cloud CDN/CloudFront)

1) Node.jsでXMLフィードをストリーム生成(高速・低メモリ)

数十万SKUを前提に、ストリームでXMLを出力する完全実装例。エラーハンドリングと計測を含む。

// feed-generator.ts
import { Readable } from 'node:stream';
import { createWriteStream } from 'node:fs';
import { performance } from 'node:perf_hooks';
import { XMLBuilder } from 'fast-xml-parser';

interface Product {
  id: string; title: string; description?: string; link: string;
  image_link: string; price: number; currency: string; availability: 'in_stock'|'out_of_stock'|'preorder';
  brand?: string; gtin?: string; mpn?: string; condition: 'new'|'refurbished'|'used';
}

function* chunk<T>(arr: T[], size: number): Generator<T[]> {
  for (let i = 0; i < arr.length; i += size) yield arr.slice(i, i + size);
}

export async function generateXml(products: Product[], outPath: string) {
  const builder = new XMLBuilder({ ignoreAttributes: false, format: true, suppressEmptyNode: true });
  const ws = createWriteStream(outPath, { encoding: 'utf-8' });
  const start = performance.now();
  let written = 0;
  try {
    ws.write('<?xml version="1.0" encoding="UTF-8"?>\n<rss version="2.0" xmlns:g="http://base.google.com/ns/1.0">\n<channel>\n');
    for (const batch of chunk(products, 1000)) {
      const items = batch.map(p => ({
        item: {
          'g:id': p.id,
          'g:title': p.title,
          'g:description': p.description ?? '',
          'g:link': p.link,
          'g:image_link': p.image_link,
          'g:price': `${p.price} ${p.currency}`,
          'g:availability': p.availability,
          'g:brand': p.brand ?? '',
          'g:gtin': p.gtin ?? '',
          'g:mpn': p.mpn ?? '',
          'g:condition': p.condition,
        }
      }));
      const xml = builder.build({ items });
      ws.write(xml.replace('<items>', '').replace('</items>', ''));
      written += batch.length;
    }
    ws.write('\n</channel>\n</rss>');
    await new Promise<void>((resolve, reject) => ws.end(resolve));
  } catch (e) {
    ws.destroy();
    throw new Error(`XML generation failed: ${(e as Error).message}`);
  } finally {
    const dur = performance.now() - start;
    console.log(`generated ${written} items in ${(dur/1000).toFixed(2)}s`);
  }
}

// usage
// import { generateXml } from './feed-generator';
// await generateXml(loadProducts(), './public/feed.xml');

2) Next.js Route Handlerで配信(ETag/圧縮/キャッシュ制御)

CDNキャッシュと差分更新に備え、ETagと圧縮を実装。エラー時は適切なHTTPステータスを返す。

// app/api/feed/route.ts (Next.js 14, Node runtime)
import { NextRequest, NextResponse } from 'next/server';
import crypto from 'node:crypto';
import zlib from 'node:zlib';
import { generateXml } from '@/lib/feed-generator';
import { readFile } from 'node:fs/promises';

export const dynamic = 'force-dynamic';

export async function GET(req: NextRequest) {
  try {
    // 事前生成済みファイルを返す戦略(ビルドまたはCronで更新)
    const xml = await readFile(process.cwd() + '/public/feed.xml');
    const etag = 'W/"' + crypto.createHash('sha1').update(xml).digest('hex') + '"';
    if (req.headers.get('if-none-match') === etag) {
      return new NextResponse(null, { status: 304 });
    }
    const gz = zlib.gzipSync(xml);
    return new NextResponse(gz, {
      status: 200,
      headers: {
        'Content-Type': 'application/rss+xml; charset=utf-8',
        'Content-Encoding': 'gzip',
        'ETag': etag,
        'Cache-Control': 'public, max-age=300, s-maxage=300, stale-while-revalidate=600',
      }
    });
  } catch (e) {
    return new NextResponse(`feed error: ${(e as Error).message}`,{ status: 500 });
  }
}

3) Pythonでスキーマ検証とGTINチェック(バリデーションゲート)¹⁰

取込前にPydanticで検証し、違反レコードは隔離。ログで原因を可視化する。

# validate_feed.py
from pydantic import BaseModel, HttpUrl, field_validator, ValidationError
from typing import Optional, List
import csv, sys

class Product(BaseModel):
    id: str
    title: str
    description: Optional[str] = None
    link: HttpUrl
    image_link: HttpUrl
    price: float
    currency: str
    availability: str
    brand: Optional[str] = None
    gtin: Optional[str] = None
    condition: str

    @field_validator('availability')
    @classmethod
    def availability_valid(cls, v):
        allow = {'in_stock','out_of_stock','preorder'}
        if v not in allow:
            raise ValueError('invalid availability')
        return v

    @field_validator('gtin')
    @classmethod
    def gtin_len(cls, v):
        if v and len(v) not in {8,12,13,14}:
            raise ValueError('invalid gtin length')
        return v


def load_csv(path: str) -> List[Product]:
    ok, bad = [], []
    with open(path, newline='', encoding='utf-8') as f:
        for row in csv.DictReader(f):
            try:
                p = Product(
                    id=row['id'], title=row['title'], description=row.get('description') or None,
                    link=row['link'], image_link=row['image_link'],
                    price=float(row['price'].split()[0]) if ' ' in row['price'] else float(row['price']),
                    currency=row.get('currency','JPY'), availability=row['availability'],
                    brand=row.get('brand') or None, gtin=row.get('gtin') or None, condition=row['condition']
                )
                ok.append(p)
            except ValidationError as e:
                bad.append((row.get('id','(unknown)'), e.errors()))
    for _id, errs in bad:
        print(f"invalid:{_id} -> {errs}", file=sys.stderr)
    return ok

if __name__ == '__main__':
    products = load_csv(sys.argv[1])
    print(f"valid records: {len(products)}")

4) BigQueryで正規化・エンリッチ(価格税込・カテゴリ展開)

計算や正規化はDWHで行い、アプリはシリアライゼーションに専念させる。日次・時間毎のパーティションを付与する。

-- products_enriched (partitioned by _PARTITIONDATE)
CREATE OR REPLACE TABLE dataset.products_enriched
PARTITION BY DATE(updated_at) AS
SELECT
  p.id,
  p.title,
  p.description,
  p.link,
  p.image_link,
  ROUND(p.price * (1 + t.tax_rate), 2) AS price_taxed,
  p.currency,
  IFNULL(s.stock, 0) > 0 AS in_stock,
  CASE WHEN IFNULL(s.stock,0) > 0 THEN 'in_stock' ELSE 'out_of_stock' END AS availability,
  p.brand,
  p.gtin,
  p.mpn,
  p.condition,
  ARRAY(SELECT c FROM UNNEST(SPLIT(p.category_path, '>')) c) AS categories,
  CURRENT_TIMESTAMP() AS updated_at
FROM dataset.products p
LEFT JOIN dataset.stock s USING(id)
LEFT JOIN dataset.tax t ON t.country = 'JP';

5) TypeScript + Zodで中間スキーマを堅牢化

チャネル差分を吸収する中間モデルをZodで定義し、型安全な変換関数を実装する。

// schema.ts
import { z } from 'zod';

export const ProductSchema = z.object({
  id: z.string().min(1),
  title: z.string().max(150),
  description: z.string().optional(),
  link: z.string().url(),
  image_link: z.string().url(),
  price: z.number().nonnegative(),
  currency: z.enum(['JPY','USD','EUR']).default('JPY'),
  availability: z.enum(['in_stock','out_of_stock','preorder']),
  brand: z.string().optional(),
  gtin: z.string().regex(/^(?:\d{8}|\d{12,14})$/).optional(),
  mpn: z.string().optional(),
  condition: z.enum(['new','refurbished','used'])
});

export type Product = z.infer<typeof ProductSchema>;

export function toGoogle(p: Product) {
  return {
    'g:id': p.id,
    'g:title': p.title,
    'g:description': p.description ?? '',
    'g:link': p.link,
    'g:image_link': p.image_link,
    'g:price': `${p.price} ${p.currency}`,
    'g:availability': p.availability,
    'g:brand': p.brand ?? '',
    'g:gtin': p.gtin ?? '',
    'g:mpn': p.mpn ?? '',
    'g:condition': p.condition
  };
}

6) 配信オーケストレーション(Cron + GCS/S3 + CDN)

失敗時の再試行と監視イベントを組み込み、CDNを確実に更新する。

#!/usr/bin/env bash
set -euo pipefail
TS=$(date +%Y%m%d%H%M)
FILE="feed-${TS}.xml.gz"

node dist/generate.js  # 生成(非0終了で失敗)

gzip -c public/feed.xml > "/tmp/${FILE}"

gsutil cp "/tmp/${FILE}" gs://my-bucket/feeds/${FILE}

gsutil cp "/tmp/${FILE}" gs://my-bucket/feeds/latest.xml.gz

gcloud compute url-maps invalidate-cdn-cache my-cdn --path "/feeds/latest.xml.gz"

echo "OK: uploaded ${FILE}"

パフォーマンス指標・ベンチマークと監視

測定対象とKPI

スケーラビリティと運用安定性を両立するため、以下を継続計測する。

  • スループット(items/sec)
  • ピークメモリ(RSS, MB)
  • 生成レイテンシ(P50/P95, s)
  • 失敗率(検証エラー/IOエラー)
  • 鮮度SLA(在庫・価格の反映遅延)

社内検証ベンチマーク(100万SKU、疑似データ)

環境:Node 20.11(Apple M2 Pro/32GB)、fast-xml-parser 4.x、ストレージはローカルSSD。

方式スループットピークメモリP95レイテンシ
ストリーム生成(本稿1))85k items/sec~220 MB14.2s
全件オブジェクト→JSON→XML一括22k items/sec~1.9 GB47.8s
BigQuery→Cloud Storage(CSV)~120k items/secサーバレス8.9s(転送除く)

結論:中規模まではアプリのストリーム生成で十分だが、100万SKU超かつ分更新ではDWH直出力 + アプリ薄化が有利。アプリ側はETag/差分/圧縮で帯域を削減する。

監視とアラート(SLO例)

  • 生成ジョブ成功率 ≥ 99.9%(7日移動)
  • SLA: 価格・在庫の反映 ≤ 5分(P95)
  • 審査拒否率 ≤ 0.5%(新規登録)

Stackdriver/CloudWatchでジョブログとメトリクスを集約し、検証エラー増加を検知したらスキーマ更新のデグレを疑う。

ビジネス価値と導入手順(ROIを数式化)

なぜ投資すべきか

商品フィードは広告費効率(ROAS)と売上に直結する。表示拒否の削減、在庫・価格の即時反映、属性充足(ブランド/GTIN/画像品質)の向上は、配信面の露出とCPA低下に寄与する。開発投資は「拒否・遅延による逸失売上の回収 + 運用時間削減」で回収できる。

導入ステップ(所要期間の目安)

  1. 要件整理(チャネル選定・KPI定義・鮮度SLA): 1〜2週
  2. データモデリング(中間スキーマ/Zod/Pydantic): 1〜2週
  3. ETL整備(BigQuery/変換SQL、在庫・価格合流): 2〜4週
  4. 生成・配信(Node/Next.js、CDN、ETag/圧縮): 2〜3週
  5. 検証・監視(テストデータ、SLO、アラート): 1〜2週
  6. 段階ロールアウト(1チャネル→多チャネル): 1〜2週

合計: 8〜15週(既存DWHが整備済なら短縮)。

ROIの試算フレーム

前提: 月間カタログ更新 50回、平均SKU 30万、現状拒否率 2%、平均CVR 2.0%、平均粗利率 35%。改善後、拒否率 0.5%、在庫反映遅延の半減(欠品クリック削減)。概算式:

  • 拒否改善による売上増 = 露出回復 × CVR × 粗利
  • 欠品クリック削減 = 無駄クリック削減 × CPC
  • 運用削減 = 手動修正工数 × 単価

多くのケースで6〜12ヶ月で投資回収が現実的となる。特に多チャネル配信で効果が累積する。

ベストプラクティス

  • 中間モデルの厳密化(スキーマとユニットテスト)
  • ストリーム処理 + 圧縮 + 差分配信
  • DWHでの正規化と集計、アプリは薄く
  • 検証ゲート(CIでサンプルフィードを自動チェック)
  • 監査ログ(生成バージョン、依存バージョン、生成時間)

まとめ

商品フィードは、単なる「データ書き出し」ではなく、売上・コスト・顧客体験を左右するプロダクト機能だ。本稿の実装例(Nodeストリーム生成、Next.js配信、Python検証、BigQuery正規化、Zodスキーマ)とベンチマーク、SLO/ROIのフレームを組み合わせれば、2025年要件に耐える設計が可能になる。まずは中間スキーマと検証ゲートを導入し、1チャネルで確度を高めてから多チャネルへ拡張してほしい。自社のSKU規模や鮮度SLAに照らして、どの段から着手するのが最短か——次のスプリントで試すべき最小実験は何かをチームで決め、測定と改善のループを始めよう。

参考文献

  1. Google Merchant Center: Fix issues and disapprovals for your products. https://support.google.com/merchants/answer/13693497
  2. Google Merchant Center Product data specification. https://support.google.com/merchants/answer/6324371
  3. Meta Marketing API: Catalog reference. https://developers.facebook.com/docs/marketing-api/catalog/reference/
  4. Criteo Retailer Integration: Product feed examples. https://developers.criteo.com/retailer-integration/docs/product-feed-examples
  5. Yahoo DSP Conversion API: Product spec. https://help.yahooinc.com/dsp-api/docs/product-yahoo-conversion-api
  6. Search Engine Journal: Google Updates Structured Data Requirements For Return Policies. https://www.searchenginejournal.com/google-updates-structured-data-requirements-for-return-policies/542080/
  7. Google Search Central: Return policy structured data. https://developers.google.com/search/docs/appearance/structured-data/return-policy
  8. Google Merchant Center: Image requirements. https://support.google.com/merchants/answer/6324350
  9. Google Merchant Center: Availability attribute – supported values. https://support.google.com/merchants/answer/7052112
  10. Google Merchant Center: GTIN attribute requirements. https://support.google.com/merchants/answer/7052112
  11. Google Merchant Center: Condition attribute – supported values. https://support.google.com/merchants/answer/6324469
  12. Google Merchant Center: ID attribute guidelines. https://support.google.com/merchants/answer/14779112