Skip to content

Commit

Permalink
refactor: Рефактор метода showRecommendations
Browse files Browse the repository at this point in the history
  • Loading branch information
vvbakhanovich committed Feb 14, 2024
1 parent 39aeca9 commit 9d007e9
Show file tree
Hide file tree
Showing 7 changed files with 147 additions and 104 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import ru.yandex.practicum.filmorate.dto.FilmSearchDto;
import ru.yandex.practicum.filmorate.model.Film;
import ru.yandex.practicum.filmorate.model.FilmMark;

import java.util.Collection;
import java.util.Map;
Expand All @@ -13,7 +14,7 @@ public interface FilmStorage extends Dao<Film> {

void removeMarkFromFilm(long filmId, long userId);

Map<Long, Map<Long, Integer>> getUsersAndFilmLikes();
Map<Long, Set<FilmMark>> findUserIdFilmMarks();

Collection<Film> findFilmsByIds(Set<Long> filmIds);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -233,10 +233,9 @@ public void removeMarkFromFilm(long filmId, long userId) {
jdbcTemplate.update(sql, filmId, userId);
}

@Override
public Map<Long, Map<Long, Integer>> getUsersAndFilmLikes() {
public Map<Long, Set<FilmMark>> findUserIdFilmMarks() {
String filmsIdsSql = "SELECT user_id, film_id, mark FROM film_mark";
return jdbcTemplate.query(filmsIdsSql, this::extractToUserIdLikedFilmsIdsMap);
return jdbcTemplate.query(filmsIdsSql, this::extractToUserIdFilmMarks);
}

private void setGenresForFilms(Collection<Film> films) {
Expand Down Expand Up @@ -389,32 +388,21 @@ private Director mapToDirector(ResultSet rs, int i) throws SQLException {
.build();
}

private Collection<FilmMarkByUser> extractToUserIdLikedFilmsIdsMap2(ResultSet rs) throws SQLException, DataAccessException {
Map<Long, FilmMarkByUser> userFilmMarks = new HashMap<>();
while (rs.next()) {
final Long userId = rs.getLong("user_id");
FilmMarkByUser filmMarkByUser = userFilmMarks.get(userId);
if (filmMarkByUser == null) {
filmMarkByUser = new FilmMarkByUser();
}
Map<Long, Integer> filmIdMarks = filmMarkByUser.getFilmIdMarks();
filmIdMarks.put(rs.getLong("film_id"), rs.getInt("mark"));
userFilmMarks.put(userId, filmMarkByUser);
}
return new ArrayList<>(userFilmMarks.values());
}

private Map<Long, Map<Long, Integer>> extractToUserIdLikedFilmsIdsMap(ResultSet rs) throws SQLException, DataAccessException {
final Map<Long, Map<Long, Integer>> userFilmLikesMap = new HashMap<>();
private Map<Long, Set<FilmMark>> extractToUserIdFilmMarks(ResultSet rs) throws SQLException, DataAccessException {
Map<Long, Set<FilmMark>> userFilmMarks = new HashMap<>();
while (rs.next()) {
final Long userId = rs.getLong("user_id");
Map<Long, Integer> filmLikes = userFilmLikesMap.get(userId);
if (filmLikes == null) {
filmLikes = new HashMap<>();
Set<FilmMark> filmMarks = userFilmMarks.get(userId);
if (filmMarks == null) {
filmMarks = new LinkedHashSet<>();
}
filmLikes.put(rs.getLong("film_id"), rs.getInt("mark"));
userFilmLikesMap.put(userId, filmLikes);
FilmMark filmMark = FilmMark.builder()
.filmId(rs.getLong("film_id"))
.mark(rs.getInt("mark"))
.build();
filmMarks.add(filmMark);
userFilmMarks.put(userId, filmMarks);
}
return userFilmLikesMap;
return userFilmMarks;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package ru.yandex.practicum.filmorate.dto;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class RecommendationsCurrentParams {

private int currentDiff;
private int currentNumberOfMatches;
private int currentNumberOfLikedFilms;
}
15 changes: 15 additions & 0 deletions src/main/java/ru/yandex/practicum/filmorate/model/FilmMark.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package ru.yandex.practicum.filmorate.model;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class FilmMark {
private long filmId;
private int mark;
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,30 @@
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import ru.yandex.practicum.filmorate.dao.*;
import ru.yandex.practicum.filmorate.dao.EventStorage;
import ru.yandex.practicum.filmorate.dao.FilmStorage;
import ru.yandex.practicum.filmorate.dao.FriendshipStorage;
import ru.yandex.practicum.filmorate.dao.UserStorage;
import ru.yandex.practicum.filmorate.dto.FeedDto;
import ru.yandex.practicum.filmorate.dto.FilmDto;
import ru.yandex.practicum.filmorate.dto.RecommendationsCurrentParams;
import ru.yandex.practicum.filmorate.dto.UserDto;
import ru.yandex.practicum.filmorate.mapper.FeedMapper;
import ru.yandex.practicum.filmorate.mapper.FilmMapper;
import ru.yandex.practicum.filmorate.mapper.UserMapper;
import ru.yandex.practicum.filmorate.model.*;
import ru.yandex.practicum.filmorate.model.EventType;
import ru.yandex.practicum.filmorate.model.FilmMark;
import ru.yandex.practicum.filmorate.model.Friendship;
import ru.yandex.practicum.filmorate.model.Operation;
import ru.yandex.practicum.filmorate.model.User;
import ru.yandex.practicum.filmorate.service.UserService;

import java.util.*;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;

import static ru.yandex.practicum.filmorate.model.FriendshipStatus.ACKNOWLEDGED;
Expand Down Expand Up @@ -172,7 +185,10 @@ public void removeUser(long userId) {
}

/**
* Получение списка рекомендованных к просмотру фильмов, которые могут понравиться пользователю.
* Получение списка рекомендованных к просмотру фильмов, которые могут понравиться пользователю. Алгоритм
* выбора рекомендаций определяет пользователя с наиболее схожими оценками с пользователем, который хочет получить
* рекомендации, и возвращает те фильмы, которые не были оценены искомым пользователем и у которых положительный
* рейтинг.
*
* @param id идентификатор пользователя, который хочет получить рекомендации.
* @return список рекомендованных фильмов.
Expand All @@ -181,47 +197,20 @@ public void removeUser(long userId) {
@Override
public Collection<FilmDto> showRecommendations(long id) {
log.info("Получение списка рекомендаций фильмов для пользователя с id {}.", id);
int positiveRating = 6;
Map<Long, Map<Long, Integer>> usersLikes = filmStorage.getUsersAndFilmLikes(); // мапа id пользователя - мапа id фильма и поставленая оценка
Map<Long, Integer> userFilmIdRating = usersLikes.get(id); // список id лайкнутых фильмов и их оценок от искомого пользователя
double bestMatch = Double.MAX_VALUE; // начальное значение для лучшего соотношения совпавших оценок
long bestUserId = 0; // начальное значение id пользователя с наибольшим количеством совпадений по оценкам
int numberOfLikedFilms = 0; // начальное значение для количества фильмов, которым пользователь поставил оценки
for (Long userId : usersLikes.keySet()) { // проходимся по всем пользователям, которые оценивали фильмы
if (userId == id) { // искомого пользователя пропускаем
continue;
}
Map<Long, Integer> currentUserFilmRating = usersLikes.get(userId); // фильмы и их оценки от текущего пользователся
int currentDiff = 0; // текущая разница оценок
int currentNumberOfMatches = 0; // текущее количество совпавших фильмов
int currentNumberOfLikedFilms = 0;
for (Long filmId : userFilmIdRating.keySet()) { // проходимся по всем фильмам, которые оценил искомый пользователь
currentNumberOfLikedFilms = userFilmIdRating.keySet().size();
int rateDiff = userFilmIdRating.get(filmId) - currentUserFilmRating.getOrDefault(filmId, 0); // высчитываем разницу между оценками. Если текущий пользователь не поставил оценку фильму, то используем 0.
currentDiff += rateDiff; // прибавляем к текущей разнице оценок
if (currentUserFilmRating.get(filmId) != null) { // если текущий пользователь не поставил оценку, то не учитываем его при подсчете
currentNumberOfMatches++;
}
}
if (currentNumberOfMatches == 0) { // если нет ни одного совпадения, то переходим к следующему пользователю
continue;
}
double match = Math.abs((double) currentDiff / currentNumberOfMatches); // считаем насколько близки оценки между двумя пользователями
if (match < bestMatch) { // чем меньше показатель, тем больше похожи оценки
bestMatch = match;
bestUserId = userId;
}
if (match == bestMatch && currentNumberOfLikedFilms > numberOfLikedFilms) { // если показатели одинаковые, но при этом у текущего пользователя больше фильмов получили оценки, то выбираем его
bestUserId = userId;
}
}
if (bestUserId == 0) { // если не было найдено совпадений, то возвращаем пустой список
Map<Long, Set<FilmMark>> usersFilmMarks = filmStorage.findUserIdFilmMarks();
Set<FilmMark> searchedUserFilmMarks = usersFilmMarks.get(id);
Long userIdWithClosestMarks = findUserIdWithClosestMarks(usersFilmMarks, id);
if (userIdWithClosestMarks == null) {
return Collections.emptyList();
}
Set<Long> bestUserLikedFilms = usersLikes.get(bestUserId).keySet(); // если пользователь найден, то получаем список лайкнутых им фильмов
bestUserLikedFilms.removeAll(userFilmIdRating.keySet()); // оставляем только те, которые не лайкал искомый пользователь
return filmStorage.findFilmsByIds(bestUserLikedFilms).stream()
.filter(film -> film.getRating() >= positiveRating)
Set<Long> matchedUserMarkedFilmIds = usersFilmMarks.get(userIdWithClosestMarks).stream()
.map(FilmMark::getFilmId)
.collect(Collectors.toSet());
matchedUserMarkedFilmIds.removeAll(searchedUserFilmMarks.stream()
.map(FilmMark::getFilmId)
.collect(Collectors.toSet()));
return filmStorage.findFilmsByIds(matchedUserMarkedFilmIds).stream()
.filter(film -> film.getRating() >= POSITIVE_RATING)
.map(FilmMapper::toDto)
.collect(Collectors.toList());
}
Expand All @@ -248,4 +237,52 @@ private UserDto validateUserName(final UserDto userDto) {
userDto.setName(validatedName);
return userDto;
}

private RecommendationsCurrentParams compareToCurrentUserMarks(Set<FilmMark> searchedUserFilmMarks,
Set<FilmMark> currentUserFilmMarks) {
RecommendationsCurrentParams currentParams = new RecommendationsCurrentParams();
for (FilmMark filmMark : searchedUserFilmMarks) {
long filmId = filmMark.getFilmId();
currentParams.setCurrentNumberOfLikedFilms(currentUserFilmMarks.size());
Optional<FilmMark> currentUserFilmMark = currentUserFilmMarks.stream()
.filter(currentFilmMark -> currentFilmMark.getFilmId() == filmId)
.findAny();
int rateDiff = currentUserFilmMark
.map(mark -> filmMark.getMark() - mark.getMark())
.orElseGet(filmMark::getMark);
currentParams.setCurrentDiff(currentParams.getCurrentDiff() + rateDiff);
if (currentUserFilmMark.isPresent()) {
currentParams.setCurrentNumberOfMatches(currentParams.getCurrentNumberOfMatches() + 1);
}
}
return currentParams;
}

private Long findUserIdWithClosestMarks(Map<Long, Set<FilmMark>> usersFilmMarks, long searchedUserId) {
double closestMarksDiff = Double.MAX_VALUE;
Long userIdWithClosestMarks = null;
int numberOfLikedFilms = 0;
for (Long userId : usersFilmMarks.keySet()) {
if (userId == searchedUserId) {
continue;
}
Set<FilmMark> currentUserFilmMarks = usersFilmMarks.get(userId);
RecommendationsCurrentParams currentParams =
compareToCurrentUserMarks(usersFilmMarks.get(searchedUserId), currentUserFilmMarks);
if (currentParams.getCurrentNumberOfMatches() == 0) {
continue;
}
double marksDiff =
Math.abs((double) currentParams.getCurrentDiff() / currentParams.getCurrentNumberOfMatches());
if (marksDiff < closestMarksDiff) {
closestMarksDiff = marksDiff;
userIdWithClosestMarks = userId;
numberOfLikedFilms = currentParams.getCurrentNumberOfLikedFilms();
}
if (marksDiff == closestMarksDiff && currentParams.getCurrentNumberOfLikedFilms() > numberOfLikedFilms) {
userIdWithClosestMarks = userId;
}
}
return userIdWithClosestMarks;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -62,34 +62,9 @@ public void setUp() {
filmStorage.add(film);
userStorage.add(user);


review1 = Review.builder()
.reviewId(1)
.content("review 2")
.isPositive(true)
.useful(1)
.userId(1)
.filmId(1)
.build();

review2 = Review.builder()
.reviewId(2)
.content("review 1")
.isPositive(false)
.useful(2)
.userId(1)
.filmId(1)
.build();

review3 = Review.builder()
.reviewId(3)
.content("review 3")
.isPositive(true)
.useful(3)
.userId(1)
.filmId(1)
.build();

review1 = createReview(1);
review2 = createReview(2);
review3 = createReview(3);
updatedReview = Review.builder()
.reviewId(1)
.content("updated review 1")
Expand Down Expand Up @@ -292,4 +267,14 @@ public void addDislikeToReviewNegativeUseful() {
assertEquals(-2, storedReview.getUseful());
}

private Review createReview(long id) {
return Review.builder()
.reviewId(id)
.content("review " + id)
.isPositive(true)
.useful(id)
.userId(1)
.filmId(1)
.build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -377,21 +377,21 @@ void testGetRecommendationsList() {
filmStorage.addMarkToFilm(filmOne.getId(), anotherUser.getId(), 10);
filmStorage.addMarkToFilm(filmTwo.getId(), anotherUser.getId(), 10);

Map<Long, Map<Long, Integer>> filmRecommendations = filmStorage.getUsersAndFilmLikes();
Map<Long, Set<FilmMark>> filmRecommendations = filmStorage.findUserIdFilmMarks();

assertThat(filmRecommendations.get(1L))
.isNotNull()
.isNotEmpty()
.usingRecursiveComparison()
.isEqualTo(Map.of(filmOne.getId(), 10));
.isEqualTo(Set.of(new FilmMark(filmOne.getId(), 10)));

assertThat(filmRecommendations.get(2L))
.isNotNull()
.isNotEmpty()
.usingRecursiveComparison()
.isEqualTo(Map.of(
filmOne.getId(), 10,
filmTwo.getId(), 10)
.isEqualTo(Set.of(
new FilmMark(filmOne.getId(), 10),
new FilmMark(filmTwo.getId(), 10))
);
}

Expand All @@ -402,7 +402,7 @@ void testGetRecommendationsListNoLikes() {
userStorage.add(anotherUser);
filmStorage.add(filmOne);

Map<Long, Map<Long, Integer>> filmRecommendations = filmStorage.getUsersAndFilmLikes();
Map<Long, Set<FilmMark>> filmRecommendations = filmStorage.findUserIdFilmMarks();

assertThat(filmRecommendations)
.isNotNull()
Expand Down

0 comments on commit 9d007e9

Please sign in to comment.