Files
minstrel/internal/db/dbq/similarity.sql.go
T
bvandeusenandClaude Opus 5.5 e44025435e
release / govulncheck (push) Successful in 15s
release / web (push) Successful in 1m23s
release / go (push) Successful in 1m36s
release / integration (push) Successful in 5m4s
release / android (push) Successful in 5m5s
release / Build signed APK (releases and dev) (push) Successful in 5m18s
release / Attach APK to the Release (tag releases only) (push) Skipped
release / Build + push container image (push) Successful in 30s
release / Verify release artifacts (tag releases only) (push) Skipped
fix: ListenBrainz similarity is stored relative to the seed's best, and the candidate arms keep tier order (#6177)
LB's similarity scores are counts (16 to 1445 for recordings, to 10,875
for artists), and they reached a scorer that reads similarity as 0 to 1.
One edge added 64 to 5,800 points under Songs-like, so likes, skips and
recency stopped mattering for any candidate with an edge.

- ResolveListenBrainzTrackEdges divides each edge by the seed's best
  score in its whole cached answer. The cache keeps the raw answer.
- The worker stores matched and unmatched artist rows over the seed's
  top artist.
- Migration 0096 rescales the rows already stored.
- The candidate arms hold sim_score bands so tier 1 stays above tier 2:
  lb_similar 0.5 + 0.5 x edge, tag_overlap halved, likes_overlap 0.3.
  Otherwise a seed's weaker LB edges fell below a shared genre tag.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 10:56:26 -04:00

375 lines
12 KiB
Go

// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.31.1
// source: similarity.sql
package dbq
import (
"context"
"github.com/jackc/pgx/v5/pgtype"
)
const deleteSimilarRecordingsForSeed = `-- name: DeleteSimilarRecordingsForSeed :exec
DELETE FROM listenbrainz_similar_recordings WHERE seed_track_id = $1
`
// A fetch replaces the seed's cached answer whole.
func (q *Queries) DeleteSimilarRecordingsForSeed(ctx context.Context, seedTrackID pgtype.UUID) error {
_, err := q.db.Exec(ctx, deleteSimilarRecordingsForSeed, seedTrackID)
return err
}
const getArtistsByMBIDs = `-- name: GetArtistsByMBIDs :many
SELECT id, mbid FROM artists WHERE mbid = ANY($1::text[])
`
type GetArtistsByMBIDsRow struct {
ID pgtype.UUID
Mbid *string
}
func (q *Queries) GetArtistsByMBIDs(ctx context.Context, dollar_1 []string) ([]GetArtistsByMBIDsRow, error) {
rows, err := q.db.Query(ctx, getArtistsByMBIDs, dollar_1)
if err != nil {
return nil, err
}
defer rows.Close()
var items []GetArtistsByMBIDsRow
for rows.Next() {
var i GetArtistsByMBIDsRow
if err := rows.Scan(&i.ID, &i.Mbid); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const insertSimilarRecordings = `-- name: InsertSimilarRecordings :exec
INSERT INTO listenbrainz_similar_recordings (seed_track_id, recording_mbid, score)
SELECT $1::uuid, r.mbid, max(r.score)
FROM (SELECT unnest($2::text[]) AS mbid,
unnest($3::float8[]) AS score) r
WHERE r.mbid <> ''
GROUP BY r.mbid
`
type InsertSimilarRecordingsParams struct {
SeedTrackID pgtype.UUID
Mbids []string
Scores []float64
}
// The seed's whole answer, in or out of the library. An MBID LB lists twice
// keeps its best score.
func (q *Queries) InsertSimilarRecordings(ctx context.Context, arg InsertSimilarRecordingsParams) error {
_, err := q.db.Exec(ctx, insertSimilarRecordings, arg.SeedTrackID, arg.Mbids, arg.Scores)
return err
}
const listPlayedArtistsNeedingSimilarity = `-- name: ListPlayedArtistsNeedingSimilarity :many
SELECT ar.id, ar.mbid
FROM artists ar
LEFT JOIN artist_similarity_fetches f ON f.artist_id = ar.id
WHERE ar.mbid IS NOT NULL
AND EXISTS (
SELECT 1 FROM tracks t
JOIN play_events pe ON pe.track_id = t.id
WHERE t.artist_id = ar.id
)
AND (f.artist_id IS NULL OR f.fetched_at < now() - interval '30 days')
ORDER BY f.fetched_at NULLS FIRST, ar.id
LIMIT $1
`
type ListPlayedArtistsNeedingSimilarityRow struct {
ID pgtype.UUID
Mbid *string
}
// The artist queue, with the same shape and the same reason.
func (q *Queries) ListPlayedArtistsNeedingSimilarity(ctx context.Context, limit int32) ([]ListPlayedArtistsNeedingSimilarityRow, error) {
rows, err := q.db.Query(ctx, listPlayedArtistsNeedingSimilarity, limit)
if err != nil {
return nil, err
}
defer rows.Close()
var items []ListPlayedArtistsNeedingSimilarityRow
for rows.Next() {
var i ListPlayedArtistsNeedingSimilarityRow
if err := rows.Scan(&i.ID, &i.Mbid); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const listPlayedTracksNeedingSimilarity = `-- name: ListPlayedTracksNeedingSimilarity :many
SELECT t.id, t.mbid
FROM tracks t
LEFT JOIN track_similarity_fetches f ON f.track_id = t.id
WHERE t.mbid IS NOT NULL
AND t.missing_since IS NULL
AND EXISTS (SELECT 1 FROM play_events pe WHERE pe.track_id = t.id)
AND (f.track_id IS NULL OR f.fetched_at < now() - interval '30 days')
ORDER BY f.fetched_at NULLS FIRST, t.id
LIMIT $1
`
type ListPlayedTracksNeedingSimilarityRow struct {
ID pgtype.UUID
Mbid *string
}
// The worker's track queue: played, present tracks with a recording MBID that
// ListenBrainz has never answered for, or not in 30 days. Never-fetched first,
// then the oldest answer. Freshness is read from track_similarity_fetches, not
// from the edges, so a seed whose answer matched nothing in the library still
// counts as fetched (#5296); reading the edges is what let 25 such seeds hold
// the head of the queue forever.
func (q *Queries) ListPlayedTracksNeedingSimilarity(ctx context.Context, limit int32) ([]ListPlayedTracksNeedingSimilarityRow, error) {
rows, err := q.db.Query(ctx, listPlayedTracksNeedingSimilarity, limit)
if err != nil {
return nil, err
}
defer rows.Close()
var items []ListPlayedTracksNeedingSimilarityRow
for rows.Next() {
var i ListPlayedTracksNeedingSimilarityRow
if err := rows.Scan(&i.ID, &i.Mbid); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const listSimilarArtistsForArtist = `-- name: ListSimilarArtistsForArtist :many
SELECT artists.id, artists.name, artists.sort_name, artists.mbid, artists.created_at, artists.updated_at, artists.artist_thumb_path, artists.artist_fanart_path, artists.artist_art_source, artists.artist_art_sources_version,
cov.id AS cover_album_id,
cnt.album_count::bigint AS album_count
FROM (
SELECT artist_b_id, max(score) AS sim_score
FROM artist_similarity
WHERE artist_a_id = $1
GROUP BY artist_b_id
) s
JOIN artists ON artists.id = s.artist_b_id
LEFT JOIN LATERAL (
SELECT id FROM albums
WHERE artist_id = artists.id AND cover_art_path IS NOT NULL
ORDER BY created_at DESC LIMIT 1
) cov ON true
LEFT JOIN LATERAL (
SELECT count(*) AS album_count
FROM albums WHERE artist_id = artists.id
) cnt ON true
ORDER BY s.sim_score DESC, artists.sort_name
LIMIT $2
`
type ListSimilarArtistsForArtistParams struct {
SeedArtistID pgtype.UUID
ResultLimit int32
}
type ListSimilarArtistsForArtistRow struct {
Artist Artist
CoverAlbumID pgtype.UUID
AlbumCount int64
}
// In-library artists similar to a seed artist, ranked by best similarity
// score across sources (deduped per candidate). cover_album_id + album_count
// mirror ListArtistsAlphaWithCovers so the strip renders identical cards.
func (q *Queries) ListSimilarArtistsForArtist(ctx context.Context, arg ListSimilarArtistsForArtistParams) ([]ListSimilarArtistsForArtistRow, error) {
rows, err := q.db.Query(ctx, listSimilarArtistsForArtist, arg.SeedArtistID, arg.ResultLimit)
if err != nil {
return nil, err
}
defer rows.Close()
var items []ListSimilarArtistsForArtistRow
for rows.Next() {
var i ListSimilarArtistsForArtistRow
if err := rows.Scan(
&i.Artist.ID,
&i.Artist.Name,
&i.Artist.SortName,
&i.Artist.Mbid,
&i.Artist.CreatedAt,
&i.Artist.UpdatedAt,
&i.Artist.ArtistThumbPath,
&i.Artist.ArtistFanartPath,
&i.Artist.ArtistArtSource,
&i.Artist.ArtistArtSourcesVersion,
&i.CoverAlbumID,
&i.AlbumCount,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const recordArtistSimilarityFetch = `-- name: RecordArtistSimilarityFetch :exec
INSERT INTO artist_similarity_fetches (artist_id, fetched_at, returned)
VALUES ($1, now(), $2)
ON CONFLICT (artist_id) DO UPDATE SET fetched_at = now(), returned = EXCLUDED.returned
`
type RecordArtistSimilarityFetchParams struct {
ArtistID pgtype.UUID
Returned int32
}
func (q *Queries) RecordArtistSimilarityFetch(ctx context.Context, arg RecordArtistSimilarityFetchParams) error {
_, err := q.db.Exec(ctx, recordArtistSimilarityFetch, arg.ArtistID, arg.Returned)
return err
}
const recordTrackSimilarityFetch = `-- name: RecordTrackSimilarityFetch :exec
INSERT INTO track_similarity_fetches (track_id, fetched_at, returned)
VALUES ($1, now(), $2)
ON CONFLICT (track_id) DO UPDATE SET fetched_at = now(), returned = EXCLUDED.returned
`
type RecordTrackSimilarityFetchParams struct {
TrackID pgtype.UUID
Returned int32
}
func (q *Queries) RecordTrackSimilarityFetch(ctx context.Context, arg RecordTrackSimilarityFetchParams) error {
_, err := q.db.Exec(ctx, recordTrackSimilarityFetch, arg.TrackID, arg.Returned)
return err
}
const resolveListenBrainzTrackEdges = `-- name: ResolveListenBrainzTrackEdges :exec
WITH best AS (
SELECT seed_track_id, max(score) AS score
FROM listenbrainz_similar_recordings
WHERE $1::uuid[] IS NULL
OR seed_track_id = ANY($1::uuid[])
GROUP BY seed_track_id
),
want AS (
SELECT DISTINCT ON (c.seed_track_id, c.recording_mbid)
c.seed_track_id AS track_a_id, t.id AS track_b_id,
CASE WHEN b.score > 0 THEN c.score / b.score ELSE 0 END AS score
FROM listenbrainz_similar_recordings c
JOIN best b ON b.seed_track_id = c.seed_track_id
JOIN tracks t ON t.mbid = c.recording_mbid
AND t.missing_since IS NULL
AND t.id <> c.seed_track_id
WHERE $1::uuid[] IS NULL
OR c.seed_track_id = ANY($1::uuid[])
ORDER BY c.seed_track_id, c.recording_mbid, t.id
),
upserted AS (
INSERT INTO track_similarity (track_a_id, track_b_id, score, source, fetched_at)
SELECT track_a_id, track_b_id, score, 'listenbrainz', now() FROM want
ON CONFLICT (track_a_id, track_b_id, source) DO UPDATE
SET score = EXCLUDED.score, fetched_at = EXCLUDED.fetched_at
WHERE track_similarity.score IS DISTINCT FROM EXCLUDED.score
RETURNING 1
)
DELETE FROM track_similarity ts
WHERE ts.source = 'listenbrainz'
AND ($1::uuid[] IS NULL OR ts.track_a_id = ANY($1::uuid[]))
AND EXISTS (SELECT 1 FROM track_similarity_fetches f WHERE f.track_id = ts.track_a_id)
AND NOT EXISTS (
SELECT 1 FROM want w
WHERE w.track_a_id = ts.track_a_id AND w.track_b_id = ts.track_b_id
)
`
// Derives the listenbrainz edges in track_similarity from the cached answers:
// every cached recording that is a present track in the library becomes an
// edge, whenever it got there. Scoped to the given seeds, or every seed when
// seed_ids is NULL.
//
// One track per recording MBID: two copies of a song (an mp3 beside its flac,
// or one recording on two releases) would otherwise be two candidates, and the
// pool would offer the same song twice. No cap per seed: LB's answer is already
// at most 50.
//
// The score is RELATIVE: each edge's LB score over the seed's best score in its
// whole cached answer, so 1.0 is the seed's closest recording and the rest fall
// below it (#6177). LB's raw scores are counts (16 to 1445 on the operator's
// library) and reached the scorer, which reads similarity as 0 to 1, so one
// edge outweighed likes, skips and recency together. The cache keeps the raw
// answer; only the edge is scaled.
//
// Only edges of seeds the cache owns are removed, those with a fetch row.
// Edges written before the cache existed, or copied onto a survivor by a fold
// (merge.sql), stay until their seed is fetched.
func (q *Queries) ResolveListenBrainzTrackEdges(ctx context.Context, seedIds []pgtype.UUID) error {
_, err := q.db.Exec(ctx, resolveListenBrainzTrackEdges, seedIds)
return err
}
const upsertArtistSimilarity = `-- name: UpsertArtistSimilarity :exec
INSERT INTO artist_similarity (artist_a_id, artist_b_id, score, source, fetched_at)
VALUES ($1, $2, $3, 'listenbrainz', now())
ON CONFLICT (artist_a_id, artist_b_id, source)
DO UPDATE SET score = EXCLUDED.score, fetched_at = EXCLUDED.fetched_at
`
type UpsertArtistSimilarityParams struct {
ArtistAID pgtype.UUID
ArtistBID pgtype.UUID
Score float64
}
func (q *Queries) UpsertArtistSimilarity(ctx context.Context, arg UpsertArtistSimilarityParams) error {
_, err := q.db.Exec(ctx, upsertArtistSimilarity, arg.ArtistAID, arg.ArtistBID, arg.Score)
return err
}
const upsertArtistSimilarityUnmatched = `-- name: UpsertArtistSimilarityUnmatched :exec
INSERT INTO artist_similarity_unmatched (
seed_artist_id, candidate_mbid, candidate_name, score, source
) VALUES ($1, $2, $3, $4, $5)
ON CONFLICT (seed_artist_id, candidate_mbid, source) DO UPDATE SET
candidate_name = EXCLUDED.candidate_name,
score = EXCLUDED.score,
fetched_at = now()
`
type UpsertArtistSimilarityUnmatchedParams struct {
SeedArtistID pgtype.UUID
CandidateMbid string
CandidateName string
Score float64
Source string
}
// Persists an out-of-library similar-artist MBID. Idempotent on
// (seed_artist_id, candidate_mbid, source) — re-fetches refresh the
// name/score and bump fetched_at.
func (q *Queries) UpsertArtistSimilarityUnmatched(ctx context.Context, arg UpsertArtistSimilarityUnmatchedParams) error {
_, err := q.db.Exec(ctx, upsertArtistSimilarityUnmatched,
arg.SeedArtistID,
arg.CandidateMbid,
arg.CandidateName,
arg.Score,
arg.Source,
)
return err
}