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 let result = apply_diversity_rules(ranked, user_settings);
113
114 result
115}
116
117pub fn rank_games_collaborative(
119 candidates: &[GameWithDetails],
120 cf_scores: &HashMap<u32, f32>,
121 ignored_ids: &HashSet<String>,
122 user_settings: &UserSettings,
123) -> Vec<(GameWithDetails, f32, RecommendationReason)> {
124 let filtered = apply_hard_filters(candidates, user_settings);
126
127 let mut scored: Vec<_> = filtered
129 .iter()
130 .filter(|g| !ignored_ids.contains(&g.game.id))
131 .filter_map(|g| {
132 let steam_id = g.steam_app_id?;
133 let score = cf_scores.get(&steam_id).cloned()?;
134
135 if score <= 0.0 {
136 return None;
137 }
138
139 Some((g.clone(), score))
140 })
141 .collect();
142
143 scored.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
145
146 let with_reason: Vec<_> = scored
148 .into_iter()
149 .map(|(g, score)| {
150 (
151 g,
152 score,
153 RecommendationReason {
154 label: "Tendência na Comunidade".to_string(),
155 type_id: "community".to_string(),
156 },
157 )
158 })
159 .collect();
160
161 apply_diversity_rules(with_reason, user_settings)
163}
164
165fn determine_hybrid_reason(
168 weighted_cb: f32,
169 weighted_cf: f32,
170 cb_reason: Option<RecommendationReason>,
171) -> RecommendationReason {
172 match (weighted_cb > 0.0, weighted_cf > 0.0) {
173 (true, true) => RecommendationReason {
174 label: "Afinidade + Popular na comunidade".to_string(),
175 type_id: "hybrid".to_string(),
176 },
177 (false, true) => RecommendationReason {
178 label: "Popular na comunidade".to_string(),
179 type_id: "community".to_string(),
180 },
181 _ => cb_reason.unwrap_or(RecommendationReason {
182 label: "Baseado no seu perfil".to_string(),
183 type_id: "general".to_string(),
184 }),
185 }
186}