The linked SQLite documentation in the question clarifies what the rank
function does in the comments above the query it is used in:
If the application supplies an SQLite user function called "rank" that interprets the blob of data returned by matchinfo and returns a numeric relevancy based on it, then the following SQL may be used to return the titles of the 10 most relevant documents in the dataset for a users query.
rank
is expected to be a user supplied function. It does not ship with SQLite.
Here's an implementation of the rank
function in Kotlin which calculates the relevance score on the basis of the data supplied by matchinfo
using the default "pcx" argument:
fun rank(matchInfo: IntArray): Double {
val numPhrases = matchInfo[0]
val numColumns = matchInfo[1]
var score = 0.0
for (phrase in 0 until numPhrases) {
val offset = 2 + phrase * numColumns * 3
for (column in 0 until numColumns) {
val numHitsInRow = matchInfo[offset + 3 * column]
val numHitsInAllRows = matchInfo[offset + 3 * column + 1]
if (numHitsInAllRows > 0) {
score += numHitsInRow.toDouble() / numHitsInAllRows.toDouble()
}
}
}
return score
}
To understand how this code works, you should go through the rankfunc
example given in the official docs.
Since our rank function is a Kotlin function, it can not be used directly by SQLite. Instead, we will need to first retrieve the matchinfo
blob from the database, and then pass it to our rank function.
Here's an example on how to do it with Room:
@Dao
interface PersonsDao {
@Query("""
SELECT *, matchinfo(persons_fts, 'pcx') as mi
FROM persons
JOIN persons_fts ON persons.name = persons_fts.name
WHERE persons_fts MATCH :query
""")
suspend fun search(query: String): List<PersonWithMatchInfo>
}
data class PersonWithMatchInfo(
@Embedded
val person: Person
@ColumnInfo(name = "mi")
val matchInfo: ByteArray
)
The retrieved ByteArray
contains a sequence of numbers representing the match info, in which each number is represented by 4 bytes. The first byte is the actual value, and the next three bytes are zero. Therefore we need to remove the redundant zeroes before passing this ByteArray to rank
. This can be done with a simple method:
fun ByteArray.skip(skipSize: Int): IntArray {
val cleanedArr = IntArray(this.size / skipSize)
var pointer = 0
for (i in this.indices step skipSize) {
cleanedArr[pointer] = this[i].toInt()
pointer++
}
return cleanedArr
}
This setup can be used like this:
suspend fun searchWithRanks(query: String): List<Person> {
return personDao.search(query)
.sortedByDescending { result -> rank(result.matchInfo.skip(4)) }
.map { result -> result.person }
}