Spaces:
Paused
Paused
Download Controller/recommendation.js from imkrish/campverse-backend: direct link, hf CLI and curl.
- Browser
- Download file 13.2 kB
-
https://huggingface.co/spaces/imkrish/campverse-backend/resolve/main/Controller/recommendation.js
- Command line
-
hf download hf://spaces/imkrish/campverse-backend/Controller/recommendation.js
-
curl -L -o recommendation.js https://huggingface.co/spaces/imkrish/campverse-backend/resolve/main/Controller/recommendation.js
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, | |
| }; | |