Improve app result ranking
This commit is contained in:
@@ -52,6 +52,8 @@ dependencies {
|
||||
runtimeOnly(libs.androidx.compose.runtime)
|
||||
implementation(libs.androidx.compose.materialicons)
|
||||
|
||||
implementation(libs.stringsimilarity)
|
||||
|
||||
implementation(project(":core:ktx"))
|
||||
implementation(project(":core:i18n"))
|
||||
implementation(project(":libs:material-color-utilities"))
|
||||
|
||||
@@ -220,4 +220,12 @@ val OpenSourceLicenses = arrayOf(
|
||||
url = "https://github.com/woheller69/AndroidAddressFormatter",
|
||||
copyrightNote = "Copyright (c) 2022 woheller69",
|
||||
),
|
||||
OpenSourceLibrary(
|
||||
name = "String Similarity for Kotlin",
|
||||
description = "A library that implements various measures of string similarity and distance.",
|
||||
licenseName = R.string.mit_license_name,
|
||||
licenseText = R.raw.license_mit,
|
||||
url = "https://github.com/aallam/string-similarity-kotlin",
|
||||
copyrightNote = "Copyright (c) 2023 Mouaad Aallam",
|
||||
),
|
||||
)
|
||||
@@ -0,0 +1,94 @@
|
||||
package de.mm20.launcher2.search
|
||||
|
||||
import com.aallam.similarity.JaroWinkler
|
||||
import de.mm20.launcher2.ktx.normalize
|
||||
|
||||
@JvmInline
|
||||
value class ResultScore private constructor(private val packed: Long) : Comparable<ResultScore> {
|
||||
constructor(
|
||||
isPrefix: Boolean,
|
||||
isSubstring: Boolean,
|
||||
isPrimary: Boolean,
|
||||
similarity: Float,
|
||||
) : this(
|
||||
(similarity.toRawBits().toLong()) or
|
||||
(if (isPrefix) (1L shl 32) else 0) or
|
||||
(if (isSubstring) (1L shl 33) else 0) or
|
||||
(if (isPrimary) (1L shl 34) else 0)
|
||||
)
|
||||
|
||||
/**
|
||||
* Whether the query is a literal prefix of the result.
|
||||
*/
|
||||
val isPrefix: Boolean
|
||||
get() = (packed and (1L shl 32)) != 0L
|
||||
|
||||
/**
|
||||
* Whether the query is a substring of the result.
|
||||
*/
|
||||
val isSubstring: Boolean
|
||||
get() = (packed and (1L shl 33)) != 0L
|
||||
|
||||
/**
|
||||
* Whether the query was matched against a primary field.
|
||||
*/
|
||||
val isPrimary: Boolean
|
||||
get() = (packed and (1L shl 34)) != 0L
|
||||
|
||||
/**
|
||||
* The Jaro-Winkler similarity between the query and the result.
|
||||
*/
|
||||
val similarity: Float
|
||||
get() = Float.fromBits((packed and 0xffffffffL).toInt())
|
||||
|
||||
/**
|
||||
* A total score for the result, combining the similarity with additional factors.
|
||||
*/
|
||||
val score: Float
|
||||
get() = (similarity + (if (isPrefix) 0.8f else 0f) + (if (isSubstring) 0.8f else 0f)) * (if (isPrimary) 1f else 0.5f)
|
||||
|
||||
override fun compareTo(other: ResultScore): Int {
|
||||
return score.compareTo(other.score)
|
||||
}
|
||||
|
||||
companion object {
|
||||
operator fun invoke(
|
||||
query: String,
|
||||
primaryFields: Iterable<String> = emptyList(),
|
||||
secondaryFields: Iterable<String> = emptyList(),
|
||||
): ResultScore {
|
||||
val normalizedQuery = query.normalize()
|
||||
val jaroWinkler = JaroWinkler()
|
||||
val bestPrimaryScore = primaryFields.maxOfOrNull {
|
||||
val normalizedTerm = it.normalize()
|
||||
val sim = jaroWinkler.similarity(normalizedQuery, normalizedTerm).toFloat()
|
||||
ResultScore(
|
||||
isPrefix = normalizedTerm.startsWith(normalizedQuery),
|
||||
isSubstring = normalizedQuery in normalizedTerm,
|
||||
isPrimary = true,
|
||||
similarity = sim
|
||||
)
|
||||
} ?: Zero
|
||||
val bestSecondaryScore = secondaryFields.maxOfOrNull {
|
||||
val normalizedTerm = it.normalize()
|
||||
val sim = jaroWinkler.similarity(normalizedQuery, normalizedTerm).toFloat()
|
||||
ResultScore(
|
||||
isPrefix = normalizedTerm.startsWith(normalizedQuery),
|
||||
isSubstring = normalizedQuery in normalizedTerm,
|
||||
isPrimary = false,
|
||||
similarity = sim
|
||||
)
|
||||
} ?: Zero
|
||||
|
||||
return maxOf(bestPrimaryScore, bestSecondaryScore)
|
||||
}
|
||||
|
||||
val Zero = ResultScore(
|
||||
isPrefix = false,
|
||||
isSubstring = false,
|
||||
isPrimary = false,
|
||||
similarity = 0f
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -2,4 +2,7 @@ package de.mm20.launcher2.search
|
||||
|
||||
import kotlinx.coroutines.Deferred
|
||||
|
||||
interface Searchable
|
||||
interface Searchable {
|
||||
val score: ResultScore
|
||||
get() = ResultScore.Zero
|
||||
}
|
||||
Reference in New Issue
Block a user