Diverse developer blog

株式会社Diverse(ダイバース) 開発者ブログです。

出会いの「温度感」をマッチング検索に乗せるために Two-Tower Model と Solr KNN でやったこと

こんにちは、Diverse Developer Blog です。

今回は、YYC のマッチング検索に、SageMaker 上で学習した Two-Tower Model のユーザー Embedding を使ったベクトル検索 (Solr KNN) を追加した話をします。

チャンスタイムやデイリーミッションといった既存のルールベースの行動導線は引き続き動かしつつ、「自分と温度感が近い相手」を ML ベースの検索で並行して返せるようにする、という構成です。

TL;DR

  • これまで YYC のマッチング機会は、チャンスタイム(新規登録直後)やデイリーミッション(継続期)といったルールベースの行動導線で設計していました
  • ただ、ユーザーの目的が多様化するにつれて「全員一律の導線」では受け止めきれない部分が出てきたため、温度感 という軸を決定論的に定義し、それを教師情報として 64 次元のユーザー Embedding を学習する仕組みを追加しました
  • 学習は Amazon SageMaker 上の Two-Tower Model で行い、推論結果は Solr の Dense Vector フィールドに投入、KNN 検索で「温度感の近い相手」を返せるようにしています
  • 既存のルールベース導線とは置き換えではなく併用で、検索結果の一部を ML 側から差し込む形で運用しています

1. これまでの設計

YYC には、ユーザーのライフサイクルに応じた行動導線がいくつかあります。

  • チャンスタイム(新規登録直後): チュートリアル完了時に時間制限で、一定時間のあいだプロフィール閲覧やメッセージ送信が無料になる施策です。初動のアクションを後押しして、最初の 1 通までの距離を縮める役割を担っています。
  • デイリーミッション(継続期): 一定の条件で抽出された相手に対してあいさつ(初回メッセージ)が無料で送れる仕組みで、日々の行動を後押しして継続利用につなげる役割を担っています。
  • 検索ロジックのルールベースの並び替え :「新規ユーザー優先」「最終ログインが近い順」など、経験則ベースの並び替えです。

いずれも「誰にどの動線を踏ませるか」をルール側で先に決め打つ設計で、登録直後はチャンスタイム、その後はデイリーミッションという役割分担になっていました。

ルールベースの強み

先に書いておくと、ルールベースの設計自体に問題があるわけではありません。数年単位で YYC を支えてきた土台で、以下のような強みがあります。

  • 説明可能性が高い: 「なぜこの動線が出たか」をコードと config で完全に説明できます。運営や CS からの問い合わせにもすぐ答えられます。
  • 即効性がある: 新しい施策を思いついてから数日で本番に出せます。
  • 数値設計が容易: 送信上限や有効時間などのパラメータを A/B で直接調整できます。
  • 障害耐性が高い: 壊れても原因が特定しやすく、切り戻しも簡単です。
  • 運用知見が蓄積しやすい: 長年の施策で「どの数字を動かすと何が起きるか」が体感値として溜まっています。

新規プロダクトや、シグナルが十分に溜まっていないフェーズでは、ルールベースは今でも最短ルートだと思います。

見えてきた課題

一方で、ユーザー層と目的の多様性が広がってくると、次のような課題が見えてきました。

  • ユーザー多様性への追従が遅い: 新しい利用目的や層が出てくるたびに、ルールを増やすか既存ルールを改修するしかありません。
  • 個別最適ができない: 全員一律のルールなので、同じ温度感を持つ人同士をピンポイントで繋ぐことができません。
  • トラブルの事後学習ができない: ブロックや通報があっても、ルール側に自動で反映されるわけではなく、改善のたびに人間が仮説を立てて手を動かす必要があります。
  • 初動と継続で導線が分断する: チャンスタイム(新規)とデイリーミッション(継続)の接続が、ロジックとしても体験としても連続しません。
  • ルールの複雑化: これが一番重くなっていく課題です。

ルールの複雑化についてもう少し書くと、

  • 例外が例外を呼ぶ: 「このユーザー群だけ除外」「この時間帯だけ特別扱い」「このアプリバージョン以降はこう」といった分岐が、一度入ると消せずに積もっていきます。
  • ルール同士の相互作用が読めなくなる: 単体では正しいルールが、他のルールと組み合わさることで予期しない動作をします。
  • 変更のリスクが非対称: 新しいルールの追加は安全に見える一方、既存ルールの削除や改修は副作用が読めず怖くなり、結果として削除されないコードが溜まり続けます。

ルールベースは初速こそ速いのですが、メンテコストは時間とともに増えていきます。ある時点からは、新しい施策を入れるより「既存ルールと衝突しないか確認する」ほうが時間を食うようになります。

ユーザーの目的が多様化するほど、全員一律の導線では受け止めきれない領域が広がってきたので、ここを ML 検索で補完することにしました。


2. 温度感を 1 次元スコアとして定義する

まず取り組んだのは、モデルに学習させる前に「温度感とは何か」を自分たちで定義することです。

ユーザーの「出会いに対する姿勢」は、実は 1 本の軸にかなり綺麗に乗ります。

  じっくり  ←─────────────────────────────→  カジュアル
   0.0                                        1.0
  • 左寄り: じっくり話したい、ゆっくり関係を築きたい層
  • 中央: 一般的な出会い目的の層
  • 右寄り: 気軽につながりたい、カジュアル志向の層

さらに、それぞれのユーザーには「自分のスコア位置」だけでなく「どの範囲の相手なら噛み合うか」という希望レンジもあります。ざっくり書くとこういうイメージです。

   じっくり                                            カジュアル
    0.0                                                   1.0
     ├──────────────────────────────────────────────────────┤

       ◆                                                       ← ユーザー A
       ├───────┤  A の希望レンジ

               ◆                                               ← ユーザー B
          ├───────────────┤  B の希望レンジ

                             ◆                                 ← ユーザー C
                        ├──────────────────────────┤  C の希望レンジ

ポイントは、希望レンジがユーザー (カテゴリ) ごとに形が違うという点です。左寄りの層は同じ左寄りの相手を狭めに希望することが多く、中央・右寄りの層は逆に広めのレンジを持ちやすい、といった非対称な分布になります。実際にはこれをもっと細かい複数カテゴリに分けていますが、構造としては同じです。

この軸を決定論的なロジックとして実装しています。

スコア算出のしくみ

  1. フラグ優先判定: 悪質行為フラグや属性フラグなど、確実にカテゴリが決まる条件を最優先で判定します。
  2. 目的ベーススコア: フラグに該当しないユーザーは、プロフィールの「出会いの目的」から平均スコアを算出します。
  3. 希望レンジ: 自分のスコアに対して「どの範囲の相手なら噛み合うか」を希望の中心値として定義します。

この段階ではまだ機械学習は出てきません。プロフィール・属性・目的コードから、決定論的に scorematch_center の 2 値を出しているだけです。

ただ、この 2 値が後段の学習モデルの教師情報になります。


3. Two-Tower Model で 64 次元に埋め込む

次に、各ユーザーを 64 次元のベクトル (Embedding) に変換する学習パイプラインを、Amazon SageMaker 上に構築しました。採用したのは、推薦系で定番の Two-Tower Model です。

   [User Tower]                [Target Tower]
        │                            │
        ▼                            ▼
   64 次元ベクトル ──コサイン類似度──▶ マッチングスコア
  • User Tower と Target Tower は同じ構造の別重みです
  • 入力は user_id / sex / score / match_center の 4 カラムのみ
  • 出力は 64 次元の密ベクトルです

なぜ学習させるのか

ここで「scorematch_center が決定論的に出るなら、その 2 値でソートすればいいのでは?」と思われるかもしれません。

実は、Embedding の先頭 2 次元はスコアをそのままピン留めしているので、そこだけ見ればロジック単体でも再現できます。ML を使う価値は、むしろ残りの 62 次元のほうにあります。

この 62 次元は実際のユーザー行動 (特にブロック) から学習されるので、

  • 温度感スコアが同じでも、「実際にはブロックされがちな特徴」を持つ人は遠ざけられます
  • プロフィールに明示されていない共通点で近い人同士は、自然と近くなります
  • 過去の行動パターンから、「この相手は希望に合いそうか」という期待度合いを織り込めます

静的な属性ベースのスコアリングに、行動データから学習した「振る舞いの近さ」を重ねる、ということです。先頭 2 次元で説明可能性を担保しつつ、残り 62 次元で汎化を稼ぐ、という役割分担になっています。

損失関数

学習の損失関数は 2 つの項を組み合わせています。

1 つ目は、推薦系で標準的に使われる InfoNCE です。バッチ内のペアを正例・負例としてコントラストさせ、良い組み合わせは近く、悪い組み合わせは遠くなるように学習させます。これに加えて、後述するブロックを明示的な負例として投入しています。

2 つ目は、Embedding の先頭 2 次元を scorematch_center に誘導する MSE 項です。解釈可能なスコアがあるので、それを Embedding の先頭次元にそのまま載せてしまおう、という発想です。これによって、

  • 「なぜ近いのか説明できない」マッチングを返しにくくなります
  • 温度感レンジでのフィルタリングを、ベクトル検索側で低コストに再現できます

ブロックを負例として使う

ユーザー間の明示的なブロックは、マッチングでは強い負例シグナルです。学習パイプラインでは、直近 90 日のブロックを weight = -1.0 の負例インタラクションとして投入しています。

ただし、この負例が担っているのは「ブロック当人同士を離す」ことではなく、「ブロック相手に似た別人を遠ざける」ことです。

ブロック関係にある当人同士は、Solr の検索フィルタ側で確定的に除外しているので、設計としては二層になっています。

担当 効果
Solr の検索フィルタ 確定フィルタ ブロック当人同士は 100% 出ない
負例 -1.0 ML 学習 ブロック相手に似た別人を Embedding 空間で遠ざける

フィルタで当人は既に落ちているので、ML 側の負例は「似た傾向の他人がまた現れる」事故を確率的に減らす役割に専念できます。片方が外れても最低限の安全性は担保されます。


4. 推論結果の配信(Solr KNN)で配信する

学習した Embedding は Solr の Dense Vector フィールドに投入し、KNN 検索で「自分のベクトルに近いユーザーを k 件返す」形で配信しています。

既存の属性フィルタ (性別・年齢・エリア・ブロック除外など) は Graph Pre-Filter として併用できるので、ベクトル検索と条件検索のハイブリッドになります。


5. 何が変わるのか

今回追加した温度感ベクトル KNN 検索と、これまでのルールベース検索との違いは次のとおりです。

観点 これまで (ルールベース) 追加した ML 検索 (温度感ベクトル KNN)
推薦の基準 新規ユーザー優先・最終ログイン順などの並び替え ユーザー同士の温度感の近さ
新規目的への対応 ルール追加や改修 プロフィール更新 → 次回学習で反映
トラブル抑止 人手でルールを足す ブロック負例でモデルが学習
説明可能性 ルールを辿れば説明可 先頭 2 次元にスコアを固定して担保
検索性能 属性 INDEX 頼り Solr KNN で高速ハイブリッド検索

6. 直近のネクスト: 数字を見ながら ML 側に寄せていく

ここまでで「決定論的スコア + Embedding ベースの類似検索」という土台はできましたが、現時点では既存のルールベース導線と ML 検索を併用しながら効果を計測している段階です。マッチング検索全体を ML 側に寄せるかどうかは、数字を見ながら判断していくことになります。

直近のネクストは、

  1. マッチ成立率、メッセージ継続率といったプロダクト側の KPI で ML 検索の効果を計測する
  2. 結果を見ながら、学習データ・特徴量・損失設計を改善していく
  3. 数字が安定して良くなったところで、ML 検索の比率を上げていき、最終的にはマッチング検索をルールベース側から ML 側に寄せていく

という流れを想定しています。ML モデルは学習データや特徴量を入れ替えれば継続的に改善できるので、しばらくはルールベースと併用しながら、数字を見て少しずつ振り方を ML に寄せていく構成で考えています。

その先の改善余地としては、以下のような項目も見えています。

  • シグナル設計の余地: 現状はブロックを負例として使っているのみで、ポジティブな行動 (メッセージ継続、マッチング成立など) をどう反映させるか?
  • コールドスタート: 行動ログがない新規ユーザーに向けてどう最適化するか?
  • 再学習サイクル: 現状は日次バッチですが、更新頻度と計算コストのバランスをどう取るか?

まとめ

今回は、出会いの「温度感」を マッチング検索に乗せるために、Two-Tower Model と Solr KNN でやったことを書きました。

  • 温度感を決定論的なスコアとして定義した上で、それを教師情報として Two-Tower Model を学習させ、Solr KNN で配信する構成にしました
  • Embedding の先頭 2 次元にスコアをピン留めすることで、説明可能性を保ちつつ、残り 62 次元で行動データからの学習を効かせています
  • ブロックは確定フィルタ (Solr) と ML 負例の二層で効かせる設計にしました
  • ML 検索の効果はまだ計測中で、既存のルールベース導線と併用しながらチューニングを続けていく予定です