Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 13 additions & 12 deletions bottlenote-batch/src/main/resources/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -82,23 +82,24 @@ popularity:
rating: 0.35
pick: 0.20
engagement: 0.40
# 단위별 각 축을 0~1로 누르는 분모. 실제 분포를 확인하기 전까지 같은 임시값을 쓴다.
# 단위별 각 축을 0~1로 누르는 분모. 2026-10-27 운영 분포(주간 조회 최대 146, 별점 19, 찜 5, 참여 94)를 기준으로
# 최댓값이 0.5~0.8에 오도록 잡았다. 조회는 기간 합산이라 단위별로 다르고, 나머지 세 축은 누적 상태라 단위와 무관하다.
reference:
hour:
interest: 50
rating: 100
pick: 200
engagement: 300
rating: 25
pick: 10
engagement: 120
week:
interest: 50
rating: 100
pick: 200
engagement: 300
interest: 200
rating: 25
pick: 10
engagement: 120
month:
interest: 50
rating: 100
pick: 200
engagement: 300
interest: 600
rating: 25
pick: 10
engagement: 120

batch:
visitor-telemetry:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,19 +35,28 @@ public List<PopularItem> getPopularOfWeeks(Long userId, Pageable pageable) {
userId,
pageable.getPageSize(),
BucketGranularity.WEEK,
alcoholPopularitySnapshot.popularityScore);
alcoholPopularitySnapshot.popularityScore,
alcoholPopularitySnapshot.interestValue);
}

@Override
public List<PopularItem> getPopularByInterestWeekly(Long userId, int limit) {
return getPopularBySnapshot(
userId, limit, BucketGranularity.WEEK, alcoholPopularitySnapshot.interestScore);
userId,
limit,
BucketGranularity.WEEK,
alcoholPopularitySnapshot.interestScore,
alcoholPopularitySnapshot.interestValue);
}

@Override
public List<PopularItem> getPopularByInterestMonthly(Long userId, int limit) {
return getPopularBySnapshot(
userId, limit, BucketGranularity.MONTH, alcoholPopularitySnapshot.interestScore);
userId,
limit,
BucketGranularity.MONTH,
alcoholPopularitySnapshot.interestScore,
alcoholPopularitySnapshot.interestValue);
}

@Override
Expand Down Expand Up @@ -87,8 +96,17 @@ public List<PopularItem> getSpringItems(
.fetch();
}

/**
* 정규화 점수는 분모에 닿으면 만점으로 묶이므로 동점이 생긴다. 동점은 원시값 내림차순으로 풀고, 그래도 같으면 id로 고정한다.
*
* <p>종합 인기도의 동점은 기간마다 새로 쌓이는 조회수로 푼다. 나머지 축은 누적 상태라 주차가 바뀌어도 순서가 거의 바뀌지 않기 때문이다.
*/
private List<PopularItem> getPopularBySnapshot(
Long userId, int limit, BucketGranularity granularity, NumberPath<BigDecimal> snapshotScore) {
Long userId,
int limit,
BucketGranularity granularity,
NumberPath<BigDecimal> snapshotScore,
NumberPath<Long> tieBreakValue) {
if (limit <= 0) {
return List.of();
}
Expand All @@ -114,8 +132,9 @@ private List<PopularItem> getPopularBySnapshot(
alcohol.korCategory,
alcohol.engCategory,
alcohol.imageUrl,
snapshotScore)
.orderBy(snapshotScore.desc(), alcohol.id.asc())
snapshotScore,
tieBreakValue)
.orderBy(snapshotScore.desc(), tieBreakValue.desc(), alcohol.id.asc())
.limit(limit)
.fetch();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,53 @@ void springRecommendation_countsRatingsWithoutTagJoinDuplication() {
assertThat(result).singleElement().extracting(PopularItem::ratingCount).isEqualTo(3L);
}

@Test
@DisplayName("주간 관심도 순위는 정규화 점수가 같을 때 원시 조회수가 많은 주류를 먼저 반환한다")
void weeklyInterest_breaksTiesByRawViewCountBeforeId() {
Alcohol lowerIdFewerViews = alcoholTestFactory.persistAlcohol();
Alcohol higherIdMoreViews = alcoholTestFactory.persistAlcohol();
LocalDateTime bucket = BucketGranularity.WEEK.startAt(LocalDateTime.now());
alcoholTestFactory.persistPopularitySnapshot(
lowerIdFewerViews.getId(),
BucketGranularity.WEEK,
bucket,
54L,
BigDecimal.ONE,
BigDecimal.ZERO);
alcoholTestFactory.persistPopularitySnapshot(
higherIdMoreViews.getId(),
BucketGranularity.WEEK,
bucket,
105L,
BigDecimal.ONE,
BigDecimal.ZERO);

List<PopularItem> result = popularQueryRepository.getPopularByInterestWeekly(-1L, 2);

assertThat(result)
.extracting(PopularItem::alcoholId)
.containsExactly(higherIdMoreViews.getId(), lowerIdFewerViews.getId());
}

@Test
@DisplayName("주간 인기 순위는 종합 점수가 같을 때 원시 조회수가 많은 주류를 먼저 반환한다")
void weeklyPopular_breaksTiesByRawViewCountBeforeId() {
Alcohol lowerIdFewerViews = alcoholTestFactory.persistAlcohol();
Alcohol higherIdMoreViews = alcoholTestFactory.persistAlcohol();
LocalDateTime bucket = BucketGranularity.WEEK.startAt(LocalDateTime.now());
BigDecimal sameScore = new BigDecimal("0.05");
alcoholTestFactory.persistPopularitySnapshot(
lowerIdFewerViews.getId(), BucketGranularity.WEEK, bucket, 54L, BigDecimal.ONE, sameScore);
alcoholTestFactory.persistPopularitySnapshot(
higherIdMoreViews.getId(), BucketGranularity.WEEK, bucket, 105L, BigDecimal.ONE, sameScore);

List<PopularItem> result = popularQueryRepository.getPopularOfWeeks(-1L, Pageable.ofSize(2));

assertThat(result)
.extracting(PopularItem::alcoholId)
.containsExactly(higherIdMoreViews.getId(), lowerIdFewerViews.getId());
}

@Test
@DisplayName("마이보틀 isHot5는 최신 WEEK Snapshot 상위 5개만 true로 반환한다")
void myBottle_marksOnlyTopFiveWeeklySnapshotsAsHot() throws Exception {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -505,13 +505,32 @@ public AlcoholPopularitySnapshot persistPopularitySnapshot(
@NotNull LocalDateTime bucketAt,
@NotNull BigDecimal interestScore,
@NotNull BigDecimal popularityScore) {
return persistPopularitySnapshot(
alcoholId,
granularity,
bucketAt,
interestScore.movePointRight(4).longValue(),
interestScore,
popularityScore);
}

/** 정규화 점수가 같아도 원시 조회수가 다른 경우를 만들 때 쓴다. */
@Transactional
@NotNull
public AlcoholPopularitySnapshot persistPopularitySnapshot(
@NotNull Long alcoholId,
@NotNull BucketGranularity granularity,
@NotNull LocalDateTime bucketAt,
long interestValue,
@NotNull BigDecimal interestScore,
@NotNull BigDecimal popularityScore) {
AlcoholPopularitySnapshot snapshot =
AlcoholPopularitySnapshot.builder()
.alcoholId(alcoholId)
.bucketGranularity(granularity)
.bucketAt(bucketAt)
.observedAt(bucketAt.plusHours(1))
.interestValue(interestScore.movePointRight(4).longValue())
.interestValue(interestValue)
.interestSourceBucketAt(bucketAt)
.interestScore(interestScore)
.ratingValue(0L)
Expand Down
Loading