Optimize global search
This commit is contained in:
@@ -54,6 +54,7 @@ tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all {
|
||||
jvmTarget = JavaVersion.VERSION_1_8.toString()
|
||||
freeCompilerArgs += [
|
||||
'-Xopt-in=kotlinx.coroutines.ExperimentalCoroutinesApi',
|
||||
'-Xopt-in=kotlinx.coroutines.FlowPreview',
|
||||
'-Xopt-in=org.koin.core.component.KoinApiExtension'
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,40 +1,46 @@
|
||||
package org.koitharu.kotatsu.search.domain
|
||||
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import android.annotation.SuppressLint
|
||||
import kotlinx.coroutines.flow.*
|
||||
import org.koitharu.kotatsu.base.domain.MangaProviderFactory
|
||||
import org.koitharu.kotatsu.core.model.Manga
|
||||
import org.koitharu.kotatsu.core.model.MangaSource
|
||||
import org.koitharu.kotatsu.core.model.SortOrder
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import java.util.*
|
||||
import org.koitharu.kotatsu.utils.ext.levenshteinDistance
|
||||
|
||||
class MangaSearchRepository(private val settings: AppSettings) {
|
||||
|
||||
fun globalSearch(query: String, batchSize: Int = 4): Flow<List<Manga>> = flow {
|
||||
val sources = MangaProviderFactory.getSources(settings, includeHidden = false)
|
||||
val lists = EnumMap<MangaSource, List<Manga>>(MangaSource::class.java)
|
||||
var i = 0
|
||||
while (true) {
|
||||
var isEmitted = false
|
||||
for (source in sources) {
|
||||
val list = lists.getOrPut(source) {
|
||||
try {
|
||||
source.repository.getList(0, query, SortOrder.POPULARITY)
|
||||
} catch (e: Throwable) {
|
||||
e.printStackTrace()
|
||||
emptyList<Manga>()
|
||||
fun globalSearch(query: String, concurrency: Int = DEFAULT_CONCURRENCY): Flow<Manga> =
|
||||
MangaProviderFactory.getSources(settings, includeHidden = false).asFlow()
|
||||
.flatMapMerge(concurrency) { source ->
|
||||
runCatching {
|
||||
source.repository.getList(0, query, SortOrder.POPULARITY)
|
||||
}.getOrElse {
|
||||
emptyList()
|
||||
}.asFlow()
|
||||
}.filter {
|
||||
match(it, query)
|
||||
}
|
||||
|
||||
private companion object {
|
||||
|
||||
private val REGEX_SPACE = Regex("\\s+")
|
||||
|
||||
@SuppressLint("DefaultLocale")
|
||||
fun match(manga: Manga, query: String): Boolean {
|
||||
val words = HashSet<String>()
|
||||
words += manga.title.toLowerCase().split(REGEX_SPACE)
|
||||
words += manga.altTitle?.toLowerCase()?.split(REGEX_SPACE).orEmpty()
|
||||
val words2 = query.toLowerCase().split(REGEX_SPACE).toSet()
|
||||
for (w in words) {
|
||||
for (w2 in words2) {
|
||||
val diff = w.levenshteinDistance(w2) / ((w.length + w2.length) / 2f)
|
||||
if (diff < 0.5) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
if (i < list.size) {
|
||||
emit(list.subList(i, (i + batchSize).coerceAtMost(list.lastIndex)))
|
||||
isEmitted = true
|
||||
}
|
||||
}
|
||||
i += batchSize
|
||||
if (!isEmitted) {
|
||||
return@flow
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -62,8 +62,8 @@ class GlobalSearchViewModel(
|
||||
.catch { e ->
|
||||
listError.value = e
|
||||
isLoading.postValue(false)
|
||||
}.filterNot { x -> x.isEmpty() }
|
||||
.onStart {
|
||||
}.onStart {
|
||||
mangaList.value = null
|
||||
listError.value = null
|
||||
isLoading.postValue(true)
|
||||
hasNextPage.value = true
|
||||
@@ -75,7 +75,7 @@ class GlobalSearchViewModel(
|
||||
}.onFirst {
|
||||
isLoading.postValue(false)
|
||||
}.onEach {
|
||||
mangaList.value = mangaList.value?.plus(it) ?: it
|
||||
mangaList.value = mangaList.value?.plus(it) ?: listOf(it)
|
||||
}.launchIn(viewModelScope + Dispatchers.Default)
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import java.math.BigInteger
|
||||
import java.net.URLEncoder
|
||||
import java.security.MessageDigest
|
||||
import java.util.*
|
||||
import kotlin.math.min
|
||||
|
||||
fun String.longHashCode(): Long {
|
||||
var h = 1125899906842597L
|
||||
@@ -65,7 +66,7 @@ fun String.toUriOrNull(): Uri? = if (isEmpty()) {
|
||||
Uri.parse(this)
|
||||
}
|
||||
|
||||
fun ByteArray.byte2HexFormatted(): String? {
|
||||
fun ByteArray.byte2HexFormatted(): String {
|
||||
val str = StringBuilder(size * 2)
|
||||
for (i in indices) {
|
||||
var h = Integer.toHexString(this[i].toInt())
|
||||
@@ -104,4 +105,42 @@ fun String.substringBetween(from: String, to: String, fallbackValue: String): St
|
||||
}
|
||||
}
|
||||
|
||||
fun String.find(regex: Regex) = regex.find(this)?.value
|
||||
fun String.find(regex: Regex) = regex.find(this)?.value
|
||||
|
||||
fun String.levenshteinDistance(other: String): Int {
|
||||
if (this == other) {
|
||||
return 0
|
||||
}
|
||||
if (this.isEmpty()) {
|
||||
return other.length
|
||||
}
|
||||
if (other.isEmpty()) {
|
||||
return this.length
|
||||
}
|
||||
|
||||
val lhsLength = this.length + 1
|
||||
val rhsLength = other.length + 1
|
||||
|
||||
var cost = Array(lhsLength) { it }
|
||||
var newCost = Array(lhsLength) { 0 }
|
||||
|
||||
for (i in 1 until rhsLength) {
|
||||
newCost[0] = i
|
||||
|
||||
for (j in 1 until lhsLength) {
|
||||
val match = if (this[j - 1] == other[i - 1]) 0 else 1
|
||||
|
||||
val costReplace = cost[j - 1] + match
|
||||
val costInsert = cost[j] + 1
|
||||
val costDelete = newCost[j - 1] + 1
|
||||
|
||||
newCost[j] = min(min(costInsert, costDelete), costReplace)
|
||||
}
|
||||
|
||||
val swap = cost
|
||||
cost = newCost
|
||||
newCost = swap
|
||||
}
|
||||
|
||||
return cost[lhsLength - 1]
|
||||
}
|
||||
Reference in New Issue
Block a user