feat(recommendation): add Shuffle orchestrator
Composes Score over a candidate slice, sorts descending, truncates. Pure — no IO. Liked-rank-higher and high-skip-ranks-last behaviors are stable across RNG seeds (jitter band is smaller than LikeBoost and SkipPenalty).
This commit is contained in:
@@ -0,0 +1,48 @@
|
||||
package recommendation
|
||||
|
||||
import (
|
||||
"sort"
|
||||
"time"
|
||||
|
||||
"git.fabledsword.com/bvandeusen/minstrel/internal/db/dbq"
|
||||
)
|
||||
|
||||
// Candidate pairs a track with the inputs needed to score it.
|
||||
type Candidate struct {
|
||||
Track dbq.Track
|
||||
Inputs ScoringInputs
|
||||
}
|
||||
|
||||
// Shuffle scores each candidate, sorts descending by score, and returns
|
||||
// the top `limit` candidates. limit <= 0 returns nil; nil input returns
|
||||
// nil. Pure — no IO, no global state beyond the rng callback.
|
||||
func Shuffle(
|
||||
candidates []Candidate,
|
||||
weights ScoringWeights,
|
||||
now time.Time,
|
||||
rng func() float64,
|
||||
limit int,
|
||||
) []Candidate {
|
||||
if len(candidates) == 0 || limit <= 0 {
|
||||
return nil
|
||||
}
|
||||
scored := make([]struct {
|
||||
c Candidate
|
||||
score float64
|
||||
}, len(candidates))
|
||||
for i, c := range candidates {
|
||||
scored[i].c = c
|
||||
scored[i].score = Score(c.Inputs, weights, now, rng)
|
||||
}
|
||||
sort.Slice(scored, func(i, j int) bool {
|
||||
return scored[i].score > scored[j].score
|
||||
})
|
||||
if limit > len(scored) {
|
||||
limit = len(scored)
|
||||
}
|
||||
out := make([]Candidate, limit)
|
||||
for i := 0; i < limit; i++ {
|
||||
out[i] = scored[i].c
|
||||
}
|
||||
return out
|
||||
}
|
||||
Reference in New Issue
Block a user