perf: optimize ML/reco module for home view loading
- Add composite indexes on reco_events for GROUP BY + ORDER BY queries (event_type + album_id/track_id/artist_id + created_at DESC) - Add composite index on reco_scores for scored lookups (score_type + item_type + score DESC) - Eliminate duplicate fallback queries in get_home_seeds_internal by inlining the empty check into the scored branch - Parallelize resolve_favorites with recent albums, tracks, and artists (4-way tokio::try_join instead of 3 + sequential) - Batch cache writes: collect all Tier 3 API results first, then write to API cache and reco meta with single lock acquisitions instead of locking per-item inside spawned tasks
This commit is contained in:
2 files changed
+194
-135
No files matched your search
@@ -671,64 +671,74 @@ fn get_home_seeds_internal(
|
||||
) -> Result<HomeSeeds, String> {
|
||||
let has_scores = db.has_scores("all")?;
|
||||
|
||||
let recent_fresh_albums = db.get_recent_album_ids(4)?;
|
||||
let recent_fresh_tracks = db.get_recent_track_ids(4)?;
|
||||
|
||||
let mut recently_played_album_ids = if has_scores {
|
||||
// When scores exist, merge scored results with recent items.
|
||||
// When no scores (or scored results empty), fall back to event-based queries.
|
||||
// Avoid duplicate queries: only call fallback if scored path was taken but empty.
|
||||
let recently_played_album_ids = if has_scores {
|
||||
let recent_fresh = db.get_recent_album_ids(4)?;
|
||||
let scored = db.get_scored_album_ids("all", limit_recent_albums + 4)?;
|
||||
merge_unique_preserve_order(recent_fresh_albums, scored, limit_recent_albums as usize)
|
||||
let merged = merge_unique_preserve_order(recent_fresh, scored, limit_recent_albums as usize);
|
||||
if merged.is_empty() {
|
||||
db.get_recent_album_ids(limit_recent_albums)?
|
||||
} else {
|
||||
merged
|
||||
}
|
||||
} else {
|
||||
db.get_recent_album_ids(limit_recent_albums)?
|
||||
};
|
||||
|
||||
let mut continue_listening_track_ids = if has_scores {
|
||||
let continue_listening_track_ids = if has_scores {
|
||||
let recent_fresh = db.get_recent_track_ids(4)?;
|
||||
let scored = db.get_scored_track_ids("all", limit_continue_tracks + 4)?;
|
||||
merge_unique_preserve_order(recent_fresh_tracks, scored, limit_continue_tracks as usize)
|
||||
let merged = merge_unique_preserve_order(recent_fresh, scored, limit_continue_tracks as usize);
|
||||
if merged.is_empty() {
|
||||
db.get_recent_track_ids(limit_continue_tracks)?
|
||||
} else {
|
||||
merged
|
||||
}
|
||||
} else {
|
||||
db.get_recent_track_ids(limit_continue_tracks)?
|
||||
};
|
||||
|
||||
let mut top_artist_ids = if has_scores {
|
||||
db.get_scored_artist_scores("all", limit_top_artists)?
|
||||
let top_artist_ids = if has_scores {
|
||||
let scored: Vec<TopArtistSeed> = db.get_scored_artist_scores("all", limit_top_artists)?
|
||||
.into_iter()
|
||||
.map(|(artist_id, score)| TopArtistSeed {
|
||||
artist_id,
|
||||
play_count: score.round().max(1.0) as u32,
|
||||
})
|
||||
.collect()
|
||||
.collect();
|
||||
if scored.is_empty() {
|
||||
db.get_top_artist_ids(limit_top_artists)?
|
||||
} else {
|
||||
scored
|
||||
}
|
||||
} else {
|
||||
Vec::new()
|
||||
db.get_top_artist_ids(limit_top_artists)?
|
||||
};
|
||||
|
||||
let mut favorite_album_ids = if has_scores {
|
||||
db.get_scored_album_ids("favorite", limit_favorites)?
|
||||
let favorite_album_ids = if has_scores {
|
||||
let scored = db.get_scored_album_ids("favorite", limit_favorites)?;
|
||||
if scored.is_empty() {
|
||||
db.get_favorite_album_ids(limit_favorites)?
|
||||
} else {
|
||||
scored
|
||||
}
|
||||
} else {
|
||||
Vec::new()
|
||||
db.get_favorite_album_ids(limit_favorites)?
|
||||
};
|
||||
|
||||
let mut favorite_track_ids = if has_scores {
|
||||
db.get_scored_track_ids("favorite", limit_favorites)?
|
||||
let favorite_track_ids = if has_scores {
|
||||
let scored = db.get_scored_track_ids("favorite", limit_favorites)?;
|
||||
if scored.is_empty() {
|
||||
db.get_favorite_track_ids(limit_favorites)?
|
||||
} else {
|
||||
scored
|
||||
}
|
||||
} else {
|
||||
Vec::new()
|
||||
db.get_favorite_track_ids(limit_favorites)?
|
||||
};
|
||||
|
||||
// Fallbacks for empty results
|
||||
if recently_played_album_ids.is_empty() {
|
||||
recently_played_album_ids = db.get_recent_album_ids(limit_recent_albums)?;
|
||||
}
|
||||
if continue_listening_track_ids.is_empty() {
|
||||
continue_listening_track_ids = db.get_recent_track_ids(limit_continue_tracks)?;
|
||||
}
|
||||
if top_artist_ids.is_empty() {
|
||||
top_artist_ids = db.get_top_artist_ids(limit_top_artists)?;
|
||||
}
|
||||
if favorite_album_ids.is_empty() {
|
||||
favorite_album_ids = db.get_favorite_album_ids(limit_favorites)?;
|
||||
}
|
||||
if favorite_track_ids.is_empty() {
|
||||
favorite_track_ids = db.get_favorite_track_ids(limit_favorites)?;
|
||||
}
|
||||
|
||||
Ok(HomeSeeds {
|
||||
recently_played_album_ids,
|
||||
continue_listening_track_ids,
|
||||
@@ -794,14 +804,20 @@ pub async fn reco_get_home_resolved(
|
||||
.map(|s| (s.artist_id, s.play_count))
|
||||
.collect();
|
||||
|
||||
// Step 3: Resolve albums, tracks, AND artists in parallel
|
||||
// Artists are independent of albums/tracks, so start them concurrently.
|
||||
let albums_fut = resolve_albums(
|
||||
// Step 3: Resolve all entity types in parallel.
|
||||
// Recent albums, favorite albums, tracks, and artists are all independent.
|
||||
let recent_albums_fut = resolve_albums(
|
||||
&seeds.recently_played_album_ids,
|
||||
&reco_state,
|
||||
&app_state,
|
||||
&cache_state,
|
||||
);
|
||||
let favorite_albums_fut = resolve_albums(
|
||||
&seeds.favorite_album_ids,
|
||||
&reco_state,
|
||||
&app_state,
|
||||
&cache_state,
|
||||
);
|
||||
let tracks_fut = resolve_tracks(&all_track_ids, &reco_state, &app_state, &cache_state);
|
||||
let artists_fut = resolve_artists(
|
||||
&all_artist_ids,
|
||||
@@ -811,8 +827,8 @@ pub async fn reco_get_home_resolved(
|
||||
&cache_state,
|
||||
);
|
||||
|
||||
let (recently_played_albums, all_tracks, top_artists) =
|
||||
tokio::try_join!(albums_fut, tracks_fut, artists_fut)?;
|
||||
let (recently_played_albums, resolved_favorites, all_tracks, top_artists) =
|
||||
tokio::try_join!(recent_albums_fut, favorite_albums_fut, tracks_fut, artists_fut)?;
|
||||
|
||||
// Build track lookup
|
||||
let track_map: HashMap<u64, &TrackDisplayMeta> =
|
||||
@@ -824,18 +840,9 @@ pub async fn reco_get_home_resolved(
|
||||
.filter_map(|id| track_map.get(id).map(|tr| (*tr).clone()))
|
||||
.collect();
|
||||
|
||||
// Step 5: "More From Favorites" = discover albums by favorite artists,
|
||||
// Step 4: "More From Favorites" = discover albums by favorite artists,
|
||||
// excluding albums the user already has in favorites / recently played.
|
||||
|
||||
// 5a: Resolve favorite albums to get their artist IDs
|
||||
let resolved_favorites = resolve_albums(
|
||||
&seeds.favorite_album_ids,
|
||||
&reco_state,
|
||||
&app_state,
|
||||
&cache_state,
|
||||
)
|
||||
.await?;
|
||||
|
||||
// 5b: Collect unique artist IDs from favorite albums + favorite tracks
|
||||
let favorite_artist_ids: Vec<u64> = {
|
||||
let mut ids = Vec::new();
|
||||
@@ -989,6 +996,7 @@ async fn resolve_albums(
|
||||
|
||||
// Tier 2: API cache (24h TTL)
|
||||
let mut api_cache_resolved: HashMap<String, AlbumCardMeta> = HashMap::new();
|
||||
let mut tier2_album_metas: Vec<AlbumCardMeta> = Vec::new();
|
||||
if !missing_ids.is_empty() {
|
||||
let cached = {
|
||||
let guard__ = cache_state.cache.lock().await;
|
||||
@@ -1000,16 +1008,19 @@ async fn resolve_albums(
|
||||
for (album_id, json_str) in cached {
|
||||
if let Ok(album) = serde_json::from_str::<Album>(&json_str) {
|
||||
let meta = album_to_card_meta(&album);
|
||||
// Write through to reco meta for future instant hits
|
||||
{
|
||||
let guard__ = reco_state.db.lock().await;
|
||||
if let Some(db) = guard__.as_ref() {
|
||||
let _ = db.set_album_meta(&meta);
|
||||
}
|
||||
}
|
||||
tier2_album_metas.push(meta.clone());
|
||||
api_cache_resolved.insert(album_id, meta);
|
||||
}
|
||||
}
|
||||
// Batch write tier-2 hits to reco meta (single lock)
|
||||
if !tier2_album_metas.is_empty() {
|
||||
let guard__ = reco_state.db.lock().await;
|
||||
if let Some(db) = guard__.as_ref() {
|
||||
for meta in &tier2_album_metas {
|
||||
let _ = db.set_album_meta(meta);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let still_missing: Vec<String> = missing_ids
|
||||
@@ -1018,7 +1029,7 @@ async fn resolve_albums(
|
||||
.cloned()
|
||||
.collect();
|
||||
|
||||
// Tier 3: Qobuz API (parallel with semaphore)
|
||||
// Tier 3: Qobuz API — fetch in parallel, defer cache writes to after all complete
|
||||
let mut api_resolved: HashMap<String, AlbumCardMeta> = HashMap::new();
|
||||
if !still_missing.is_empty() {
|
||||
log::info!(
|
||||
@@ -1027,15 +1038,11 @@ async fn resolve_albums(
|
||||
);
|
||||
let sem = std::sync::Arc::new(tokio::sync::Semaphore::new(10));
|
||||
let client = app_state.client.clone();
|
||||
let cache_arc = cache_state.cache.clone();
|
||||
let reco_arc = reco_state.db.clone();
|
||||
|
||||
let mut handles = Vec::new();
|
||||
for album_id in &still_missing {
|
||||
let sem = sem.clone();
|
||||
let client = client.clone();
|
||||
let cache_arc = cache_arc.clone();
|
||||
let reco_arc = reco_arc.clone();
|
||||
let album_id = album_id.clone();
|
||||
|
||||
handles.push(tokio::spawn(async move {
|
||||
@@ -1045,31 +1052,42 @@ async fn resolve_albums(
|
||||
c.get_album(&album_id).await.ok()?
|
||||
};
|
||||
let meta = album_to_card_meta(&album);
|
||||
|
||||
// Cache the full API response
|
||||
if let Ok(json) = serde_json::to_string(&album) {
|
||||
let guard__ = cache_arc.lock().await;
|
||||
if let Some(cache) = guard__.as_ref() {
|
||||
let _ = cache.set_album(&album_id, &json);
|
||||
}
|
||||
}
|
||||
// Write to reco meta
|
||||
{
|
||||
let guard__ = reco_arc.lock().await;
|
||||
if let Some(db) = guard__.as_ref() {
|
||||
let _ = db.set_album_meta(&meta);
|
||||
}
|
||||
}
|
||||
|
||||
Some((album_id, meta))
|
||||
let json = serde_json::to_string(&album).ok();
|
||||
Some((album_id, meta, json))
|
||||
}));
|
||||
}
|
||||
|
||||
// Collect results, then batch-write caches
|
||||
let mut api_albums_to_cache: Vec<(String, String)> = Vec::new();
|
||||
let mut api_album_metas: Vec<AlbumCardMeta> = Vec::new();
|
||||
for handle in handles {
|
||||
if let Ok(Some((id, meta))) = handle.await {
|
||||
if let Ok(Some((id, meta, json))) = handle.await {
|
||||
if let Some(j) = json {
|
||||
api_albums_to_cache.push((id.clone(), j));
|
||||
}
|
||||
api_album_metas.push(meta.clone());
|
||||
api_resolved.insert(id, meta);
|
||||
}
|
||||
}
|
||||
|
||||
// Batch write to API cache (single lock)
|
||||
if !api_albums_to_cache.is_empty() {
|
||||
let guard__ = cache_state.cache.lock().await;
|
||||
if let Some(cache) = guard__.as_ref() {
|
||||
for (album_id, json) in &api_albums_to_cache {
|
||||
let _ = cache.set_album(album_id, json);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Batch write to reco meta (single lock)
|
||||
if !api_album_metas.is_empty() {
|
||||
let guard__ = reco_state.db.lock().await;
|
||||
if let Some(db) = guard__.as_ref() {
|
||||
for meta in &api_album_metas {
|
||||
let _ = db.set_album_meta(meta);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Assemble in seed order
|
||||
@@ -1117,6 +1135,7 @@ async fn resolve_tracks(
|
||||
|
||||
// Tier 2: API cache
|
||||
let mut api_cache_resolved: HashMap<u64, TrackDisplayMeta> = HashMap::new();
|
||||
let mut tier2_track_metas: Vec<TrackDisplayMeta> = Vec::new();
|
||||
if !missing_ids.is_empty() {
|
||||
let cached = {
|
||||
let guard__ = cache_state.cache.lock().await;
|
||||
@@ -1128,15 +1147,19 @@ async fn resolve_tracks(
|
||||
for (track_id, json_str) in cached {
|
||||
if let Ok(track) = serde_json::from_str::<Track>(&json_str) {
|
||||
let meta = track_to_display_meta(&track);
|
||||
{
|
||||
let guard__ = reco_state.db.lock().await;
|
||||
if let Some(db) = guard__.as_ref() {
|
||||
let _ = db.set_track_meta(&meta);
|
||||
}
|
||||
}
|
||||
tier2_track_metas.push(meta.clone());
|
||||
api_cache_resolved.insert(track_id, meta);
|
||||
}
|
||||
}
|
||||
// Batch write tier-2 hits to reco meta (single lock)
|
||||
if !tier2_track_metas.is_empty() {
|
||||
let guard__ = reco_state.db.lock().await;
|
||||
if let Some(db) = guard__.as_ref() {
|
||||
for meta in &tier2_track_metas {
|
||||
let _ = db.set_track_meta(meta);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let still_missing: Vec<u64> = missing_ids
|
||||
@@ -1145,7 +1168,7 @@ async fn resolve_tracks(
|
||||
.copied()
|
||||
.collect();
|
||||
|
||||
// Tier 3: Qobuz API
|
||||
// Tier 3: Qobuz API — fetch in parallel, defer cache writes
|
||||
let mut api_resolved: HashMap<u64, TrackDisplayMeta> = HashMap::new();
|
||||
if !still_missing.is_empty() {
|
||||
log::info!(
|
||||
@@ -1154,15 +1177,11 @@ async fn resolve_tracks(
|
||||
);
|
||||
let sem = std::sync::Arc::new(tokio::sync::Semaphore::new(10));
|
||||
let client = app_state.client.clone();
|
||||
let cache_arc = cache_state.cache.clone();
|
||||
let reco_arc = reco_state.db.clone();
|
||||
|
||||
let mut handles = Vec::new();
|
||||
for track_id in &still_missing {
|
||||
let sem = sem.clone();
|
||||
let client = client.clone();
|
||||
let cache_arc = cache_arc.clone();
|
||||
let reco_arc = reco_arc.clone();
|
||||
let track_id = *track_id;
|
||||
|
||||
handles.push(tokio::spawn(async move {
|
||||
@@ -1172,29 +1191,42 @@ async fn resolve_tracks(
|
||||
c.get_track(track_id).await.ok()?
|
||||
};
|
||||
let meta = track_to_display_meta(&track);
|
||||
|
||||
if let Ok(json) = serde_json::to_string(&track) {
|
||||
let guard__ = cache_arc.lock().await;
|
||||
if let Some(cache) = guard__.as_ref() {
|
||||
let _ = cache.set_track(track_id, &json);
|
||||
}
|
||||
}
|
||||
{
|
||||
let guard__ = reco_arc.lock().await;
|
||||
if let Some(db) = guard__.as_ref() {
|
||||
let _ = db.set_track_meta(&meta);
|
||||
}
|
||||
}
|
||||
|
||||
Some((track_id, meta))
|
||||
let json = serde_json::to_string(&track).ok();
|
||||
Some((track_id, meta, json))
|
||||
}));
|
||||
}
|
||||
|
||||
// Collect results, then batch-write caches
|
||||
let mut api_tracks_to_cache: Vec<(u64, String)> = Vec::new();
|
||||
let mut api_track_metas: Vec<TrackDisplayMeta> = Vec::new();
|
||||
for handle in handles {
|
||||
if let Ok(Some((id, meta))) = handle.await {
|
||||
if let Ok(Some((id, meta, json))) = handle.await {
|
||||
if let Some(j) = json {
|
||||
api_tracks_to_cache.push((id, j));
|
||||
}
|
||||
api_track_metas.push(meta.clone());
|
||||
api_resolved.insert(id, meta);
|
||||
}
|
||||
}
|
||||
|
||||
// Batch write to API cache (single lock)
|
||||
if !api_tracks_to_cache.is_empty() {
|
||||
let guard__ = cache_state.cache.lock().await;
|
||||
if let Some(cache) = guard__.as_ref() {
|
||||
for (track_id, json) in &api_tracks_to_cache {
|
||||
let _ = cache.set_track(*track_id, json);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Batch write to reco meta (single lock)
|
||||
if !api_track_metas.is_empty() {
|
||||
let guard__ = reco_state.db.lock().await;
|
||||
if let Some(db) = guard__.as_ref() {
|
||||
for meta in &api_track_metas {
|
||||
let _ = db.set_track_meta(meta);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let result: Vec<TrackDisplayMeta> = ids
|
||||
@@ -1246,6 +1278,7 @@ async fn resolve_artists(
|
||||
};
|
||||
|
||||
let mut api_cache_resolved: HashMap<u64, ArtistCardMeta> = HashMap::new();
|
||||
let mut tier2_metas_to_write: Vec<ArtistCardMeta> = Vec::new();
|
||||
if !missing_ids.is_empty() {
|
||||
let cached = {
|
||||
let guard__ = cache_state.cache.lock().await;
|
||||
@@ -1257,15 +1290,19 @@ async fn resolve_artists(
|
||||
for (artist_id, json_str) in cached {
|
||||
if let Ok(artist) = serde_json::from_str::<Artist>(&json_str) {
|
||||
let meta = artist_to_card_meta(&artist, None);
|
||||
{
|
||||
let guard__ = reco_state.db.lock().await;
|
||||
if let Some(db) = guard__.as_ref() {
|
||||
let _ = db.set_artist_meta(&meta);
|
||||
}
|
||||
}
|
||||
tier2_metas_to_write.push(meta.clone());
|
||||
api_cache_resolved.insert(artist_id, meta);
|
||||
}
|
||||
}
|
||||
// Batch write tier-2 hits to reco meta (single lock acquisition)
|
||||
if !tier2_metas_to_write.is_empty() {
|
||||
let guard__ = reco_state.db.lock().await;
|
||||
if let Some(db) = guard__.as_ref() {
|
||||
for meta in &tier2_metas_to_write {
|
||||
let _ = db.set_artist_meta(meta);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let still_missing: Vec<u64> = missing_ids
|
||||
@@ -1274,7 +1311,7 @@ async fn resolve_artists(
|
||||
.copied()
|
||||
.collect();
|
||||
|
||||
// Tier 3: Qobuz API
|
||||
// Tier 3: Qobuz API — fetch in parallel, defer cache writes to after all complete
|
||||
let mut api_resolved: HashMap<u64, ArtistCardMeta> = HashMap::new();
|
||||
if !still_missing.is_empty() {
|
||||
log::info!(
|
||||
@@ -1283,17 +1320,11 @@ async fn resolve_artists(
|
||||
);
|
||||
let sem = std::sync::Arc::new(tokio::sync::Semaphore::new(10));
|
||||
let client = app_state.client.clone();
|
||||
let cache_arc = cache_state.cache.clone();
|
||||
let reco_arc = reco_state.db.clone();
|
||||
let locale_clone = locale.clone();
|
||||
|
||||
let mut handles = Vec::new();
|
||||
for artist_id in &still_missing {
|
||||
let sem = sem.clone();
|
||||
let client = client.clone();
|
||||
let cache_arc = cache_arc.clone();
|
||||
let reco_arc = reco_arc.clone();
|
||||
let locale = locale_clone.clone();
|
||||
let artist_id = *artist_id;
|
||||
|
||||
handles.push(tokio::spawn(async move {
|
||||
@@ -1303,29 +1334,42 @@ async fn resolve_artists(
|
||||
c.get_artist_basic(artist_id).await.ok()?
|
||||
};
|
||||
let meta = artist_to_card_meta(&artist, None);
|
||||
|
||||
if let Ok(json) = serde_json::to_string(&artist) {
|
||||
let guard__ = cache_arc.lock().await;
|
||||
if let Some(cache) = guard__.as_ref() {
|
||||
let _ = cache.set_artist(artist_id, &locale, &json);
|
||||
}
|
||||
}
|
||||
{
|
||||
let guard__ = reco_arc.lock().await;
|
||||
if let Some(db) = guard__.as_ref() {
|
||||
let _ = db.set_artist_meta(&meta);
|
||||
}
|
||||
}
|
||||
|
||||
Some((artist_id, meta))
|
||||
let json = serde_json::to_string(&artist).ok();
|
||||
Some((artist_id, meta, json))
|
||||
}));
|
||||
}
|
||||
|
||||
// Collect results without holding any locks
|
||||
let mut api_artists_to_cache: Vec<(u64, String)> = Vec::new();
|
||||
let mut api_metas_to_write: Vec<ArtistCardMeta> = Vec::new();
|
||||
for handle in handles {
|
||||
if let Ok(Some((id, meta))) = handle.await {
|
||||
if let Ok(Some((id, meta, json))) = handle.await {
|
||||
if let Some(j) = json {
|
||||
api_artists_to_cache.push((id, j));
|
||||
}
|
||||
api_metas_to_write.push(meta.clone());
|
||||
api_resolved.insert(id, meta);
|
||||
}
|
||||
}
|
||||
|
||||
// Batch write to API cache (single lock)
|
||||
if !api_artists_to_cache.is_empty() {
|
||||
let guard__ = cache_state.cache.lock().await;
|
||||
if let Some(cache) = guard__.as_ref() {
|
||||
for (artist_id, json) in &api_artists_to_cache {
|
||||
let _ = cache.set_artist(*artist_id, &locale, json);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Batch write to reco meta (single lock)
|
||||
if !api_metas_to_write.is_empty() {
|
||||
let guard__ = reco_state.db.lock().await;
|
||||
if let Some(db) = guard__.as_ref() {
|
||||
for meta in &api_metas_to_write {
|
||||
let _ = db.set_artist_meta(meta);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Assemble in seed order, attaching play_counts
|
||||
|
||||
@@ -64,6 +64,17 @@ impl RecoStoreDb {
|
||||
CREATE INDEX IF NOT EXISTS idx_reco_events_artist ON reco_events(artist_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_reco_events_created ON reco_events(created_at);
|
||||
|
||||
-- Composite indexes for GROUP BY + ORDER BY queries in get_home_seeds
|
||||
CREATE INDEX IF NOT EXISTS idx_reco_events_play_albums
|
||||
ON reco_events(event_type, album_id, created_at DESC)
|
||||
WHERE album_id IS NOT NULL;
|
||||
CREATE INDEX IF NOT EXISTS idx_reco_events_play_tracks
|
||||
ON reco_events(event_type, track_id, created_at DESC)
|
||||
WHERE track_id IS NOT NULL;
|
||||
CREATE INDEX IF NOT EXISTS idx_reco_events_play_artists
|
||||
ON reco_events(event_type, artist_id, created_at DESC)
|
||||
WHERE artist_id IS NOT NULL;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS reco_scores (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
score_type TEXT NOT NULL,
|
||||
@@ -79,6 +90,10 @@ impl RecoStoreDb {
|
||||
CREATE INDEX IF NOT EXISTS idx_reco_scores_track ON reco_scores(track_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_reco_scores_album ON reco_scores(album_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_reco_scores_artist ON reco_scores(artist_id);
|
||||
|
||||
-- Composite index for scored lookups (score_type + item_type + ORDER BY score)
|
||||
CREATE INDEX IF NOT EXISTS idx_reco_scores_lookup
|
||||
ON reco_scores(score_type, item_type, score DESC);
|
||||
"#,
|
||||
)
|
||||
.map_err(|e| format!("Failed to initialize reco database: {}", e))?;
|
||||
|
||||
Reference in new issue
Block a user