diff --git a/app/src/main/java/com/example/research/core/util/NaturalSort.kt b/app/src/main/java/com/example/research/core/util/NaturalSort.kt index 581ed6c..680e226 100644 --- a/app/src/main/java/com/example/research/core/util/NaturalSort.kt +++ b/app/src/main/java/com/example/research/core/util/NaturalSort.kt @@ -118,7 +118,12 @@ private fun compareNatural(a: String, b: String): Int { j = endB } else -> { - if (ca != cb) return ca.compareTo(cb) + if (ca != cb) { + val numeralA = !asciiA && isNumeralChar(ca) + val numeralB = !asciiB && isNumeralChar(cb) + if (numeralA != numeralB) return if (numeralA) -1 else 1 + return ca.compareTo(cb) + } i++ j++ } diff --git a/app/src/main/java/com/example/research/data/paging/IndexEntryPagingSource.kt b/app/src/main/java/com/example/research/data/paging/IndexEntryPagingSource.kt index ec29031..6e6efe6 100644 --- a/app/src/main/java/com/example/research/data/paging/IndexEntryPagingSource.kt +++ b/app/src/main/java/com/example/research/data/paging/IndexEntryPagingSource.kt @@ -5,10 +5,9 @@ import androidx.paging.PagingState import com.example.research.core.domain.model.Dictionary import com.example.research.core.domain.model.IndexEntry import com.example.research.core.performance.ReSearchTrace -import com.example.research.core.util.sanitizeQuery import com.example.research.data.parser.IndexSearcher -import com.example.research.data.parser.SearchRankingContext -import com.example.research.data.parser.rankBySearchRelevance +import com.example.research.data.parser.sortByCharCountThenNatural + import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll import kotlinx.coroutines.coroutineScope @@ -68,9 +67,8 @@ class IndexEntryPagingSource( } } - val normalizedQuery = query.sanitizeQuery().lowercase().trim() val ranked = rawResults - .rankBySearchRelevance(SearchRankingContext(normalizedQuery)) + .sortByCharCountThenNatural() .distinctBy { Triple(it.dictionaryPath, it.word, it.offset.value) } LoadResult.Page( diff --git a/app/src/main/java/com/example/research/data/parser/IndexSearcher.kt b/app/src/main/java/com/example/research/data/parser/IndexSearcher.kt index 6b5efd9..0cf6817 100644 --- a/app/src/main/java/com/example/research/data/parser/IndexSearcher.kt +++ b/app/src/main/java/com/example/research/data/parser/IndexSearcher.kt @@ -122,9 +122,7 @@ class IndexSearcher(private val context: Context) { } } } - (prefixMatches + substringMatches) - .rankBySearchRelevance(SearchRankingContext(normalizedQuery, foldedQuery)) - .distinctBy { it.articleKey() } + (prefixMatches + substringMatches).distinctBy { it.articleKey() } } resultsCache.put(cacheKey, results) results diff --git a/app/src/main/java/com/example/research/data/parser/SearchRanking.kt b/app/src/main/java/com/example/research/data/parser/SearchRanking.kt index e3887ef..70ae872 100644 --- a/app/src/main/java/com/example/research/data/parser/SearchRanking.kt +++ b/app/src/main/java/com/example/research/data/parser/SearchRanking.kt @@ -2,50 +2,37 @@ package com.example.research.data.parser import com.example.research.core.domain.model.IndexEntry import com.example.research.core.util.NaturalStringComparator -import com.example.research.core.util.foldLatinDiacritics -internal data class SearchRankingContext( - val normalizedQuery: String, - val foldedQuery: String = normalizedQuery.foldLatinDiacritics(), -) - -private data class SortKey( - val matchTier: Int, - val exactMatch: Int, - val charCount: Int, - val word: String, - val entry: IndexEntry, -) - -internal fun List.rankBySearchRelevance( - ranking: SearchRankingContext -): List { +internal fun List.sortByCharCountThenNatural(): List { if (size < 2) return this - return map { entry -> - val word = entry.word + val charCounts = IntArray(size) + var minCount = Int.MAX_VALUE + var maxCount = Int.MIN_VALUE + for (i in indices) { + val entry = this[i] val displayKey = entry.originalWord .trim() .ifEmpty { entry.word.trim() } - - SortKey( - matchTier = when { - !entry.isAlias && word.startsWith(ranking.normalizedQuery) -> 0 - entry.isAlias && word.startsWith(ranking.foldedQuery) -> 1 - !entry.isAlias && word.foldLatinDiacritics().startsWith(ranking.foldedQuery) -> 1 - else -> 2 - }, - exactMatch = if (word == ranking.normalizedQuery) 0 else 1, - charCount = displayKey.codePointCount(0, displayKey.length), - word = word, - entry = entry, - ) + val count = displayKey.codePointCount(0, displayKey.length) + charCounts[i] = count + if (count < minCount) minCount = count + if (count > maxCount) maxCount = count } - .sortedWith( - compareBy { it.matchTier } - .thenBy { it.exactMatch } - .thenBy { it.charCount } - .thenBy(NaturalStringComparator) { it.word } - ) - .map { it.entry } + + val buckets = arrayOfNulls>(maxCount - minCount + 1) + for (i in indices) { + val bucketIndex = charCounts[i] - minCount + (buckets[bucketIndex] ?: ArrayList().also { buckets[bucketIndex] = it }) + .add(this[i]) + } + + val naturalOrder = compareBy(NaturalStringComparator) { it.word } + val result = ArrayList(size) + for (bucket in buckets) { + if (bucket == null) continue + if (bucket.size > 1) bucket.sortWith(naturalOrder) + result.addAll(bucket) + } + return result } diff --git a/app/src/main/java/com/example/research/feature/search/SearchViewModel.kt b/app/src/main/java/com/example/research/feature/search/SearchViewModel.kt index 0621eca..a02cc58 100644 --- a/app/src/main/java/com/example/research/feature/search/SearchViewModel.kt +++ b/app/src/main/java/com/example/research/feature/search/SearchViewModel.kt @@ -112,9 +112,7 @@ class SearchViewModel( Pager( config = PagingConfig( pageSize = 12, - prefetchDistance = 1, - enablePlaceholders = false, - initialLoadSize = 12 + enablePlaceholders = false ), pagingSourceFactory = { IndexEntryPagingSource(