-- M4b: ListenBrainz similarity ingest. Two symmetric tables — tracks and -- artists — both keyed by (a_id, b_id, source) so the schema reserves room -- for additional similarity sources (musicbrainz_tag, user_cooccurrence) -- alongside listenbrainz. M4b only writes 'listenbrainz' rows. -- -- The (a_id, score DESC) index satisfies M4c's hot path: "for seed S, give -- me top-N similar tracks/artists descending by score." CREATE TABLE track_similarity ( track_a_id uuid NOT NULL REFERENCES tracks(id) ON DELETE CASCADE, track_b_id uuid NOT NULL REFERENCES tracks(id) ON DELETE CASCADE, score DOUBLE PRECISION NOT NULL, source TEXT NOT NULL CHECK (source IN ('listenbrainz', 'musicbrainz_tag', 'user_cooccurrence')), fetched_at timestamptz NOT NULL DEFAULT now(), PRIMARY KEY (track_a_id, track_b_id, source), CHECK (track_a_id <> track_b_id) ); CREATE INDEX track_similarity_a_score_idx ON track_similarity (track_a_id, score DESC); CREATE TABLE artist_similarity ( artist_a_id uuid NOT NULL REFERENCES artists(id) ON DELETE CASCADE, artist_b_id uuid NOT NULL REFERENCES artists(id) ON DELETE CASCADE, score DOUBLE PRECISION NOT NULL, source TEXT NOT NULL CHECK (source IN ('listenbrainz', 'musicbrainz_tag', 'user_cooccurrence')), fetched_at timestamptz NOT NULL DEFAULT now(), PRIMARY KEY (artist_a_id, artist_b_id, source), CHECK (artist_a_id <> artist_b_id) ); CREATE INDEX artist_similarity_a_score_idx ON artist_similarity (artist_a_id, score DESC);