125 lines
3.6 KiB
Markdown
125 lines
3.6 KiB
Markdown
# Trending
|
|
|
|
## Route
|
|
|
|
- URL: `GET /discover/trending`
|
|
- Controller: `App\Http\Controllers\Web\DiscoverController::trending()`
|
|
- Service: `App\Services\ArtworkSearchService::discoverTrending()`
|
|
|
|
## What the page actually reads
|
|
|
|
Primary ranking comes from Meilisearch, not directly from MySQL.
|
|
|
|
The controller flow is:
|
|
|
|
1. Query Meilisearch through `ArtworkSearchService`
|
|
2. If the search-backed query throws or returns no items, fall back to a direct MySQL query against `artworks` + `artwork_stats`
|
|
3. Hydrate the returned IDs back into full `Artwork` Eloquent models when needed
|
|
4. Render `resources/views/web/discover/index.blade.php`
|
|
|
|
So the page is effectively:
|
|
|
|
- ranking source: Meilisearch
|
|
- fallback ranking source: MySQL + `artwork_stats`
|
|
- display hydration source: MySQL
|
|
|
|
## Search query
|
|
|
|
`discoverTrending()` uses:
|
|
|
|
- filter: `is_public = true AND is_approved = true AND created_at >= cutoff`
|
|
- sort:
|
|
- `ranking_score:desc`
|
|
- `engagement_velocity:desc`
|
|
- `views:desc`
|
|
|
|
`created_at` here is the search index field, not necessarily the raw DB column semantics.
|
|
|
|
## Time window
|
|
|
|
The cutoff is not hardcoded to 30 days in all cases.
|
|
|
|
`App\Services\EarlyGrowth\AdaptiveTimeWindow` chooses the effective look-back window:
|
|
|
|
- 7 days when uploads/day is healthy
|
|
- 30 days in moderate activity
|
|
- 90 days in low activity
|
|
|
|
That widening only changes which artworks are eligible for the query.
|
|
It does not rewrite timestamps.
|
|
|
|
## Ranking formula
|
|
|
|
The page does not sort by `trending_score_7d` anymore.
|
|
It sorts by `ranking_score`, which is produced by `App\Services\Ranking\ArtworkRankingService`.
|
|
|
|
Current V2 ranking formula:
|
|
|
|
```text
|
|
base_score
|
|
= (views_all * 0.2)
|
|
+ (downloads_all * 1.5)
|
|
+ (favourites_all * 2.5)
|
|
+ (comments_count * 3.0)
|
|
+ (shares_count * 4.0)
|
|
|
|
authority_multiplier
|
|
= 1 + ((log10(1 + author_followers_count)
|
|
+ (author_favourites_received / 1000)) * 0.05)
|
|
|
|
decay_factor
|
|
= 1 / (1 + age_hours / 48)
|
|
|
|
velocity_boost
|
|
= ((views_24h * 1.0)
|
|
+ (favourites_24h * 3.0)
|
|
+ (comments_24h * 4.0)
|
|
+ (shares_24h * 5.0)) * 0.5
|
|
|
|
ranking_score
|
|
= (base_score * authority_multiplier * decay_factor) + velocity_boost
|
|
```
|
|
|
|
`engagement_velocity` is the raw `velocity_boost` term stored separately in `artwork_stats`.
|
|
|
|
## Where the numbers come from
|
|
|
|
The page depends mainly on:
|
|
|
|
- `artwork_stats.ranking_score`
|
|
- `artwork_stats.engagement_velocity`
|
|
- `artwork_stats.views`
|
|
- `artwork_stats.downloads`
|
|
- `artwork_stats.favorites`
|
|
- `artwork_stats.comments_count`
|
|
- `artwork_stats.shares_count`
|
|
- author-level follower and favourites-received signals
|
|
|
|
Those values are copied into the search document by `Artwork::toSearchableArray()`.
|
|
|
|
## Active jobs and schedules
|
|
|
|
Relevant active schedules:
|
|
|
|
- `nova:recalculate-rankings --sync-rank-scores` every 30 minutes
|
|
- `skinbase:flush-redis-stats` every 5 minutes
|
|
|
|
Indirectly relevant:
|
|
|
|
- `artworks:publish-scheduled` every minute
|
|
- Meilisearch indexing jobs dispatched after score recalculation
|
|
|
|
## Cache behavior
|
|
|
|
- Cache key: `discover.trending.{windowDays}d.{page}`
|
|
- TTL: 300 seconds
|
|
|
|
That means a ranking update can be correct in Meilisearch but still hidden by app cache for up to 5 minutes.
|
|
|
|
If Meilisearch sort settings are missing or the index returns no results, the controller falls back to a DB query instead of rendering a 500 or an empty page.
|
|
|
|
## Notes
|
|
|
|
- The view text still says "most-viewed artworks on Skinbase over the past 7 days," but the current implementation is really a `ranking_score` page with a dynamic eligibility window.
|
|
- The page is only as fresh as both the index and the app cache.
|
|
- The page no longer uses `GridFiller`, so it should not inject older out-of-window artworks just to pad page 1. |