#![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 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 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 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 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> {
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_recommendations(request)?,
RecommendationStrategy::Personalized => {
self.get_personalized_recommendations(request)?
}
RecommendationStrategy::Trending => self.get_trending_recommendations(request)?,
};
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_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);
}
}