Skip to content

Similar(類似シナリオ) ​

シナリオ A を見ているユーザーに「A と一緒に遊ばれてる他のシナリオ」を出す枠。シナリオ詳細画面の「類似シナリオ」セクションと、感想戦画面の「次にやれるシナリオ」枠で使う。

0. レコメンドの概要 ​

項目内容
入力SimilarRequest{ ScenarioID, ViewerIDs[], Common{ K, Candidates?, ExcludeAlreadyPlayed } }。ViewerIDs は感想戦画面では event.validParticipants、シナリオ詳細画面 (単一 viewer) では要素 1 のスライス、シナリオ詳細画面 (guest) では空スライス
出力Response{ Items, ModelVersion, RecommendationID }。PerUserScores は出さない (感想戦 UI は全員で同じ結果を見る前提)
mobile / web 画面(1) シナリオ詳細画面の「類似シナリオ」セクション (2) 感想戦画面 (play_screen / results_additional_pages) の「次にやれるシナリオ」枠
本番経路(1) Scenario.similarScenarios → Recommender.Recommend(SimilarRequest{ScenarioID, ViewerIDs:[me]}) (2) Query.similarScenarios(limit, scenarioID, targetUserIDs) → Recommender.Recommend(SimilarRequest{ScenarioID, ViewerIDs:targetUserIDs})。両方とも新 pipeline 経由 (UZU-4294 で差し替え済み)
Admin Preview✅ adminSimilarScenarios (/recommendation/preview、scenarioId + viewerIds[]、SuppressLogging=true)
新 pipeline 投入状況✅ 投入済 (UZU-4294)。assignment ログに surface='similar' で BigQuery に流入する

1. 評価関数の設計 ​

backend/internal/usecase/recommendation/surfaces/similar.go の SimilarStrategy。感想戦の UX に合わせて作り込んである。

1.1 PreFilter (5 段) ​

  1. Common.Candidates で絞り込み
  2. self-exclusion: context scenario (SimilarRequest.ScenarioID) 自身は外す
  3. 人数フィルタ (similarCharacterCountWindow = 2): context の CharacterCount ± 2 の scenario だけ通す。1 人用は除外 (= 感想戦のパーティが「いま居るメンバーでそのまま次にやれる」幅を狙う)。QuickMatchMetaStore.ScenarioMeta を引く
  4. locale フィルタ: viewer 集合全員の locales 積集合と scenario.Locale の overlap を要求 (QuickMatch と同じ思想)。viewer 0 (guest 経路) または全 viewer の locale 情報が無いときは skip。感想戦パーティ向けに韓国語 / 英語版 scenario の混入を防ぐ
  5. played ハードフィルタ (similarPlayedCountHardMax = 2): viewer のうち 2 人以上がプレイ済みの scenario は除外。0 / 1 人プレイ済みの scenario はカウントを playedCountByScenario map に詰めて PostRerank に渡す。viewer 0 のときは skip

meta / played / scorer / cache が nil のときは該当ステップを skip して、self-exclusion + Candidates だけで動かす safety-degrade を仕込んである。

1.2 Aggregate (scenario-to-scenario 類似度) ​

context scenario の embedding p_context と candidate の embedding p_candidate の内積で scoring する (viewer 非依存)。

score(s) = <p_context, p_s>

「context scenario を好む人たちが他にも好む scenario」という意味論を、Base Scoring (iALS) で学習済みの scenario embedding をそのまま再利用して表現する (spec §7.2 Scorer.ScenarioSimilarityBatch)。

context scenario の embedding が引けない (新作 scenario で未学習、Scorer.ScenarioSimilarityBatch が ErrScenarioEmbeddingNotFound を返す) ときは viewer 集団の mean (FOR_YOU 流用) にフォールバックする。これにより新作 scenario でも 0 件にはならず、admin preview の見え方も既存挙動を保つ。

1.3 PostRerank: half-half ミックス ​

top-K を「全員未プレイ」と「1 人だけプレイ済み」の 2 グループで半々に揃える (偶数 index は未プレイ優先、奇数 index は 1 人プレイ済み優先、どちらかが尽きたらもう一方で埋める)。ExcludeAlreadyPlayed=false または viewer 0 のときは mixing を効かせない。

1.4 設計の意図 ​

感想戦の文脈で「全員未プレイ」だけに絞ると、「いま遊んだ scenario の系統で次にやりたい」という要望に対して候補が足りなくなる。一方で「2 人以上プレイ済み」は新規体験としての面白みが薄い。そこで「0 人プレイ済み」と「1 人プレイ済み」を pre-filter で両方残しておき、post-rerank で half-half にすることで、新規性と「いま居るメンバーで遊べる現実性」のバランスを取っている。

Aggregate を viewer mean ではなく context scenario との内積にしたのは、感想戦は viewer の好みより「いま見ている scenario との関連性」のほうが UX 的に強いシグナルだから (= viewer 個別の preference は Base Scoring の embedding に既に蓄積されており、scenario-to-scenario の内積で十分関連性を出せる)。

2. 現状の実装ファイル ​

関心ファイル
field resolver (シナリオ詳細)backend/internal/adapters/graph/scenario.resolvers.go (SimilarScenarios)
query resolver (感想戦、targetUserIDs 受け)backend/internal/adapters/graph/scenario_query.resolvers.go (SimilarScenarios)
共通型 (SimilarRequest)backend/internal/usecase/recommendation/types.go
Strategybackend/internal/usecase/recommendation/surfaces/similar.go
Registry 登録backend/internal/usecase/recommendation/surfaces/registry.go
pipeline 入口 (SIMILAR の viewer 0 緩和を含む)backend/internal/usecase/recommendation/pipeline/recommender.go (Recommend)
Base Scoring scorer (ScenarioSimilarityBatch)backend/internal/usecase/recommendation/basescoring/scorer.go / score_matrix.go
meta store (人数フィルタ + locale 解決)backend/internal/usecase/quick_match_recommendation_meta.go

3. 既知の課題 / 未実装項目 ​

  • [ ] SIMILAR の reasons 自動生成 (例: because_scenario_X_similarity_0.87) は未実装
  • [ ] 観測指標 (流量 / プレイ後 Good 率 / 作成参加率 / CTR) は admin /recommendation/metrics から見られるが、北極星 metric の閾値設定は未確定
  • [ ] cold-start (新作 scenario の embedding 未学習) 時は viewer mean フォールバックで動くが、content-based モデル (UZU-4292) で本格対応する予定