feat(reco): port RelationshipWeights

T2: 1:1 port of the artist_vectors RelationshipWeights (12 fields, Default, the
band_focused/similarity_focused presets, and the weight_for_mb_relation /
weight_for_source maps) with its tests. The presets are unused in production but
ported verbatim for parity (pub lib API, no dead-code warnings).

cc
This commit is contained in:
vicrodh committed 2026-06-26 20:15:59 -06:00
1 parent 65de984988
commit 4ec50f710d
2 files changed
+201

No files matched your search

+2
View File
@@ -9,5 +9,7 @@
//! relationship weight via the vector store) per the epic's decision D3.
mod sparse_vector;
mod weights;
pub use sparse_vector::SparseVector;
pub use weights::RelationshipWeights;
+199
View File
@@ -0,0 +1,199 @@
//! Configurable weights for different relationship types
//!
//! These weights determine how strongly different types of relationships
//! contribute to artist similarity vectors.
use serde::{Deserialize, Serialize};
/// Weights for different relationship types when building artist vectors
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RelationshipWeights {
// === MusicBrainz artist-to-artist relationships ===
/// Weight for band membership (artist was/is member of group)
/// Strongest connection - same creative unit
pub member_of_band: f32,
/// Weight for collaboration (artists worked together)
pub collaboration: f32,
/// Weight for "is person" in a group (reverse of member_of_band)
pub has_member: f32,
/// Weight for founder relationship
pub founder: f32,
// === MusicBrainz recording/release relationships ===
/// Weight for performer credit on a recording
pub performer: f32,
/// Weight for composer credit
pub composer: f32,
/// Weight for producer credit
pub producer: f32,
/// Weight for conductor credit
pub conductor: f32,
/// Weight for engineer/mixer credit
pub engineer: f32,
// === Qobuz relationships ===
/// Weight for Qobuz similar artists
pub qobuz_similar: f32,
// === Tag-based relationships ===
/// Weight for shared MusicBrainz tags (genres)
pub shared_tag: f32,
// === Behavioral relationships ===
/// Weight for user listening affinity (artists played together)
pub user_affinity: f32,
}
impl Default for RelationshipWeights {
fn default() -> Self {
Self {
// Band relationships - strongest
member_of_band: 1.0,
has_member: 0.9,
founder: 0.85,
collaboration: 0.8,
// Credit relationships - medium
performer: 0.6,
composer: 0.55,
producer: 0.5,
conductor: 0.5,
engineer: 0.3,
// Qobuz similarity - good signal
qobuz_similar: 0.7,
// Tags - weak but useful
shared_tag: 0.3,
// User behavior - medium
user_affinity: 0.5,
}
}
}
impl RelationshipWeights {
/// Create weights optimized for discovering band-related artists
pub fn band_focused() -> Self {
Self {
member_of_band: 1.0,
has_member: 1.0,
founder: 0.9,
collaboration: 0.7,
performer: 0.4,
composer: 0.3,
producer: 0.2,
conductor: 0.3,
engineer: 0.1,
qobuz_similar: 0.5,
shared_tag: 0.2,
user_affinity: 0.3,
}
}
/// Create weights optimized for sound-alike discovery
pub fn similarity_focused() -> Self {
Self {
member_of_band: 0.6,
has_member: 0.5,
founder: 0.5,
collaboration: 0.7,
performer: 0.5,
composer: 0.4,
producer: 0.3,
conductor: 0.3,
engineer: 0.2,
qobuz_similar: 1.0, // Prioritize Qobuz similarity
shared_tag: 0.5,
user_affinity: 0.6,
}
}
/// Get weight for a MusicBrainz relationship type
pub fn weight_for_mb_relation(&self, relation_type: &str) -> f32 {
match relation_type.to_lowercase().as_str() {
// Band relationships
"member of band" | "member_of_band" => self.member_of_band,
"has member" | "has_member" => self.has_member,
"founder" | "founded" => self.founder,
// Collaboration
"collaboration" | "collaborated" | "collaborator" => self.collaboration,
// Performance credits
"performer"
| "vocal"
| "instrument"
| "performing orchestra"
| "orchestra"
| "chorus master" => self.performer,
// Composition
"composer" | "writer" | "lyricist" | "librettist" | "arranger" => self.composer,
// Production
"producer" | "executive producer" | "co-producer" => self.producer,
"conductor" => self.conductor,
"engineer" | "mix" | "mixer" | "mastering" | "recording" => self.engineer,
// Unknown - use small default weight
_ => 0.2,
}
}
/// Get weight for a source type string
pub fn weight_for_source(&self, source: &str) -> f32 {
match source {
"qobuz_similar" => self.qobuz_similar,
"shared_tag" => self.shared_tag,
"user_affinity" => self.user_affinity,
s if s.starts_with("mb:") => {
let rel_type = &s[3..];
self.weight_for_mb_relation(rel_type)
}
_ => 0.2,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_default_weights() {
let weights = RelationshipWeights::default();
assert_eq!(weights.member_of_band, 1.0);
assert!(weights.qobuz_similar > weights.shared_tag);
assert!(weights.collaboration > weights.engineer);
}
#[test]
fn test_mb_relation_weight() {
let weights = RelationshipWeights::default();
assert_eq!(weights.weight_for_mb_relation("member of band"), 1.0);
assert_eq!(weights.weight_for_mb_relation("collaboration"), 0.8);
assert_eq!(weights.weight_for_mb_relation("producer"), 0.5);
// Unknown type gets default
assert_eq!(weights.weight_for_mb_relation("unknown_type"), 0.2);
}
#[test]
fn test_source_weight() {
let weights = RelationshipWeights::default();
assert_eq!(weights.weight_for_source("qobuz_similar"), 0.7);
assert_eq!(weights.weight_for_source("mb:member of band"), 1.0);
assert_eq!(weights.weight_for_source("mb:collaboration"), 0.8);
}
}