Skip to content

Commit dfb6238

Browse files
committed
feat : implement recommendation system with user skill analysis
1 parent f97a659 commit dfb6238

File tree

9 files changed

+225
-1
lines changed

9 files changed

+225
-1
lines changed
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
package com.rajat_singh.leetcode_api.controller;
2+
3+
import com.rajat_singh.leetcode_api.dto.RecommendationResponse;
4+
import com.rajat_singh.leetcode_api.dto.RecommendedQuestionDTO;
5+
import com.rajat_singh.leetcode_api.service.RecommendationService;
6+
import lombok.RequiredArgsConstructor;
7+
import org.springframework.beans.factory.annotation.Autowired;
8+
import org.springframework.http.ResponseEntity;
9+
import org.springframework.web.bind.annotation.GetMapping;
10+
import org.springframework.web.bind.annotation.PathVariable;
11+
import org.springframework.web.bind.annotation.RequestMapping;
12+
import org.springframework.web.bind.annotation.RestController;
13+
14+
@RestController()
15+
@RequestMapping("/api/v1/recommendation")
16+
@RequiredArgsConstructor
17+
public class RecommendationController {
18+
19+
@Autowired
20+
private final RecommendationService recommendationService;
21+
22+
@GetMapping("/{username}")
23+
public ResponseEntity<RecommendationResponse> getAiPoweredRecommendations(@PathVariable String username){
24+
RecommendationResponse response;
25+
try{
26+
response = recommendationService.getRecommendations(username);
27+
return ResponseEntity.ok(response);
28+
}
29+
catch (Exception e){
30+
return ResponseEntity.badRequest().build();
31+
}
32+
}
33+
34+
}

src/main/java/com/rajat_singh/leetcode_api/controller/UserController.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ public ResponseEntity<UserSkillStatsResponse.MatchedUser> getUserSkillStats(@Pat
4848
Logger.info("getUserSkillStats() method called with username :: {}", username);
4949
return leetCodeService.getUserSkillStats(username)
5050
.map(ResponseEntity::ok).orElse(ResponseEntity.notFound().build());
51-
};
51+
}
5252

5353
@GetMapping("/recentUserSubmissions/{limit}")
5454
public ResponseEntity<UserRecentSubmissionsResponse.DataNode> getUserRecentSubmissions(@PathVariable String username,@PathVariable int limit) {
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package com.rajat_singh.leetcode_api.dto;
2+
3+
import com.fasterxml.jackson.annotation.JsonProperty;
4+
import lombok.Builder;
5+
import lombok.Data;
6+
7+
import java.util.List;
8+
import java.util.Map;
9+
10+
@Data
11+
@Builder
12+
public class RecommendationResponse {
13+
@JsonProperty("weak-topics")
14+
private Map<String, WeakTopicDTO> weakTopics;
15+
16+
@JsonProperty("recommended-questions")
17+
private List<RecommendedQuestionDTO> recommendedQuestions;
18+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
package com.rajat_singh.leetcode_api.dto;
2+
3+
import com.fasterxml.jackson.annotation.JsonProperty;
4+
import lombok.Builder;
5+
import lombok.Data;
6+
7+
import java.util.List;
8+
9+
@Data
10+
@Builder
11+
public class RecommendedQuestionDTO {
12+
private String name;
13+
14+
@JsonProperty("acceptance-rate")
15+
private String acceptanceRate;
16+
17+
private List<String> tags;
18+
19+
private String link;
20+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package com.rajat_singh.leetcode_api.dto;
2+
3+
import lombok.Builder;
4+
import lombok.Data;
5+
6+
@Data
7+
@Builder
8+
public class UserTagStatDTO {
9+
private String tagName;
10+
private int solved;
11+
private long totalAvailable;
12+
private double completionPercentage;
13+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package com.rajat_singh.leetcode_api.dto;
2+
3+
import com.fasterxml.jackson.annotation.JsonProperty;
4+
import lombok.Builder;
5+
import lombok.Data;
6+
7+
@Data
8+
@Builder
9+
public class WeakTopicDTO {
10+
@JsonProperty("completion-percentage")
11+
private String completionPercentage;
12+
13+
@JsonProperty("completed-questions")
14+
private int completedQuestions;
15+
16+
@JsonProperty("total-questions")
17+
private long totalQuestions;
18+
}

src/main/java/com/rajat_singh/leetcode_api/repository/QuestionsRepository.java

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,33 @@
44
import jakarta.transaction.Transactional;
55
import org.springframework.data.jpa.repository.JpaRepository;
66
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
7+
8+
import org.springframework.data.jpa.repository.Query;
79
import org.springframework.stereotype.Repository;
810

11+
import java.util.List;
12+
913

1014
@Repository
1115
public interface QuestionsRepository extends JpaRepository<QuestionEntity,Integer>, JpaSpecificationExecutor<QuestionEntity> {
1216

1317
QuestionEntity findByTitleSlug(String title);
1418
QuestionEntity findByIsProblemOfTheDayTrue();
1519

20+
@Query("SELECT t.name as tagName, COUNT(q) as totalCount " +
21+
"FROM QuestionEntity q JOIN q.topicTags t " +
22+
"GROUP BY t.name")
23+
List<TagCountProjection> countProblemsByTag();
24+
@Query("SELECT DISTINCT q FROM QuestionEntity q JOIN q.topicTags t " +
25+
"WHERE t.name IN :targetTags AND q.difficulty = 'MEDIUM' " +
26+
"ORDER BY q.acRate DESC LIMIT 50")
27+
List<QuestionEntity> findCandidateQuestions(List<String> targetTags);
28+
29+
@Query(value = "SELECT DISTINCT q.* FROM leetcode_questions q " +
30+
"JOIN question_topic_tags t ON q.id = t.question_id " +
31+
"WHERE t.name IN :tags AND q.difficulty IN :difficulties " +
32+
"ORDER BY RANDOM() LIMIT :limit", nativeQuery = true)
33+
List<QuestionEntity> findRandomQuestionsByTagsAndDifficulty(List<String> tags, List<String> difficulties, int limit);
1634
@Transactional
1735
void deleteByIsProblemOfTheDayTrue();
1836
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package com.rajat_singh.leetcode_api.repository;
2+
3+
public interface TagCountProjection {
4+
String getTagSlug();
5+
String getTagName();
6+
Long getTotalCount();
7+
}
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
package com.rajat_singh.leetcode_api.service;
2+
3+
import com.rajat_singh.leetcode_api.dto.*;
4+
import com.rajat_singh.leetcode_api.entity.QuestionEntity;
5+
import com.rajat_singh.leetcode_api.repository.QuestionsRepository;
6+
import com.rajat_singh.leetcode_api.repository.TagCountProjection;
7+
import lombok.RequiredArgsConstructor;
8+
import org.springframework.stereotype.Service;
9+
10+
import java.util.*;
11+
import org.tinylog.Logger;
12+
import java.util.stream.Collectors;
13+
14+
@Service
15+
@RequiredArgsConstructor
16+
public class RecommendationService {
17+
18+
private final LeetCodeService leetCodeService;
19+
private final QuestionsRepository questionsRepository;
20+
21+
22+
public RecommendationResponse getRecommendations(String username) {
23+
Logger.info("Getting question recommendations for user : {}",username);
24+
var userSolvedStats = leetCodeService.getUserSkillStats(username);
25+
if (userSolvedStats.isEmpty()) return null;
26+
27+
UserSkillStatsResponse.TagProblemCounts userCounts = userSolvedStats.get().getTagProblemCounts();
28+
Map<String, Long> dbTotalCounts = questionsRepository.countProblemsByTag()
29+
.stream().collect(Collectors.toMap(TagCountProjection::getTagName, TagCountProjection::getTotalCount));
30+
31+
List<UserTagStatDTO> allStats = new ArrayList<>();
32+
List<UserSkillStatsResponse.TagProblem> allUserTags = new ArrayList<>();
33+
if (userCounts.getAdvanced() != null) allUserTags.addAll(userCounts.getAdvanced());
34+
if (userCounts.getIntermediate() != null) allUserTags.addAll(userCounts.getIntermediate());
35+
if (userCounts.getFundamental() != null) allUserTags.addAll(userCounts.getFundamental());
36+
37+
for (UserSkillStatsResponse.TagProblem t : allUserTags) {
38+
long total = dbTotalCounts.getOrDefault(t.getTagName(), 0L);
39+
if (total > 5) {
40+
allStats.add(UserTagStatDTO.builder()
41+
.tagName(t.getTagName())
42+
.solved(t.getProblemsSolved())
43+
.totalAvailable(total)
44+
.completionPercentage((double) t.getProblemsSolved() / total)
45+
.build());
46+
}
47+
}
48+
49+
List<String> top5WeakTopics = allStats.stream()
50+
.sorted((a, b) -> {
51+
double scoreA = (1 - a.getCompletionPercentage()) * Math.log(a.getTotalAvailable() + 1);
52+
double scoreB = (1 - b.getCompletionPercentage()) * Math.log(b.getTotalAvailable() + 1);
53+
return Double.compare(scoreB, scoreA);
54+
})
55+
.limit(5)
56+
.map(UserTagStatDTO::getTagName)
57+
.toList();
58+
59+
if (top5WeakTopics.isEmpty()){
60+
Logger.warn("No weak topics found for user: {}", username);
61+
return null;
62+
}
63+
64+
List<QuestionEntity> questions = questionsRepository.findRandomQuestionsByTagsAndDifficulty(
65+
top5WeakTopics, List.of("MEDIUM", "HARD"), 15);
66+
67+
Map<String, WeakTopicDTO> weakTopicsMap = new LinkedHashMap<>();
68+
for (String topic : top5WeakTopics) {
69+
UserTagStatDTO stat = allStats.stream()
70+
.filter(s -> s.getTagName().equals(topic))
71+
.findFirst().orElse(null);
72+
73+
if (stat != null) {
74+
weakTopicsMap.put(topic, WeakTopicDTO.builder()
75+
.completionPercentage(String.format("%.1f%%", stat.getCompletionPercentage() * 100))
76+
.completedQuestions(stat.getSolved())
77+
.totalQuestions(stat.getTotalAvailable())
78+
.build());
79+
}
80+
}
81+
82+
List<RecommendedQuestionDTO> recommendedQuestions = questions.stream()
83+
.map(q -> RecommendedQuestionDTO.builder()
84+
.name(q.getTitle())
85+
.acceptanceRate(String.format("%.2f%%", q.getAcRate()))
86+
.tags(q.getTopicTags().stream().map(t -> t.getName()).collect(Collectors.toList()))
87+
.link(q.getProblemUrl())
88+
.build())
89+
.collect(Collectors.toList());
90+
91+
return RecommendationResponse.builder()
92+
.weakTopics(weakTopicsMap)
93+
.recommendedQuestions(recommendedQuestions)
94+
.build();
95+
}
96+
}

0 commit comments

Comments
 (0)