feat: surface recommendation rationale
This commit is contained in:
@@ -24,7 +24,7 @@ func rerankRecommendationCandidates(candidates []recommendationCandidate, limit
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
selected = append(selected, domain.Anime{Anime: candidate.anime})
|
selected = append(selected, domain.Anime{Anime: candidate.anime, RecommendationRationale: candidate.rationale})
|
||||||
features := diversityFeatures(candidate.anime)
|
features := diversityFeatures(candidate.anime)
|
||||||
seen.add(features)
|
seen.add(features)
|
||||||
recent = append(recent, features)
|
recent = append(recent, features)
|
||||||
|
|||||||
@@ -48,9 +48,23 @@ func scoreRecommendationCandidate(
|
|||||||
themeMatches: themes,
|
themeMatches: themes,
|
||||||
studioMatches: studios,
|
studioMatches: studios,
|
||||||
demographicMatches: demos,
|
demographicMatches: demos,
|
||||||
|
rationale: buildRecommendationRationale(profile, candidate),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func buildRecommendationRationale(profile userTasteProfile, candidate jikan.Anime) []string {
|
||||||
|
rationale := make([]string, 0, 4)
|
||||||
|
rationale = append(rationale, matchedEntityNames(profile.genres, candidate.Genres)...)
|
||||||
|
rationale = append(rationale, matchedEntityNames(profile.themes, candidate.Themes)...)
|
||||||
|
rationale = append(rationale, matchedEntityNames(profile.studios, candidate.Studios)...)
|
||||||
|
rationale = append(rationale, matchedEntityNames(profile.demographics, candidate.Demographics)...)
|
||||||
|
|
||||||
|
if len(rationale) > 4 {
|
||||||
|
return rationale[:4]
|
||||||
|
}
|
||||||
|
return rationale
|
||||||
|
}
|
||||||
|
|
||||||
func recommendationCandidateScoreAdjustments(now time.Time, profile userTasteProfile, candidate jikan.Anime) float64 {
|
func recommendationCandidateScoreAdjustments(now time.Time, profile userTasteProfile, candidate jikan.Anime) float64 {
|
||||||
var score float64
|
var score float64
|
||||||
|
|
||||||
@@ -115,3 +129,22 @@ func weightedEntityMatch(weights map[int]float64, entities []jikan.NamedEntity)
|
|||||||
|
|
||||||
return matches, score
|
return matches, score
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func matchedEntityNames(weights map[int]float64, entities []jikan.NamedEntity) []string {
|
||||||
|
if len(weights) == 0 {
|
||||||
|
return []string{}
|
||||||
|
}
|
||||||
|
|
||||||
|
names := make([]string, 0, 1)
|
||||||
|
for _, entity := range entities {
|
||||||
|
if entity.Name == "" || weights[entity.MalID] <= 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
names = append(names, entity.Name)
|
||||||
|
if len(names) >= 1 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return names
|
||||||
|
}
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ type recommendationCandidate struct {
|
|||||||
themeMatches int
|
themeMatches int
|
||||||
studioMatches int
|
studioMatches int
|
||||||
demographicMatches int
|
demographicMatches int
|
||||||
|
rationale []string
|
||||||
}
|
}
|
||||||
|
|
||||||
type userTasteProfile struct {
|
type userTasteProfile struct {
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import (
|
|||||||
|
|
||||||
type Anime struct {
|
type Anime struct {
|
||||||
jikan.Anime
|
jikan.Anime
|
||||||
|
RecommendationRationale []string
|
||||||
}
|
}
|
||||||
|
|
||||||
type Genre struct {
|
type Genre struct {
|
||||||
|
|||||||
@@ -21,7 +21,17 @@
|
|||||||
{{else}}
|
{{else}}
|
||||||
<div class="grid grid-cols-2 gap-4 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 xl:grid-cols-6 2xl:grid-cols-7">
|
<div class="grid grid-cols-2 gap-4 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 xl:grid-cols-6 2xl:grid-cols-7">
|
||||||
{{range .Animes}}
|
{{range .Animes}}
|
||||||
{{template "anime_card" dict "Anime" . "WithActions" true "IsWatchlist" (index $.WatchlistMap .MalID)}}
|
<div class="flex flex-col gap-2">
|
||||||
|
{{template "anime_card" dict "Anime" . "WithActions" true "HideTitle" true "IsWatchlist" (index $.WatchlistMap .MalID)}}
|
||||||
|
<h3 class="line-clamp-2 text-sm font-normal text-foreground">{{.DisplayTitle}}</h3>
|
||||||
|
{{if .RecommendationRationale}}
|
||||||
|
<div class="flex flex-wrap gap-1.5" aria-label="Why this was picked">
|
||||||
|
{{range .RecommendationRationale}}
|
||||||
|
<span class="rounded-full bg-background-button px-2 py-1 text-[0.68rem] leading-none text-foreground-muted">{{.}}</span>
|
||||||
|
{{end}}
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|||||||
Reference in New Issue
Block a user