fix n+1 queries in comfortGenerator and deepDiveGenerator (#110)
both generators ran N sequential queries per artist/album. rewritten with ROW_NUMBER() OVER (PARTITION BY ...) to get per-group limits in a single round-trip, preserving existing semantics: - comfortGenerator: up to 2 tracks per artist (was 20 queries → 1) - deepDiveGenerator: up to 5 tracks per album (was 20 queries → 1)
This commit is contained in:
@@ -0,0 +1,20 @@
|
|||||||
|
# muzick — overnight fix plan
|
||||||
|
|
||||||
|
## tasks
|
||||||
|
1. **fix N+1 queries** in generators.service.ts (#110, prio:4)
|
||||||
|
2. **fix SSRF guard** in images.routes.ts (#111, prio:3)
|
||||||
|
3. **clean & re-enrich images** (#124, prio:0 — operational)
|
||||||
|
4. **ethos UI migration** (#34, prio:5 — dispatched to ethos-ui agent)
|
||||||
|
|
||||||
|
## 1. N+1 queries
|
||||||
|
|
||||||
|
### comfortGenerator (lines 57-87)
|
||||||
|
current: loops 20 artists, `SELECT ... WHERE object_id = $1` per artist
|
||||||
|
fix: single query with `WHERE cf.object_id = ANY($1::uuid[])` + `unnest` to get per-artist rows back
|
||||||
|
|
||||||
|
### deepDiveGenerator (lines 248-274)
|
||||||
|
current: loops 20 albums, `SELECT ... WHERE t.album_id = $1` per album
|
||||||
|
fix: single query with `WHERE t.album_id = ANY($1::uuid[])` — already batched on artist query
|
||||||
|
|
||||||
|
### verification
|
||||||
|
`comfortGenerator` and `deepDiveGenerator` are tested in `generators.test.ts`
|
||||||
@@ -52,41 +52,46 @@ async function comfortGenerator(db: DbService, ctx: GeneratorContext): Promise<C
|
|||||||
.sort((a, b) => b.value - a.value)
|
.sort((a, b) => b.value - a.value)
|
||||||
.slice(0, 20);
|
.slice(0, 20);
|
||||||
|
|
||||||
const candidates: Candidate[] = [];
|
if (topArtists.length === 0) return [];
|
||||||
|
|
||||||
for (const belief of topArtists) {
|
const artistValueMap = new Map(topArtists.map(b => [b.entity_id, b.value]));
|
||||||
const res = await db.pgClient.query(
|
const artistIds = topArtists.map(b => b.entity_id);
|
||||||
`SELECT t.id
|
|
||||||
|
const res = await db.pgClient.query(
|
||||||
|
`SELECT sub.id, sub.artist_id
|
||||||
|
FROM (
|
||||||
|
SELECT t.id, cf.object_id AS artist_id,
|
||||||
|
ROW_NUMBER() OVER (PARTITION BY cf.object_id ORDER BY cf.fused_value DESC) AS rn
|
||||||
FROM tracks t
|
FROM tracks t
|
||||||
JOIN claim_fusion cf ON cf.subject_type = 'track' AND cf.subject_id = t.id
|
JOIN claim_fusion cf ON cf.subject_type = 'track' AND cf.subject_id = t.id
|
||||||
AND cf.predicate IN ('credited_main_on', 'featured_on')
|
AND cf.predicate IN ('credited_main_on', 'featured_on')
|
||||||
AND cf.object_type = 'artist' AND cf.object_id = $1
|
AND cf.object_type = 'artist'
|
||||||
|
AND cf.object_id = ANY($1::uuid[])
|
||||||
AND (cf.user_id = $2 OR cf.user_id = $3)
|
AND (cf.user_id = $2 OR cf.user_id = $3)
|
||||||
WHERE t.state = 'LIBRARY'
|
WHERE t.state = 'LIBRARY'
|
||||||
AND NOT (t.id = ANY($4::uuid[]))
|
AND NOT (t.id = ANY($4::uuid[]))
|
||||||
ORDER BY cf.fused_value DESC
|
) sub
|
||||||
LIMIT 2`,
|
WHERE sub.rn <= 2
|
||||||
[belief.entity_id, OBJECTIVE_USER, ctx.userId, ctx.recentExclusions]
|
ORDER BY sub.artist_id, sub.rn`,
|
||||||
);
|
[artistIds, OBJECTIVE_USER, ctx.userId, ctx.recentExclusions]
|
||||||
|
);
|
||||||
|
|
||||||
for (const row of res.rows as { id: string }[]) {
|
return (res.rows as { id: string; artist_id: string }[]).map(row => {
|
||||||
candidates.push({
|
const value = artistValueMap.get(row.artist_id) ?? 0.5;
|
||||||
trackId: row.id,
|
return {
|
||||||
generatorId: 'comfort',
|
trackId: row.id,
|
||||||
explanation: [{
|
generatorId: 'comfort',
|
||||||
subjectType: 'artist',
|
explanation: [{
|
||||||
subjectId: belief.entity_id,
|
subjectType: 'artist',
|
||||||
predicate: 'credited_main_on',
|
subjectId: row.artist_id,
|
||||||
objectType: 'track',
|
predicate: 'credited_main_on',
|
||||||
objectId: row.id,
|
objectType: 'track',
|
||||||
fusedValue: belief.value,
|
objectId: row.id,
|
||||||
}],
|
fusedValue: value,
|
||||||
relevance: belief.value,
|
}],
|
||||||
});
|
relevance: value,
|
||||||
}
|
};
|
||||||
}
|
});
|
||||||
|
|
||||||
return candidates;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -243,37 +248,40 @@ async function deepDiveGenerator(db: DbService, ctx: GeneratorContext): Promise<
|
|||||||
[obsessedIds]
|
[obsessedIds]
|
||||||
);
|
);
|
||||||
|
|
||||||
const candidates: Candidate[] = [];
|
const albumRows = albumRes.rows as { album_id: string; artist_id: string }[];
|
||||||
|
if (albumRows.length === 0) return [];
|
||||||
|
|
||||||
for (const album of albumRes.rows as { album_id: string; artist_id: string }[]) {
|
const albumIds = albumRows.map(a => a.album_id);
|
||||||
const trackRes = await db.pgClient.query(
|
const albumArtistMap = new Map(albumRows.map(a => [a.album_id, a.artist_id]));
|
||||||
`SELECT t.id
|
|
||||||
|
const trackRes = await db.pgClient.query(
|
||||||
|
`SELECT sub.id, sub.album_id
|
||||||
|
FROM (
|
||||||
|
SELECT t.id, t.album_id,
|
||||||
|
ROW_NUMBER() OVER (PARTITION BY t.album_id ORDER BY t.title ASC) AS rn
|
||||||
FROM tracks t
|
FROM tracks t
|
||||||
WHERE t.album_id = $1 AND t.state = 'LIBRARY'
|
WHERE t.album_id = ANY($1::uuid[])
|
||||||
|
AND t.state = 'LIBRARY'
|
||||||
AND NOT (t.id = ANY($2::uuid[]))
|
AND NOT (t.id = ANY($2::uuid[]))
|
||||||
ORDER BY t.title ASC
|
) sub
|
||||||
LIMIT 5`,
|
WHERE sub.rn <= 5
|
||||||
[album.album_id, ctx.recentExclusions]
|
ORDER BY sub.album_id, sub.rn`,
|
||||||
);
|
[albumIds, ctx.recentExclusions]
|
||||||
|
);
|
||||||
|
|
||||||
for (const row of trackRes.rows as { id: string }[]) {
|
return (trackRes.rows as { id: string; album_id: string }[]).map(row => ({
|
||||||
candidates.push({
|
trackId: row.id,
|
||||||
trackId: row.id,
|
generatorId: 'deep-dive',
|
||||||
generatorId: 'deep-dive',
|
explanation: [{
|
||||||
explanation: [{
|
subjectType: 'artist',
|
||||||
subjectType: 'artist',
|
subjectId: albumArtistMap.get(row.album_id) ?? 'unknown',
|
||||||
subjectId: album.artist_id,
|
predicate: 'credited_main_on',
|
||||||
predicate: 'credited_main_on',
|
objectType: 'track',
|
||||||
objectType: 'track',
|
objectId: row.id,
|
||||||
objectId: row.id,
|
fusedValue: 0.7,
|
||||||
fusedValue: 0.7,
|
}],
|
||||||
}],
|
relevance: 0.7,
|
||||||
relevance: 0.7,
|
}));
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return candidates;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|||||||
Reference in New Issue
Block a user