プロダクトマッチング:ディスカバリスコアリングを用いた意味的類似性

この記事では、意味的類似性と、関連性、トラフィック、ページランクをバランスさせるオプションのディスカバリスコアリングモデルを使用して、検索クエリクラスターを製品、パーツ、記事にマッチングさせる方法を説明します。

問題:最適なマッチを見つける

ユーザーが「ミニPC」を検索したとき、そのクエリを最もよく表す製品ページを判断する必要があります。何千もの製品、パーツ、記事がありますが、どれを表示すべきでしょうか?

課題は、複数の要素をバランスさせることです:

  • 意味的関連性:ページはクエリの意味にどれだけ合致するか?

  • クエリトラフィック:このクエリはどれだけの検索トラフィックを得ているか?

  • ページ人気度:ページは既にどれだけのトラフィックを受けているか?

単純なアプローチ(純粋な意味的類似性)では、「ミニPC」を完璧な類似性を持つがトラフィックゼロの無名な製品にマッチングしてしまう可能性があります。より良いアプローチは、これら3つの要素すべてを考慮します。

2つのマッチングモード

2つのマッチングモードをサポートしています:

1. バルクマッチング(オリジナル)

各クラスターは独立して最適なマッチを見つけます。複数のクラスターが同じページにマッチすることがあります:

  • クラスターA:「ミニPC」 → 製品X(類似度 0.95)

  • クラスターB:「小型コンピュータ」 → 製品X(類似度 0.93)

  • クラスターC:「コンパクトデスクトップ」 → 製品X(類似度 0.91)

これは冗長性を生みますが、すべてのクラスターが最適なマッチを得ることを保証します。

2. 反復的排他的ルーティング(1:1マッピング)

クラスターはトラフィックスコア(高い順)で処理されます。ページが選択されると、プールから削除されます:

  • クラスターA(10K トラフィック):「ミニPC」 → 製品X(選択済み)

  • クラスターB(5K トラフィック):「小型コンピュータ」 → 製品Y(Xは利用不可)

  • クラスターC(2K トラフィック):「コンパクトデスクトップ」 → 製品Z(X, Yは利用不可)

これにより、独自のクエリページが作成され、次善のマッチへの自動的なフォールバックが行われます。

アルゴリズム:意味的類似性

ステップ1:埋め込みの読み込み

事前計算された埋め込みを読み込みます:

  1. クエリクラスター:各クラスターの中心クエリ
  2. ソースページ:製品、パーツ、記事(ステップ0から)

両方とも同じall-mpnet-base-v2モデルを使用しており、比較可能な埋め込みを保証します。

ステップ2:類似性の計算

各クラスターについて、すべてのソースページへのコサイン類似度を計算します:

similarities = util.cos_sim(cluster_embedding, source_embeddings)[0]

これにより、すべてのソースページに対する類似度スコア(0.0 から 1.0)が生成されます。

ステップ3:ディスカバリスコアリングの適用(オプション)

ディスカバリスコアリングが有効な場合、3つの要素を組み合わせます:

50:30:20 ディスカバリスコアリングモデル

discovery_score = (similarity * 0.5) + (query_score * 0.3) + (page_rank * 0.2)

ここで:

  • 類似度(50%):意味的関連性(コサイン類似度)

  • クエリスコア(30%):正規化されたクエリトラフィック(インプレッション+クリック)

  • ページランク(20%):正規化されたページトラフィック(対数スケール)

これにより、関連性とトラフィックの可能性がバランスされます。

ステップ4:スコアの正規化

組み合わせる前に、各要素を[0, 1]に正規化します:

クエリスコアの正規化

max_query_score = max(cluster.total_score for cluster in clusters)
norm_query_score = query_score / max_query_score

ページランクの正規化(高トラフィックページの支配を防ぐための対数処理):

max_page_rank = max(traffic_index.values())
norm_page_rank = log1p(page_rank) / log1p(max_page_rank)

対数スケーリングにより、ホームページ(最高トラフィック)がすべてのマッチを支配するのを防ぎます。

ステップ5:シャーシブーストの適用(製品のみ)

製品マッチングでは、シャーシタイプに基づいてスコアをブーストします:

  • Treoシャーシ:+10%(最新、最も人気)

  • Sシャーシ:+5%(コンパクト、需要が高い)

  • Hシャーシ:ブーストなし(古い、人気が低い)

if matched_type == "product":
    sku = matched_key.replace("/p/", "")
    chassis_prefix = sku.split("-")[0]
    if chassis_prefix.startswith("Treo"):
        similarity *= 1.10
    elif chassis_prefix.startswith("S"):
        similarity *= 1.05

これにより、類似度が近い場合に新しい製品が優先されます。

ステップ6:最適なマッチの選択

バルクマッチング

best_idx = similarities.argmax()
if similarities[best_idx] >= threshold:
    matches.append(cluster → source_pages[best_idx])

排他的ルーティング

# クラスターをトラフィック順(高い順)にソート
sorted_clusters = sorted(clusters, key=lambda c: c.total_score, reverse=True)

# ... (実装の詳細は省略)

設定可能な閾値

マッチングの閾値は、マッチングの厳密さを決定します:

  • 0.80(デフォルト):中程度の厳密さ、ほとんどのクラスターがマッチ

  • 0.85:より厳格、数は少ないが高品質なマッチ

  • 0.75:より寛容、より多くのクラスターがマッチ

この閾値は、設定ファイルを介してコード変更なしで調整できます。

閾値分析モード

フルパイプラインを実行する前に、閾値の影響を分析できます:

python 6_match_source_data.py --analyze-threshold

これにより、類似度範囲にわたるサンプルが生成されます:

  • 0.90-1.00:完璧なマッチ

  • 0.80-0.90:強いマッチ

  • 0.70-0.80:中程度のマッチ

  • 0.60-0.70:弱いマッチ

  • 0.50-0.60:非常に弱いマッチ

  • 0.40-0.50:貧弱なマッチ

Web UIはこれらのサンプルを表示し、手動レビューを行います。閾値を選択した後、パイプラインを再開します:

python 6_match_source_data.py --resume-after-threshold

これはUIの決定から閾値を読み込み、マッチングを完了します。

増分埋め込み

クエリとソースページの両方の埋め込みをキャッシュします。新しいデータが到着したとき:

  1. 既存の埋め込みを読み込む
  2. 新しいアイテムのみを埋め込む
  3. キャッシュに追加する

これにより、変更されていないデータの再埋め込みを回避します。詳細は埋め込み戦略を参照してください。

出力形式

マッチングは、統計とマッチを含むJSONファイルを生成します:

{
  "stats": {
    "threshold": 0.80,
# ... (実装の詳細は省略)

マッチは類似度順(高い順)にソートされます。

ディスカバリスコアリングの理由

純粋な意味的類似性には限界があります:

問題1:無名な製品

  • クエリ:「ミニPC」(10K トラフィック)

  • 最適なマッチ:無名な製品(類似度 0.98、トラフィック 0)

  • より良いマッチ:人気製品(類似度 0.95、5K トラフィック)

問題2:トラフィックの不一致

  • 高トラフィッククエリ → 低トラフィックページ(機会の浪費)

  • 低トラフィッククエリ → 高トラフィックページ(不必要)

ディスカバリスコアリングの解決策

  • 関連性(50%)とトラフィックの可能性(30% + 20%)をバランスさせる

  • 高トラフィッククエリは高トラフィックページにマッチ

  • 低トラフィッククエリはニッチなページにマッチ

  • 全体のトラフィック分布を最大化

パフォーマンス特性

典型的なサーバー上では:

  • 処理時間:12.5Kクラスター × 5Kソースページで約20分

  • メモリ使用量:約1 GB(埋め込み + 類似性行列)

  • CPU使用率:類似性計算中は高負荷

プロセスはCPUバウンドです。NumPyBLASアクセラレーションを使用すると、行列演算が大幅に高速化されます。

SEOパイプラインとの統合

プロダクトマッチングはSEOパイプラインのステップ6です:

  1. ステップ0ソースデータの埋め込み - 製品、パーツ、記事
  2. ステップ1クエリの取得 - GSC、Google Ads、ライブ、Algolia
  3. ステップ2:クエリの結合 - すべてのソースをマージ
  4. ステップ3a基本フレーズマッピングの生成 - 初期フィルター
  5. ステップ3bクエリの埋め込み - ベクトルへの変換
  6. ステップ4フレーズマッピングの拡張 - 類似フレーズの発見
  7. ステップ5クエリのクラスタリング - ページへのグループ化
  8. ステップ6プロダクトのマッチング ← ここです
  9. ステップ7:クエリページの構築 - HTMLの生成
  10. ステップ8関連検索の生成 - 関連クエリの発見
  11. ステップ11Valkeyへの移行 - 検索サービスへのロード

完全な流れについてはSEOパイプライン概要を参照してください。

バルク vs 排他的:どちらを使用するか?

バルクマッチング(オリジナル):

  • ✅ すべてのクラスターが最適なマッチを得る

  • ✅ シンプルで予測可能

  • ❌ 冗長なクエリページ(複数のクラスター → 同じ製品)

  • ❌ トラフィックの可能性の浪費

排他的ルーティング(1:1):

  • ✅ 独自のクエリページ(冗長性なし)

  • ✅ 次善のマッチへの自動的なフォールバック

  • ✅ より良いトラフィック分布

  • ❌ 低トラフィッククラスターは最適ではないマッチを得る可能性あり

  • ❌ より複雑なロジック

推奨:本番環境では排他的ルーティングを使用してください。これにより、より良いトラフィック分布を持つ、独自の高品質なクエリページが作成されます。

トラフィックインデックスと対数スケーリング

トラフィックインデックスは、すべてのソースページのページビューを追跡します:

{
  "/p/Treo-N100-8-256-2H-W6-11P": 5000,
  "/p/S-i5-16-512-2H-W6-11P": 3000,
  "/": 50000
}

高トラフィックページの支配を防ぐために、対数スケーリングを使用します:

norm_page_rank = log1p(page_rank) / log1p(max_page_rank)

これがないと、ホームページ(50K トラフィック)がすべてのクエリにマッチしてしまいます。対数スケーリングは範囲を圧縮し、すべてのページに公平な機会を与えます。

シャーシブーストの理論的根拠

TreoおよびSシャーシ製品をブーストする理由:

  • Treo:最新のシャーシ、最高の機能、最も高い需要

  • Sシャーシ:コンパクトなフォームファクタ、ミニPCで人気

  • Hシャーシ:古く、段階的に廃止中

類似度が近い場合(例:0.90 vs 0.89)、ブーストにより新しい製品が勝つことが保証されます。これはビジネスの優先順位と一致します。

参考文献

技術的概念

モデルドキュメント

関連記事

まとめ

クエリクラスターを製品にマッチングさせるために、意味的類似性とオプションのディスカバリスコアリングを使用します:

意味的類似性

  • クエリとソース埋め込み間のコサイン類似度を計算

  • 閾値ベースのマッチング(デフォルト 0.80)

  • Treo(+10%)およびSシャーシ(+5%)のシャーシブースト

ディスカバリスコアリング(50:30:20)

  • 50% 意味的関連性(コサイン類似度)

  • 30% クエリトラフィック(正規化されたインプレッション+クリック)

  • 20% ページランク(対数スケーリング)

2つのモード

  • バルク:すべてのクラスターが最適なマッチを得る(冗長性許可)

  • 排他的:1:1マッピング、高