game_manager_lib/services/recommendation/
ranking.rs1use super::core::*;
9use super::filtering::{apply_diversity_rules, apply_hard_filters};
10use super::scoring::{normalize_score, score_game_cb};
11use std::collections::{HashMap, HashSet};
12
13pub fn rank_games_hybrid(
15 profile: &UserPreferenceVector,
16 candidates: &[GameWithDetails],
17 cf_scores: &HashMap<u32, f32>,
18 ignored_ids: &HashSet<String>,
19 config: RecommendationConfig,
20 user_settings: UserSettings,
21) -> Vec<(GameWithDetails, f32, RecommendationReason)> {
22 let filtered = apply_hard_filters(candidates, &user_settings);
24
25 let raw_results: Vec<_> = filtered
27 .iter()
28 .filter(|g| !ignored_ids.contains(&g.game.id))
29 .map(|g| {
30 let (cb_score, cb_reason) = score_game_cb(profile, g, &config);
31
32 let cf_score = g
33 .steam_app_id
34 .and_then(|id| cf_scores.get(&id))
35 .cloned()
36 .unwrap_or(0.0);
37
38 (g.clone(), cb_score, cf_score, cb_reason)
39 })
40 .collect();
41
42 let max_cb = raw_results
44 .iter()
45 .map(|(_, c, _, _)| *c)
46 .fold(0.0, f32::max);
47 let max_cf = raw_results
48 .iter()
49 .map(|(_, _, c, _)| *c)
50 .fold(0.0, f32::max);
51
52 let mut ranked: Vec<_> = raw_results
54 .into_iter()
55 .filter_map(|(g, cb, cf, cb_reason)| {
56 if cb == 0.0 && cf == 0.0 {
57 return None;
58 }
59
60 let cb_n = normalize_score(cb, max_cb);
61 let cf_n = normalize_score(cf, max_cf);
62
63 let weighted_cb = cb_n * config.content_weight;
64 let weighted_cf = cf_n * config.collaborative_weight;
65
66 let final_score = weighted_cb + weighted_cf;
67
68 let reason = determine_hybrid_reason(weighted_cb, weighted_cf, cb_reason);
70
71 Some((g, final_score, reason))
72 })
73 .collect();
74
75 ranked.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
77
78 apply_diversity_rules(ranked, &user_settings)
80}
81
82pub fn rank_games_content_based(
84 profile: &UserPreferenceVector,
85 candidates: &[GameWithDetails],
86 config: &RecommendationConfig,
87 user_settings: &UserSettings,
88) -> Vec<(GameWithDetails, f32, RecommendationReason)> {
89 let filtered = apply_hard_filters(candidates, user_settings);
91
92 let mut ranked: Vec<_> = filtered
94 .iter()
95 .map(|g| {
96 let (score, reason) = score_game_cb(profile, g, config);
97
98 let final_reason = reason.unwrap_or(RecommendationReason {
99 label: "Baseado no seu perfil".to_string(),
100 type_id: "general".to_string(),
101 });
102
103 (g.clone(), score, final_reason)
104 })
105 .filter(|(_, score, _)| *score > 0.0)
106 .collect();
107
108 ranked.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
110
111 apply_diversity_rules(ranked, user_settings)
113}
114
115pub fn rank_games_collaborative(
117 candidates: &[GameWithDetails],
118 cf_scores: &HashMap<u32, f32>,
119 ignored_ids: &HashSet<String>,
120 user_settings: &UserSettings,
121) -> Vec<(GameWithDetails, f32, RecommendationReason)> {
122 let filtered = apply_hard_filters(candidates, user_settings);
124
125 let mut scored: Vec<_> = filtered
127 .iter()
128 .filter(|g| !ignored_ids.contains(&g.game.id))
129 .filter_map(|g| {
130 let steam_id = g.steam_app_id?;
131 let score = cf_scores.get(&steam_id).cloned()?;
132
133 if score <= 0.0 {
134 return None;
135 }
136
137 Some((g.clone(), score))
138 })
139 .collect();
140
141 scored.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
143
144 let with_reason: Vec<_> = scored
146 .into_iter()
147 .map(|(g, score)| {
148 (
149 g,
150 score,
151 RecommendationReason {
152 label: "Tendência na Comunidade".to_string(),
153 type_id: "community".to_string(),
154 },
155 )
156 })
157 .collect();
158
159 apply_diversity_rules(with_reason, user_settings)
161}
162
163fn determine_hybrid_reason(
166 weighted_cb: f32,
167 weighted_cf: f32,
168 cb_reason: Option<RecommendationReason>,
169) -> RecommendationReason {
170 match (weighted_cb > 0.0, weighted_cf > 0.0) {
171 (true, true) => RecommendationReason {
172 label: "Afinidade + Popular na comunidade".to_string(),
173 type_id: "hybrid".to_string(),
174 },
175 (false, true) => RecommendationReason {
176 label: "Popular na comunidade".to_string(),
177 type_id: "community".to_string(),
178 },
179 _ => cb_reason.unwrap_or(RecommendationReason {
180 label: "Baseado no seu perfil".to_string(),
181 type_id: "general".to_string(),
182 }),
183 }
184}