68 lines
1.4 KiB
Go
68 lines
1.4 KiB
Go
package search
|
|
|
|
import (
|
|
"math/rand/v2"
|
|
"time"
|
|
|
|
"git.vakhrushev.me/av/remembos/internal/memos"
|
|
)
|
|
|
|
// Memory is the result of the search algorithm.
|
|
type Memory struct {
|
|
Memo *memos.Memo
|
|
Tier int
|
|
YearsAgo int
|
|
ShowCount int
|
|
Date time.Time
|
|
}
|
|
|
|
// candidate is an intermediate struct during scoring.
|
|
type candidate struct {
|
|
memo *memos.Memo
|
|
yearsAgo int
|
|
showCount int
|
|
}
|
|
|
|
// weightedSelect picks one candidate using weighted random selection.
|
|
// preferOlder gives higher weight to older memos. maxYearsBack normalizes recency.
|
|
func weightedSelect(candidates []candidate, preferOlder bool, maxYearsBack int) *candidate {
|
|
if len(candidates) == 0 {
|
|
return nil
|
|
}
|
|
if len(candidates) == 1 {
|
|
return &candidates[0]
|
|
}
|
|
|
|
scores := make([]float64, len(candidates))
|
|
var total float64
|
|
|
|
for i, c := range candidates {
|
|
var recencyFactor float64
|
|
if preferOlder && maxYearsBack > 0 && c.yearsAgo > 0 {
|
|
recencyFactor = float64(c.yearsAgo) / float64(maxYearsBack)
|
|
} else {
|
|
recencyFactor = 1.0
|
|
}
|
|
|
|
showCountPenalty := 1.0 / (1.0 + float64(c.showCount))
|
|
|
|
score := recencyFactor * showCountPenalty
|
|
if score < 0.01 {
|
|
score = 0.01 // minimum weight
|
|
}
|
|
scores[i] = score
|
|
total += score
|
|
}
|
|
|
|
r := rand.Float64() * total //nolint:gosec // non-cryptographic use
|
|
var cumulative float64
|
|
for i, s := range scores {
|
|
cumulative += s
|
|
if r <= cumulative {
|
|
return &candidates[i]
|
|
}
|
|
}
|
|
|
|
return &candidates[len(candidates)-1]
|
|
}
|