Optimize global search

pull/26/head
Koitharu 5 years ago
parent d9d0656ef4
commit 64752da948

@ -54,6 +54,7 @@ tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all {
jvmTarget = JavaVersion.VERSION_1_8.toString() jvmTarget = JavaVersion.VERSION_1_8.toString()
freeCompilerArgs += [ freeCompilerArgs += [
'-Xopt-in=kotlinx.coroutines.ExperimentalCoroutinesApi', '-Xopt-in=kotlinx.coroutines.ExperimentalCoroutinesApi',
'-Xopt-in=kotlinx.coroutines.FlowPreview',
'-Xopt-in=org.koin.core.component.KoinApiExtension' '-Xopt-in=org.koin.core.component.KoinApiExtension'
] ]
} }

@ -1,40 +1,46 @@
package org.koitharu.kotatsu.search.domain package org.koitharu.kotatsu.search.domain
import kotlinx.coroutines.flow.Flow import android.annotation.SuppressLint
import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.*
import org.koitharu.kotatsu.base.domain.MangaProviderFactory import org.koitharu.kotatsu.base.domain.MangaProviderFactory
import org.koitharu.kotatsu.core.model.Manga 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.model.SortOrder
import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.prefs.AppSettings
import java.util.* import org.koitharu.kotatsu.utils.ext.levenshteinDistance
class MangaSearchRepository(private val settings: AppSettings) { class MangaSearchRepository(private val settings: AppSettings) {
fun globalSearch(query: String, batchSize: Int = 4): Flow<List<Manga>> = flow { fun globalSearch(query: String, concurrency: Int = DEFAULT_CONCURRENCY): Flow<Manga> =
val sources = MangaProviderFactory.getSources(settings, includeHidden = false) MangaProviderFactory.getSources(settings, includeHidden = false).asFlow()
val lists = EnumMap<MangaSource, List<Manga>>(MangaSource::class.java) .flatMapMerge(concurrency) { source ->
var i = 0 runCatching {
while (true) { source.repository.getList(0, query, SortOrder.POPULARITY)
var isEmitted = false }.getOrElse {
for (source in sources) { emptyList()
val list = lists.getOrPut(source) { }.asFlow()
try { }.filter {
source.repository.getList(0, query, SortOrder.POPULARITY) match(it, query)
} catch (e: Throwable) { }
e.printStackTrace()
emptyList<Manga>() 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 -> .catch { e ->
listError.value = e listError.value = e
isLoading.postValue(false) isLoading.postValue(false)
}.filterNot { x -> x.isEmpty() } }.onStart {
.onStart { mangaList.value = null
listError.value = null listError.value = null
isLoading.postValue(true) isLoading.postValue(true)
hasNextPage.value = true hasNextPage.value = true
@ -75,7 +75,7 @@ class GlobalSearchViewModel(
}.onFirst { }.onFirst {
isLoading.postValue(false) isLoading.postValue(false)
}.onEach { }.onEach {
mangaList.value = mangaList.value?.plus(it) ?: it mangaList.value = mangaList.value?.plus(it) ?: listOf(it)
}.launchIn(viewModelScope + Dispatchers.Default) }.launchIn(viewModelScope + Dispatchers.Default)
} }
} }

@ -6,6 +6,7 @@ import java.math.BigInteger
import java.net.URLEncoder import java.net.URLEncoder
import java.security.MessageDigest import java.security.MessageDigest
import java.util.* import java.util.*
import kotlin.math.min
fun String.longHashCode(): Long { fun String.longHashCode(): Long {
var h = 1125899906842597L var h = 1125899906842597L
@ -65,7 +66,7 @@ fun String.toUriOrNull(): Uri? = if (isEmpty()) {
Uri.parse(this) Uri.parse(this)
} }
fun ByteArray.byte2HexFormatted(): String? { fun ByteArray.byte2HexFormatted(): String {
val str = StringBuilder(size * 2) val str = StringBuilder(size * 2)
for (i in indices) { for (i in indices) {
var h = Integer.toHexString(this[i].toInt()) var h = Integer.toHexString(this[i].toInt())
@ -105,3 +106,41 @@ 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]
}
Loading…
Cancel
Save