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-enrolled
student-interested
student-interest-lost
course-created
course-rated
course-recommended
student-progress-registered
Starts the authentication process against Keycloak
- Verifies if user's username already exists in
users
bloom filter - Gets auth token
- Verifies if username already exists in
users
bloom filter
BF.EXISTS users '${student.username}'
- Registering user against Keycloak
- Adds user's username to
users
bloom filter - Creates user in redisGraph
- Add student's timeseries key (needed for registering student progress)
- Adds username to
users
bloom 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
courses
bloom filter - Creates course in the graph
- Publishes
course-created
event 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
courses
bloom 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-created
event 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
users
bloom filter - Gets course as JSON from redisJSON
- Creates studying relation between the student and the course in redisGraph
- Verifies if a student exists in
users
bloom 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-rated
stream
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-rated
stream
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-lost
andstudent-interested
stream
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-interested
stream
XADD student-interested $timestamp student ${student.username} topic $topic
- Publishing to
student-interest-lost
stream
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 (
studying
relation) 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_key
is the student's time series key. For example:studentprogress:codi.sipes
is the time series key for studentcodi.sipes
.three_months_back_timestamp
is aUnix Timestamp
with represents a point in time three months back thantimestamp
(in order to have a time window of three months).timestamp
the current timestamp (inUnix Timestamp
format).- We perform sum aggregation of the sample values in that time windows using a
Time Bucket
of 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!