refactor: use recommendation engine in discover for-you
This commit is contained in:
@@ -118,55 +118,84 @@ func (s *animeService) GetDiscoverForYou(ctx context.Context, userID string) (do
|
|||||||
return domain.DiscoverSectionData{}, err
|
return domain.DiscoverSectionData{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
seedIDs := make([]int, 0, 5)
|
now := time.Now()
|
||||||
for _, entry := range watchlist {
|
seedPool := buildRecommendationSeeds(now, watchlist)
|
||||||
status := strings.TrimSpace(entry.Status)
|
if len(seedPool) == 0 {
|
||||||
if status != "watching" && status != "completed" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if entry.AnimeID <= 0 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
seedIDs = append(seedIDs, int(entry.AnimeID))
|
|
||||||
if len(seedIDs) >= 5 {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(seedIDs) == 0 {
|
|
||||||
return domain.DiscoverSectionData{Animes: []domain.Anime{}}, nil
|
return domain.DiscoverSectionData{Animes: []domain.Anime{}}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type ranked struct {
|
type rankedCandidate struct {
|
||||||
id int
|
id int
|
||||||
votes int
|
collaborativeScore float64
|
||||||
}
|
}
|
||||||
|
|
||||||
recommended := map[int]ranked{}
|
watchlistAnimeIDs := make(map[int]struct{}, len(watchlist))
|
||||||
|
for _, entry := range watchlist {
|
||||||
|
if entry.AnimeID <= 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
watchlistAnimeIDs[int(entry.AnimeID)] = struct{}{}
|
||||||
|
}
|
||||||
|
|
||||||
|
seedAnimes := make([]jikan.Anime, len(seedPool))
|
||||||
|
var seedFetchGroup errgroup.Group
|
||||||
|
seedFetchGroup.SetLimit(4)
|
||||||
|
|
||||||
|
for i, seed := range seedPool {
|
||||||
|
seedFetchGroup.Go(func() error {
|
||||||
|
anime, fetchErr := s.jikan.GetAnimeByID(ctx, seed.animeID)
|
||||||
|
if fetchErr != nil {
|
||||||
|
return fetchErr
|
||||||
|
}
|
||||||
|
seedAnimes[i] = anime
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := seedFetchGroup.Wait(); err != nil {
|
||||||
|
return domain.DiscoverSectionData{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
profile := buildTasteProfile(seedAnimes)
|
||||||
|
|
||||||
|
recommended := map[int]rankedCandidate{}
|
||||||
|
var recommendedMu sync.Mutex
|
||||||
var g errgroup.Group
|
var g errgroup.Group
|
||||||
g.SetLimit(4)
|
g.SetLimit(4)
|
||||||
|
|
||||||
for _, seedID := range seedIDs {
|
for _, seed := range seedPool {
|
||||||
g.Go(func() error {
|
g.Go(func() error {
|
||||||
recs, recErr := s.jikan.GetAnimeRecommendations(ctx, seedID)
|
recs, recErr := s.jikan.GetAnimeRecommendations(ctx, seed.animeID)
|
||||||
if recErr != nil {
|
if recErr != nil {
|
||||||
return recErr
|
return recErr
|
||||||
}
|
}
|
||||||
for _, rec := range recs {
|
for i, rec := range recs {
|
||||||
|
if i >= forYouMaxRecommendations {
|
||||||
|
break
|
||||||
|
}
|
||||||
id := rec.Entry.MalID
|
id := rec.Entry.MalID
|
||||||
if id <= 0 {
|
if id <= 0 {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if id == seedID {
|
if id == seed.animeID {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
if _, exists := watchlistAnimeIDs[id]; exists {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
recommendedMu.Lock()
|
||||||
current, ok := recommended[id]
|
current, ok := recommended[id]
|
||||||
if !ok {
|
if !ok {
|
||||||
recommended[id] = ranked{id: id, votes: rec.Votes}
|
recommended[id] = rankedCandidate{
|
||||||
|
id: id,
|
||||||
|
collaborativeScore: float64(rec.Votes) * seed.weight,
|
||||||
|
}
|
||||||
|
recommendedMu.Unlock()
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
current.votes += rec.Votes
|
current.collaborativeScore += float64(rec.Votes) * seed.weight
|
||||||
recommended[id] = current
|
recommended[id] = current
|
||||||
|
recommendedMu.Unlock()
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
@@ -180,20 +209,20 @@ func (s *animeService) GetDiscoverForYou(ctx context.Context, userID string) (do
|
|||||||
return domain.DiscoverSectionData{Animes: []domain.Anime{}}, nil
|
return domain.DiscoverSectionData{Animes: []domain.Anime{}}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
rankedIDs := make([]ranked, 0, len(recommended))
|
rankedIDs := make([]rankedCandidate, 0, len(recommended))
|
||||||
for _, item := range recommended {
|
for _, item := range recommended {
|
||||||
rankedIDs = append(rankedIDs, item)
|
rankedIDs = append(rankedIDs, item)
|
||||||
}
|
}
|
||||||
sort.Slice(rankedIDs, func(i, j int) bool {
|
sort.Slice(rankedIDs, func(i, j int) bool {
|
||||||
if rankedIDs[i].votes == rankedIDs[j].votes {
|
if rankedIDs[i].collaborativeScore == rankedIDs[j].collaborativeScore {
|
||||||
return rankedIDs[i].id < rankedIDs[j].id
|
return rankedIDs[i].id < rankedIDs[j].id
|
||||||
}
|
}
|
||||||
return rankedIDs[i].votes > rankedIDs[j].votes
|
return rankedIDs[i].collaborativeScore > rankedIDs[j].collaborativeScore
|
||||||
})
|
})
|
||||||
|
|
||||||
limit := min(len(rankedIDs), 12)
|
limit := min(len(rankedIDs), forYouCandidateFetchLimit)
|
||||||
|
candidates := make([]recommendationCandidate, 0, limit)
|
||||||
animes := make([]domain.Anime, limit)
|
var candidatesMu sync.Mutex
|
||||||
g.SetLimit(6)
|
g.SetLimit(6)
|
||||||
|
|
||||||
for i := range limit {
|
for i := range limit {
|
||||||
@@ -209,7 +238,15 @@ func (s *animeService) GetDiscoverForYou(ctx context.Context, userID string) (do
|
|||||||
)
|
)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
animes[i] = domain.Anime{Anime: anime}
|
candidate := scoreRecommendationCandidate(
|
||||||
|
now,
|
||||||
|
profile,
|
||||||
|
anime,
|
||||||
|
rankedIDs[i].collaborativeScore,
|
||||||
|
)
|
||||||
|
candidatesMu.Lock()
|
||||||
|
candidates = append(candidates, candidate)
|
||||||
|
candidatesMu.Unlock()
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -218,15 +255,16 @@ func (s *animeService) GetDiscoverForYou(ctx context.Context, userID string) (do
|
|||||||
return domain.DiscoverSectionData{}, err
|
return domain.DiscoverSectionData{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Filter out empty animes if any fetch failed silently
|
sort.Slice(candidates, func(i, j int) bool {
|
||||||
filtered := make([]domain.Anime, 0, len(animes))
|
if candidates[i].score == candidates[j].score {
|
||||||
for _, a := range animes {
|
return candidates[i].anime.MalID < candidates[j].anime.MalID
|
||||||
if a.MalID > 0 {
|
|
||||||
filtered = append(filtered, a)
|
|
||||||
}
|
}
|
||||||
}
|
return candidates[i].score > candidates[j].score
|
||||||
|
})
|
||||||
|
|
||||||
return domain.DiscoverSectionData{Animes: filtered}, nil
|
return domain.DiscoverSectionData{
|
||||||
|
Animes: rerankRecommendationCandidates(candidates, forYouResultLimit),
|
||||||
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *animeService) GetAiringSchedule(ctx context.Context, userID string) ([]domain.Anime, error) {
|
func (s *animeService) GetAiringSchedule(ctx context.Context, userID string) ([]domain.Anime, error) {
|
||||||
|
|||||||
Reference in New Issue
Block a user