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