feat(home): AI 캐릭터 스냅샷 집계 쿼리를 변경한다

This commit is contained in:
2026-07-10 03:20:47 +09:00
parent eb4df66ece
commit 2a0d2a385a
3 changed files with 236 additions and 66 deletions

View File

@@ -483,74 +483,89 @@ class DefaultHomeRecommendationQueryRepository(
override fun findAiCharacterSnapshots(
windowStart: LocalDateTime,
snapshotAt: LocalDateTime,
windowEndExclusive: LocalDateTime,
limit: Int
): List<RecommendationSnapshotRecord> {
val sql = """
select cc.id as target_id,
(((select count(cm.id)
from chat_message cm
join chat_participant cp on cp.id = cm.participant_id
where cp.character_id = cc.id
and cm.created_at >= :windowStart
and cm.created_at <= :snapshotAt
and cm.is_active = true
and cp.is_active = true
and cp.participant_type = 'CHARACTER') * ${RecommendationScoreSpec.AI_RECENT_CHAT_WEIGHT} +
(select count(distinct up.member_id)
from chat_message cm
join chat_participant up on up.id = cm.participant_id
join chat_participant cp on cp.chat_room_id = cm.chat_room_id
join member m on m.id = up.member_id
where cp.character_id = cc.id
and cm.created_at >= :windowStart
and cm.created_at <= :snapshotAt
and cm.is_active = true
and up.is_active = true
and cp.is_active = true
and up.participant_type = 'USER'
and cp.participant_type = 'CHARACTER'
and m.is_active = true) * ${RecommendationScoreSpec.AI_RECENT_ACTIVE_USER_WEIGHT}) *
case
when cc.created_at >= :boost10Start then ${RecommendationScoreSpec.NEW_BOOST_10_DAYS}
when cc.created_at >= :boost20Start then ${RecommendationScoreSpec.NEW_BOOST_20_DAYS}
when cc.created_at >= :boost30Start then ${RecommendationScoreSpec.NEW_BOOST_30_DAYS}
else ${RecommendationScoreSpec.DEFAULT_NEW_BOOST}
end) as score,
rand() as random_tie_breaker
from chat_character cc
where cc.is_active = true
and (exists (
select 1
select ranked.target_id,
ranked.score,
ranked.snapshot_rank as random_tie_breaker
from (
select scored.target_id,
scored.score,
row_number() over (order by scored.score desc, scored.character_created_at desc, scored.target_id desc) as snapshot_rank
from (
select cc.id as target_id,
cc.created_at as character_created_at,
((coalesce(acs.ai_chat_count, 0) * ${RecommendationScoreSpec.AI_CHAT_WEIGHT} +
coalesce(aus.active_user_count, 0) * ${RecommendationScoreSpec.AI_ACTIVE_USER_WEIGHT} +
coalesce(fs.follow_increase_count, 0) * ${RecommendationScoreSpec.AI_FOLLOW_INCREASE_WEIGHT}) *
case
when cc.created_at >= :boost10Start then ${RecommendationScoreSpec.AI_NEW_BOOST_10_DAYS}
when cc.created_at >= :boost20Start then ${RecommendationScoreSpec.AI_NEW_BOOST_20_DAYS}
when cc.created_at >= :boost30Start then ${RecommendationScoreSpec.AI_NEW_BOOST_30_DAYS}
else ${RecommendationScoreSpec.DEFAULT_NEW_BOOST}
end) as score
from chat_character cc
join member creator_member on creator_member.id = cc.creator_member_id
left join (
select cp.character_id as character_id, count(cm.id) as ai_chat_count
from chat_message cm
join chat_participant cp on cp.id = cm.participant_id
where cp.character_id = cc.id
and cm.created_at >= :windowStart
and cm.created_at <= :snapshotAt
where cm.created_at >= :windowStart
and cm.created_at < :windowEndExclusive
and cm.is_active = true
and cp.is_active = true
and cp.participant_type = 'CHARACTER'
) or exists (
select 1
group by cp.character_id
) acs on acs.character_id = cc.id
left join (
select cp.character_id as character_id, count(distinct up.member_id) as active_user_count
from chat_message cm
join chat_participant up on up.id = cm.participant_id
join chat_participant cp on cp.chat_room_id = cm.chat_room_id
join member m on m.id = up.member_id
where cp.character_id = cc.id
and cm.created_at >= :windowStart
and cm.created_at <= :snapshotAt
where cm.created_at >= :windowStart
and cm.created_at < :windowEndExclusive
and cm.is_active = true
and up.is_active = true
and cp.is_active = true
and up.participant_type = 'USER'
and cp.participant_type = 'CHARACTER'
and m.is_active = true
))
order by score desc, random_tie_breaker asc
group by cp.character_id
) aus on aus.character_id = cc.id
left join (
select cf.creator_id as creator_id, count(cf.id) as follow_increase_count
from creator_following cf
join member follower_member on follower_member.id = cf.member_id
where cf.created_at >= :windowStart
and cf.created_at < :windowEndExclusive
and cf.is_active = true
and follower_member.is_active = true
group by cf.creator_id
) fs on fs.creator_id = cc.creator_member_id
where cc.is_active = true
and cc.created_at is not null
and cc.created_at < :windowEndExclusive
and creator_member.is_active = true
and creator_member.role = 'CREATOR'
and creator_member.member_kind = 'AI_CHARACTER'
) scored
where scored.score > 0
) ranked
order by ranked.score desc, ranked.snapshot_rank asc
limit :limit
""".trimIndent()
return executeSnapshotQuery(sql, RecommendedSectionType.AI_CHARACTER, windowStart, snapshotAt, limit)
return executeSnapshotQuery(
sql,
RecommendedSectionType.AI_CHARACTER,
windowStart,
windowEndExclusive.minusSeconds(1),
limit,
windowEndExclusive
)
}
override fun findCheerCreatorSnapshots(
@@ -1129,12 +1144,19 @@ class DefaultHomeRecommendationQueryRepository(
sectionType: RecommendedSectionType,
windowStart: LocalDateTime,
snapshotAt: LocalDateTime,
limit: Int
limit: Int,
windowEndExclusive: LocalDateTime? = null
): List<RecommendationSnapshotRecord> {
val query = entityManager.createNativeQuery(sql)
.setParameter("windowStart", windowStart)
.setParameter("snapshotAt", snapshotAt)
.setParameter("limit", limit)
if (sql.contains(":snapshotAt")) {
query.setParameter("snapshotAt", snapshotAt)
}
if (windowEndExclusive != null) {
query.setParameter("windowEndExclusive", windowEndExclusive)
}
query
.setParameter(
"boost10Start",
snapshotAt.toLocalDate().minusDays(RecommendationScoreSpec.NEW_BOOST_10_DAY_LIMIT).atStartOfDay()

View File

@@ -40,7 +40,7 @@ interface HomeRecommendationQueryPort {
fun findAiCharacterSnapshots(
windowStart: LocalDateTime,
snapshotAt: LocalDateTime,
windowEndExclusive: LocalDateTime,
limit: Int
): List<RecommendationSnapshotRecord>