如何使用 FTS4 修复 SQLite Android 上的错误 "Wrong number of arguments to function rank()"?

How to fix the error "Wrong number of arguments to function rank()" on SQLite Android using FTS4?

我有两个 table:

下面是 table 的定义:

CREATE TABLE persons(name TEXT PRIMARY KEY NOT NULL, details TEXT);

CREATE VIRTUAL TABLE persons_fts USING FTS4(name TEXT NOT NULL, details TEXT, context=persons);

我想使用 persons_fts table 上的查询进行全文搜索,并根据相关性对结果进行排名。在查看了关于如何执行此操作的 official docs 之后,我以以下查询结束:

SELECT *
FROM persons
JOIN persons_fts ON persons.name = persons_fts.name
WHERE persons_fts MATCH :query
ORDER BY rank(matchinfo(persons_fts)) DESC;

除了额外的连接外,此查询与官方文档中概述的查询完全相同。但是,当我尝试执行它时出现错误:

Error retrieving data from the table: Wrong number of arguments to function rank() (code 1 SQLITE_ERROR)

我做错了什么?

请注意,我不适合使用 FTS5。

问题中链接的 SQLite 文档阐明了 rank 函数在查询上方的注释中的作用:

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 应该是用户提供的函数。它不随 SQLite 一起提供。

这是 Kotlin 中 rank 函数的一个实现,它使用默认的“pcx”参数根据 matchinfo 提供的数据计算相关性分数:

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
}

要了解此代码的工作原理,您应该阅读官方文档中给出的rankfunc example

由于我们的rank函数是Kotlin函数,SQLite不能直接使用。相反,我们需要先从数据库中检索 matchinfo blob,然后将其传递给我们的排名函数。

以下是如何使用 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
)

检索到的ByteArray包含一个表示匹配信息的数字序列,其中每个数字由4个字节表示。第一个字节是实际值,接下来的三个字节是零。因此,我们需要在将此 ByteArray 传递给 rank 之前删除多余的零。这可以通过一个简单的方法来完成:

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
}

这个设置可以这样使用:

suspend fun searchWithRanks(query: String): List<Person> {
  return personDao.search(query)
        .sortedByDescending { result -> rank(result.matchInfo.skip(4)) }
        .map { result -> result.person }
}