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    let result = apply_diversity_rules(ranked, user_settings);
113
114    result
115}
116
117/// Ranqueia jogos usando apenas Collaborative Filtering
118pub 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    // Estágio 1: Filtros
125    let filtered = apply_hard_filters(candidates, user_settings);
126
127    // CF score puro (sem penalizações)
128    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    // Ordenar
144    scored.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
145
146    // Converter para formato com razão
147    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    // Estágio 6: Diversidade
162    apply_diversity_rules(with_reason, user_settings)
163}
164
165// === FUNÇÕES AUXILIARES ===
166
167fn 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}