Skip to main content

game_manager_lib/services/recommendation/
ranking.rs

1//! Sistema de Ranqueamento de Recomendações
2//!
3//! Este módulo implementa os diferentes algoritmos de ranqueamento:
4//! - Híbrido (CB + CF)
5//! - Content-Based puro
6//! - Collaborative Filtering puro
7
8use 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
13/// Ranqueia jogos usando abordagem híbrida (CB + CF)
14pub 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    // Estágio 1: Filtros duros
23    let filtered = apply_hard_filters(candidates, &user_settings);
24
25    // Estágio 2-3: Calcular scores CB e CF
26    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    // Estágio 4: Normalização
43    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    // Estágio 5: Combinação ponderada
53    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            // Determinar razão final
69            let reason = determine_hybrid_reason(weighted_cb, weighted_cf, cb_reason);
70
71            Some((g, final_score, reason))
72        })
73        .collect();
74
75    // Ordenar por score
76    ranked.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
77
78    // Estágio 6: Aplicar regras de diversidade
79    apply_diversity_rules(ranked, &user_settings)
80}
81
82/// Ranqueia jogos usando apenas Content-Based
83pub fn rank_games_content_based(
84    profile: &UserPreferenceVector,
85    candidates: &[GameWithDetails],
86    config: &RecommendationConfig,
87    user_settings: &UserSettings,
88) -> Vec<(GameWithDetails, f32, RecommendationReason)> {
89    // Estágio 1: Filtros
90    let filtered = apply_hard_filters(candidates, user_settings);
91
92    // Estágios 2-3: CB score
93    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    // Ordenar
109    ranked.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
110
111    // Estágio 6: Diversidade
112    apply_diversity_rules(ranked, user_settings)
113}
114
115/// Ranqueia jogos usando apenas Collaborative Filtering
116pub 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    // Estágio 1: Filtros
123    let filtered = apply_hard_filters(candidates, user_settings);
124
125    // CF score puro (sem penalizações)
126    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    // Ordenar
142    scored.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
143
144    // Converter para formato com razão
145    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    // Estágio 6: Diversidade
160    apply_diversity_rules(with_reason, user_settings)
161}
162
163// === FUNÇÕES AUXILIARES ===
164
165fn 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}