From 3088636eabe63954c2c892b515ff3fb80b625442 Mon Sep 17 00:00:00 2001 From: Nadezhda Kotegova Date: Wed, 14 May 2025 13:37:14 +0300 Subject: [PATCH 1/3] =?UTF-8?q?feature:=20add=20recommendations=20-=20?= =?UTF-8?q?=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5=D0=BD=D0=B8=D0=B5=20?= =?UTF-8?q?=D0=A0=D0=B5=D0=BA=D0=BE=D0=BC=D0=B5=D0=BD=D0=B4=D0=B0=D1=86?= =?UTF-8?q?=D0=B8=D0=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../filmorate/controller/UserController.java | 10 +++ .../filmorate/dal/JdbcUserRepository.java | 32 ++++++++- .../filmorate/dal/UserRepository.java | 6 ++ .../filmorate/service/FilmServiceImpl.java | 8 +-- .../service/RecommendationService.java | 8 +++ .../service/RecommendationServiceImpl.java | 67 +++++++++++++++++ .../practicum/filmorate/service/SlopeOne.java | 72 +++++++++++++++++++ .../filmorate/service/UserServiceImpl.java | 6 +- src/main/resources/schema.sql | 8 +-- 9 files changed, 203 insertions(+), 14 deletions(-) create mode 100644 src/main/java/ru/yandex/practicum/filmorate/service/RecommendationService.java create mode 100644 src/main/java/ru/yandex/practicum/filmorate/service/RecommendationServiceImpl.java create mode 100644 src/main/java/ru/yandex/practicum/filmorate/service/SlopeOne.java 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 b17a7e8..72da76e 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/controller/UserController.java +++ b/src/main/java/ru/yandex/practicum/filmorate/controller/UserController.java @@ -6,10 +6,13 @@ import org.springframework.http.HttpStatus; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; +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; +import java.util.List; @RestController @RequiredArgsConstructor @@ -18,6 +21,7 @@ @Validated public class UserController { private final UserServiceImpl userService; + private final RecommendationService recommendationService; @GetMapping @ResponseStatus(HttpStatus.OK) @@ -72,4 +76,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 b559549..140f57b 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/dal/JdbcUserRepository.java +++ b/src/main/java/ru/yandex/practicum/filmorate/dal/JdbcUserRepository.java @@ -19,15 +19,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"; @Override public User create(User user) { @@ -115,4 +121,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 531de81..6f9bbf6 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/dal/UserRepository.java +++ b/src/main/java/ru/yandex/practicum/filmorate/dal/UserRepository.java @@ -3,7 +3,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 { Optional getUserById(long userId); @@ -25,4 +27,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/FilmServiceImpl.java b/src/main/java/ru/yandex/practicum/filmorate/service/FilmServiceImpl.java index 5132c95..cb877cc 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/service/FilmServiceImpl.java +++ b/src/main/java/ru/yandex/practicum/filmorate/service/FilmServiceImpl.java @@ -4,10 +4,7 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import org.springframework.validation.annotation.Validated; -import ru.yandex.practicum.filmorate.dal.JdbcFilmRepository; -import ru.yandex.practicum.filmorate.dal.JdbcGenreRepository; -import ru.yandex.practicum.filmorate.dal.JdbcMpaRepository; -import ru.yandex.practicum.filmorate.dal.JdbcUserRepository; +import ru.yandex.practicum.filmorate.dal.*; import ru.yandex.practicum.filmorate.exception.NotFoundException; import ru.yandex.practicum.filmorate.exception.ValidationException; import ru.yandex.practicum.filmorate.model.Film; @@ -26,6 +23,7 @@ public class FilmServiceImpl implements FilmService { private final JdbcUserRepository jdbcUserRepository; private final JdbcGenreRepository jdbcGenreRepository; private final JdbcMpaRepository jdbcMpaRepository; + private final JdbcDirectorRepository jdbcDirectorRepository; @Override public Film getFilmById(long filmId) { @@ -137,6 +135,8 @@ public List getPopularFilms(int count, Long genreId, Integer year) { @Override public List getDirectorFilms(long id, String sortBy) { + jdbcDirectorRepository.getById(id) + .orElseThrow(() -> new NotFoundException("Режиссёр с id = " + id + " не найден")); if (sortBy.equals("year")) { return jdbcFilmRepository.getDirectorFilmsByYear(id); } else if (sortBy.equals("likes")) { 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 19c8a55..ac5d0e2 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/service/UserServiceImpl.java +++ b/src/main/java/ru/yandex/practicum/filmorate/service/UserServiceImpl.java @@ -83,9 +83,9 @@ 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 + " не найден"); - } +// if (user == null) { +// throw new NotFoundException("Пользователь с id = " + userId + " не найден"); +// } return jdbcUserRepository.getUserFriends(userId); } diff --git a/src/main/resources/schema.sql b/src/main/resources/schema.sql index 696ddca..092fdce 100644 --- a/src/main/resources/schema.sql +++ b/src/main/resources/schema.sql @@ -17,10 +17,10 @@ CREATE TABLE IF NOT EXISTS directors ( CREATE TABLE IF NOT EXISTS films ( film_id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, name VARCHAR(255) NOT NULL, - description VARCHAR(200), - release_date DATE, - duration INT CHECK (duration > 0), - mpa_id BIGINT REFERENCES mpa(mpa_id) + description VARCHAR(200) NOT NULL, + release_date DATE NOT NULL, + duration INT NOT NULL CHECK (duration > 0), + mpa_id BIGINT NOT NULL REFERENCES mpa(mpa_id) ); CREATE TABLE IF NOT EXISTS film_genres ( From a75234a6e8d7fd2e4c53d0aa3dfa5d4456e27eca Mon Sep 17 00:00:00 2001 From: Nadezhda Kotegova Date: Wed, 14 May 2025 13:56:34 +0300 Subject: [PATCH 2/3] clean ups --- .../practicum/filmorate/service/UserServiceImpl.java | 5 +---- src/main/resources/schema.sql | 8 ++++---- 2 files changed, 5 insertions(+), 8 deletions(-) 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 ac5d0e2..69c9b1c 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/service/UserServiceImpl.java +++ b/src/main/java/ru/yandex/practicum/filmorate/service/UserServiceImpl.java @@ -82,10 +82,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); } diff --git a/src/main/resources/schema.sql b/src/main/resources/schema.sql index 092fdce..696ddca 100644 --- a/src/main/resources/schema.sql +++ b/src/main/resources/schema.sql @@ -17,10 +17,10 @@ CREATE TABLE IF NOT EXISTS directors ( CREATE TABLE IF NOT EXISTS films ( film_id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, name VARCHAR(255) NOT NULL, - description VARCHAR(200) NOT NULL, - release_date DATE NOT NULL, - duration INT NOT NULL CHECK (duration > 0), - mpa_id BIGINT NOT NULL REFERENCES mpa(mpa_id) + description VARCHAR(200), + release_date DATE, + duration INT CHECK (duration > 0), + mpa_id BIGINT REFERENCES mpa(mpa_id) ); CREATE TABLE IF NOT EXISTS film_genres ( From e1c8ebb9292e066ab1353fe31829efae34a3787b Mon Sep 17 00:00:00 2001 From: Nadezhda Kotegova Date: Wed, 14 May 2025 15:58:13 +0300 Subject: [PATCH 3/3] merge commit --- .../ru/yandex/practicum/filmorate/dal/JdbcUserRepository.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) 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 805ad65..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