feat(home): 크리에이터 랭킹 갱신 산식을 적용한다
This commit is contained in:
@@ -2,7 +2,9 @@ package kr.co.vividnext.sodalive.v2.ranking.application
|
||||
|
||||
import kr.co.vividnext.sodalive.v2.home.following.application.HomeFollowingNewsPublishService
|
||||
import kr.co.vividnext.sodalive.v2.ranking.domain.CreatorRankingPeriodPolicy
|
||||
import kr.co.vividnext.sodalive.v2.ranking.domain.CreatorRankingScoreDetail
|
||||
import kr.co.vividnext.sodalive.v2.ranking.domain.CreatorRankingScorePolicy
|
||||
import kr.co.vividnext.sodalive.v2.ranking.domain.CreatorRankingScoreSpec
|
||||
import kr.co.vividnext.sodalive.v2.ranking.domain.CreatorRankingSnapshotCandidate
|
||||
import kr.co.vividnext.sodalive.v2.ranking.domain.CreatorRankingType
|
||||
import kr.co.vividnext.sodalive.v2.ranking.domain.CreatorRankingUtcRange
|
||||
@@ -38,9 +40,7 @@ class CreatorRankingSnapshotRefreshService(
|
||||
startInclusiveUtc = utcRange.startInclusiveUtc,
|
||||
endExclusiveUtc = utcRange.endExclusiveUtc
|
||||
)
|
||||
val snapshots = aggregationResult.candidates.map { it.toSnapshotRecord(utcRange) }
|
||||
.sortedByDescending { it.finalScore }
|
||||
.takeRankedBoundary(limit = SNAPSHOT_LIMIT)
|
||||
val snapshots = assembleSnapshots(aggregationResult.candidates, utcRange, visibleFromAtUtc)
|
||||
|
||||
snapshotPort.replaceSnapshots(
|
||||
rankingType = CreatorRankingType.WEEKLY,
|
||||
@@ -50,7 +50,7 @@ class CreatorRankingSnapshotRefreshService(
|
||||
newSnapshots = snapshots
|
||||
)
|
||||
afterCommit {
|
||||
snapshots.forEachIndexed { index, snapshot ->
|
||||
snapshots.forEach { snapshot ->
|
||||
runCatching {
|
||||
homeFollowingNewsPublishService.publishCreatorRankingVisible(
|
||||
creatorId = snapshot.creatorId,
|
||||
@@ -58,13 +58,13 @@ class CreatorRankingSnapshotRefreshService(
|
||||
creatorProfileImagePath = snapshot.profileImageUrl,
|
||||
aggregationStartAtUtc = utcRange.startInclusiveUtc,
|
||||
visibleFromAtUtc = visibleFromAtUtc,
|
||||
rank = index + 1
|
||||
rank = snapshot.rankNo
|
||||
)
|
||||
}.onFailure { ex ->
|
||||
log.warn(
|
||||
"event=home_following_creator_ranking_news_publish_failure creatorId={} rank={} error={}",
|
||||
snapshot.creatorId,
|
||||
index + 1,
|
||||
snapshot.rankNo,
|
||||
ex.message,
|
||||
ex
|
||||
)
|
||||
@@ -100,14 +100,117 @@ class CreatorRankingSnapshotRefreshService(
|
||||
}
|
||||
}
|
||||
|
||||
private fun CreatorRankingAggregationResult.toLogCounts(storedCount: Int): RefreshLogCounts {
|
||||
return RefreshLogCounts(
|
||||
candidateCount = candidates.size,
|
||||
storedCount = storedCount,
|
||||
lowScoreExcludedCount = lowScoreExcludedCount
|
||||
private fun assembleSnapshots(
|
||||
candidates: List<CreatorRankingSnapshotCandidate>,
|
||||
utcRange: CreatorRankingUtcRange,
|
||||
visibleFromAtUtc: java.time.LocalDateTime
|
||||
): List<CreatorRankingSnapshotRecord> {
|
||||
val liveRanks = candidates.toSqlRanks { it.liveCanAmount }
|
||||
val contentRanks = candidates.toSqlRanks { it.contentPurchaseCanAmount }
|
||||
val followRanks = candidates.toSqlRanks { it.followIncrease.coerceAtLeast(0L) }
|
||||
val aiChatRanks = candidates.toSqlRanks { it.aiChatCount }
|
||||
|
||||
return candidates.map { candidate ->
|
||||
val live = metric(
|
||||
candidate.liveCanAmount,
|
||||
liveRanks.getValue(candidate.creatorId),
|
||||
CreatorRankingScoreSpec.LIVE_REVENUE_WEIGHT
|
||||
)
|
||||
val content = metric(
|
||||
candidate.contentPurchaseCanAmount,
|
||||
contentRanks.getValue(candidate.creatorId),
|
||||
CreatorRankingScoreSpec.CONTENT_REVENUE_WEIGHT
|
||||
)
|
||||
val followRaw = scorePolicy.normalizeFollowIncrease(candidate.followIncrease)
|
||||
val follow = metric(
|
||||
followRaw,
|
||||
followRanks.getValue(candidate.creatorId),
|
||||
CreatorRankingScoreSpec.FOLLOW_INCREASE_WEIGHT
|
||||
)
|
||||
val aiChat = metric(
|
||||
candidate.aiChatCount,
|
||||
aiChatRanks.getValue(candidate.creatorId),
|
||||
CreatorRankingScoreSpec.AI_CHAT_WEIGHT
|
||||
)
|
||||
val finalScore = scorePolicy.calculateFinalScore(
|
||||
live.rankScore,
|
||||
content.rankScore,
|
||||
follow.rankScore,
|
||||
aiChat.rankScore
|
||||
)
|
||||
val detail = CreatorRankingScoreDetail(
|
||||
metrics = CreatorRankingScoreDetail.Metrics(
|
||||
liveRevenue = live,
|
||||
contentRevenue = content,
|
||||
followIncrease = follow,
|
||||
aiChat = aiChat
|
||||
),
|
||||
tieBreakers = CreatorRankingScoreDetail.TieBreakers(
|
||||
creatorDebutAt = candidate.creatorDebutAt,
|
||||
creatorId = candidate.creatorId
|
||||
)
|
||||
)
|
||||
ScoredCandidate(candidate, finalScore, detail.toJson())
|
||||
}.filter { it.finalScore > 0.0 }
|
||||
.sortedWith(
|
||||
compareByDescending<ScoredCandidate> { it.finalScore }
|
||||
.thenByDescending { it.candidate.creatorDebutAt ?: java.time.LocalDateTime.MIN }
|
||||
.thenByDescending { it.candidate.creatorId }
|
||||
)
|
||||
.take(SNAPSHOT_LIMIT)
|
||||
.mapIndexed { index, scored ->
|
||||
CreatorRankingSnapshotRecord(
|
||||
rankingType = CreatorRankingType.WEEKLY,
|
||||
aggregationStartAtUtc = utcRange.startInclusiveUtc,
|
||||
aggregationEndAtUtc = utcRange.endExclusiveUtc,
|
||||
visibleFromAtUtc = visibleFromAtUtc,
|
||||
creatorId = scored.candidate.creatorId,
|
||||
nickname = scored.candidate.nickname,
|
||||
profileImageUrl = scored.candidate.profileImageUrl,
|
||||
rankNo = index + 1,
|
||||
finalScore = scored.finalScore,
|
||||
scorePolicyVersion = CreatorRankingScoreSpec.SCORE_POLICY_VERSION,
|
||||
scoreDetailJson = scored.scoreDetailJson
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun List<CreatorRankingSnapshotCandidate>.toSqlRanks(
|
||||
metric: (CreatorRankingSnapshotCandidate) -> Long
|
||||
): Map<Long, Int> {
|
||||
var previousValue: Long? = null
|
||||
var currentRank = 0
|
||||
return sortedByDescending(metric).mapIndexed { index, candidate ->
|
||||
val value = metric(candidate)
|
||||
if (previousValue == null || previousValue != value) {
|
||||
currentRank = index + 1
|
||||
previousValue = value
|
||||
}
|
||||
candidate.creatorId to currentRank
|
||||
}.toMap()
|
||||
}
|
||||
|
||||
private fun metric(rawValue: Long, rank: Int, weight: Double): CreatorRankingScoreDetail.Metric {
|
||||
val rankScore = scorePolicy.calculateRankScore(rawValue, rank)
|
||||
return CreatorRankingScoreDetail.Metric(
|
||||
rawValue = rawValue,
|
||||
rank = rank,
|
||||
rankScore = rankScore,
|
||||
weight = weight,
|
||||
weightedScore = rankScore * weight
|
||||
)
|
||||
}
|
||||
|
||||
private data class ScoredCandidate(
|
||||
val candidate: CreatorRankingSnapshotCandidate,
|
||||
val finalScore: Double,
|
||||
val scoreDetailJson: String
|
||||
)
|
||||
|
||||
private fun CreatorRankingAggregationResult.toLogCounts(storedCount: Int): RefreshLogCounts {
|
||||
return RefreshLogCounts(candidates.size, storedCount, lowScoreExcludedCount)
|
||||
}
|
||||
|
||||
private data class RefreshLogCounts(
|
||||
val candidateCount: Int,
|
||||
val storedCount: Int,
|
||||
@@ -132,62 +235,6 @@ class CreatorRankingSnapshotRefreshService(
|
||||
)
|
||||
}
|
||||
|
||||
private fun CreatorRankingSnapshotCandidate.toSnapshotRecord(utcRange: CreatorRankingUtcRange): CreatorRankingSnapshotRecord {
|
||||
val calculatedContentLiveScore = scorePolicy.calculateContentLiveScore(
|
||||
liveCanAmount = liveCanAmount,
|
||||
contentPurchaseCanAmount = contentPurchaseCanAmount
|
||||
)
|
||||
val calculatedEngagementScore = scorePolicy.calculateEngagementScore(
|
||||
contentLikeCount = contentLikeCount,
|
||||
contentCommentCount = contentCommentCount
|
||||
)
|
||||
val calculatedSupportScore = scorePolicy.calculateSupportScore(
|
||||
channelDonationCanAmount = channelDonationCanAmount,
|
||||
channelDonationCount = channelDonationCount,
|
||||
fanTalkCount = fanTalkCount
|
||||
)
|
||||
val calculatedFanLoyaltyScore = scorePolicy.calculateFanLoyaltyScore(
|
||||
finalFollowerCount = finalFollowerCount,
|
||||
followIncrease = followIncrease
|
||||
)
|
||||
val calculatedFinalScore = scorePolicy.calculateFinalScore(
|
||||
contentLiveScore = calculatedContentLiveScore,
|
||||
engagementScore = calculatedEngagementScore,
|
||||
supportScore = calculatedSupportScore,
|
||||
fanLoyaltyScore = calculatedFanLoyaltyScore
|
||||
)
|
||||
|
||||
return CreatorRankingSnapshotRecord(
|
||||
rankingType = CreatorRankingType.WEEKLY,
|
||||
aggregationStartAtUtc = utcRange.startInclusiveUtc,
|
||||
aggregationEndAtUtc = utcRange.endExclusiveUtc,
|
||||
visibleFromAtUtc = utcRange.endExclusiveUtc.plusHours(9),
|
||||
creatorId = creatorId,
|
||||
nickname = nickname,
|
||||
profileImageUrl = profileImageUrl,
|
||||
finalScore = calculatedFinalScore,
|
||||
contentLiveScore = calculatedContentLiveScore,
|
||||
engagementScore = calculatedEngagementScore,
|
||||
supportScore = calculatedSupportScore,
|
||||
fanLoyaltyScore = calculatedFanLoyaltyScore,
|
||||
liveCanAmount = liveCanAmount,
|
||||
contentPurchaseCanAmount = contentPurchaseCanAmount,
|
||||
contentLikeCount = contentLikeCount,
|
||||
contentCommentCount = contentCommentCount,
|
||||
channelDonationCanAmount = channelDonationCanAmount,
|
||||
channelDonationCount = channelDonationCount,
|
||||
fanTalkCount = fanTalkCount,
|
||||
finalFollowerCount = finalFollowerCount,
|
||||
followIncrease = followIncrease
|
||||
)
|
||||
}
|
||||
|
||||
private fun List<CreatorRankingSnapshotRecord>.takeRankedBoundary(limit: Int): List<CreatorRankingSnapshotRecord> {
|
||||
if (size <= limit) return this
|
||||
val boundaryScore = this[limit - 1].finalScore
|
||||
return filter { it.finalScore >= boundaryScore }
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val SNAPSHOT_LIMIT = 20
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user