feat: migration 12 — explore_index + FTS5 search index schema
Add explore_index table (entity_type, mbid, title, artist_name, artist_mbid, popularity, extra_json) with a unique index on (entity_type, mbid). FTS5 virtual table explore_index_fts backed by the content table with auto-sync triggers for insert/update/delete. explore_index_meta table tracks build timestamps.
This commit is contained in:
@@ -359,6 +359,17 @@ func runMigrations(
|
||||
}
|
||||
}
|
||||
|
||||
// Migration 12: explore_index + FTS5 for the popularity search
|
||||
// index. Stores the top albums and tracks from the most popular
|
||||
// ListenBrainz artists for instant local search.
|
||||
if version < 12 { //nolint:mnd
|
||||
if err := migration12ExploreSearchIndex(
|
||||
ctx, db, logger,
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1424,6 +1435,101 @@ func migration11ExploreCache(
|
||||
return nil
|
||||
}
|
||||
|
||||
// migration12ExploreSearchIndex creates the explore_index table,
|
||||
// the FTS5 virtual table for full-text search, sync triggers, and
|
||||
// the explore_index_meta table for build tracking.
|
||||
func migration12ExploreSearchIndex(
|
||||
ctx context.Context,
|
||||
db *sql.DB,
|
||||
logger *slog.Logger,
|
||||
) error {
|
||||
logger.Info("applying migration 12: explore search index")
|
||||
|
||||
// Content table — slim denormalized rows for search.
|
||||
if _, err := db.ExecContext(ctx, `
|
||||
CREATE TABLE IF NOT EXISTS explore_index (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
entity_type TEXT NOT NULL,
|
||||
mbid TEXT NOT NULL,
|
||||
title TEXT NOT NULL,
|
||||
artist_name TEXT NOT NULL,
|
||||
artist_mbid TEXT NOT NULL,
|
||||
popularity INTEGER NOT NULL DEFAULT 0,
|
||||
extra_json TEXT
|
||||
)
|
||||
`); err != nil {
|
||||
return fmt.Errorf("migration 12: create explore_index: %w", err)
|
||||
}
|
||||
|
||||
if _, err := db.ExecContext(ctx, `
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_explore_index_mbid
|
||||
ON explore_index(entity_type, mbid)
|
||||
`); err != nil {
|
||||
return fmt.Errorf("migration 12: create mbid index: %w", err)
|
||||
}
|
||||
|
||||
// FTS5 virtual table backed by the content table.
|
||||
if _, err := db.ExecContext(ctx, `
|
||||
CREATE VIRTUAL TABLE IF NOT EXISTS explore_index_fts USING fts5(
|
||||
title, artist_name,
|
||||
content='explore_index',
|
||||
content_rowid='id'
|
||||
)
|
||||
`); err != nil {
|
||||
return fmt.Errorf("migration 12: create FTS5 table: %w", err)
|
||||
}
|
||||
|
||||
// Triggers to keep FTS in sync.
|
||||
if _, err := db.ExecContext(ctx, `
|
||||
CREATE TRIGGER IF NOT EXISTS explore_index_ai AFTER INSERT ON explore_index BEGIN
|
||||
INSERT INTO explore_index_fts(rowid, title, artist_name)
|
||||
VALUES (new.id, new.title, new.artist_name);
|
||||
END
|
||||
`); err != nil {
|
||||
return fmt.Errorf("migration 12: create insert trigger: %w", err)
|
||||
}
|
||||
|
||||
if _, err := db.ExecContext(ctx, `
|
||||
CREATE TRIGGER IF NOT EXISTS explore_index_ad AFTER DELETE ON explore_index BEGIN
|
||||
INSERT INTO explore_index_fts(explore_index_fts, rowid, title, artist_name)
|
||||
VALUES ('delete', old.id, old.title, old.artist_name);
|
||||
END
|
||||
`); err != nil {
|
||||
return fmt.Errorf("migration 12: create delete trigger: %w", err)
|
||||
}
|
||||
|
||||
if _, err := db.ExecContext(ctx, `
|
||||
CREATE TRIGGER IF NOT EXISTS explore_index_au AFTER UPDATE ON explore_index BEGIN
|
||||
INSERT INTO explore_index_fts(explore_index_fts, rowid, title, artist_name)
|
||||
VALUES ('delete', old.id, old.title, old.artist_name);
|
||||
INSERT INTO explore_index_fts(rowid, title, artist_name)
|
||||
VALUES (new.id, new.title, new.artist_name);
|
||||
END
|
||||
`); err != nil {
|
||||
return fmt.Errorf("migration 12: create update trigger: %w", err)
|
||||
}
|
||||
|
||||
// Metadata table for build tracking.
|
||||
if _, err := db.ExecContext(ctx, `
|
||||
CREATE TABLE IF NOT EXISTS explore_index_meta (
|
||||
key TEXT PRIMARY KEY,
|
||||
value TEXT NOT NULL
|
||||
)
|
||||
`); err != nil {
|
||||
return fmt.Errorf("migration 12: create meta table: %w", err)
|
||||
}
|
||||
|
||||
if _, err := db.ExecContext(
|
||||
ctx, "PRAGMA user_version = 12",
|
||||
); err != nil {
|
||||
return fmt.Errorf("could not set user_version to 12: %w", err)
|
||||
}
|
||||
|
||||
logger.Info("migration 12 complete")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// readLibraryDirFromTOML reads the TOML config file and returns
|
||||
// the Library.DirectoryPath value, or "" if not configured.
|
||||
func readLibraryDirFromTOML(logger *slog.Logger) string {
|
||||
|
||||
Reference in New Issue
Block a user