Yahoo!ショッピング API でランキング広告を自作した話

Yahoo!ショッピング API を使って、自分のサイトにランキング広告を作ってみました。

最初に公開したときは、記事を見に来た人のアクセスをきっかけに、Vercel Function から Yahoo!ショッピング API を呼んでランキングデータを作る構成でした。

記事を開く
↓
/api/yahoo-ranking を呼ぶ
↓
Function 内のメモリキャッシュがあれば保存済みデータを返す
↓
なければ Yahoo!ショッピング API から取得
↓
表示用データを作成
↓
ランキング広告を表示

ページが閲覧されるたびに Yahoo!ショッピング API を呼ぶ構成はまずいので Function 内のメモリキャッシュに保存して再利用してましたが、キャッシュがないときは API を何度も呼んで、表示用のデータを作る形です。

なので、キャッシュがない状態では広告のデータが返るまでにかなり時間がかかって、広告表示前に離脱するユーザーも多くいるだろうという状況でした。

まあ、それ自体はキャッシュがないときにだけ起こることなのでいいのですが、それより問題かなーと思ったのは、ページへの訪問時に、外部 API へ何度もリクエストする重い処理を走らせる設計そのものです。

見る人が来てからじゃなく、裏で定期的に作って保存しておく方がいいよね

と当たり前のことを思って、GitHub Actions で定期的にデータを作り、Vercel Runtime Cache へ保存しておく構成に変更した次第です。

ただ、その構成変更の過程でちょっとごちゃついた、とかそんな話です。


そもそも作ろうとしていたランキング広告

今回作ったのは、LifeTools Lab のブログ記事の PC サイドバーに表示する Yahoo!ショッピングのランキング広告です。

上段は Yahoo!ショッピングの「高評価トレンドランキング」から10商品を表示。 下段は「急上昇ワード」を10件取得してからそれぞれのキーワードで商品検索を行って、検索結果の先頭商品を表示します。

こんな構成です。

上段
Yahoo!ショッピング
高評価トレンドランキング
↓
10商品を順番に表示

下段
Yahoo!ショッピング
急上昇ワード10件
↓
各キーワードで商品検索
↓
見つかった商品を順番に表示

Yahoo!ショッピングの公式 API には、高評価トレンドランキング、キーワードランキング、商品検索などが用意されています。

高評価トレンドランキングでは affiliate_type=vc と affiliate_id を指定すると、バリューコマースのアフィリエイト用 URL を返してもらえるため、今回の広告でもこの仕組みを使っています。

→ Yahoo!デベロッパーネットワーク:高評価トレンドランキング
→ Yahoo!デベロッパーネットワーク:キーワードランキング(v2)
→ Yahoo!デベロッパーネットワーク:商品検索(v3)


最初の実装は「閲覧されたときに作る」方式だった

最初の更新 兼 表示用 API には、30分のメモリキャッシュを入れていました。

/api/yahoo-ranking へアクセス
↓
Function のメモリに取得から30分以内のキャッシュが残っているか?
├─ YES → そのデータを返す
└─ NO  → Yahoo!ショッピング API からランキングを作る
          ↓
        Function メモリへ保存
          ↓
        データを返す

ただし、Function 内のメモリキャッシュは新しい Function に引き継がれないため、デプロイ後など使えるキャッシュがない状態になると、デプロイ後の初めての訪問者の中で下記処理が走ります。

高評価トレンドランキングを取得
急上昇キーワードランキングを取得
↓
高評価10商品について商品情報を追加取得(itemLookup)
急上昇10キーワードについて商品を検索(itemSearch)
↓
表示用データを完成させる

通常どおり10件ずつ取得できる場合は、これで基本22回です。

内訳は、

各ランキング取得                     2回
高評価10商品の商品情報取得           10回
急上昇10キーワードからの商品検索     10回
----------------------------------------
合計                                22回

になります。

上段の itemLookup は、高評価トレンドランキングで得た商品の画像について、より大きい画像を取るための追加取得です。

下段の itemSearch は、急上昇キーワードランキングで得た10個のキーワードそれぞれについて、実際に表示する商品を1つずつ取得するために呼んでいます。

急上昇キーワード10件を取得
↓
キーワード1で商品検索
キーワード2で商品検索
...
キーワード10で商品検索

という追加処理が必要です。

Yahoo!ショッピング API の案内には、短時間で同一 URL に大量アクセスした場合に一定時間利用できなくなる場合があり、目安として「1クエリ/秒」と記載されています。

→ Yahoo!デベロッパーネットワーク:ショッピング Web API

なので、実装でも itemLookup は1.3秒、itemSearch は2.1秒の間隔を空けていました。

これらは並行して進むので単純計算はできませんが、急上昇キーワード側だけでも9回の待機が入るため、それだけで約19秒になります。

これが、キャッシュがないと表示が遅くなる理由でした。

ですが、そもそもページを閲覧されたタイミングでそんな重い処理が走ることがあること自体、設計としてよくないと思ったわけです。


GitHub Actions + Vercel Runtime Cache で事前取得する構成に変更

事前取得後の構成はこうしました。

GitHub Actions
毎時17分 または 本番デプロイ後
↓
POST 更新用 API
↓
Yahoo!ショッピング API から必要なデータを取得
↓
表示用データを作成
↓
Vercel Runtime Cache に保存

ユーザーが記事を開く
↓
GET 表示用 API
↓
Runtime Cache の保存済みデータだけ読む
↓
商品カードを表示

まず、これまで使っていた Function 内の一時的なメモリキャッシュではなく、Vercel の Runtime Cache に表示用データを保存する構成へ変更しました。

Runtime Cache は、Vercel Functions などから getCache() を使って値を保存・取得できる仕組みです。

→ Vercel Docs:Runtime Cache

また、今回の実装では、更新側と表示側を分離しました。

更新用 API は Yahoo!ショッピング API を呼び出して、表示用データを作ってキャッシュへ書き込む側。

表示用 API はキャッシュを読むだけです。

表示側の処理には、キャッシュが空だからといってその場で Yahoo!ショッピング API を取り直す処理は入れていません。

キャッシュがなければ、

{"error":"Yahoo ranking snapshot is not ready"}

として 503 を返します。

これは広告が表示されないケースが起こりうることになりますが、仮に閲覧が集中したときに「キャッシュがないから全員で Yahoo!ショッピング API を取りにいく」という状態を避けるためにそうしました。


GitHub Actions は成功。でも Runtime Cache には何もない

ここで一発目のトラブルです。

本番へ反映したあと、表示用 APIを開いても 503 のままでした。

一方、GitHub Actions の Yahoo Ranking Refresh は緑(成功)でした。

Vercel のログを見ると、表示用 API は動いていた一方で、更新用 API の実行ログが見当たりませんでした。

表示用 API
↓
Runtime Cache を読みにいっている
↓
でも値がない
↓
503

という状態です。


trailingSlash: true と curl の組み合わせで「緑なのに未実行」が起きうる状態だった

Workflow では、次の URL へ POST していました。

https://lifetools-lab.com/xxxxx

一方、サイトの vercel.json には、

{
  "trailingSlash": true
}

があります。

Vercel の公式ドキュメントでは、trailingSlash: true の場合、末尾 / のないパスは / ありのパスへリダイレクトされます。

→ Vercel Docs:Project Configuration

そして Workflow の curl は、当初こういう考え方でした。

curl \
  --fail-with-body \
  --request POST \
  https://lifetools-lab.com/xxxxx

ここで勘違いしていたのが --fail-with-body です。

名前だけ見て、「正常な200系以外なら失敗にしてくれるのかな」と思っていましたが、curl の公式マニュアルでは --fail-with-body がエラーとして扱うのは HTTP 400 以上です。

3xx のリダイレクトはこれだけではエラーになりません。また、curl でリダイレクト先まで追うには --location が必要です。

→ curl man page

ただ、

  • Vercel 側は trailingSlash: true
  • Workflow の URL には末尾 / がない
  • curl はリダイレクト追従なし
  • --fail-with-body は3xxを失敗扱いしない
  • Actions は緑なのに更新用 Function の実行を確認できない

という状態でした。

少なくとも、GitHub Actions が緑でも「ランキング更新に成功した」とは言えない作りになっていました。

そこで URL を最初から、

https://lifetools-lab.com/xxxxx/

へ変更しました。

さらに、HTTP リクエストが終わっただけでは成功扱いにせず、更新用 API のレスポンス JSON に、

{"ok":true}

が入っていることまで Workflow 側で確認するようにしました。

今の Workflow は、簡略化するとこうです。

response="$(curl ... https://lifetools-lab.com/xxxxx/)"

printf '%s' "$response" | python3 -c '
import json
import sys

data = json.load(sys.stdin)

if data.get("ok") is not True:
    raise SystemExit("refresh failed")
'

これなら、通信そのものが終わっただけで緑になるのではなく、こちらが期待している「ランキング更新成功」のレスポンスまで確認できます。


直したら今度は401。Function までは届いた

末尾の / と成功判定を直して再実行すると、今度は成功扱いにならず Workflow がちゃんと赤くなりました。

そして表示されたのは 401 Unauthorized です。

refresh Function 側では、GitHub Actions から誰でも更新処理を呼べないよう、Authorization ヘッダーに専用のシークレットを付けています。

GitHub Actions
YAHOO_RANKING_REFRESH_SECRET
↓
Authorization: Bearer ...
↓
Vercel Function
YAHOO_RANKING_REFRESH_SECRET
↓
一致した場合だけ更新

401 が返ったことで、少なくともリクエストが refresh Function までは届いていて、認証判定で止まっていることが分かりました。

同じもの入れたはずなんだけどなーともやもやしながら、再度シークレットを生成して GitHub Actions と Vercel Production の両方へ同じシークレットを入れ直しました。

その後デプロイしたら401を抜けました。


やっと Yahoo!ショッピング API まで進んだら、今度は「6番目の商品がない」

認証は通過しましたが別のエラーが出ました。

[Yahoo ranking refresh] failed:
itemSearch returned no product for rising keyword position 6

急上昇ワードの6番目で商品検索をしたところ、該当商品が0件でした。

これは Yahoo!ショッピング API の障害ではなく、こちらの実装によるものです。

最初のコードでは、急上昇ワード10件のうち1件でも商品が見つからなければ、処理を失敗させていました。

1位 → 商品あり
2位 → 商品あり
3位 → 商品あり
4位 → 商品あり
5位 → 商品あり
6位 → 商品なし
↓
全部失敗
↓
キャッシュを書かない

ここは、取得できたものだけで表示用データを作成するようにして解消。


表示できたと思ったら、レビュー情報が前の商品から残っていた

データ取得まわりを直して広告は表示されるようになりましたが、今度は表示側にも1つバグがありました。

商品は2秒ごとに切り替えているのですが、商品名や画像は次の商品になっているのに、★評価やレビュー件数が前の商品から残ることがありました。

商品A
★4.72 レビュー123件

↓ 2秒後

商品B
★4.72 レビュー123件 ← Aのレビュー情報が残っている

原因は、商品カードの同じ表示領域を使い回していたのに、商品を切り替えるときに前の商品レビューをいったん消してから描画していなかったことです。

商品名や画像などは切り替えるたびに新しい値へ更新していましたが、レビュー部分は新しい商品にレビュー情報がある場合だけ書き換える作りになっていました。

ただの初歩的なミスです。


更新頻度とキャッシュの生存期間を分けて、前回成功したデータを残す

ランキングは1時間ごとに更新しますが、Runtime Cache の TTL は30日にしています。

30日間同じデータを見せるためではなく、更新失敗時に「最後に正常取得できたデータ」を消してしまわないための対応です。

10:17 更新成功
↓
正常データを保存

11:17 Yahoo API 側の一時エラー
↓
新しいキャッシュは書かない
↓
10:17の正常データをそのまま使う

12:17 更新成功
↓
新しいデータへ更新

普通の last-known-good です。


最後は Warning を出しながら200で更新成功

最終的な本番ログでは、急上昇ワード6番目の商品が見つからなかったため、次の Warning が出ました。

[Yahoo ranking] no recommended item for rising keyword position 6; skipping
[Yahoo ranking] recommended items are partial: 9/10

が、これは意図どおりで、更新用 API はそのまま 200 を返して、Runtime Cache へ snapshot を保存できました。

記事を開くと、無事自作のYahoo!ショッピングのランキング広告が表示されました(!)


参考

← ブログ一覧に戻る