目次11
楽天とYahoo!の商品APIを横断して同じ商品を比べようとすると、最初の壁が「そもそも同じ商品をどう同定するか」だった。価格をどう計算するかの前に、AモールのこれとBモールのこれが同一商品だ、と機械が判断できないと比較表が作れない。
ここで詰まったのが JAN(商品に付く 13 桁/8 桁のバーコード番号)の扱いだ。Yahoo! ショッピングは jan_code で正引きできるのに、楽天の商品検索APIはレスポンスにJANを返さない。同じJANで両モールを引けば一発で結べる、という前提が片側で崩れる。
個人でヤスゴロという価格ウォッチャーを作って公開した。複数モールの実質価格を横断で比べ、ウォッチした消耗品が安くなったら通知する、というものだ。その横断比較を成立させるために、JANが揃わない前提でマッチングを確実性で3段階に倒した。その設計を共有します。
楽天とYahoo!でJANの扱いは非対称
楽天とYahoo!は、JANの取得しやすさがそもそも違う。ここを揃っているものとして設計すると、楽天側で必ず破綻する。
Yahoo! ショッピングの商品検索は jan_code という専用パラメータを持ち、JANで直接引ける。レスポンスにも janCode が乗ることがある。つまりJANが分かっていれば、その商品ページを正引きできる。
楽天の商品検索APIにはJAN専用パラメータが無い。実装では JAN 文字列を keyword に投げる間接的なやり方になる。しかもJANはレスポンスの構造化フィールドではなく、商品説明文(itemCaption)の中に紛れていることが多い(Anker のような型番商品で実際にそうだった)。だから楽天側は「keyword=JANで出たヒットが、本当にそのJANの商品か」を後から確かめる工程が要る。
この非対称が出発点になる。Yahoo!は信頼してJAN確定に使え、楽天は確証を挟まないと使えない。
確実性で3段階に倒す
同一商品の同定を、確実性の高い順に3段階へ落とした。全件をJANで突合しようとせず、確実に結べるものだけを最上段に置く。
| 段 | 方法 | 横断比較に使う | 通知を出す |
|---|---|---|---|
| ① JAN突合 | Yahoo! 正引き+楽天 keyword=JAN を確証スコアで判定 | 使う(確定したものだけ) | 出す |
| ② 名前ファジー一致 | 型番・容量・ブランドでスコア化し候補提示→ユーザー確認 | ユーザー確認後に使う | 確認済みのみ |
| ③ マッチ不能 | 単一モール(URL貼り付け等)として扱う | 使わない | 単一モールの値下げのみ |
下段ほど確証が弱いので、横断比較と通知の権利を絞る。①で結べたものだけが「誰が見ても同じ商品」として比較表に並び、②③は確証の度合いに応じて扱いを下げる。
flowchart TD
In[入力: JAN / 商品名 / URL]
Kind{種別判定}
Jan[① JAN突合]
Score{確証スコア}
Fuzzy[② 名前ファジー一致]
Cand{候補あり?}
Single[③ 単一モール]
Cross[横断比較に採用・通知可]
Pending[候補提示→ユーザー確認待ち]
Watch[単一モールの値下げのみ追う]
In --> Kind
Kind -->|JAN| Jan
Kind -->|商品名| Fuzzy
Kind -->|URL| Single
Jan --> Score
Score -->|high ≥0.8| Cross
Score -->|mid ≥0.5| Pending
Score -->|low <0.5| Single
Fuzzy --> Cand
Cand -->|あり| Pending
Cand -->|なし| Single
Pending -->|確認| Cross
Single --> Watch
入力がJANなら①、商品名なら②、URLなら③へ振り分ける。①の中でも確証が弱ければ②③へ落ちる、という二段構えにした。
①JAN突合: 楽天は「JANらしさ」を加点で確かめる
JAN突合といっても、楽天は「keyword=JANで出たヒットが本当に同じ商品か」を確証する必要がある。ここを確証スコアの加点方式にした。
Yahoo!は jan_code 正引きなのでそのまま確定(通知も発火可)に置ける。問題は楽天で、1ヒットごとに次を足し引きしてスコアを出す。
- 商品名か説明文にJAN文字列を含む … +0.5(最も直接的な証拠)
- 製品検索API由来の価格レンジ内 … +0.3 / レンジ外 … −0.4
- 型番か容量が読み取れる … +0.2
スコアの閾値で3つに割る。0.8 以上なら確定(jan・通知発火可)、0.5 以上は要確認(fuzzy に落として通知不可)、0.5 未満は破棄(横断に出さない)。確証の弱いヒットを確定扱いしないことが、誤名寄せを防ぐ一番効く制御だった。
// 楽天1ヒットのJAN確証スコア(抜粋)
let s = 0;
if (hit.itemCaption.includes(jan) || hit.itemName.includes(jan)) s += 0.5;
if (range) {
const lo = range.minPrice * 0.7; // 下振れ30%許容(型落ち・並行品)
const hi = range.maxPrice * 1.5; // 上振れ50%許容(送料込・セット品)
if (hit.itemPrice >= lo && hit.itemPrice <= hi) s += 0.3;
else s -= 0.4; // レンジ外=異常マッチ強疑い
}
if (modelOrCapacityConsistent(hit)) s += 0.2;
価格レンジは楽天の製品検索API(productCode=JAN)が返す最小〜最大価格から作る。型落ちや並行輸入で下に外れるぶんは 0.7 倍まで、送料込みやセット品で上に外れるぶんは 1.5 倍まで許容する。レンジから大きく外れた価格は、容量違い・色違い・別商品の混入を疑って減点する。
ただ、この製品検索APIが実測でかなり404を返した。価格レンジが取れないヒットが多く、レンジ加点に頼り切れない。そこで横断相手(Yahoo!の同JANヒット)の代表価格を基準にした確証を足した。楽天ヒットの名前に、Yahoo!側から抜いた型番トークン(英数混在5字以上=ほぼ一意)が含まれ、かつ価格が相手価格の ±40% 以内なら確定へ昇格させる。型番が一致しても価格バンドで偶然一致を弾く、という二重条件にしている。
実際に動く比較画面はヤスゴロのライブアプリで見られる(ログイン不要・無料)。同一商品が複数モールで並ぶのは、この①で確定したものだ。
②名前ファジー一致: 候補提示までで自動確定しない
商品名しか手がかりが無い入力は、型番・容量・ブランドでスコア化し、候補を出すところまでで止める。自動で同一商品と断定はしない。
ファジースコアの配点はこうした。型番が最も強いシグナルになる。
- 型番一致 … +0.5
- ブランド一致 … +0.2
- 容量一致 … +0.2
- 残りトークンの重なり(Jaccard係数)… +0.1 × 係数
EH-NA0J のような型番は商品名から正規表現で抜き、容量は 54枚×3 を「1パック54枚 × 3パック = 合計162枚」のように分解して比べる。スコアが 0.6 以上の候補だけを提示し、一つも超えなければ最良の1件を③の単一モールへ落とす。
候補を確定するのはユーザーだ。提示された候補から「これは同じ商品」と確認したものだけをマスタ化する。逆に、誤って結ばれた組み合わせはワンタップで除外でき、その組は突合候補から永久に外れる(再び候補に上がってこない)。自動マッチの精度を上げ続けるより、確認導線と取り消し導線を素直に置くほうが、消耗品の横断比較では現実的だと思う。
③マッチ不能: 単一モールの値下げ通知に倒す
どの段でも結べなかった商品は、単一モール扱いにする。横断の最安比較には混ぜず、そのページ自身の値下がりだけを追う。
URL貼り付けで登録した単品も最初からここに入る。横断比較の土俵に乗らないので「他モールより安い」とは言わないが、「前より安くなった」は言える。比較できないものを無理に比較群へ入れて誤った最安を出すより、できることだけを正直にやる方針にした。
誤名寄せを防ぐのは「確定だけ通知する」設計
3段階に分けた一番の理由は、間違った同定のまま通知を飛ばさないためだった。通知が発火できるのは、JAN確定(jan)とユーザー確認済み(user_confirmed)だけ。ファジー段の要確認も、単一モールも、通知の発火条件からは外している。
価格を計算する前に、同定の確実性で通知の権利を握っておく。ここを握らないと、別商品の値下がりを「ウォッチ中の商品が安くなった」と誤報してしまう。横断比較の信頼は、結局この同定の足場が決めていた。
ポイント還元込みの実質価格をどう計算したか(確実性で価格を3層に分けた話)は、実質価格の計算メモに書いた。あちらが「価格をどう計算したか」、この記事が「同じ商品をどう同定したか」で、2つ揃って横断比較が成立する。動くものはヤスゴロで確かめてほしい。
よくある質問
楽天 API は JAN をまったく取得できないの?
商品検索APIのレスポンスにJAN専用フィールドはなく、JAN専用の検索パラメータもありません。実装上は keyword に JAN 文字列を投げる間接突合になり、JAN自体は商品説明文(itemCaption)に含まれていることが多い、という形です。一方 Yahoo! ショッピングは jan_code パラメータで正引きでき、レスポンスにも janCode が乗るので、この非対称が横断比較の出発点になります。
なぜ全部を JAN で突合しないの?
JANが揃わない商品で無理に突合すると、別商品を同一商品として束ねる誤名寄せが起きるからです。楽天はJANを直接返さないので、keyword=JANで出たヒットが本当に同じ商品かは別途確証が要ります。確実にJANで結べるものだけを①の確定層に置き、確証が弱いものは②の候補提示、結べないものは③の単一モール通知へ落とす、と確実性で段階を分けています。
名前ファジー一致を自動で確定しないのはなぜ?
型番・容量・ブランドが一致してもスコアは確率でしかなく、自動で同一商品と断定すると誤マッチのまま通知が飛ぶからです。ファジー段は候補を提示してユーザーが確認したものだけをマスタ化し、誤って結ばれた組み合わせはワンタップで除外(その組を突合候補から永久に外す)できるようにしました。通知が発火するのはJAN確定とユーザー確認済みだけに限っています。
マッチできなかった商品はどうなる?
③のマッチ不能として単一モール扱いになり、横断比較からは外して、そのページ自身の値下がりだけを追います。URL貼り付けで登録した単品もここに入ります。横断の最安比較には混ぜず、単独の価格ウォッチとして通知する形です。