diff --git a/src/main/java/org/petmarket/advertisements/advertisement/controller/AdvertisementAdminController.java b/src/main/java/org/petmarket/advertisements/advertisement/controller/AdvertisementAdminController.java index 7fcb2ae2..8ccafd9e 100644 --- a/src/main/java/org/petmarket/advertisements/advertisement/controller/AdvertisementAdminController.java +++ b/src/main/java/org/petmarket/advertisements/advertisement/controller/AdvertisementAdminController.java @@ -97,4 +97,22 @@ public List setStatusDraft( .map(adv -> advertisementMapper.mapEntityToDto(adv, defaultSiteLanguage)) .toList(); } + + @Operation(summary = "Update Advertisements top rating") + @ApiResponseSuccessful + @ApiResponseUnauthorized + @ApiResponseForbidden + @PutMapping("/top-rating") + public void updateAllTopRatings() { + advertisementService.updateAllTopRatings(); + } + + @Operation(summary = "Update Advertisements rating") + @ApiResponseSuccessful + @ApiResponseUnauthorized + @ApiResponseForbidden + @PutMapping("/rating") + public void updateAllRatings() { + advertisementService.updateAllRatings(); + } } diff --git a/src/main/java/org/petmarket/advertisements/advertisement/entity/Advertisement.java b/src/main/java/org/petmarket/advertisements/advertisement/entity/Advertisement.java index cd42cdca..1eec8fb7 100644 --- a/src/main/java/org/petmarket/advertisements/advertisement/entity/Advertisement.java +++ b/src/main/java/org/petmarket/advertisements/advertisement/entity/Advertisement.java @@ -4,6 +4,7 @@ import lombok.*; import org.hibernate.search.engine.backend.types.Sortable; import org.hibernate.search.mapper.pojo.mapping.definition.annotation.*; +import org.petmarket.advertisements.advertisement.listener.AdvertisementListener; import org.petmarket.advertisements.attributes.entity.Attribute; import org.petmarket.advertisements.category.entity.AdvertisementCategory; import org.petmarket.advertisements.images.entity.AdvertisementImage; @@ -32,7 +33,7 @@ @Builder @NoArgsConstructor @AllArgsConstructor -@EntityListeners(AuditingEntityListener.class) +@EntityListeners({AuditingEntityListener.class, AdvertisementListener.class}) @Indexed public class Advertisement implements TranslateHolder { @@ -92,6 +93,9 @@ public class Advertisement implements TranslateHolder { @GenericField(sortable = Sortable.YES) private int rating; + @Column(name = "top_rating") + private int topRating; + @ManyToOne @JoinColumn(name = "breed_id") @IndexedEmbedded(includeEmbeddedObjectId = true) diff --git a/src/main/java/org/petmarket/advertisements/advertisement/listener/AdvertisementListener.java b/src/main/java/org/petmarket/advertisements/advertisement/listener/AdvertisementListener.java new file mode 100644 index 00000000..6102c352 --- /dev/null +++ b/src/main/java/org/petmarket/advertisements/advertisement/listener/AdvertisementListener.java @@ -0,0 +1,21 @@ +package org.petmarket.advertisements.advertisement.listener; + +import jakarta.persistence.PostPersist; +import jakarta.persistence.PostUpdate; +import org.petmarket.advertisements.advertisement.entity.Advertisement; +import org.petmarket.advertisements.advertisement.service.AdvertisementService; +import org.springframework.context.annotation.Lazy; + +public class AdvertisementListener { +// private final AdvertisementService advertisementService; +// +// public AdvertisementListener(@Lazy AdvertisementService advertisementService) { +// this.advertisementService = advertisementService; +// } +// +// @PostPersist +// @PostUpdate +// public void updateTopRating(Advertisement advertisement) { +// advertisementService.updateTopRating(advertisement); +// } +} diff --git a/src/main/java/org/petmarket/advertisements/advertisement/repository/AdvertisementRepository.java b/src/main/java/org/petmarket/advertisements/advertisement/repository/AdvertisementRepository.java index c2147055..b843508a 100644 --- a/src/main/java/org/petmarket/advertisements/advertisement/repository/AdvertisementRepository.java +++ b/src/main/java/org/petmarket/advertisements/advertisement/repository/AdvertisementRepository.java @@ -68,7 +68,7 @@ public interface AdvertisementRepository extends AdvertisementRepositoryBasic { WHERE a.category IN :categories AND a.status = :status AND a.author.status <> 'DELETED' - ORDER BY a.created DESC + ORDER BY a.topRating DESC, a.created DESC """) Page findAllByCategoryInAndStatusOrderByCreatedDesc( @Param("categories") List categories, @@ -85,7 +85,7 @@ Page findAllByAuthorIdAndStatusAndIdNotOrderByCreatedDesc(Long au JOIN a.author WHERE a.status = :status AND a.author.status <> 'DELETED' - ORDER BY a.created DESC + ORDER BY a.topRating DESC, a.created DESC """) Page findAllByStatusOrderByCreatedDesc(@Param("status") AdvertisementStatus status, Pageable pageable); @@ -166,4 +166,6 @@ void updateStatus(@Param("oldStatus") AdvertisementStatus oldStatus, List findAllByOrderId(Long orderId); List findAllByAuthorId(Long authorId); + + Page findAllByStatus(AdvertisementStatus status, Pageable pageable); } diff --git a/src/main/java/org/petmarket/advertisements/advertisement/service/AdvertisementService.java b/src/main/java/org/petmarket/advertisements/advertisement/service/AdvertisementService.java index 8aacef68..6ef11503 100644 --- a/src/main/java/org/petmarket/advertisements/advertisement/service/AdvertisementService.java +++ b/src/main/java/org/petmarket/advertisements/advertisement/service/AdvertisementService.java @@ -12,6 +12,7 @@ import org.hibernate.search.engine.search.sort.dsl.FieldSortOptionsStep; import org.hibernate.search.engine.search.sort.dsl.SearchSortFactory; import org.hibernate.search.mapper.orm.Search; +import org.hibernate.search.mapper.orm.massindexing.MassIndexer; import org.hibernate.search.mapper.orm.scope.SearchScope; import org.hibernate.search.mapper.orm.session.SearchSession; import org.petmarket.advertisements.advertisement.dto.AdvertisementDetailsResponseDto; @@ -282,15 +283,15 @@ public Collection getReviewsByAdvertisementId(Lo @Transactional public AdvertisementReviewResponseDto addReview(Long id, AdvertisementReviewRequestDto request, BindingResult bindingResult, Authentication authentication) { - ErrorUtils.checkItemNotCreatedException(bindingResult); +// ErrorUtils.checkItemNotCreatedException(bindingResult); User author = getUserByEmail(authentication.getName()); User user = getAdvertisement(id).getAuthor(); Advertisement advertisement = getAdvertisement(id); - - if (reviewService.existsByAuthorIdAndUserId(author.getId(), user.getId())) { - throw new ItemNotCreatedException(REVIEW_ALREADY_EXISTS); - } +// +// if (reviewService.existsByAuthorIdAndUserId(author.getId(), user.getId())) { +// throw new ItemNotCreatedException(REVIEW_ALREADY_EXISTS); +// } Review review = Review.builder() .author(author) @@ -301,8 +302,9 @@ public AdvertisementReviewResponseDto addReview(Long id, AdvertisementReviewRequ .advertisement(advertisement) .build(); reviewRepository.save(review); - userCacheService.evictCaches(user); - reviewService.updateAdvertisementIndexes(List.of(advertisement)); +// entityManager.flush(); +// userCacheService.evictCaches(user); +// reviewService.updateAdvertisementIndexes(List.of(advertisement)); return reviewMapper.mapEntityToAdvertisementDto(review); } @@ -412,6 +414,112 @@ public List getAdvertisementsByImageIds(List imageIds) { return advertisementRepository.findAdvertisementsByImageIds(imageIds); } + @Transactional + public void updateTopRating(Advertisement advertisement) { + advertisement.setTopRating(calculateRating(advertisement)); + entityManager.merge(advertisement); + } + + @Transactional + public void updateAllTopRatings() { + int batchSize = 1000; + int page = 0; + Page advertisements; + + do { + advertisements = advertisementRepository + .findAllByStatus(AdvertisementStatus.ACTIVE, PageRequest.of(page, batchSize)); + + for (Advertisement advertisement : advertisements) { + updateTopRating(advertisement); + } + + entityManager.flush(); + entityManager.clear(); + page++; + } while (advertisements.hasNext()); + } + + public void updateRating(Review review) { + Advertisement advertisement = review.getAdvertisement(); + advertisement.setRating(reviewRepository.findAverageRatingByAdvertisementId(advertisement.getId())); + + entityManager.refresh(advertisement); + Search.session(entityManager).indexingPlan().addOrUpdate(advertisement); + } + + @Transactional + public void updateAllRatings() { + int batchSize = 1000; + int page = 0; + Page reviews; + + do { + reviews = reviewRepository + .findAllByAdvertisementStatus(AdvertisementStatus.ACTIVE, PageRequest.of(page, batchSize)); + + for (Review review : reviews) { + updateRating(review); + } + + entityManager.flush(); + entityManager.clear(); + page++; + } while (reviews.hasNext()); + + page = 0; + Page users; + + do { + users = userRepository.findAll(PageRequest.of(page, batchSize)); + + for (User user : users) { + user.setRating(reviewRepository.findAverageRatingByUserId(user.getId())); + entityManager.merge(user); + } + + entityManager.flush(); + entityManager.clear(); + page++; + } while (users.hasNext()); + + SearchSession searchSession = Search.session(entityManager); + MassIndexer indexer = searchSession.massIndexer() + .idFetchSize(150) + .batchSizeToLoadObjects(25) + .threadsToLoadObjects(12); + try { + indexer.startAndWait(); + } catch (InterruptedException e) { + log.warn("Failed to load data from database"); + Thread.currentThread().interrupt(); + } + + log.info("All ratings have been updated"); + } + + private int calculateDescriptionLengthBonus(Advertisement advertisement) { + return (advertisement.getTranslations() != null && advertisement.getTranslations().stream() + .anyMatch(tr -> tr.getDescription().length() > 20)) ? 30 : 0; + } + + private int calculateImageBonus(Advertisement advertisement) { + return (advertisement.getImages() != null && !advertisement.getImages().isEmpty()) ? 150 : 0; + } + + private int calculateAttributesBonus(Advertisement advertisement) { + int bonus = (advertisement.getAttributes() != null ? advertisement.getAttributes().size() : 0) * 30; + return Math.min(bonus, 150); + } + + private int calculateRating(Advertisement advertisement) { + return advertisement.getRating() * 10 + + advertisement.getAuthor().getRating() * 10 + + calculateDescriptionLengthBonus(advertisement) + + calculateImageBonus(advertisement) + + calculateAttributesBonus(advertisement); + } + private Long getCategoryIdFromSearch(String search) { if (search == null || search.isBlank()) { return null; diff --git a/src/main/java/org/petmarket/advertisements/advertisement/service/AdvertisementServiceProxy.java b/src/main/java/org/petmarket/advertisements/advertisement/service/AdvertisementServiceProxy.java new file mode 100644 index 00000000..0a9383d4 --- /dev/null +++ b/src/main/java/org/petmarket/advertisements/advertisement/service/AdvertisementServiceProxy.java @@ -0,0 +1,16 @@ +package org.petmarket.advertisements.advertisement.service; + +import org.aspectj.lang.annotation.Aspect; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +@Aspect +@Component +public class AdvertisementServiceProxy { + private final AdvertisementService advertisementService; + + @Autowired + public AdvertisementServiceProxy(AdvertisementService advertisementService) { + this.advertisementService = advertisementService; + } +} diff --git a/src/main/java/org/petmarket/config/HibernateSearchIndexBuild.java b/src/main/java/org/petmarket/config/HibernateSearchIndexBuild.java index 1b1e8730..778f60c4 100644 --- a/src/main/java/org/petmarket/config/HibernateSearchIndexBuild.java +++ b/src/main/java/org/petmarket/config/HibernateSearchIndexBuild.java @@ -7,6 +7,7 @@ import org.hibernate.search.mapper.orm.massindexing.MassIndexer; import org.hibernate.search.mapper.orm.session.SearchSession; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.context.event.ApplicationReadyEvent; import org.springframework.context.ApplicationListener; import org.springframework.context.annotation.Configuration; @@ -18,9 +19,16 @@ public class HibernateSearchIndexBuild implements ApplicationListener findReviewByAuthorIdAndUserId(@Param("authorId") Long authorId, @Param("userId") Long userId); + + @Query(value = """ + SELECT COALESCE(AVG(review_value), 0) FROM reviews + WHERE advertisement_id = :id + """, nativeQuery = true) + Integer findAverageRatingByAdvertisementId(@Param("id") Long id); + + @Query(value = """ + SELECT COALESCE(AVG(review_value), 0) FROM reviews + WHERE user_id = :id + """, nativeQuery = true) + Integer findAverageRatingByUserId(@Param("id") Long id); + + Page findAllByAdvertisementStatus(AdvertisementStatus status, Pageable pageable); } diff --git a/src/main/java/org/petmarket/review/service/ReviewService.java b/src/main/java/org/petmarket/review/service/ReviewService.java index 202b5aeb..7dc1ab4b 100644 --- a/src/main/java/org/petmarket/review/service/ReviewService.java +++ b/src/main/java/org/petmarket/review/service/ReviewService.java @@ -5,7 +5,10 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.hibernate.search.mapper.orm.Search; +import org.hibernate.search.mapper.orm.massindexing.MassIndexer; +import org.hibernate.search.mapper.orm.session.SearchSession; import org.petmarket.advertisements.advertisement.entity.Advertisement; +import org.petmarket.advertisements.advertisement.entity.AdvertisementStatus; import org.petmarket.advertisements.advertisement.repository.AdvertisementRepository; import org.petmarket.errorhandling.ItemNotFoundException; import org.petmarket.order.repository.OrderRepository; @@ -15,6 +18,8 @@ import org.petmarket.users.entity.User; import org.petmarket.users.repository.UserRepository; import org.petmarket.users.service.UserCacheService; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; import org.springframework.stereotype.Service; import java.util.Collections; diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 175187a1..1205b9ab 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -10,6 +10,7 @@ spring.jpa.show-sql=false spring.jpa.properties.hibernate.search.lucene.version=LATEST spring.jpa.properties.hibernate.search.backend.directory.type=local-filesystem spring.jpa.properties.hibernate.search.backend.directory.root=index +hibernate.search.indexing.startup.enabled=${HIBERNATE_SEARCH_INDEXING_STARTUP_ENABLED:true} # # Swagger springdoc.swagger-ui.path=/swagger-ui-custom.html diff --git a/src/main/resources/db/migration/V2024.06.27.001__add_adv_top_rating.sql b/src/main/resources/db/migration/V2024.06.27.001__add_adv_top_rating.sql new file mode 100644 index 00000000..26440f35 --- /dev/null +++ b/src/main/resources/db/migration/V2024.06.27.001__add_adv_top_rating.sql @@ -0,0 +1,2 @@ +ALTER TABLE advertisements + ADD COLUMN top_rating INTEGER NOT NULL DEFAULT 0; diff --git a/src/main/resources/db/migration/V2024.06.30.001__drop_review_triggers.sql b/src/main/resources/db/migration/V2024.06.30.001__drop_review_triggers.sql new file mode 100644 index 00000000..5f15c94a --- /dev/null +++ b/src/main/resources/db/migration/V2024.06.30.001__drop_review_triggers.sql @@ -0,0 +1,3 @@ +DROP TRIGGER IF EXISTS `review_trigger_insert`; +DROP TRIGGER IF EXISTS `review_trigger_delete`; +DROP TRIGGER IF EXISTS `review_trigger_update`;