Skip to content

Commit 3f0d6f1

Browse files
refactor(FuzzyFinder): Enhance search ranking logic with detailed scoring and deterministic ordering
- Introduced a `RankedItem` data class to encapsulate item scoring and indexing. - Improved filtering logic based on user preferences for search strength and starting matches. - Ensured deterministic ordering of results by incorporating original indices in the ranking process.
1 parent 730a113 commit 3f0d6f1

1 file changed

Lines changed: 53 additions & 14 deletions

File tree

  • app/src/main/java/com/github/codeworkscreativehub/fuzzywuzzy

app/src/main/java/com/github/codeworkscreativehub/fuzzywuzzy/FuzzyFinder.kt

Lines changed: 53 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -157,23 +157,62 @@ object FuzzyFinder {
157157
): MutableList<T> {
158158
if (query.isEmpty()) return itemsList.toMutableList()
159159

160-
return if (prefs.enableFilterStrength) {
161-
// 1. Calculate scores and filter by threshold
162-
itemsList.mapNotNull { item ->
160+
val normalizedQuery = normalizeSearch(query)
161+
162+
// Keep original index as a final tie-breaker to preserve deterministic ordering.
163+
data class RankedItem<T>(
164+
val item: T,
165+
val score: Int,
166+
val startIndex: Int,
167+
val startsWith: Boolean,
168+
val wordStartsWith: Boolean,
169+
val originalIndex: Int
170+
)
171+
172+
val rankedMatches = itemsList.mapIndexedNotNull { index, item ->
173+
val label = labelProvider(item)
174+
val normalizedTarget = normalizeTarget(label)
175+
val normalizedTargetCompact = normalizedTarget.replace(" ", emptyString())
176+
val startIndex = normalizedTargetCompact.indexOf(normalizedQuery)
177+
val startsWith = normalizedTargetCompact.startsWith(normalizedQuery)
178+
val wordStartsWith = normalizedTarget
179+
.split(Regex("\\s+"))
180+
.any { it.startsWith(normalizedQuery) }
181+
182+
if (prefs.enableFilterStrength) {
163183
val score = scoreProvider(item, query)
164-
AppLogger.d(loggerTag, "item: ${labelProvider(item)} | score: $score")
165-
if (score > prefs.filterStrength) item else null
166-
}.toMutableList()
167-
} else {
168-
// 2. Simple Boolean matching
169-
itemsList.filter { item ->
170-
val target = labelProvider(item).lowercase()
171-
if (prefs.searchFromStart) {
172-
target.startsWith(query)
184+
AppLogger.d(loggerTag, "item: $label | score: $score")
185+
val blockedByStartSetting = prefs.searchFromStart && !startsWith
186+
if (score <= prefs.filterStrength || blockedByStartSetting) {
187+
null
173188
} else {
174-
isMatch(target, query)
189+
RankedItem(item, score, startIndex, startsWith, wordStartsWith, index)
175190
}
176-
}.toMutableList()
191+
} else {
192+
val targetLower = label.lowercase()
193+
val matched = if (prefs.searchFromStart) {
194+
targetLower.startsWith(query)
195+
} else {
196+
isMatch(targetLower, query)
197+
}
198+
if (!matched) {
199+
null
200+
} else {
201+
// Keep non-fuzzy mode deterministic but still prioritize stronger matches.
202+
RankedItem(item, 0, startIndex, startsWith, wordStartsWith, index)
203+
}
204+
}
177205
}
206+
207+
return rankedMatches
208+
.sortedWith(
209+
compareByDescending<RankedItem<T>> { it.startsWith }
210+
.thenByDescending { it.wordStartsWith }
211+
.thenByDescending { it.score }
212+
.thenBy { if (it.startIndex >= 0) it.startIndex else Int.MAX_VALUE }
213+
.thenBy { it.originalIndex }
214+
)
215+
.map { it.item }
216+
.toMutableList()
178217
}
179218
}

0 commit comments

Comments
 (0)