feat(recommend): 홈 장르 추천 크리에이터 중복 보충을 개선한다
This commit is contained in:
@@ -116,14 +116,38 @@ class HomeRecommendationQueryService(
|
|||||||
creatorLimit: Int = DEFAULT_GENRE_CREATOR_CREATOR_LIMIT
|
creatorLimit: Int = DEFAULT_GENRE_CREATOR_CREATOR_LIMIT
|
||||||
): List<HomeGenreCreatorRecommendationGroup> {
|
): List<HomeGenreCreatorRecommendationGroup> {
|
||||||
val selectedCreatorIds = mutableSetOf<Long>()
|
val selectedCreatorIds = mutableSetOf<Long>()
|
||||||
val candidateLimit = genreLimit * creatorLimit
|
val partialFallbackGroups = mutableListOf<HomeGenreCreatorRecommendationGroup>()
|
||||||
|
val selectedGroups = mutableListOf<HomeGenreCreatorRecommendationGroup>()
|
||||||
|
|
||||||
return queryPort.findGenreCreatorRecommendations(memberId, includeAdultGenres, genreLimit, candidateLimit)
|
queryPort.findGenreCreatorRecommendations(memberId, includeAdultGenres, genreLimit, creatorLimit)
|
||||||
.map { group ->
|
.forEach { group ->
|
||||||
group.copy(creators = group.creators.filter { selectedCreatorIds.add(it.creatorId) }.take(creatorLimit))
|
if (selectedGroups.size >= genreLimit) return@forEach
|
||||||
|
|
||||||
|
val creators = group.creators.filter { it.creatorId !in selectedCreatorIds }.take(creatorLimit)
|
||||||
|
if (creators.isEmpty()) return@forEach
|
||||||
|
|
||||||
|
val deduplicatedGroup = group.copy(creators = creators)
|
||||||
|
if (group.isViewedTheme || creators.size == creatorLimit) {
|
||||||
|
selectedGroups.add(deduplicatedGroup)
|
||||||
|
selectedCreatorIds.addAll(creators.map { it.creatorId })
|
||||||
|
} else {
|
||||||
|
partialFallbackGroups.add(group)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.filter { it.creators.isNotEmpty() }
|
|
||||||
.take(genreLimit)
|
if (selectedGroups.size < genreLimit) {
|
||||||
|
partialFallbackGroups.forEach { group ->
|
||||||
|
if (selectedGroups.size >= genreLimit) return@forEach
|
||||||
|
|
||||||
|
val creators = group.creators.filter { it.creatorId !in selectedCreatorIds }.take(creatorLimit)
|
||||||
|
if (creators.isEmpty()) return@forEach
|
||||||
|
|
||||||
|
selectedGroups.add(group.copy(creators = creators))
|
||||||
|
selectedCreatorIds.addAll(creators.map { it.creatorId })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return selectedGroups.take(genreLimit)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun resolveAudioContentActivityType(theme: String): RecommendedActivityType {
|
fun resolveAudioContentActivityType(theme: String): RecommendedActivityType {
|
||||||
|
|||||||
@@ -374,7 +374,7 @@ class HomeRecommendationQueryServiceTest {
|
|||||||
assertEquals(100L, port.genreCreatorMemberId)
|
assertEquals(100L, port.genreCreatorMemberId)
|
||||||
assertEquals(true, port.genreCreatorIncludeAdultGenres)
|
assertEquals(true, port.genreCreatorIncludeAdultGenres)
|
||||||
assertEquals(5, port.genreCreatorGenreLimit)
|
assertEquals(5, port.genreCreatorGenreLimit)
|
||||||
assertEquals(40, port.genreCreatorCreatorLimit)
|
assertEquals(8, port.genreCreatorCreatorLimit)
|
||||||
assertEquals(listOf(10L, 11L), recommendations[0].creators.map { it.creatorId })
|
assertEquals(listOf(10L, 11L), recommendations[0].creators.map { it.creatorId })
|
||||||
assertEquals(listOf(12L, 13L), recommendations[1].creators.map { it.creatorId })
|
assertEquals(listOf(12L, 13L), recommendations[1].creators.map { it.creatorId })
|
||||||
}
|
}
|
||||||
@@ -429,6 +429,150 @@ class HomeRecommendationQueryServiceTest {
|
|||||||
assertEquals(false, recommendations.any { it.creators.isEmpty() })
|
assertEquals(false, recommendations.any { it.creators.isEmpty() })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("장르 기반 크리에이터 추천은 중복 제거 후 랜덤 보충 장르가 8명이 되지 않으면 뒤 후보로 대체한다")
|
||||||
|
fun shouldBackfillRandomThemesWhenCreatorDeduplicationMakesThemeUnderfilled() {
|
||||||
|
val sharedCreators = (1L..8L).map { creatorId ->
|
||||||
|
HomeGenreCreatorRecommendationRecord(
|
||||||
|
creatorId = creatorId,
|
||||||
|
creatorNickname = "shared-$creatorId",
|
||||||
|
creatorProfileImage = null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
val uniqueCreators = (101L..108L).map { creatorId ->
|
||||||
|
HomeGenreCreatorRecommendationRecord(
|
||||||
|
creatorId = creatorId,
|
||||||
|
creatorNickname = "unique-$creatorId",
|
||||||
|
creatorProfileImage = null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
port.genreCreatorRecommendations = listOf(
|
||||||
|
HomeGenreCreatorRecommendationGroup(
|
||||||
|
genreId = 1L,
|
||||||
|
genreName = "first-random-theme",
|
||||||
|
creators = sharedCreators
|
||||||
|
),
|
||||||
|
HomeGenreCreatorRecommendationGroup(
|
||||||
|
genreId = 2L,
|
||||||
|
genreName = "underfilled-after-dedup-theme",
|
||||||
|
creators = sharedCreators
|
||||||
|
),
|
||||||
|
HomeGenreCreatorRecommendationGroup(
|
||||||
|
genreId = 3L,
|
||||||
|
genreName = "backfill-random-theme",
|
||||||
|
creators = uniqueCreators
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
val recommendations = service.findGenreCreatorRecommendations(
|
||||||
|
memberId = null,
|
||||||
|
includeAdultGenres = false,
|
||||||
|
genreLimit = 2,
|
||||||
|
creatorLimit = 8
|
||||||
|
)
|
||||||
|
|
||||||
|
assertEquals(listOf(1L, 3L), recommendations.map { it.genreId })
|
||||||
|
assertEquals(true, recommendations.all { it.creators.size == 8 })
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("장르 기반 크리에이터 추천은 보류된 부분 후보의 크리에이터를 뒤 충분 후보에서 중복 제거하지 않는다")
|
||||||
|
fun shouldNotConsumeCreatorsFromDeferredPartialFallbackCandidates() {
|
||||||
|
val firstCreators = (1L..8L).map { creatorId ->
|
||||||
|
HomeGenreCreatorRecommendationRecord(
|
||||||
|
creatorId = creatorId,
|
||||||
|
creatorNickname = "first-$creatorId",
|
||||||
|
creatorProfileImage = null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
val partialUniqueCreator = HomeGenreCreatorRecommendationRecord(
|
||||||
|
creatorId = 101L,
|
||||||
|
creatorNickname = "partial-unique",
|
||||||
|
creatorProfileImage = null
|
||||||
|
)
|
||||||
|
val fullBackfillCreators = (101L..108L).map { creatorId ->
|
||||||
|
HomeGenreCreatorRecommendationRecord(
|
||||||
|
creatorId = creatorId,
|
||||||
|
creatorNickname = "full-$creatorId",
|
||||||
|
creatorProfileImage = null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
port.genreCreatorRecommendations = listOf(
|
||||||
|
HomeGenreCreatorRecommendationGroup(
|
||||||
|
genreId = 1L,
|
||||||
|
genreName = "first-random-theme",
|
||||||
|
creators = firstCreators
|
||||||
|
),
|
||||||
|
HomeGenreCreatorRecommendationGroup(
|
||||||
|
genreId = 2L,
|
||||||
|
genreName = "deferred-partial-theme",
|
||||||
|
creators = firstCreators.take(7) + partialUniqueCreator
|
||||||
|
),
|
||||||
|
HomeGenreCreatorRecommendationGroup(
|
||||||
|
genreId = 3L,
|
||||||
|
genreName = "full-backfill-theme",
|
||||||
|
creators = fullBackfillCreators
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
val recommendations = service.findGenreCreatorRecommendations(
|
||||||
|
memberId = null,
|
||||||
|
includeAdultGenres = false,
|
||||||
|
genreLimit = 2,
|
||||||
|
creatorLimit = 8
|
||||||
|
)
|
||||||
|
|
||||||
|
assertEquals(listOf(1L, 3L), recommendations.map { it.genreId })
|
||||||
|
assertEquals((101L..108L).toList(), recommendations[1].creators.map { it.creatorId })
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("장르 기반 크리에이터 추천은 조회 이력 장르는 중복 제거 후 8명 미만이어도 유지한다")
|
||||||
|
fun shouldKeepViewedThemeWhenCreatorDeduplicationMakesThemeUnderfilled() {
|
||||||
|
val sharedCreators = (1L..8L).map { creatorId ->
|
||||||
|
HomeGenreCreatorRecommendationRecord(
|
||||||
|
creatorId = creatorId,
|
||||||
|
creatorNickname = "shared-$creatorId",
|
||||||
|
creatorProfileImage = null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
val uniqueCreators = (101L..108L).map { creatorId ->
|
||||||
|
HomeGenreCreatorRecommendationRecord(
|
||||||
|
creatorId = creatorId,
|
||||||
|
creatorNickname = "unique-$creatorId",
|
||||||
|
creatorProfileImage = null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
port.genreCreatorRecommendations = listOf(
|
||||||
|
HomeGenreCreatorRecommendationGroup(
|
||||||
|
genreId = 1L,
|
||||||
|
genreName = "first-random-theme",
|
||||||
|
creators = sharedCreators
|
||||||
|
),
|
||||||
|
HomeGenreCreatorRecommendationGroup(
|
||||||
|
genreId = 2L,
|
||||||
|
genreName = "viewed-theme",
|
||||||
|
creators = sharedCreators + uniqueCreators.first(),
|
||||||
|
isViewedTheme = true
|
||||||
|
),
|
||||||
|
HomeGenreCreatorRecommendationGroup(
|
||||||
|
genreId = 3L,
|
||||||
|
genreName = "backfill-random-theme",
|
||||||
|
creators = uniqueCreators
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
val recommendations = service.findGenreCreatorRecommendations(
|
||||||
|
memberId = 100L,
|
||||||
|
includeAdultGenres = false,
|
||||||
|
genreLimit = 2,
|
||||||
|
creatorLimit = 8
|
||||||
|
)
|
||||||
|
|
||||||
|
assertEquals(listOf(1L, 2L), recommendations.map { it.genreId })
|
||||||
|
assertEquals(1, recommendations[1].creators.size)
|
||||||
|
}
|
||||||
|
|
||||||
private class FakeHomeRecommendationQueryPort : HomeRecommendationQueryPort {
|
private class FakeHomeRecommendationQueryPort : HomeRecommendationQueryPort {
|
||||||
var liveLimit: Int? = null
|
var liveLimit: Int? = null
|
||||||
var liveOffset: Int? = null
|
var liveOffset: Int? = null
|
||||||
|
|||||||
Reference in New Issue
Block a user