feat(discover): max-normalize similarity scores across sources
_record_contributions now normalizes each source's top-N to [0,1] by its own top score before summing across sources. ListenBrainz co-occurrence counts (thousands) no longer drown Last.fm's 0–1 match scores, so a small-scale source contributes real ranking signal instead of only adding candidates. Discovery score assertions updated to the normalized scale (single-candidate fakes normalize to 1.0); added a cross-source test showing a small-scale source's candidate ranks equal to a large-scale one. min_score is now on the [0, n_sources] scale (default 0.0 → unaffected). worker 231 tests. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -181,11 +181,18 @@ def _record_contributions(conn: psycopg.Connection, seed_mbid: str, live, follow
|
||||
except Exception as e: # a bad source/seed must not abort the sweep
|
||||
print(f"worker: discovery source {src.name} failed for {seed_mbid}: {e}", flush=True)
|
||||
continue
|
||||
for sa in sorted(similar, key=lambda a: a.score, reverse=True)[: cfg.similar_per_seed]:
|
||||
top = sorted(similar, key=lambda a: a.score, reverse=True)[: cfg.similar_per_seed]
|
||||
# Max-normalize each source to [0, 1] by its own top score before summing across
|
||||
# sources, so sources on wildly different scales contribute comparably (Last.fm's
|
||||
# `match` is 0–1; ListenBrainz co-occurrence counts run to the thousands — raw sums
|
||||
# let ListenBrainz drown Last.fm's ranking signal).
|
||||
max_score = max((sa.score for sa in top), default=0.0)
|
||||
for sa in top:
|
||||
if sa.mbid in followed:
|
||||
continue
|
||||
norm = sa.score / max_score if max_score > 0 else 0.0
|
||||
c = per_cand.setdefault(sa.mbid, {"name": sa.name, "score": 0.0, "sources": set()})
|
||||
c["score"] += sa.score
|
||||
c["score"] += norm
|
||||
c["sources"].add(src.name)
|
||||
if sa.name and not c["name"]:
|
||||
c["name"] = sa.name
|
||||
|
||||
Reference in New Issue
Block a user