Implement full text search
This commit is contained in:
@@ -12,4 +12,6 @@ interface RecipeRepo {
|
||||
suspend fun refreshRecipeInfo(recipeSlug: String): Result<Unit>
|
||||
|
||||
suspend fun loadRecipeInfo(recipeId: String): FullRecipeEntity?
|
||||
|
||||
fun setSearchName(name: String?)
|
||||
}
|
||||
@@ -9,7 +9,7 @@ import gq.kirmanak.mealient.database.recipe.entity.RecipeSummaryEntity
|
||||
interface RecipeStorage {
|
||||
suspend fun saveRecipes(recipes: List<RecipeSummaryInfo>)
|
||||
|
||||
fun queryRecipes(): PagingSource<Int, RecipeSummaryEntity>
|
||||
fun queryRecipes(query: String?): PagingSource<Int, RecipeSummaryEntity>
|
||||
|
||||
suspend fun refreshAll(recipes: List<RecipeSummaryInfo>)
|
||||
|
||||
|
||||
@@ -35,9 +35,11 @@ class RecipeStorageImpl @Inject constructor(
|
||||
}
|
||||
|
||||
|
||||
override fun queryRecipes(): PagingSource<Int, RecipeSummaryEntity> {
|
||||
logger.v { "queryRecipes() called" }
|
||||
return recipeDao.queryRecipesByPages()
|
||||
override fun queryRecipes(query: String?): PagingSource<Int, RecipeSummaryEntity> {
|
||||
logger.v { "queryRecipes() called with: query = $query" }
|
||||
return if (query == null) recipeDao.queryRecipesByPages()
|
||||
else recipeDao.queryRecipesByPages(query)
|
||||
|
||||
}
|
||||
|
||||
override suspend fun refreshAll(recipes: List<RecipeSummaryInfo>) {
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
package gq.kirmanak.mealient.data.recipes.impl
|
||||
|
||||
import androidx.paging.PagingSource
|
||||
import gq.kirmanak.mealient.database.recipe.entity.RecipeSummaryEntity
|
||||
|
||||
interface RecipePagingSourceFactory : () -> PagingSource<Int, RecipeSummaryEntity> {
|
||||
fun setQuery(newQuery: String?)
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package gq.kirmanak.mealient.data.recipes.impl
|
||||
|
||||
import androidx.paging.PagingSource
|
||||
import gq.kirmanak.mealient.data.recipes.db.RecipeStorage
|
||||
import gq.kirmanak.mealient.database.recipe.entity.RecipeSummaryEntity
|
||||
import gq.kirmanak.mealient.logging.Logger
|
||||
import java.util.concurrent.atomic.AtomicReference
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Singleton
|
||||
class RecipePagingSourceFactoryImpl @Inject constructor(
|
||||
private val recipeStorage: RecipeStorage,
|
||||
private val logger: Logger,
|
||||
) : RecipePagingSourceFactory {
|
||||
|
||||
private val query = AtomicReference<String>(null)
|
||||
|
||||
override fun invoke(): PagingSource<Int, RecipeSummaryEntity> {
|
||||
val currentQuery = query.get()
|
||||
logger.d { "Creating paging source, query is $currentQuery" }
|
||||
return recipeStorage.queryRecipes(currentQuery)
|
||||
}
|
||||
|
||||
override fun setQuery(newQuery: String?) {
|
||||
logger.v { "setQuery() called with: newQuery = $newQuery" }
|
||||
query.set(newQuery)
|
||||
}
|
||||
}
|
||||
@@ -19,10 +19,12 @@ import javax.inject.Singleton
|
||||
class RecipeRepoImpl @Inject constructor(
|
||||
private val mediator: RecipesRemoteMediator,
|
||||
private val storage: RecipeStorage,
|
||||
private val pagingSourceFactory: InvalidatingPagingSourceFactory<Int, RecipeSummaryEntity>,
|
||||
private val pagingSourceFactory: RecipePagingSourceFactory,
|
||||
private val invalidatingPagingSourceFactory: InvalidatingPagingSourceFactory<Int, RecipeSummaryEntity>,
|
||||
private val dataSource: RecipeDataSource,
|
||||
private val logger: Logger,
|
||||
) : RecipeRepo {
|
||||
|
||||
override fun createPager(): Pager<Int, RecipeSummaryEntity> {
|
||||
logger.v { "createPager() called" }
|
||||
val pagingConfig = PagingConfig(pageSize = 5, enablePlaceholders = true)
|
||||
@@ -53,4 +55,10 @@ class RecipeRepoImpl @Inject constructor(
|
||||
logger.v { "loadRecipeInfo() returned: $recipeInfo" }
|
||||
return recipeInfo
|
||||
}
|
||||
|
||||
override fun setSearchName(name: String?) {
|
||||
logger.v { "setSearchName() called with: name = $name" }
|
||||
pagingSourceFactory.setQuery(name)
|
||||
invalidatingPagingSourceFactory.invalidate()
|
||||
}
|
||||
}
|
||||
@@ -13,9 +13,7 @@ import gq.kirmanak.mealient.data.network.MealieDataSourceWrapper
|
||||
import gq.kirmanak.mealient.data.recipes.RecipeRepo
|
||||
import gq.kirmanak.mealient.data.recipes.db.RecipeStorage
|
||||
import gq.kirmanak.mealient.data.recipes.db.RecipeStorageImpl
|
||||
import gq.kirmanak.mealient.data.recipes.impl.RecipeImageUrlProvider
|
||||
import gq.kirmanak.mealient.data.recipes.impl.RecipeImageUrlProviderImpl
|
||||
import gq.kirmanak.mealient.data.recipes.impl.RecipeRepoImpl
|
||||
import gq.kirmanak.mealient.data.recipes.impl.*
|
||||
import gq.kirmanak.mealient.data.recipes.network.RecipeDataSource
|
||||
import gq.kirmanak.mealient.database.recipe.entity.RecipeSummaryEntity
|
||||
import gq.kirmanak.mealient.ui.recipes.images.RecipeModelLoaderFactory
|
||||
@@ -46,13 +44,17 @@ interface RecipeModule {
|
||||
@Singleton
|
||||
fun bindModelLoaderFactory(recipeModelLoaderFactory: RecipeModelLoaderFactory): ModelLoaderFactory<RecipeSummaryEntity, InputStream>
|
||||
|
||||
@Binds
|
||||
@Singleton
|
||||
fun bindRecipePagingSourceFactory(recipePagingSourceFactoryImpl: RecipePagingSourceFactoryImpl): RecipePagingSourceFactory
|
||||
|
||||
companion object {
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideRecipePagingSourceFactory(
|
||||
recipeStorage: RecipeStorage
|
||||
) = InvalidatingPagingSourceFactory { recipeStorage.queryRecipes() }
|
||||
factory: RecipePagingSourceFactory,
|
||||
) = InvalidatingPagingSourceFactory(factory)
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
|
||||
@@ -6,6 +6,7 @@ import gq.kirmanak.mealient.R
|
||||
import gq.kirmanak.mealient.data.auth.AuthRepo
|
||||
import gq.kirmanak.mealient.data.baseurl.ServerInfoRepo
|
||||
import gq.kirmanak.mealient.data.disclaimer.DisclaimerStorage
|
||||
import gq.kirmanak.mealient.data.recipes.RecipeRepo
|
||||
import gq.kirmanak.mealient.logging.Logger
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
@@ -18,6 +19,7 @@ class MainActivityViewModel @Inject constructor(
|
||||
private val logger: Logger,
|
||||
private val disclaimerStorage: DisclaimerStorage,
|
||||
private val serverInfoRepo: ServerInfoRepo,
|
||||
private val recipeRepo: RecipeRepo,
|
||||
) : ViewModel() {
|
||||
|
||||
private val _uiState = MutableLiveData(MainActivityUiState())
|
||||
@@ -55,5 +57,6 @@ class MainActivityViewModel @Inject constructor(
|
||||
|
||||
fun onSearchQuery(query: String) {
|
||||
logger.v { "onSearchQuery() called with: query = $query" }
|
||||
recipeRepo.setSearchName(query)
|
||||
}
|
||||
}
|
||||
201
database/schemas/gq.kirmanak.mealient.database.AppDb/7.json
Normal file
201
database/schemas/gq.kirmanak.mealient.database.AppDb/7.json
Normal file
@@ -0,0 +1,201 @@
|
||||
{
|
||||
"formatVersion": 1,
|
||||
"database": {
|
||||
"version": 7,
|
||||
"identityHash": "1def22b22cb1f09a27de1b3188b857d2",
|
||||
"entities": [
|
||||
{
|
||||
"tableName": "recipe_summaries",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`remote_id` TEXT NOT NULL, `name` TEXT NOT NULL, `slug` TEXT NOT NULL, `description` TEXT NOT NULL, `date_added` INTEGER NOT NULL, `date_updated` INTEGER NOT NULL, `image_id` TEXT, PRIMARY KEY(`remote_id`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "remoteId",
|
||||
"columnName": "remote_id",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "name",
|
||||
"columnName": "name",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "slug",
|
||||
"columnName": "slug",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "description",
|
||||
"columnName": "description",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "dateAdded",
|
||||
"columnName": "date_added",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "dateUpdated",
|
||||
"columnName": "date_updated",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "imageId",
|
||||
"columnName": "image_id",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"columnNames": [
|
||||
"remote_id"
|
||||
],
|
||||
"autoGenerate": false
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"tableName": "recipe",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`remote_id` TEXT NOT NULL, `recipe_yield` TEXT NOT NULL, PRIMARY KEY(`remote_id`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "remoteId",
|
||||
"columnName": "remote_id",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "recipeYield",
|
||||
"columnName": "recipe_yield",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"columnNames": [
|
||||
"remote_id"
|
||||
],
|
||||
"autoGenerate": false
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"tableName": "recipe_ingredient",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`local_id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `recipe_id` TEXT NOT NULL, `note` TEXT NOT NULL)",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "localId",
|
||||
"columnName": "local_id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "recipeId",
|
||||
"columnName": "recipe_id",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "note",
|
||||
"columnName": "note",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"columnNames": [
|
||||
"local_id"
|
||||
],
|
||||
"autoGenerate": true
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"tableName": "recipe_instruction",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`local_id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `recipe_id` TEXT NOT NULL, `text` TEXT NOT NULL)",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "localId",
|
||||
"columnName": "local_id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "recipeId",
|
||||
"columnName": "recipe_id",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "text",
|
||||
"columnName": "text",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"columnNames": [
|
||||
"local_id"
|
||||
],
|
||||
"autoGenerate": true
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"ftsVersion": "FTS4",
|
||||
"ftsOptions": {
|
||||
"tokenizer": "simple",
|
||||
"tokenizerArgs": [],
|
||||
"contentTable": "recipe_summaries",
|
||||
"languageIdColumnName": "",
|
||||
"matchInfo": "FTS4",
|
||||
"notIndexedColumns": [],
|
||||
"prefixSizes": [],
|
||||
"preferredOrder": "ASC"
|
||||
},
|
||||
"contentSyncTriggers": [
|
||||
"CREATE TRIGGER IF NOT EXISTS room_fts_content_sync_recipe_summaries_fts_BEFORE_UPDATE BEFORE UPDATE ON `recipe_summaries` BEGIN DELETE FROM `recipe_summaries_fts` WHERE `docid`=OLD.`rowid`; END",
|
||||
"CREATE TRIGGER IF NOT EXISTS room_fts_content_sync_recipe_summaries_fts_BEFORE_DELETE BEFORE DELETE ON `recipe_summaries` BEGIN DELETE FROM `recipe_summaries_fts` WHERE `docid`=OLD.`rowid`; END",
|
||||
"CREATE TRIGGER IF NOT EXISTS room_fts_content_sync_recipe_summaries_fts_AFTER_UPDATE AFTER UPDATE ON `recipe_summaries` BEGIN INSERT INTO `recipe_summaries_fts`(`docid`, `remote_id`, `name`) VALUES (NEW.`rowid`, NEW.`remote_id`, NEW.`name`); END",
|
||||
"CREATE TRIGGER IF NOT EXISTS room_fts_content_sync_recipe_summaries_fts_AFTER_INSERT AFTER INSERT ON `recipe_summaries` BEGIN INSERT INTO `recipe_summaries_fts`(`docid`, `remote_id`, `name`) VALUES (NEW.`rowid`, NEW.`remote_id`, NEW.`name`); END"
|
||||
],
|
||||
"tableName": "recipe_summaries_fts",
|
||||
"createSql": "CREATE VIRTUAL TABLE IF NOT EXISTS `${TABLE_NAME}` USING FTS4(`remote_id` TEXT NOT NULL, `name` TEXT NOT NULL, content=`recipe_summaries`)",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "remoteId",
|
||||
"columnName": "remote_id",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "name",
|
||||
"columnName": "name",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"columnNames": [],
|
||||
"autoGenerate": false
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
}
|
||||
],
|
||||
"views": [],
|
||||
"setupQueries": [
|
||||
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
||||
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '1def22b22cb1f09a27de1b3188b857d2')"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -6,12 +6,13 @@ import gq.kirmanak.mealient.database.recipe.RecipeDao
|
||||
import gq.kirmanak.mealient.database.recipe.entity.*
|
||||
|
||||
@Database(
|
||||
version = 6,
|
||||
version = 7,
|
||||
entities = [
|
||||
RecipeSummaryEntity::class,
|
||||
RecipeEntity::class,
|
||||
RecipeIngredientEntity::class,
|
||||
RecipeInstructionEntity::class
|
||||
RecipeInstructionEntity::class,
|
||||
RecipeSummaryFtsEntity::class,
|
||||
],
|
||||
exportSchema = true,
|
||||
autoMigrations = [
|
||||
@@ -19,6 +20,7 @@ import gq.kirmanak.mealient.database.recipe.entity.*
|
||||
AutoMigration(from = 3, to = 4),
|
||||
AutoMigration(from = 4, to = 5, spec = AppDb.From4To5Migration::class),
|
||||
AutoMigration(from = 5, to = 6, spec = AppDb.From5To6Migration::class),
|
||||
AutoMigration(from = 6, to = 7),
|
||||
]
|
||||
)
|
||||
@TypeConverters(RoomTypeConverters::class)
|
||||
|
||||
@@ -9,6 +9,9 @@ interface RecipeDao {
|
||||
@Query("SELECT * FROM recipe_summaries ORDER BY date_added DESC")
|
||||
fun queryRecipesByPages(): PagingSource<Int, RecipeSummaryEntity>
|
||||
|
||||
@Query("SELECT * FROM recipe_summaries JOIN recipe_summaries_fts ON recipe_summaries_fts.remote_id == recipe_summaries.remote_id WHERE recipe_summaries_fts.name MATCH :query ORDER BY date_added DESC")
|
||||
fun queryRecipesByPages(query: String): PagingSource<Int, RecipeSummaryEntity>
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
suspend fun insertRecipe(recipeSummaryEntity: RecipeSummaryEntity)
|
||||
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
package gq.kirmanak.mealient.database.recipe.entity
|
||||
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Entity
|
||||
import androidx.room.Fts4
|
||||
|
||||
|
||||
@Entity(tableName = "recipe_summaries_fts")
|
||||
@Fts4(contentEntity = RecipeSummaryEntity::class)
|
||||
data class RecipeSummaryFtsEntity(
|
||||
@ColumnInfo(name = "remote_id") val remoteId: String,
|
||||
@ColumnInfo(name = "name") val name: String,
|
||||
)
|
||||
Reference in New Issue
Block a user