#![forbid(unsafe_code)]
#![warn(missing_docs)]
#![allow(clippy::module_name_repetitions)]
#![allow(clippy::similar_names)]
#![allow(clippy::cast_possible_truncation)]
#![allow(clippy::cast_sign_loss)]
#![allow(clippy::cast_precision_loss)]
#![allow(clippy::too_many_arguments)]
#![allow(clippy::too_many_lines)]
#![allow(clippy::missing_errors_doc)]
#![allow(dead_code)]
pub mod ab_test;
pub mod als;
pub mod bandits;
pub mod calibration;
pub mod cold_start;
pub mod collab_filter;
pub mod collaborative;
pub mod content;
pub mod content_based;
pub mod context_signal;
pub mod decay_model;
pub mod dense_linalg;
pub mod diversity;
pub mod error;
pub mod explain;
pub mod exploration_policy;
pub mod feature_store;
pub mod feedback_signal;
pub mod freshness;
pub mod history;
pub mod hybrid;
pub mod impression_tracker;
pub mod item_similarity;
pub mod lsh;
pub mod personalize;
pub mod popularity_bias;
pub mod profile;
pub mod rank;
pub mod ranking;
pub mod rating;
pub mod recommendation_score;
pub mod score_cache;
pub mod sequence_model;
pub mod session;
pub mod svd_pp;
pub mod trending;
pub mod user_profile;
pub use error::{RecommendError, RecommendResult};
use serde::{Deserialize, Serialize};
use uuid::Uuid;
pub struct RecommendationEngine {
content_recommender: content::similarity::ContentRecommender,
collaborative_engine: collaborative::matrix::CollaborativeEngine,
hybrid_combiner: hybrid::combine::HybridCombiner,
profile_manager: profile::user::UserProfileManager,
history_tracker: history::track::HistoryTracker,
rating_manager: rating::explicit::RatingManager,
trending_detector: trending::detect::TrendingDetector,
personalization_engine: personalize::engine::PersonalizationEngine,
diversity_enforcer: diversity::ensure::DiversityEnforcer,
freshness_balancer: freshness::balance::FreshnessBalancer,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RecommendationRequest {
pub user_id: Uuid,
pub limit: usize,
pub content_id: Option<Uuid>,
pub strategy: RecommendationStrategy,
pub context: RecommendationContext,
pub diversity: DiversitySettings,
pub include_explanations: bool,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
pub enum RecommendationStrategy {
ContentBased,
Collaborative,
Hybrid,
Personalized,
Trending,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct RecommendationContext {
pub timestamp: Option<i64>,
pub device: Option<String>,
pub location: Option<String>,
pub session_id: Option<Uuid>,
pub time_of_day: Option<TimeOfDay>,
pub day_of_week: Option<u8>,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
pub enum TimeOfDay {
Morning,
Afternoon,
Evening,
Night,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DiversitySettings {
pub enabled: bool,
pub category_diversity: f32,
pub include_serendipity: bool,
pub serendipity_weight: f32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Recommendation {
pub content_id: Uuid,
pub score: f32,
pub rank: usize,
pub reasons: Vec<RecommendationReason>,
pub metadata: ContentMetadata,
pub explanation: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum RecommendationReason {
SimilarToLiked {
content_id: Uuid,
similarity: f32,
},
CollaborativeFiltering {
confidence: f32,
},
Trending {
trending_score: f32,
},
MatchesProfile {
categories: Vec<String>,
},
FreshContent {
published_days_ago: u32,
},
Popular {
view_count: u64,
},
ContinueWatching {
progress: f32,
},
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ContentMetadata {
pub title: String,
pub description: Option<String>,
pub categories: Vec<String>,
pub duration_ms: Option<i64>,
pub thumbnail_url: Option<String>,
pub created_at: i64,
pub avg_rating: Option<f32>,
pub view_count: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RecommendationResults {
pub user_id: Uuid,
pub recommendations: Vec<Recommendation>,
pub total_candidates: usize,
pub processing_time_ms: u64,
pub strategy: RecommendationStrategy,
}
impl RecommendationEngine {
#[must_use]
pub fn new() -> Self {
Self {
content_recommender: content::similarity::ContentRecommender::new(),
collaborative_engine: collaborative::matrix::CollaborativeEngine::new(),
hybrid_combiner: hybrid::combine::HybridCombiner::new(),
profile_manager: profile::user::UserProfileManager::new(),
history_tracker: history::track::HistoryTracker::new(),
rating_manager: rating::explicit::RatingManager::new(),
trending_detector: trending::detect::TrendingDetector::new(),
personalization_engine: personalize::engine::PersonalizationEngine::new(),
diversity_enforcer: diversity::ensure::DiversityEnforcer::new(),
freshness_balancer: freshness::balance::FreshnessBalancer::new(0.3, 30),
}
}
pub fn recommend(
&self,
request: &RecommendationRequest,
) -> RecommendResult<RecommendationResults> {
use std::collections::HashMap;
let start = std::time::Instant::now();
let candidates = match request.strategy {
RecommendationStrategy::ContentBased => {
self.get_content_based_recommendations(request)?
}
RecommendationStrategy::Collaborative => {
self.get_collaborative_recommendations(request)?
}
RecommendationStrategy::Hybrid => self.get_hybrid_parallel(request)?,
RecommendationStrategy::Personalized => {
self.get_personalized_recommendations(request)?
}
RecommendationStrategy::Trending => self.get_trending_recommendations(request)?,
};
let candidates = {
let mut best: HashMap<uuid::Uuid, Recommendation> = HashMap::new();
for rec in candidates {
let entry = best.entry(rec.content_id);
entry
.and_modify(|existing| {
if rec.score > existing.score {
*existing = rec.clone();
}
})
.or_insert(rec);
}
let mut deduped: Vec<Recommendation> = best.into_values().collect();
deduped.sort_by(|a, b| {
b.score
.partial_cmp(&a.score)
.unwrap_or(std::cmp::Ordering::Equal)
});
deduped
};
let diverse_candidates = if request.diversity.enabled {
self.diversity_enforcer
.enforce_diversity(candidates, &request.diversity)?
} else {
candidates
};
let balanced_candidates = self.freshness_balancer.balance(diverse_candidates)?;
let mut ranked = self.rank_recommendations(balanced_candidates)?;
ranked.truncate(request.limit);
if request.include_explanations {
self.add_explanations(&mut ranked)?;
}
let processing_time_ms = start.elapsed().as_millis() as u64;
let total_candidates = ranked.len();
Ok(RecommendationResults {
user_id: request.user_id,
recommendations: ranked,
total_candidates,
processing_time_ms,
strategy: request.strategy,
})
}
fn get_content_based_recommendations(
&self,
request: &RecommendationRequest,
) -> RecommendResult<Vec<Recommendation>> {
self.content_recommender.recommend(request)
}
fn get_collaborative_recommendations(
&self,
request: &RecommendationRequest,
) -> RecommendResult<Vec<Recommendation>> {
self.collaborative_engine.recommend(request)
}
fn get_hybrid_recommendations(
&self,
request: &RecommendationRequest,
) -> RecommendResult<Vec<Recommendation>> {
self.hybrid_combiner.recommend(request)
}
fn get_hybrid_parallel(
&self,
request: &RecommendationRequest,
) -> RecommendResult<Vec<Recommendation>> {
use rayon::prelude::*;
let strategies: &[RecommendationStrategy] = &[
RecommendationStrategy::ContentBased,
RecommendationStrategy::Collaborative,
RecommendationStrategy::Personalized,
RecommendationStrategy::Trending,
];
let parallel_results: Vec<Vec<Recommendation>> = strategies
.par_iter()
.filter_map(|strategy| {
let result = match strategy {
RecommendationStrategy::ContentBased => {
self.get_content_based_recommendations(request)
}
RecommendationStrategy::Collaborative => {
self.get_collaborative_recommendations(request)
}
RecommendationStrategy::Personalized => {
self.get_personalized_recommendations(request)
}
RecommendationStrategy::Trending => self.get_trending_recommendations(request),
RecommendationStrategy::Hybrid => return None,
};
result.ok()
})
.collect();
let combiner_result = self.hybrid_combiner.recommend(request).unwrap_or_default();
let mut merged: Vec<Recommendation> = parallel_results.into_iter().flatten().collect();
merged.extend(combiner_result);
Ok(merged)
}
fn get_personalized_recommendations(
&self,
request: &RecommendationRequest,
) -> RecommendResult<Vec<Recommendation>> {
self.personalization_engine.recommend(request)
}
fn get_trending_recommendations(
&self,
request: &RecommendationRequest,
) -> RecommendResult<Vec<Recommendation>> {
self.trending_detector.get_trending(request.limit)
}
fn rank_recommendations(
&self,
candidates: Vec<Recommendation>,
) -> RecommendResult<Vec<Recommendation>> {
rank::score::rank_recommendations(candidates)
}
fn add_explanations(&self, recommendations: &mut [Recommendation]) -> RecommendResult<()> {
for rec in recommendations {
let explanation = explain::generate::generate_explanation(rec)?;
rec.explanation = Some(explanation);
}
Ok(())
}
pub fn record_view(
&mut self,
user_id: Uuid,
content_id: Uuid,
watch_time_ms: i64,
completed: bool,
) -> RecommendResult<()> {
self.history_tracker
.record_view(user_id, content_id, watch_time_ms, completed)?;
self.profile_manager
.update_from_view(user_id, content_id, watch_time_ms, completed)?;
self.rating_manager.update_implicit_rating(
user_id,
content_id,
watch_time_ms,
completed,
)?;
Ok(())
}
pub fn record_rating(
&mut self,
user_id: Uuid,
content_id: Uuid,
rating: f32,
) -> RecommendResult<()> {
self.rating_manager
.record_rating(user_id, content_id, rating)?;
self.profile_manager
.update_from_rating(user_id, content_id, rating)?;
Ok(())
}
pub fn update_trending(&mut self) -> RecommendResult<()> {
self.trending_detector.update_scores()
}
pub fn get_user_profile(&self, user_id: Uuid) -> RecommendResult<profile::user::UserProfile> {
self.profile_manager.get_profile(user_id)
}
pub fn get_similar_users(&self, user_id: Uuid, limit: usize) -> RecommendResult<Vec<Uuid>> {
self.profile_manager.get_similar_users(user_id, limit)
}
}
impl Default for RecommendationEngine {
fn default() -> Self {
Self::new()
}
}
impl Default for RecommendationRequest {
fn default() -> Self {
Self {
user_id: Uuid::new_v4(),
limit: 10,
content_id: None,
strategy: RecommendationStrategy::Hybrid,
context: RecommendationContext::default(),
diversity: DiversitySettings::default(),
include_explanations: false,
}
}
}
impl Default for DiversitySettings {
fn default() -> Self {
Self {
enabled: true,
category_diversity: 0.3,
include_serendipity: true,
serendipity_weight: 0.1,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_recommendation_engine_creation() {
let engine = RecommendationEngine::new();
assert!(std::mem::size_of_val(&engine) > 0);
}
#[test]
fn test_recommendation_request_default() {
let request = RecommendationRequest::default();
assert_eq!(request.limit, 10);
assert!(matches!(request.strategy, RecommendationStrategy::Hybrid));
}
#[test]
fn test_diversity_settings_default() {
let settings = DiversitySettings::default();
assert!(settings.enabled);
assert!((settings.category_diversity - 0.3).abs() < f32::EPSILON);
}
#[test]
fn test_recommendation_strategy_variants() {
let strategies = [
RecommendationStrategy::ContentBased,
RecommendationStrategy::Collaborative,
RecommendationStrategy::Hybrid,
RecommendationStrategy::Personalized,
RecommendationStrategy::Trending,
];
assert_eq!(strategies.len(), 5);
}
#[test]
fn test_recommend_hybrid_parallel_succeeds() {
let engine = RecommendationEngine::new();
let request = RecommendationRequest {
strategy: RecommendationStrategy::Hybrid,
limit: 10,
..Default::default()
};
let result = engine.recommend(&request);
assert!(result.is_ok());
assert!(matches!(
result.expect("hybrid recommend should succeed").strategy,
RecommendationStrategy::Hybrid
));
}
#[test]
fn test_recommend_all_strategies_run() {
let engine = RecommendationEngine::new();
for strategy in [
RecommendationStrategy::ContentBased,
RecommendationStrategy::Collaborative,
RecommendationStrategy::Hybrid,
RecommendationStrategy::Personalized,
RecommendationStrategy::Trending,
] {
let request = RecommendationRequest {
strategy,
limit: 5,
..Default::default()
};
let result = engine.recommend(&request);
assert!(result.is_ok(), "strategy {strategy:?} failed");
}
}
}