diff --git a/src/main/java/ru/yandex/practicum/filmorate/controller/UserController.java b/src/main/java/ru/yandex/practicum/filmorate/controller/UserController.java index 488b5ac..53ccbcd 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/controller/UserController.java +++ b/src/main/java/ru/yandex/practicum/filmorate/controller/UserController.java @@ -7,7 +7,9 @@ import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; import ru.yandex.practicum.filmorate.model.Activity; +import ru.yandex.practicum.filmorate.model.Film; import ru.yandex.practicum.filmorate.model.User; +import ru.yandex.practicum.filmorate.service.RecommendationService; import ru.yandex.practicum.filmorate.service.UserServiceImpl; import java.util.Collection; @@ -20,6 +22,7 @@ @Validated public class UserController { private final UserServiceImpl userService; + private final RecommendationService recommendationService; @GetMapping("/{userId}/feed") @ResponseStatus(HttpStatus.OK) @@ -80,4 +83,10 @@ public Collection getAllFriends(@PathVariable("userId") long userId) { public Collection getCommonFriends(@PathVariable("userId") long userId, @PathVariable("otherId") long otherId) { return userService.getCommonFriends(userId, otherId); } + + @GetMapping("/{userId}/recommendations") + @ResponseStatus(HttpStatus.OK) + public List getUserRecommendations(@PathVariable("userId") long userId) { + return recommendationService.getRecommendations(userId); + } } \ No newline at end of file diff --git a/src/main/java/ru/yandex/practicum/filmorate/dal/JdbcUserRepository.java b/src/main/java/ru/yandex/practicum/filmorate/dal/JdbcUserRepository.java index 4b14936..e1ad717 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/dal/JdbcUserRepository.java +++ b/src/main/java/ru/yandex/practicum/filmorate/dal/JdbcUserRepository.java @@ -13,8 +13,7 @@ import ru.yandex.practicum.filmorate.model.User; import java.time.Instant; -import java.util.List; -import java.util.Optional; +import java.util.*; import java.util.stream.Stream; @Repository @@ -26,15 +25,21 @@ public class JdbcUserRepository implements UserRepository { private static final String CREATE_USER_QUERY = "INSERT INTO users (login, email, name, birthday) VALUES(:login,:email,:name,:birthday)"; private static final String UPDATE_USER_QUERY = "UPDATE users SET login=:login, email=:email, name=:name, birthday=:birthday WHERE user_id=:user_id"; - private static final String DELETE_USER_QUERY = "DELETE FROM users WHERE user_id = :user_id"; - private static final String GET_USER_BY_ID_QUERY = "SELECT * FROM users WHERE user_id = :user_id"; + private static final String DELETE_USER_QUERY = "DELETE FROM users WHERE user_id=:user_id"; + private static final String GET_USER_BY_ID_QUERY = "SELECT * FROM users WHERE user_id=:user_id"; private static final String GET_ALL_USERS_QUERY = "SELECT * FROM users"; private static final String ADD_FRIEND_QUERY = "INSERT INTO friends(user_id, friend_id) VALUES(:user_id,:friend_id)"; private static final String DELETE_FRIEND_QUERY = "DELETE FROM friends WHERE user_id=:user_id AND friend_id=:friend_id"; - private static final String GET_USER_FRIENDS_QUERY = "SELECT * FROM users WHERE user_id IN (SELECT friend_id FROM friends WHERE user_id = :user_id)"; + private static final String GET_USER_FRIENDS_QUERY = "SELECT * FROM users WHERE user_id IN (SELECT friend_id FROM friends WHERE user_id=:user_id)"; private static final String FIND_COMMON_FRIENDS = "SELECT u.* FROM users u JOIN friends f1 ON u.user_id = f1.friend_id " + "JOIN friends f2 ON u.user_id = f2.friend_id WHERE f1.user_id = :user_id AND f2.user_id = :other_id"; private static final String FIND_USERS_BY_IDS_QUERY = "SELECT * FROM users WHERE user_id IN (:ids)"; + private static final String GET_USER_LIKED_FILMS_QUERY = """ + SELECT f.film_id + FROM likes l + WHERE l.user_id =:user_id + """; + private static final String GET_ALL_USERS_LIKES = "SELECT user_id, film_id FROM likes"; private static final String ACTIVITY_GENERAL = "INSERT INTO activity (userId, entityId, eventType, operation, timestamp) VALUES(:userId, :entityId, '"; @@ -154,4 +159,24 @@ public List getUsersByIds(List ids) { params.addValue("ids", ids); return jdbc.query(FIND_USERS_BY_IDS_QUERY, params, mapper); } + + @Override + public Set getUserLikedFilms(long userId) { + MapSqlParameterSource params = new MapSqlParameterSource("user_id", userId); + List result = jdbc.queryForList(GET_USER_LIKED_FILMS_QUERY, params, Long.class); + return new HashSet<>(result); + } + + @Override + public Map> getAllUserLikes() { + List> rows = jdbc.queryForList(GET_ALL_USERS_LIKES, new MapSqlParameterSource()); + + Map> userLikes = new HashMap<>(); + for (Map row : rows) { + Long userId = ((Number) row.get("user_id")).longValue(); + Long filmId = ((Number) row.get("film_id")).longValue(); + userLikes.computeIfAbsent(userId, k -> new HashSet<>()).add(filmId); + } + return userLikes; + } } \ No newline at end of file diff --git a/src/main/java/ru/yandex/practicum/filmorate/dal/UserRepository.java b/src/main/java/ru/yandex/practicum/filmorate/dal/UserRepository.java index 1d08cf9..12481ac 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/dal/UserRepository.java +++ b/src/main/java/ru/yandex/practicum/filmorate/dal/UserRepository.java @@ -4,7 +4,9 @@ import ru.yandex.practicum.filmorate.model.User; import java.util.List; +import java.util.Map; import java.util.Optional; +import java.util.Set; public interface UserRepository { List getActivityById(long activityId); @@ -28,4 +30,8 @@ public interface UserRepository { List getUserFriends(long userId); List getCommonFriends(long userId, long otherId); + + Set getUserLikedFilms(long userId); + + Map> getAllUserLikes(); } diff --git a/src/main/java/ru/yandex/practicum/filmorate/service/RecommendationService.java b/src/main/java/ru/yandex/practicum/filmorate/service/RecommendationService.java new file mode 100644 index 0000000..b3f6f3d --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/service/RecommendationService.java @@ -0,0 +1,8 @@ +package ru.yandex.practicum.filmorate.service; + +import ru.yandex.practicum.filmorate.model.Film; +import java.util.List; + +public interface RecommendationService { + List getRecommendations(long userId); +} \ No newline at end of file diff --git a/src/main/java/ru/yandex/practicum/filmorate/service/RecommendationServiceImpl.java b/src/main/java/ru/yandex/practicum/filmorate/service/RecommendationServiceImpl.java new file mode 100644 index 0000000..95ee2ec --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/service/RecommendationServiceImpl.java @@ -0,0 +1,67 @@ +package ru.yandex.practicum.filmorate.service; + +import jakarta.annotation.PostConstruct; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import ru.yandex.practicum.filmorate.dal.JdbcUserRepository; +import ru.yandex.practicum.filmorate.dal.JdbcFilmRepository; +import ru.yandex.practicum.filmorate.model.Film; + +import java.util.*; +import java.util.stream.Collectors; + +@Service +@Slf4j +@RequiredArgsConstructor +public class RecommendationServiceImpl implements RecommendationService { + private final JdbcUserRepository userRepository; + private final JdbcFilmRepository filmRepository; + private final SlopeOne slopeOne; + + @PostConstruct + public void init() { + slopeOne.buildDifferences(Collections.emptyMap()); + } + + private void rebuildModel() { + var raw = userRepository.getAllUserLikes(); + var data = raw.entrySet().stream() + .collect(Collectors.toMap( + Map.Entry::getKey, + e -> e.getValue().stream().collect(Collectors.toMap(f -> f, f -> 1.0)) + )); + slopeOne.buildDifferences(data); + } + + @Override + public List getRecommendations(long userId) { + var myLikes = userRepository.getAllUserLikes() + .getOrDefault(userId, Collections.emptySet()); + if (myLikes.isEmpty()) { + return Collections.emptyList(); + } + + rebuildModel(); + + var predictions = slopeOne.predictRatings( + myLikes.stream().collect(Collectors.toMap(f -> f, f -> 1.0)) + ); + predictions.keySet().removeAll(myLikes); + + if (predictions.isEmpty()) { + return Collections.emptyList(); + } + + var recommendations = predictions.entrySet().stream() + .sorted(Map.Entry.comparingByValue().reversed()) + .map(e -> filmRepository.getFilmById(e.getKey())) + .flatMap(Optional::stream) + .collect(Collectors.toList()); + + filmRepository.connectGenres(recommendations); + filmRepository.connectDirectors(recommendations); + + return recommendations; + } +} \ No newline at end of file diff --git a/src/main/java/ru/yandex/practicum/filmorate/service/SlopeOne.java b/src/main/java/ru/yandex/practicum/filmorate/service/SlopeOne.java new file mode 100644 index 0000000..be76669 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/service/SlopeOne.java @@ -0,0 +1,72 @@ +package ru.yandex.practicum.filmorate.service; + +import org.springframework.stereotype.Component; + +import java.util.HashMap; +import java.util.Map; + +@Component +public class SlopeOne { + private final Map> diffMatrix = new HashMap<>(); + + private final Map> freqMatrix = new HashMap<>(); + + public void buildDifferences(Map> data) { + for (Map userRatings : data.values()) { + for (Map.Entry e1 : userRatings.entrySet()) { + long i = e1.getKey(); + double r1 = e1.getValue(); + + diffMatrix.computeIfAbsent(i, k -> new HashMap<>()); + freqMatrix.computeIfAbsent(i, k -> new HashMap<>()); + + for (Map.Entry e2 : userRatings.entrySet()) { + long j = e2.getKey(); + double r2 = e2.getValue(); + + + diffMatrix.get(i).merge(j, r1 - r2, Double::sum); + + freqMatrix.get(i).merge(j, 1, Integer::sum); + } + } + } + + for (Long i : diffMatrix.keySet()) { + for (Long j : diffMatrix.get(i).keySet()) { + double totalDiff = diffMatrix.get(i).get(j); + int count = freqMatrix.get(i).get(j); + diffMatrix.get(i).put(j, totalDiff / count); + } + } + } + + public Map predictRatings(Map userRatings) { + Map predictions = new HashMap<>(); + Map counts = new HashMap<>(); + + for (Map.Entry entry : userRatings.entrySet()) { + long j = entry.getKey(); + double rj = entry.getValue(); + + for (Map.Entry> row : diffMatrix.entrySet()) { + long i = row.getKey(); + Map diffs = row.getValue(); + + if (diffs.containsKey(j)) { + double diff = diffs.get(j); + int freq = freqMatrix.get(i).get(j); + + predictions.merge(i, (diff + rj) * freq, Double::sum); + counts.merge(i, freq, Integer::sum); + } + } + } + + for (Long i : predictions.keySet()) { + predictions.put(i, predictions.get(i) / counts.get(i)); + } + + return predictions; + } +} \ No newline at end of file diff --git a/src/main/java/ru/yandex/practicum/filmorate/service/UserServiceImpl.java b/src/main/java/ru/yandex/practicum/filmorate/service/UserServiceImpl.java index 5357699..05d6655 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/service/UserServiceImpl.java +++ b/src/main/java/ru/yandex/practicum/filmorate/service/UserServiceImpl.java @@ -89,10 +89,7 @@ public void deleteFriend(long userId, long friendId) { @Override public List getAllFriends(long userId) { - User user = getUserById(userId); - if (user == null) { - throw new NotFoundException("Пользователь с id = " + userId + " не найден"); - } + getUserById(userId); return jdbcUserRepository.getUserFriends(userId); }