Compare commits
5
Commits
9c76ebe3ff
...
3bb4e09b04
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3bb4e09b04 | ||
|
|
ff4ba62bbd | ||
|
|
123615e1f8 | ||
|
|
6889dd2c45 | ||
|
|
f22028d126 |
No files matched your search
@@ -90,10 +90,10 @@ The main search screen remains usable as long as at least one indexed dictionary
|
||||
- Material 3 1.5.0-beta01
|
||||
- Coroutines and Flow 1.11.0
|
||||
- DataStore Preferences 1.2.1
|
||||
- Paging 3.5.2
|
||||
- Paging 3.5.3
|
||||
- OkHttp 5.5.0
|
||||
- kotlinx.serialization 1.11.0
|
||||
- AboutLibraries 15.2.0 metadata generation
|
||||
- AboutLibraries 15.3.0 metadata generation
|
||||
- Android Gradle Plugin 9.4.1
|
||||
|
||||
## Build
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<IndexEntry>.rankBySearchRelevance(
|
||||
ranking: SearchRankingContext
|
||||
): List<IndexEntry> {
|
||||
internal fun List<IndexEntry>.sortByCharCountThenNatural(): List<IndexEntry> {
|
||||
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() }
|
||||
val count = displayKey.codePointCount(0, displayKey.length)
|
||||
charCounts[i] = count
|
||||
if (count < minCount) minCount = count
|
||||
if (count > maxCount) maxCount = count
|
||||
}
|
||||
|
||||
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 buckets = arrayOfNulls<ArrayList<IndexEntry>>(maxCount - minCount + 1)
|
||||
for (i in indices) {
|
||||
val bucketIndex = charCounts[i] - minCount
|
||||
(buckets[bucketIndex] ?: ArrayList<IndexEntry>().also { buckets[bucketIndex] = it })
|
||||
.add(this[i])
|
||||
}
|
||||
.sortedWith(
|
||||
compareBy<SortKey> { it.matchTier }
|
||||
.thenBy { it.exactMatch }
|
||||
.thenBy { it.charCount }
|
||||
.thenBy(NaturalStringComparator) { it.word }
|
||||
)
|
||||
.map { it.entry }
|
||||
|
||||
val naturalOrder = compareBy<IndexEntry, String>(NaturalStringComparator) { it.word }
|
||||
val result = ArrayList<IndexEntry>(size)
|
||||
for (bucket in buckets) {
|
||||
if (bucket == null) continue
|
||||
if (bucket.size > 1) bucket.sortWith(naturalOrder)
|
||||
result.addAll(bucket)
|
||||
}
|
||||
return result
|
||||
}
|
||||
@@ -112,9 +112,7 @@ class SearchViewModel(
|
||||
Pager(
|
||||
config = PagingConfig(
|
||||
pageSize = 12,
|
||||
prefetchDistance = 1,
|
||||
enablePlaceholders = false,
|
||||
initialLoadSize = 12
|
||||
enablePlaceholders = false
|
||||
),
|
||||
pagingSourceFactory = {
|
||||
IndexEntryPagingSource(
|
||||
|
||||
@@ -216,14 +216,21 @@ class SettingsViewModel(
|
||||
}
|
||||
}
|
||||
is DownloadState.Error -> {
|
||||
val startedHere = statusBeforeDownload != DictionaryStatus.Unknown ||
|
||||
isDownloadInProgress
|
||||
isDownloadInProgress = false
|
||||
if (!errorStateHandled) {
|
||||
errorStateHandled = true
|
||||
dictionaryStatus.value = if (dictionaryState.dictionaries.isEmpty()) {
|
||||
DictionaryStatus.Empty
|
||||
} else when (statusBeforeDownload) {
|
||||
DictionaryStatus.Unknown, DictionaryStatus.Checking -> DictionaryStatus.UpToDate
|
||||
else -> statusBeforeDownload
|
||||
if (dictionaryState.dictionaries.isEmpty()) {
|
||||
dictionaryStatus.value = DictionaryStatus.Empty
|
||||
} else if (startedHere) {
|
||||
when (statusBeforeDownload) {
|
||||
DictionaryStatus.Unknown, DictionaryStatus.Checking ->
|
||||
viewModelScope.launch {
|
||||
restoreOrEvaluateDictionaryStatus()
|
||||
}
|
||||
else -> dictionaryStatus.value = statusBeforeDownload
|
||||
}
|
||||
}
|
||||
if (pendingSourceUrls.isNotEmpty()) {
|
||||
pendingSourceUrls.forEach { urlTemplate ->
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
[versions]
|
||||
aboutlibraries = "15.2.0"
|
||||
aboutlibraries = "15.3.0"
|
||||
activity_compose = "1.13.0"
|
||||
agp = "9.4.1"
|
||||
compose_bom = "2026.09.00"
|
||||
@@ -10,7 +10,7 @@ kotlin = "2.4.21"
|
||||
lifecycle_runtime_ktx = "2.11.0"
|
||||
material3 = "1.5.0-beta01"
|
||||
okhttp = "5.5.0"
|
||||
paging = "3.5.2"
|
||||
paging = "3.5.3"
|
||||
profileinstaller = "1.4.1"
|
||||
kotlinx_coroutines = "1.11.0"
|
||||
kotlinx_serialization = "1.11.0"
|
||||
|
||||
Reference in new issue
Block a user