Optimize global search

This commit is contained in:
Koitharu
2021-04-02 14:47:07 +03:00
parent d9d0656ef4
commit 64752da948
4 changed files with 76 additions and 30 deletions

View File

@@ -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'
]
}

View File

@@ -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
}
}
}

View File

@@ -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)
}
}

View File

@@ -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]
}