1

我有两张桌子:

  • persons
  • persons_fts.

以下是表的定义:

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,并根据相关性对结果进行排名。在查看了有关如何执行此操作的官方文档后,我以以下查询结束:

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

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

从表中检索数据时出错:函数 rank() 的参数数量错误(代码 1 SQLITE_ERROR)

我究竟做错了什么?

请注意,我不能选择使用 FTS5。

4

1 回答 1

2

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

如果应用程序提供了一个名为“rank”的 SQLite 用户函数,它解释 matchinfo 返回的数据块并基于它返回一个数字相关性,那么可以使用以下 SQL 来返回数据集中 10 个最相关文档的标题用于用户查询。

rank预计是用户提供的功能。它不随 SQLite 一起提供。

这是 Kotlin 中函数的实现,它根据使用默认“pcx”参数rank提供的数据计算相关性分数: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示例。

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

这是一个关于如何使用 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 }
}
于 2020-08-31T04:57:50.920 回答