199fec2058
Milestone #160 Opt 5. A collaborative candidate arm: tracks by artists co-played across the instance with the seed's artist. Minstrel is a single shared-library, multi-user server (no per-user library ACL — verified: no owner/share/group model), so the "household" is the whole instance's user set; the rule #47 scoping is satisfied by the shared-library boundary. Single-user servers produce no edges. - No migration: source='user_cooccurrence' was pre-whitelisted in the 0009 similarity CHECK from day one. - internal/db/queries/coplay.sql: Delete + Insert artist co-play edges. Score = Jaccard of the two artists' distinct-player sets (controls for globally-popular artists); >= 2 co-players AND Jaccard >= floor kept (the floor also self-limits hub artists). Completed plays, 365d window. - internal/coplay: periodic worker (6h) that atomic-replaces the user_cooccurrence edge set from play_events — pure local SQL, no external calls. Wired in main.go alongside the similarity worker. - LoadRadioCandidatesV2: new coplay_artists arm (source='user_cooccurrence', seed-artist based, 0.5 damp like similar_artists) + $11 limit; CandidateSourceLimits.UserCoplay (default 20, For-You 40). - Integration tests: perfect-overlap Jaccard=1.0 edge + single-user empty-set gate. Device axis and AcousticBrainz (Opt 4) are separately tracked; this closes the milestone-#160 sequential options. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
79 lines
2.9 KiB
Go
79 lines
2.9 KiB
Go
// Code generated by sqlc. DO NOT EDIT.
|
||
// versions:
|
||
// sqlc v1.31.1
|
||
// source: coplay.sql
|
||
|
||
package dbq
|
||
|
||
import (
|
||
"context"
|
||
)
|
||
|
||
const deleteArtistCoplayEdges = `-- name: DeleteArtistCoplayEdges :exec
|
||
|
||
DELETE FROM artist_similarity WHERE source = 'user_cooccurrence'
|
||
`
|
||
|
||
// Household co-play similarity (#1533, milestone #160 Opt 5). Minstrel is a
|
||
// single shared-library, multi-user server (no per-user library ACL), so the
|
||
// "household" is the whole instance's user set. These queries recompute
|
||
// artist–artist co-occurrence edges from play_events and store them in
|
||
// artist_similarity under the pre-provisioned source = 'user_cooccurrence'
|
||
// (whitelisted in the 0009 CHECK from day one). A periodic worker DELETEs then
|
||
// re-INSERTs so edges that fall below threshold disappear. Single-user servers
|
||
// produce no edges (the >= 2 co-player gate), so the arm is empty there.
|
||
func (q *Queries) DeleteArtistCoplayEdges(ctx context.Context) error {
|
||
_, err := q.db.Exec(ctx, deleteArtistCoplayEdges)
|
||
return err
|
||
}
|
||
|
||
const insertArtistCoplayEdges = `-- name: InsertArtistCoplayEdges :exec
|
||
WITH user_artists AS (
|
||
SELECT DISTINCT pe.user_id, t.artist_id
|
||
FROM play_events pe
|
||
JOIN tracks t ON t.id = pe.track_id
|
||
WHERE pe.was_skipped = false
|
||
AND pe.started_at > now() - interval '365 days'
|
||
),
|
||
artist_players AS (
|
||
SELECT artist_id, count(*)::float8 AS players
|
||
FROM user_artists
|
||
GROUP BY artist_id
|
||
),
|
||
pairs AS (
|
||
SELECT ua.artist_id AS a_id,
|
||
ub.artist_id AS b_id,
|
||
count(*)::float8 AS coplayers
|
||
FROM user_artists ua
|
||
JOIN user_artists ub
|
||
ON ua.user_id = ub.user_id AND ua.artist_id <> ub.artist_id
|
||
GROUP BY ua.artist_id, ub.artist_id
|
||
HAVING count(*) >= 2
|
||
),
|
||
scored AS (
|
||
SELECT p.a_id, p.b_id,
|
||
p.coplayers / (pa.players + pb.players - p.coplayers) AS score
|
||
FROM pairs p
|
||
JOIN artist_players pa ON pa.artist_id = p.a_id
|
||
JOIN artist_players pb ON pb.artist_id = p.b_id
|
||
)
|
||
INSERT INTO artist_similarity (artist_a_id, artist_b_id, score, source, fetched_at)
|
||
SELECT a_id, b_id, score, 'user_cooccurrence', now()
|
||
FROM scored
|
||
WHERE score >= $1
|
||
ON CONFLICT (artist_a_id, artist_b_id, source)
|
||
DO UPDATE SET score = EXCLUDED.score, fetched_at = EXCLUDED.fetched_at
|
||
`
|
||
|
||
// Two artists are "co-played" when the same users play both. score is the
|
||
// Jaccard of their distinct-player sets — coplayers / (playersA + playersB −
|
||
// coplayers) in (0,1] — which controls for globally-popular artists (an artist
|
||
// everyone plays would otherwise co-occur with everything). Only pairs with
|
||
// >= 2 distinct co-players AND Jaccard >= $1 (a floor that both prunes weak
|
||
// edges and self-limits hub artists, whose large denominators drag their
|
||
// Jaccard down) are kept. Completed plays only, 365-day window.
|
||
func (q *Queries) InsertArtistCoplayEdges(ctx context.Context, score float64) error {
|
||
_, err := q.db.Exec(ctx, insertArtistCoplayEdges, score)
|
||
return err
|
||
}
|