Files
silo-server/internal/recommendations/accuracy_test.go
T

271 lines
8.7 KiB
Go

package recommendations
import (
"slices"
"testing"
"github.com/Silo-Server/silo-server/internal/config"
)
func TestApplyGenreCapCountsAllGenres(t *testing.T) {
items := []ScoredItem{
{MediaItemID: "a", Score: 1.0},
{MediaItemID: "b", Score: 0.9},
{MediaItemID: "c", Score: 0.8},
{MediaItemID: "d", Score: 0.7},
}
genres := map[string][]string{
"a": {"Action", "Drama"},
"b": {"Action", "Comedy"},
"c": {"Action", "Thriller"},
"d": {"Comedy"},
}
capped := applyGenreCap(items, genres, 0.67)
if len(capped) != 3 {
t.Fatalf("expected 3 capped items, got %d", len(capped))
}
for _, item := range capped {
if item.MediaItemID == "c" {
t.Fatalf("expected lowest-scored Action item to be removed, got %#v", capped)
}
}
}
func TestHNSWEfSearchUsesCandidateLimitFloor(t *testing.T) {
tests := []struct {
name string
candidateLimit int
want int
}{
{name: "raises small scans", candidateLimit: 40, want: minHNSWEfSearch},
{name: "keeps exact floor", candidateLimit: minHNSWEfSearch, want: minHNSWEfSearch},
{name: "keeps larger scans", candidateLimit: 900, want: 900},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := hnswEfSearch(tt.candidateLimit); got != tt.want {
t.Fatalf("hnswEfSearch(%d) = %d, want %d", tt.candidateLimit, got, tt.want)
}
})
}
}
func TestApplyMediaTypeFloorAddsAvailableSupplementalType(t *testing.T) {
items := []ScoredItem{
{MediaItemID: "s1", Score: 1.00},
{MediaItemID: "s2", Score: 0.99},
{MediaItemID: "s3", Score: 0.98},
{MediaItemID: "s4", Score: 0.97},
{MediaItemID: "s5", Score: 0.96},
{MediaItemID: "s6", Score: 0.95},
{MediaItemID: "s7", Score: 0.94},
{MediaItemID: "s8", Score: 0.93},
{MediaItemID: "s9", Score: 0.92},
{MediaItemID: "s10", Score: 0.91},
}
candidates := append([]ScoredItem(nil), items...)
candidates = append(candidates,
ScoredItem{MediaItemID: "m1", Score: 0.90},
ScoredItem{MediaItemID: "m2", Score: 0.89},
)
mediaTypes := map[string]string{
"s1": "series", "s2": "series", "s3": "series", "s4": "series", "s5": "series",
"s6": "series", "s7": "series", "s8": "series", "s9": "series", "s10": "series",
"m1": "movie", "m2": "movie",
}
mixed := applyMediaTypeFloor(items, candidates, mediaTypes)
if got := len(mixed); got != len(items) {
t.Fatalf("expected result length to stay %d, got %d", len(items), got)
}
if got := countMediaType(mixed, mediaTypes, "movie"); got != 2 {
t.Fatalf("expected 2 movies from supplemental candidates, got %d in %#v", got, mixed)
}
if slices.ContainsFunc(mixed, func(item ScoredItem) bool { return item.MediaItemID == "s10" }) {
t.Fatalf("expected lowest-ranked series tail item to be replaced, got %#v", mixed)
}
}
func TestApplyMediaTypeFloorNoopsWithoutSupplementalType(t *testing.T) {
items := []ScoredItem{
{MediaItemID: "s1", Score: 1.00},
{MediaItemID: "s2", Score: 0.99},
{MediaItemID: "s3", Score: 0.98},
{MediaItemID: "s4", Score: 0.97},
{MediaItemID: "s5", Score: 0.96},
}
mediaTypes := map[string]string{
"s1": "series", "s2": "series", "s3": "series", "s4": "series", "s5": "series",
}
mixed := applyMediaTypeFloor(items, items, mediaTypes)
if !slices.EqualFunc(mixed, items, func(a, b ScoredItem) bool {
return a.MediaItemID == b.MediaItemID && a.Score == b.Score
}) {
t.Fatalf("expected unchanged result without supplemental type, got %#v", mixed)
}
}
func TestApplyGenreCapDoesNotCollapseConcentratedRows(t *testing.T) {
items := []ScoredItem{
{MediaItemID: "a", Score: 1.0},
{MediaItemID: "b", Score: 0.9},
{MediaItemID: "c", Score: 0.8},
{MediaItemID: "d", Score: 0.7},
{MediaItemID: "e", Score: 0.6},
{MediaItemID: "f", Score: 0.5},
}
genres := map[string][]string{
"a": {"Science Fiction", "Drama"},
"b": {"Science Fiction", "Drama"},
"c": {"Science Fiction", "Drama"},
"d": {"Science Fiction", "Drama"},
"e": {"Science Fiction", "Drama"},
"f": {"Science Fiction", "Drama"},
}
capped := applyGenreCap(items, genres, 0.4)
if len(capped) != 3 {
t.Fatalf("got %d capped items, want retained half of concentrated row", len(capped))
}
if capped[0].MediaItemID != "a" || capped[1].MediaItemID != "b" || capped[2].MediaItemID != "c" {
t.Fatalf("unexpected capped items: %#v", capped)
}
}
func TestCollaborativeSupportAggregatesAcrossPeers(t *testing.T) {
candidates := map[string]collaborativeCandidate{}
addCollaborativeSupport(candidates, "shared", 0.4)
addCollaborativeSupport(candidates, "shared", 0.3)
addCollaborativeSupport(candidates, "single", 0.6)
if candidates["shared"].score <= candidates["single"].score {
t.Fatalf("shared score = %f, single score = %f; expected aggregated shared support to win", candidates["shared"].score, candidates["single"].score)
}
if candidates["shared"].support != 2 {
t.Fatalf("shared support = %d, want 2", candidates["shared"].support)
}
}
func TestCowatchMatrixTreatsProfilesAsDistinctWatchers(t *testing.T) {
watchers := map[string][]string{
"a": {"1:p1", "1:p2"},
"b": {"1:p1", "1:p2"},
}
pairs := computeCowatchMatrix(watchers, 2, 2, 10)
if len(pairs) != 2 {
t.Fatalf("got %d co-watch pairs, want 2: %#v", len(pairs), pairs)
}
for _, pair := range pairs {
if pair.CowatchCount != 2 {
t.Fatalf("cowatch count = %d, want two profile identities: %#v", pair.CowatchCount, pair)
}
}
}
func TestCompatiblePeerContentRatingsIncludesLowerAndExcludesHigher(t *testing.T) {
ratings := compatiblePeerContentRatings("PG-13")
for _, want := range []string{"G", "PG", "PG-13", "TV-14"} {
if !slices.Contains(ratings, want) {
t.Fatalf("expected %q in compatible ratings: %#v", want, ratings)
}
}
for _, blocked := range []string{"R", "NC-17", "TV-MA"} {
if slices.Contains(ratings, blocked) {
t.Fatalf("did not expect %q in compatible ratings: %#v", blocked, ratings)
}
}
}
func TestMMRLambdaUsesConfiguredGlobalOverride(t *testing.T) {
engine := &Engine{cfg: config.RecommendationsConfig{DiversityLambda: 0.25}}
if got := engine.mmrLambda(0.8); got != 0.25 {
t.Fatalf("mmrLambda = %f, want configured override", got)
}
engine.cfg.DiversityLambda = 1.2
if got := engine.mmrLambda(0.8); got != 0.8 {
t.Fatalf("mmrLambda = %f, want fallback default for invalid override", got)
}
}
func TestEmbeddingTextNeedsRefreshIncludesEmptyCanonicalText(t *testing.T) {
if !embeddingTextNeedsRefresh("model-a", "", "generated text", "model-a") {
t.Fatal("expected same-model row with empty canonical text to be stale")
}
if !embeddingTextNeedsRefresh("model-a", "old text", "generated text", "model-a") {
t.Fatal("expected changed canonical text to be stale")
}
if embeddingTextNeedsRefresh("model-a", "generated text", "generated text", "model-a") {
t.Fatal("did not expect matching model and canonical text to be stale")
}
}
func TestBuildTasteClustersDeterministic(t *testing.T) {
items := []clusterItem{
clusterTestItem("a1", []float32{1, 0}, 1, "Action"),
clusterTestItem("a2", []float32{0.98, 0.02}, 0.9, "Action"),
clusterTestItem("a3", []float32{0.95, 0.05}, 0.8, "Action"),
clusterTestItem("a4", []float32{0.9, 0.1}, 0.7, "Action"),
clusterTestItem("a5", []float32{0.88, 0.12}, 0.6, "Action"),
clusterTestItem("a6", []float32{0.86, 0.14}, 0.5, "Action"),
clusterTestItem("d1", []float32{0, 1}, 1, "Drama"),
clusterTestItem("d2", []float32{0.02, 0.98}, 0.9, "Drama"),
clusterTestItem("d3", []float32{0.05, 0.95}, 0.8, "Drama"),
clusterTestItem("d4", []float32{0.1, 0.9}, 0.7, "Drama"),
clusterTestItem("d5", []float32{0.12, 0.88}, 0.6, "Drama"),
clusterTestItem("d6", []float32{0.14, 0.86}, 0.5, "Drama"),
}
first := buildTasteClusters(items)
second := buildTasteClusters(items)
if len(first) != len(second) {
t.Fatalf("cluster count changed: %d vs %d", len(first), len(second))
}
for i := range first {
if first[i].Label != second[i].Label ||
first[i].MemberCount != second[i].MemberCount ||
first[i].TotalWeight != second[i].TotalWeight {
t.Fatalf("cluster %d changed: %#v vs %#v", i, first[i], second[i])
}
}
}
func TestDeduplicateThenTrimKeepsBackfillCandidates(t *testing.T) {
seen := map[string]struct{}{"already-seen": {}}
items := []ScoredItem{
{MediaItemID: "already-seen", Score: 1.0},
{MediaItemID: "next-best", Score: 0.9},
{MediaItemID: "backfill", Score: 0.8},
}
row := ForYouRow{Items: deduplicateItems(items, seen)}
rows := trimRows([]ForYouRow{row}, 2)
if len(rows[0].Items) != 2 {
t.Fatalf("got %d items, want 2", len(rows[0].Items))
}
if rows[0].Items[0].MediaItemID != "next-best" || rows[0].Items[1].MediaItemID != "backfill" {
t.Fatalf("unexpected retained items: %#v", rows[0].Items)
}
}
func clusterTestItem(id string, embedding []float32, weight float64, genre string) clusterItem {
return clusterItem{
itemID: id,
embedding: embedding,
weight: weight,
genres: []string{genre},
}
}