174 lines
4.6 KiB
Markdown
174 lines
4.6 KiB
Markdown
# For You
|
|
|
|
## Route
|
|
|
|
- URL: `GET /discover/for-you`
|
|
- Auth required: yes
|
|
- Controller: `App\Http\Controllers\Web\DiscoverController::forYou()`
|
|
- Entry service: `App\Services\Recommendations\RecommendationFeedResolver`
|
|
|
|
## High-level flow
|
|
|
|
`For You` is not a simple search sort.
|
|
It is a recommendation pipeline with engine selection, caching, layered candidate generation, reranking, and cursor pagination.
|
|
|
|
The controller:
|
|
|
|
1. reads `limit` and `cursor`
|
|
2. calls `RecommendationFeedResolver::getFeed()`
|
|
3. converts feed items into the artwork card view model
|
|
4. returns HTML or JSON depending on request type
|
|
|
|
## Engine selection
|
|
|
|
`RecommendationFeedResolver` chooses between two implementations:
|
|
|
|
- V2: `App\Services\Recommendations\RecommendationServiceV2`
|
|
- V1: `App\Services\Recommendations\PersonalizedFeedService`
|
|
|
|
Selection is based on:
|
|
|
|
- `config('discovery.v2.enabled')`
|
|
- rollout percentage bucket for the current user
|
|
- explicit `algo_version` override
|
|
|
|
## Cache model
|
|
|
|
Both engines use `user_recommendation_cache`.
|
|
|
|
At request time:
|
|
|
|
1. Load cache row for `(user_id, algo_version)`
|
|
2. Check cache version and `expires_at`
|
|
3. If missing or stale, dispatch `RegenerateUserRecommendationCacheJob`
|
|
4. If the row is empty, build fallback recommendations inline for the current request
|
|
|
|
This means the page is usually cache-backed, but it does not hard-fail if the cache is cold.
|
|
|
|
## V2 pipeline
|
|
|
|
V2 is the richer layered engine.
|
|
|
|
### Candidate layers
|
|
|
|
The candidate pool is blended from:
|
|
|
|
- personalized layer
|
|
- social layer
|
|
- trending layer
|
|
- exploration layer
|
|
- vector layer (only when V3/vector support is enabled and configured)
|
|
|
|
Default target ratios from `config/discovery.php`:
|
|
|
|
- personalized: 50%
|
|
- social: 20%
|
|
- trending: 20%
|
|
- exploration: 10%
|
|
|
|
### Main V2 score
|
|
|
|
For each candidate row:
|
|
|
|
```text
|
|
score
|
|
= (base_score * weight_base)
|
|
+ session_boost
|
|
+ social_boost
|
|
+ trending_boost
|
|
+ exploration_boost
|
|
+ creator_boost
|
|
+ vector_boost
|
|
- negative_penalty
|
|
- repetition_penalty
|
|
```
|
|
|
|
Where:
|
|
|
|
- `session_boost` comes from merged session/profile signals
|
|
- `social_boost` comes from followed creators and artworks liked by followed creators
|
|
- `trending_boost` is built from `trending_score_1h`, `trending_score_24h`, `trending_score_7d`
|
|
- `exploration_boost` rewards fresh uploads, new creators, and unseen tags
|
|
- `creator_boost` uses creator follower count plus artwork momentum metrics
|
|
- `vector_boost` comes from visual similarity when vector mode is enabled
|
|
- `negative_penalty` reflects hidden artworks and disliked tags
|
|
- `repetition_penalty` suppresses creator and tag repetition inside one result page
|
|
|
|
### Trending contribution inside V2
|
|
|
|
V2 combines the artwork's stored trending columns like this:
|
|
|
|
```text
|
|
trendingBoost
|
|
= (trending_score_1h * weight_1h)
|
|
+ (trending_score_24h * weight_24h)
|
|
+ (trending_score_7d * weight_7d)
|
|
```
|
|
|
|
Then divides by 100 before merging into the final score.
|
|
|
|
### Negative signals
|
|
|
|
V2 reads `user_negative_signals` and applies:
|
|
|
|
- hidden artwork exclusion
|
|
- disliked tag penalty
|
|
|
|
### Supporting data sources
|
|
|
|
V2 reads from:
|
|
|
|
- `user_recommendation_cache`
|
|
- session/profile signal builders
|
|
- `artworks`
|
|
- `artwork_stats`
|
|
- `artwork_similarities`
|
|
- `artwork_embeddings` and vector service, when enabled
|
|
- `user_followers`
|
|
- `artwork_favourites`
|
|
- `user_negative_signals`
|
|
|
|
## V1 pipeline
|
|
|
|
V1 is simpler and category-affinity driven.
|
|
|
|
It reads `user_interest_profiles`, then scores candidates with a weighted blend:
|
|
|
|
```text
|
|
score = (w1 * affinity)
|
|
+ (w2 * recency)
|
|
+ (w3 * popularity)
|
|
+ (w4 * novelty)
|
|
```
|
|
|
|
Cold start falls back to a blend of popular artworks and `artwork_similarities` seeds.
|
|
|
|
## Background jobs and schedules
|
|
|
|
### Directly relevant
|
|
|
|
- `RegenerateUserRecommendationCacheJob`
|
|
- dispatched on demand when cache is missing or stale
|
|
|
|
### Support jobs for candidate quality
|
|
|
|
- `RecBuildItemPairsFromFavouritesJob` every 4 hours
|
|
- `RecComputeSimilarByTagsJob` daily at 02:00
|
|
- `RecComputeSimilarByBehaviorJob` daily at 02:15
|
|
- `RecComputeSimilarHybridJob` daily at 02:30
|
|
- `analytics:aggregate-discovery-feedback` daily at 03:25
|
|
|
|
These jobs do not directly render the page, but they improve the offline inputs and behavioral data used by the recommender.
|
|
|
|
## Cache behavior
|
|
|
|
- V1 cache TTL default: 60 minutes
|
|
- V2 cache TTL default: 15 minutes
|
|
|
|
Cursor pagination is offset-based under the hood.
|
|
|
|
## Notes
|
|
|
|
- `For You` is the most configuration-sensitive page in this set.
|
|
- What a given user sees can differ by rollout bucket, `algo_version`, cache state, and whether V2/V3 features are enabled.
|
|
- If you are debugging a single user's page, inspect `RecommendationFeedResolver::inspectDecision()` first. |