drop the unused FTS5 search index

This commit is contained in:
austinried
2026-10-04 00:29:56 +00:00
parent bf3afb9c17
commit ef5bf40bf7
9 changed files with 796 additions and 114 deletions
@@ -86,5 +86,22 @@ val MIGRATION_8_9 =
}
}
val MIGRATION_9_10 =
object : Migration(9, 10) {
override suspend fun migrate(connection: SQLiteConnection) {
connection.execSQL("DROP TABLE IF EXISTS `search_index`")
}
}
val MIGRATIONS: Array<Migration> =
arrayOf(MIGRATION_1_2, MIGRATION_2_3, MIGRATION_3_4, MIGRATION_4_5, MIGRATION_5_6, MIGRATION_6_7, MIGRATION_7_8, MIGRATION_8_9)
arrayOf(
MIGRATION_1_2,
MIGRATION_2_3,
MIGRATION_3_4,
MIGRATION_4_5,
MIGRATION_5_6,
MIGRATION_6_7,
MIGRATION_7_8,
MIGRATION_8_9,
MIGRATION_9_10,
)
@@ -1,44 +0,0 @@
package com.subtracks.data.db
import androidx.room3.Dao
import androidx.room3.Insert
import androidx.room3.Query
import com.subtracks.data.model.SearchIndex
@Dao
interface SearchDao {
@Query("DELETE FROM search_index WHERE sourceId = :sourceId")
suspend fun clear(sourceId: String)
@Insert
suspend fun insert(items: List<SearchIndex>)
@Query(
"""
SELECT * FROM search_index
WHERE search_index MATCH '"' || replace(:query, '"', '""') || '"'
AND sourceId = :sourceId
ORDER BY rank
LIMIT :limit
""",
)
suspend fun searchPhrase(
sourceId: String,
query: String,
limit: Int,
): List<SearchIndex>
suspend fun search(
sourceId: String,
query: String,
limit: Int,
): List<SearchIndex> {
val phrase = query.trim()
if (phrase.length < MIN_QUERY_LENGTH) return emptyList()
return searchPhrase(sourceId, phrase, limit)
}
companion object {
const val MIN_QUERY_LENGTH = 3
}
}
@@ -3,7 +3,6 @@ package com.subtracks.data.db
import androidx.room3.Dao
import androidx.room3.Insert
import androidx.room3.Query
import androidx.room3.Transaction
import androidx.room3.Upsert
import com.subtracks.data.model.Source
import com.subtracks.data.model.SubsonicConfig
@@ -42,14 +41,7 @@ interface SourcesDao {
@Query("DELETE FROM sources WHERE id = :id")
suspend fun deleteSourceRow(id: Long)
@Query("DELETE FROM search_index WHERE sourceId = :sourceId")
suspend fun clearSearchIndex(sourceId: String)
@Transaction
suspend fun deleteSource(id: Long) {
deleteSourceRow(id)
clearSearchIndex(id.toString())
}
suspend fun deleteSource(id: Long) = deleteSourceRow(id)
@Query("UPDATE sources SET isActive = (id = :sourceId)")
suspend fun setActiveSource(sourceId: Long)
@@ -11,7 +11,6 @@ import com.subtracks.data.model.Playlist
import com.subtracks.data.model.PlaylistSong
import com.subtracks.data.model.QueueEntry
import com.subtracks.data.model.QueueKindConverter
import com.subtracks.data.model.SearchIndex
import com.subtracks.data.model.ShuffleOrder
import com.subtracks.data.model.Song
import com.subtracks.data.model.Source
@@ -26,13 +25,12 @@ import com.subtracks.data.model.SubsonicSource
Playlist::class,
PlaylistSong::class,
Song::class,
SearchIndex::class,
QueueEntry::class,
PlaybackCursor::class,
ShuffleOrder::class,
ArtworkSeed::class,
],
version = 9,
version = 10,
exportSchema = true,
)
@ColumnTypeConverters(QueueKindConverter::class)
@@ -41,8 +39,6 @@ abstract class SubtracksDatabase : RoomDatabase() {
abstract fun sourcesDao(): SourcesDao
abstract fun searchDao(): SearchDao
abstract fun queueDao(): QueueDao
abstract fun artworkSeedDao(): ArtworkSeedDao
@@ -5,8 +5,6 @@ import androidx.room3.ColumnTypeConverter
import androidx.room3.Embedded
import androidx.room3.Entity
import androidx.room3.ForeignKey
import androidx.room3.Fts5
import androidx.room3.FtsOptions
import androidx.room3.Index
import androidx.room3.PrimaryKey
@@ -168,16 +166,6 @@ data class SongListItem(
val coverArt: String?,
)
@Entity(tableName = "search_index")
@Fts5(tokenizer = FtsOptions.TOKENIZER_TRIGRAM, notIndexed = ["sourceId", "type", "itemId"])
data class SearchIndex(
@PrimaryKey(autoGenerate = true) @ColumnInfo(name = "rowid") val rowId: Long = 0,
val sourceId: String,
val type: String,
val itemId: String,
val title: String,
)
enum class QueueKind { Playlist, Album, Song, Songs }
@Entity(
@@ -9,7 +9,6 @@ import com.subtracks.data.model.Album
import com.subtracks.data.model.Artist
import com.subtracks.data.model.Playlist
import com.subtracks.data.model.PlaylistSong
import com.subtracks.data.model.SearchIndex
import com.subtracks.data.model.Song
import com.subtracks.data.source.MusicSource
import kotlinx.coroutines.sync.Mutex
@@ -53,18 +52,6 @@ class SyncService(
library.deletePlaylistSongsFrom(source.id, playlist.id, (playlistSongCounts[playlist.id] ?: 0).toLong())
}
(existingPlaylistIds - playlists.map { it.id }).forEach { library.deletePlaylistSongsFrom(source.id, it, 0) }
val search = db.searchDao()
val sourceKey = source.id.toString()
search.clear(sourceKey)
search.insert(
buildList {
artists.forEach { add(SearchIndex(sourceId = sourceKey, type = "artist", itemId = it.id, title = it.name)) }
albums.forEach { add(SearchIndex(sourceId = sourceKey, type = "album", itemId = it.id, title = it.name)) }
songs.forEach { add(SearchIndex(sourceId = sourceKey, type = "song", itemId = it.id, title = it.title)) }
playlists.forEach { add(SearchIndex(sourceId = sourceKey, type = "playlist", itemId = it.id, title = it.name)) }
},
)
}
}
}
@@ -160,35 +160,6 @@ class SyncServiceTest {
)
}
@Test
fun searchIndexMatchesSubstrings() =
runTest {
insertSource()
val source =
FakeMusicSource(
artists = listOf(artist("a1", name = "Radiohead")),
songs = listOf(song("s1")),
)
SyncService(db, source).sync()
val results = db.searchDao().search("1", "adio", 10)
assertEquals(1, results.size)
assertEquals("a1", results.single().itemId)
assertEquals("artist", results.single().type)
}
@Test
fun searchIgnoresTooShortQueries() =
runTest {
insertSource()
SyncService(db, FakeMusicSource(artists = listOf(artist("a1", name = "Radiohead")))).sync()
assertEquals(0, db.searchDao().search("1", "ad", 10).size)
assertEquals(0, db.searchDao().search("1", "", 10).size)
assertEquals(1, db.searchDao().search("1", "adi", 10).size)
}
@Test
fun syncUpdatesExistingRows() =
runTest {