feat(trending): switch trending endpoints to Ranking V2 ranking_score\n\n- discoverTrending() now sorts by ranking_score:desc + engagement_velocity:desc\n- HomepageService::getTrending() now sorts by ranking_score:desc + velocity\n- DB fallback joins artwork_stats for ranking_score sort\n- Both trending endpoints filter to last 30 days (spec §6)\n- Add created_at to Meilisearch filterableAttributes for date filtering\n- Synced index settings"

This commit is contained in:
2026-02-28 16:47:08 +01:00
parent de3ec22ee5
commit 916bb29a53
3 changed files with 26 additions and 16 deletions

View File

@@ -133,20 +133,22 @@ final class HomepageService
}
/**
* Trending: up to 12 artworks sorted by pre-computed trending_score_7d.
* Trending: up to 12 artworks sorted by Ranking V2 `ranking_score`.
*
* Uses Meilisearch sorted by the pre-computed score (updated every 30 min).
* Falls back to DB ORDER BY trending_score_7d if Meilisearch is unavailable.
* Spec: no heavy joins in the hot path.
* Uses Meilisearch sorted by the V2 score (updated every 30 min).
* Falls back to DB ORDER BY ranking_score if Meilisearch is unavailable.
* Spec §6: ranking_score, last 30 days, highlight high-velocity artworks.
*/
public function getTrending(int $limit = 10): array
{
return Cache::remember("homepage.trending.{$limit}", self::CACHE_TTL, function () use ($limit): array {
$cutoff = now()->subDays(30)->toDateString();
return Cache::remember("homepage.trending.{$limit}", self::CACHE_TTL, function () use ($limit, $cutoff): array {
try {
$results = Artwork::search('')
->options([
'filter' => 'is_public = true AND is_approved = true',
'sort' => ['trending_score_7d:desc', 'trending_score_24h:desc', 'views:desc'],
'filter' => 'is_public = true AND is_approved = true AND created_at >= "' . $cutoff . '"',
'sort' => ['ranking_score:desc', 'engagement_velocity:desc', 'views:desc'],
])
->paginate($limit, 'page', 1);
@@ -172,15 +174,18 @@ final class HomepageService
/**
* DB-only fallback for trending (Meilisearch unavailable).
* Uses pre-computed trending_score_7d column no correlated subqueries.
* Joins artwork_stats to sort by V2 ranking_score.
*/
private function getTrendingFromDb(int $limit): array
{
return Artwork::public()
->published()
->with(['user:id,name,username', 'user.profile:user_id,avatar_hash'])
->orderByDesc('trending_score_7d')
->orderByDesc('trending_score_24h')
->leftJoin('artwork_stats', 'artwork_stats.artwork_id', '=', 'artworks.id')
->select('artworks.*')
->where('artworks.published_at', '>=', now()->subDays(30))
->orderByDesc('artwork_stats.ranking_score')
->orderByDesc('artwork_stats.engagement_velocity')
->limit($limit)
->get()
->map(fn ($a) => $this->serializeArtwork($a))