campverse-backend / Controller /recommendation.js
CampVerse CI
Deploy backend from GitHub Actions
89a74d9
Raw History Blame Contribute Delete
13.2 kB
const Event = require('../Models/Event');
const User = require('../Models/User');
const EventParticipationLog = require('../Models/EventParticipationLog');
const axios = require('axios');
const logger = require('winston');
const { config } = require('../config/environment');
// ML API configuration
const ML_API_URL = config.ml.apiUrl;
const ML_RECOMMENDATION_ENABLED = config.ml.recommendationEnabled;
const ML_API_TIMEOUT = config.ml.timeout;
/**
* Get personalized event recommendations for a user
*/
async function getEventRecommendations(req, res) {
try {
const userId = req.user.id;
const { limit = 10, page = 1 } = req.query;
// Get user profile data
const user = await User.findById(userId);
if (!user) {
return res.status(404).json({ error: 'User not found' });
}
// Get user's event history
const userHistory = await EventParticipationLog.find({ userId })
.populate('eventId', 'title description tags type organizer date')
.sort({ timestamp: -1 })
.limit(20);
// Get user's interests and skills
const userProfile = {
userId: user._id,
userName: user.name,
userEmail: user.email,
interests: user.interests || [],
skills: user.skills || [],
attendedEvents: userHistory
.filter((log) => log.eventId && log.status === 'attended')
.map((log) => ({
eventId: log.eventId._id,
title: log.eventId.title,
type: log.eventId.type,
tags: log.eventId.tags || [],
organizer: log.eventId.organizer,
})),
registeredEvents: userHistory
.filter((log) => log.eventId && log.status === 'registered')
.map((log) => ({
eventId: log.eventId._id,
title: log.eventId.title,
type: log.eventId.type,
tags: log.eventId.tags || [],
organizer: log.eventId.organizer,
})),
};
// Get available events for recommendation
const maxCandidates = Math.min(parseInt(limit) * 50, 500);
const availableEvents = await Event.find({
verificationStatus: 'approved',
date: { $gte: new Date() },
})
.select('title description tags type organizer schedule hostUserId institutionId date')
.sort({ date: 1 })
.limit(maxCandidates);
// Prepare data for ML API
const recommendationData = {
userProfile,
availableEvents: availableEvents.map((event) => ({
eventId: event._id,
title: event.title,
description: event.description,
type: event.type,
tags: event.tags || [],
organizer: event.organizer,
date: event.date,
})),
limit: parseInt(limit),
page: parseInt(page),
};
// Call ML API for recommendations
try {
// Check if ML recommendations are enabled
if (!ML_RECOMMENDATION_ENABLED) {
logger.info('ML recommendations disabled, using fallback');
const fallbackRecommendations = await getFallbackRecommendations(
userProfile,
availableEvents,
limit,
);
return res.json({
recommendations: fallbackRecommendations,
pagination: {
page: parseInt(page),
limit: parseInt(limit),
total: fallbackRecommendations.length,
},
userProfile: {
interests: userProfile.interests,
skills: userProfile.skills,
attendedCount: userProfile.attendedEvents.length,
registeredCount: userProfile.registeredEvents.length,
},
note: 'Using fallback recommendations (ML feature disabled)',
});
}
const mlResponse = await axios.post(
`${ML_API_URL}/recommend`,
recommendationData,
{
timeout: ML_API_TIMEOUT,
},
);
const recommendations = mlResponse.data.recommendations || [];
// Get full event details for recommended events
const recommendedEventIds = recommendations.map((rec) => rec.eventId);
const recommendedEvents = await Event.find({
_id: { $in: recommendedEventIds },
}).populate('hostUserId', 'name email');
// Map recommendations with full event details
const finalRecommendations = recommendations
.map((rec) => {
if (!rec.eventId) return null;
const eventIdStr = rec.eventId.toString();
const event = recommendedEvents.find(
(e) => e && e._id && e._id.toString() === eventIdStr,
);
return {
eventId: rec.eventId,
similarityScore: rec.similarityScore,
reason: rec.reason,
event: event
? {
_id: event._id,
title: event.title,
description: event.description,
tags: event.tags,
type: event.type,
organizer: event.organizer,
schedule: event.schedule,
host: event.hostUserId,
verificationStatus: event.verificationStatus,
isPaid: event.isPaid,
price: event.price,
}
: null,
};
})
.filter((rec) => rec.event !== null);
return res.json({
recommendations: finalRecommendations,
pagination: {
page: parseInt(page),
limit: parseInt(limit),
total: finalRecommendations.length,
},
userProfile: {
interests: userProfile.interests,
skills: userProfile.skills,
attendedCount: userProfile.attendedEvents.length,
registeredCount: userProfile.registeredEvents.length,
},
});
} catch (mlError) {
logger.error('ML API error:', mlError);
// Fallback: return events based on user interests
const fallbackRecommendations = await getFallbackRecommendations(
userProfile,
availableEvents,
limit,
);
return res.json({
recommendations: fallbackRecommendations,
pagination: {
page: parseInt(page),
limit: parseInt(limit),
total: fallbackRecommendations.length,
},
userProfile: {
interests: userProfile.interests,
skills: userProfile.skills,
attendedCount: userProfile.attendedEvents.length,
registeredCount: userProfile.registeredEvents.length,
},
note: 'Using fallback recommendations (ML API unavailable)',
});
}
} catch (error) {
logger.error('Event recommendation error:', error);
return res
.status(500)
.json({ error: 'Error getting event recommendations' });
}
}
/**
* Fallback recommendation system based on user interests
*/
async function getFallbackRecommendations(userProfile, availableEvents, limit) {
const userInterests = userProfile.interests.map((interest) =>
interest.toLowerCase(),
);
const userSkills = userProfile.skills.map((skill) => skill.toLowerCase());
// Score events based on user interests and skills
const scoredEvents = availableEvents.map((event) => {
let score = 0;
const eventTags = (event.tags || []).map((tag) => tag.toLowerCase());
const eventType = (event.type || '').toLowerCase();
const eventDescription = (event.description || '').toLowerCase();
// Score based on interests
userInterests.forEach((interest) => {
if (eventTags.includes(interest)) score += 3;
if (eventType.includes(interest)) score += 2;
if (eventDescription.includes(interest)) score += 1;
});
// Score based on skills
userSkills.forEach((skill) => {
if (eventTags.includes(skill)) score += 2;
if (eventType.includes(skill)) score += 1;
});
// Bonus for events from same organizer as attended events
const attendedOrganizers = userProfile.attendedEvents.map(
(e) => e.organizer,
);
if (attendedOrganizers.includes(event.organizer)) score += 1;
return {
eventId: event._id,
similarityScore: score,
reason:
score > 0 ? 'Based on your interests and skills' : 'Popular events',
event: {
_id: event._id,
title: event.title,
description: event.description,
tags: event.tags,
type: event.type,
organizer: event.organizer,
schedule: event.schedule,
},
};
});
// Sort by score and return top recommendations
return scoredEvents
.sort((a, b) => b.similarityScore - a.similarityScore)
.slice(0, parseInt(limit));
}
/**
* Get similar events based on a specific event
*/
async function getSimilarEvents(req, res) {
try {
const { eventId } = req.params;
const { limit = 5 } = req.query;
const event = await Event.findById(eventId);
if (!event) {
return res.status(404).json({ error: 'Event not found' });
}
// Find events with similar tags, type, or organizer
const similarEvents = await Event.find({
_id: { $ne: eventId },
verificationStatus: 'approved',
date: { $gte: new Date() },
$or: [
{ tags: { $in: event.tags || [] } },
{ type: event.type },
{ organizer: event.organizer },
],
})
.populate('hostUserId', 'name email')
.limit(parseInt(limit))
.sort({ date: 1 });
const recommendations = similarEvents.map((similarEvent) => ({
eventId: similarEvent._id,
similarityScore: calculateSimilarityScore(event, similarEvent),
reason: getSimilarityReason(event, similarEvent),
event: {
_id: similarEvent._id,
title: similarEvent.title,
description: similarEvent.description,
tags: similarEvent.tags,
type: similarEvent.type,
organizer: similarEvent.organizer,
date: similarEvent.date,
host: similarEvent.hostUserId,
verificationStatus: similarEvent.verificationStatus,
isPaid: similarEvent.isPaid,
price: similarEvent.price,
},
}));
return res.json({
baseEvent: {
_id: event._id,
title: event.title,
description: event.description,
tags: event.tags,
type: event.type,
organizer: event.organizer,
},
similarEvents: recommendations,
});
} catch (error) {
logger.error('Similar events error:', error);
return res.status(500).json({ error: 'Error getting similar events' });
}
}
/**
* Calculate similarity score between two events
*/
function calculateSimilarityScore(event1, event2) {
let score = 0;
// Tag similarity
const tags1 = new Set(event1.tags || []);
const tags2 = new Set(event2.tags || []);
const tagIntersection = new Set([...tags1].filter((x) => tags2.has(x)));
score += tagIntersection.size * 2;
// Type similarity
if (event1.type === event2.type) score += 3;
// Organizer similarity
if (event1.organizer === event2.organizer) score += 2;
return score;
}
/**
* Get reason for similarity
*/
function getSimilarityReason(event1, event2) {
const reasons = [];
if (event1.type === event2.type) {
reasons.push('Same event type');
}
const tags1 = new Set(event1.tags || []);
const tags2 = new Set(event2.tags || []);
const commonTags = [...tags1].filter((x) => tags2.has(x));
if (commonTags.length > 0) {
reasons.push(`Similar tags: ${commonTags.join(', ')}`);
}
if (event1.organizer === event2.organizer) {
reasons.push('Same organizer');
}
return reasons.join(', ');
}
/**
* Update user preferences based on event interaction
*/
async function updateUserPreferences(req, res) {
try {
const userId = req.user.id;
const { eventId, action } = req.body; // action: 'view', 'like', 'attend', 'register'
const user = await User.findById(userId);
if (!user) {
return res.status(404).json({ error: 'User not found' });
}
const event = await Event.findById(eventId);
if (!event) {
return res.status(404).json({ error: 'Event not found' });
}
// Update user preferences based on action
const updatedInterests = [...(user.interests || [])];
const updatedSkills = [...(user.skills || [])];
if (action === 'like' || action === 'attend') {
// Add event tags to interests if not already present
(event.tags || []).forEach((tag) => {
if (!updatedInterests.includes(tag)) {
updatedInterests.push(tag);
}
});
// Add event type to interests if not already present
if (event.type && !updatedInterests.includes(event.type)) {
updatedInterests.push(event.type);
}
}
// Update user profile
await User.findByIdAndUpdate(userId, {
interests: updatedInterests,
skills: updatedSkills,
});
return res.json({
message: 'User preferences updated successfully',
updatedInterests,
updatedSkills,
});
} catch (error) {
logger.error('Update user preferences error:', error);
return res.status(500).json({ error: 'Error updating user preferences' });
}
}
module.exports = {
getEventRecommendations,
getSimilarEvents,
updateUserPreferences,
};