feat(similarity): persist unmatched similar-artist MBIDs for M5c

upsertArtistSimilar keeps the existing matched path (top-K rows into
artist_similarity) and adds a parallel unmatched-persist loop with the
same top-K cap. Empty-name rows are skipped — we can't render a
suggestion card without a name. Logs unmatched-side errors at WARN
without aborting the tick (mirrors the matched-path policy).
This commit is contained in:
2026-05-01 06:07:22 -04:00
parent be23ae488d
commit 5e73f590a9
2 changed files with 102 additions and 3 deletions
+32 -3
View File
@@ -189,9 +189,10 @@ func (w *Worker) upsertArtistSimilar(ctx context.Context, q *dbq.Queries, artist
}
}
taken := 0
// Matched: in-library similars → artist_similarity (existing path).
takenMatched := 0
for _, r := range results {
if taken >= w.topK {
if takenMatched >= w.topK {
break
}
localID, ok := idByMBID[r.MBID]
@@ -207,6 +208,34 @@ func (w *Worker) upsertArtistSimilar(ctx context.Context, q *dbq.Queries, artist
w.logger.Warn("similarity: UpsertArtistSimilarity", "err", uerr)
continue
}
taken++
takenMatched++
}
// Unmatched: out-of-library similars → artist_similarity_unmatched (M5c).
// Same top-K cap as the matched path. Skip rows missing a name — we can't
// render a suggestion card without one.
takenUnmatched := 0
for _, r := range results {
if takenUnmatched >= w.topK {
break
}
if _, inLib := idByMBID[r.MBID]; inLib {
continue
}
if r.Name == "" {
w.logger.Debug("similarity: skipping unmatched similar with empty name", "mbid", r.MBID)
continue
}
if uerr := q.UpsertArtistSimilarityUnmatched(ctx, dbq.UpsertArtistSimilarityUnmatchedParams{
SeedArtistID: artistAID,
CandidateMbid: r.MBID,
CandidateName: r.Name,
Score: r.Score,
Source: "listenbrainz",
}); uerr != nil {
w.logger.Warn("similarity: UpsertArtistSimilarityUnmatched", "err", uerr)
continue
}
takenUnmatched++
}
}