['trending_score_24h', 7], default => ['trending_score_7d', 30], }; // Use the windowed counters: views_24h/views_7d and downloads_24h/downloads_7d // instead of all-time totals so trending reflects recent activity. [$viewCol, $dlCol] = match ($period) { '24h' => ['views_24h', 'downloads_24h'], default => ['views_7d', 'downloads_7d'], }; $cutoff = now()->subDays($windowDays)->toDateTimeString(); $updated = 0; Artwork::query() ->select('id') ->where('is_public', true) ->where('is_approved', true) ->whereNull('deleted_at') ->whereNotNull('published_at') ->where('published_at', '>=', $cutoff) ->orderBy('id') ->chunkById($chunkSize, function ($artworks) use ($column, $viewCol, $dlCol, &$updated): void { $ids = $artworks->pluck('id')->toArray(); $inClause = implode(',', array_fill(0, count($ids), '?')); // One bulk UPDATE per chunk – uses pre-computed windowed counters // for views and downloads (accurate rolling windows, reset nightly/weekly) // rather than all-time totals. All other signals use correlated subqueries. // Column name ($column) is controlled internally, not user-supplied. DB::update( "UPDATE artworks SET {$column} = GREATEST( COALESCE((SELECT score_total FROM artwork_award_stats WHERE artwork_award_stats.artwork_id = artworks.id), 0) * ? + COALESCE((SELECT favorites FROM artwork_stats WHERE artwork_stats.artwork_id = artworks.id), 0) * ? + COALESCE((SELECT COUNT(*) FROM artwork_reactions WHERE artwork_reactions.artwork_id = artworks.id), 0) * ? + COALESCE((SELECT {$dlCol} FROM artwork_stats WHERE artwork_stats.artwork_id = artworks.id), 0) * ? + COALESCE((SELECT {$viewCol} FROM artwork_stats WHERE artwork_stats.artwork_id = artworks.id), 0) * ? - (TIMESTAMPDIFF(HOUR, artworks.published_at, NOW()) * ?) , 0), last_trending_calculated_at = NOW() WHERE id IN ({$inClause})", array_merge( [self::W_AWARD, self::W_FAVORITE, self::W_REACTION, self::W_DOWNLOAD, self::W_VIEW, self::DECAY_RATE], $ids ) ); $updated += count($ids); }); Log::info('TrendingService: recalculation complete', [ 'period' => $period, 'column' => $column, 'updated' => $updated, ]); return $updated; } /** * Dispatch Meilisearch re-index jobs for artworks in the trending window. * Called after recalculate() to keep the search index current. */ public function syncToSearchIndex(string $period = '7d', int $chunkSize = 500): void { $windowDays = $period === '24h' ? 7 : 30; $cutoff = now()->subDays($windowDays)->toDateTimeString(); Artwork::query() ->select('id') ->where('is_public', true) ->where('is_approved', true) ->whereNull('deleted_at') ->where('published_at', '>=', $cutoff) ->chunkById($chunkSize, function ($artworks): void { foreach ($artworks as $artwork) { \App\Jobs\IndexArtworkJob::dispatch($artwork->id); } }); } }