X-Mentor is an e-Learning platform which not only tries to connect students and teachers, but also ideas, emotions and knowledge. We want people to progress and learn how to use their powers, because everyone has something to teach and everyone has something to learn, but everyone has a power and remember, with great power comes great responsability.
- X-Mentor - Where Heroes Learn
- Screenshots
- Stack
- Main features
- Architecture, Data Model and Domain Events
- How it works
- How to run it locally? Run the docker-compose.yml
- Scala/Play Framework/Akka Streams
- React
- Redis Graph
- RediStreams
- Redis Blooms
- Redis Gears
- RediSearch
- Redis JSON
- Redis TimeSeries
- Keycloak
- Login
- Sign Up
- Course Creation
- Course Enrollment
- Course Review
- Course Search
- Course Recommendation System
- Student's Interests
- Student Progress Registration
- Leaderboard
- Real Time Course Creation Notifications
The following picture gives a high level overview of the system architecture:
Our data model is expressed through nodes and relations using Redis Graph. The model is very simple: just Student, Course and Topic entities expressing different kind of relations between each other.
X-Mentor follows an Event Driven Architecture approach in which the following Domain Events are considered:
student-enrolledstudent-interestedstudent-interest-lostcourse-createdcourse-ratedcourse-recommendedstudent-progress-registered
Starts the authentication process against Keycloak
- Verifies if user's username already exists in
usersbloom filter - Gets auth token
- Verifies if username already exists in
usersbloom filter
BF.EXISTS users '${student.username}'
- Registering user against Keycloak
- Adds user's username to
usersbloom filter - Creates user in redisGraph
- Add student's timeseries key (needed for registering student progress)
- Adds username to
usersbloom filter
BF.ADD users '${student.username}'
- Creates student into the graph
GRAPH.QUERY xmentor "CREATE (:Student {username: '${student.username}', email: '${student.email}'})"
- Creates student progress timeseries key
TS.CREATE studentprogress:${username} RETENTION 0 LABELS student ${username}
Creates a course which is going to be stored as a JSON in redisJSON
- Gets the last course id from redis key
course-last-index - Increases course id key in 1
- Stores course as JSON in redisJSON
- Adds course id to
coursesbloom filter - Creates course in the graph
- Publishes
course-createdevent which sends notifications by Server Sent Event to the frontend
- Gets the last course id from redis key
course-last-index
GET course-last-index
- Increases course id key in 1
INCR course-last-index
- Stores course as JSON in redisJSON
JSON.SET course:${course.id} . '${course.asJson}'
- Adds course id to
coursesbloom filter
BF.ADD coourses '${course.id}'
- Creates course in the graph
GRAPH.QUERY xmentor "CREATE (:Course {name: '${course.title}', id: '${course.id.get}', preview: '${course.preview}'})"
- Publishes
course-createdevent which sends notifications by Server Sent Event to the frontend
XADD course-created $timestamp title ${course.title} topic ${course.topic}
Enrolls a student in a specific course
- Verifies if a student exists in
usersbloom filter - Gets course as JSON from redisJSON
- Creates studying relation between the student and the course in redisGraph
- Verifies if a student exists in
usersbloom filter
BF.EXISTS users ${student.username}
- Gets course as JSON from redisJSON
JSON.GET course:${course.id}
- Creates studying relation between the student and the course in redisGraph
GRAPH.QUERY xmentor "MATCH (s:Student), (c:Course) WHERE s.username = '${studying.student}' AND c.name = '${studying.course}' CREATE (s)-[:studying]->(c)"
It is the functionallity that allows a student to rate a course. For that purpose, it do the following:
- Verifies if a studying relation exists between the student and the course
- Verifies that a rates relation does not exists between the student and the course
- Creates the rate realation (see the diagramn below) in the graph.
- Publish event
course-ratedstream
The following diagram shows the interaction with Redis Graph and Redis Streams
The commands are used:
- Get courses by student
GRAPH.QUERY xmentor "MATCH (student)-[:studying]->(course) where student.username = '$student' RETURN course"
- Get courses rated by user
GRAPH.QUERY xmentor "MATCH (student)-[:rates]->(course) where student.username ='$student' RETURN course"
- Create rates relation in the graph
GRAPH.QUERY xmentor "MATCH (s:Student), (c:Course) WHERE s.username = '${rating.student}' AND c.name = '${rating.course}' CREATE (s)-[:rates {rating:${rating.stars}}]->(c)"
- Publish event to
course-ratedstream
XADD course-rated $timestamp student $student_username course $course starts $stars
Retrieves courses by query from redisJSON with rediSearch
FT.SEARCH courses-idx ${query}*
BF.EXISTS courses ${course.id}
JSON.GET course:${course.id}
GRAPH.QUERY xmentor "MATCH (student)-[:studying]->(course) where student.username = '$student' RETURN course"
FT.SEARCH courses-idx ${course.title}
- Gets all interested relations from redisGraph
- Gets difference between already existed relations and new ones (it allow us to separate new interests from existing ones and also to identify lost of interest)
- Creates new interested relations into redisGraph
- Removes interested relations that don't apply anymore
- Publishes to
student-interest-lostandstudent-interestedstream
The following diagram shows the interaction with Redis Graph and Redis Streams
- Get all student's interests
GRAPH.QUERY xmentor "MATCH (student)-[:interested]->(topic) WHERE student.username ='$student' RETURN topic"
- Create interest relation
GRAPH.QUERY xmentor "MATCH (s:Student), (t:Topic) WHERE s.username = '${interest.student}' AND t.name = '${interest.topic}' CREATE (s)-[:interested]->(t)"
- Delete interest relation
GRAPH.QUERY xmentor "MATCH (student)-[interest:interested]->(topic) WHERE student.username='${interest.student}' and topic.name='${interest.topic}' DELETE interest"
- Publishing to
student-interestedstream
XADD student-interested $timestamp student ${student.username} topic $topic
- Publishing to
student-interest-loststream
XADD student-interest-lost $timestamp student ${student.username} topic $topic
In order to implement a Course Recommendation System that suggest users different kind courses to take, we decided to rely on the power of Redis Graph. Searching for relations between nodes in the graph database give us an easy way to implement different king of recommendation strategies.
- Random select a course the student is enrolled in
- Get the topic of the course
- Look for students enrolled to the same course
- Look for courses of the same topic when those students are enrolled
- Recommend those courses.
- Random select a student's interest
- Look for students that are enrolled to course of that topic
- Look for other courses of the same topic we students are enrolled in
- Return the recommended courses (having into account those which the student isn't already enrolled)
- Get all topics
- Get student's interest topics
- Get topics the user is enrolled in
- Get a topic the user is neither interesting nor enrolled
- Get courses of that topic and recomend them
- All student's courses
GRAPH.QUERY xmentor "MATCH (student)-[:studying]->(course) where student.username = '$student' RETURN course"
- Get all topics
GRAPH.QUERY xmentor "MATCH (topic:Topic) RETURN topic"
- Get topic by course
GRAPH.QUERY xmentor "MATCH (topic:Topic)-[:has]->(course:Course) WHERE course.name = '$course' RETURN topic"
- Get students that are enrolled in (
studyingrelation) a course
GRAPH.QUERY xmentor "MATCH (student)-[:studying]->(course) WHERE course.name = '$course' RETURN student"
- Get courses by topic
GRAPH.QUERY xmentor "MATCH (topic)-[:has]->(course) WHERE topic.name = '${topic.name}' RETURN course"
- Get student's interests
GRAPH.QUERY xmentor "MATCH (student)-[:interested]->(topic) WHERE student.username ='$student' RETURN topic"
- Get courses the student is enrolled in by topic
GRAPH.QUERY xmentor "MATCH (student)-[:studying]->(course), (topic)-[:has]->(course) where student.username = '${student.username}' and topic.name = '${topic.name}' RETURN course"
- Get topics the user is enrolled in
GRAPH.QUERY xmentor "MATCH (student)-[:studying]->(course), (topic)-[:has]->(course) WHERE student.username = '${student.username}' RETURN topic"
This functionallity allow us to track the time the user spend in the platform watching courses. That info is then used to implement the Leaderboard.
x-mentor-core receives the request. Then, it publishes the Student Progress Registration Domain Event, which ends up as an element inside student-progress-registered stream (which is a Redis Stream) via the following command:
XADD student-progress-registered $timestamp student $student_username duration $duration
Redis Gears listen to elements pushed to the stream and then sinks this data into Redis TimeSeries using the following command:
TS.ADD studentprogress:$student_username $timestamp $duration RETENTION 0 LABELS student $student_username
Leaderboard is the functionality that allow us to have a board with the ranking of top students that uses X-Mentor. Top students are those who have more watching time using the platform. To accomplish that, we need to separate two functionallities:
- Register the student progress
- Getting the board data
When the user request for the leaderboard data, we first look at Redis for the time series keys
LRANGE student-progress-list 0 -1 // to retrieve all the list elements
For each key, we use Redis TimeSeries to get the range of samples in a time window of three months performing sum aggregation.
TS.RANGE $student_key $thee_months_back_timestamp $timestamp AGGREGATION sum 1000
where:
student_keyis the student's time series key. For example:studentprogress:codi.sipesis the time series key for studentcodi.sipes.three_months_back_timestampis aUnix Timestampwith represents a point in time three months back thantimestamp(in order to have a time window of three months).timestampthe current timestamp (inUnix Timestampformat).- We perform sum aggregation of the sample values in that time windows using a
Time Bucketof 1000 milliseconds.
That way we can get the accumulated watching time of every student. After that, we select the top 5 highest accumulated watching time and retrive that information to visualize the board.
- Docker Engine and Docker Compose
docker-compose up
Wait until Keycloak and x-mentor-core are ready, then go to http://localhost:3000. Welcome to X-Mentor!












