[官文翻译]Futter超快数据库Isar - 技巧 - 全文搜索

449 阅读4分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第20天,点击查看活动详情


Isar:用于 Futter 可跨平台的超快数据库

官方文档:Home | Isar Database

pub:isar | Dart Package (flutter-io.cn)

本文翻译自:Full-text search | Isar Database

译时版本:3.0.2


全文搜索

全文搜索是在数据库中搜索文本的强大方式。 你应该已经习惯了 索引 是如何工作的,但现在我们来复习下基础知识。

索引的工作像是一个查找表,它允许查询引擎快速找到给定值的记录。 例如,如果你的对象中有一个 title 字段, 你可以在这个字段上创建索引,用给定的 title 查找对象时可以更快。

为什么全文搜索有用?

可以方便地使用过滤器检索文本。 有各种字符串的操作,如 .startsWith()、 .contains() 和 .matches() 。 过滤器的问题是它们的运行时间是 O(n) , n 是集合中的记录数。 字符串操作如 .matches() 的成本尤其高。

全文检索比过滤器快很多,但是索引有一些局限性。 在该篇方法中,我们会看一下围绕这些局限如何工作。

基础示例

想法总是相同的:代替索引整个文本,我们对文本中的单词进行索引,这样就可以分别查找它们了。

我们创建一下最基础的全文搜索索引:

class Message {
  Id? id;

  late String content;

  @Index()
  List<String> get contentWords => content.split(' ');
}

现在我们可以查找带有 content 中特定单词的消息:

final posts = await isar.messages
  .where()
  .contentWordsAnyEqualTo('hello')
  .findAll();

该查询超级快,但是有一些问题:

  1. 只能查找整个单词
  2. 不考虑标点
  3. 不支持其它空白字符

以正确的方式分割文本

让我们尝试改善下前面的示例。 我们能尝试引入复杂的正则表达式来确定单词分割,但是它会比较慢,边界情况也有错误。

Unicode Annex #29 定义了如何为大多数语言正确地将文本分割为单词。这相当复杂但是幸运的是,Isar 为我们做了繁重的工作:

Isar.splitWords('hello world'); // -> ['hello', 'world']

Isar.splitWords('The quick (“brown”) fox can’t jump 32.3 feet, right?');
// -> ['The', 'quick', 'brown', 'fox', 'can’t', 'jump', '32.3', 'feet', 'right']

我想要更多地控制

十分简单! 我们可以改变索引同时支持前缀匹配和大小写不敏感的匹配:

class Post {
    int? id;

    late String title;

    @Index(type: IndexType.value, caseSensitive: false)
    List<String> get titleWords => title.split(' ');
}

默认情况下,Isar 会将单词存储为哈希值,因为这样能更快和更高的空间效率。 但是哈希值不能用于前缀匹配。 可以使用 IndexType.value 改变索引为直接使用单词来代替。 它提供了 .titleWordsAnyStartsWith() where 子句:

final posts = await isar.posts
  .where()
  .titleWordsAnyStartsWith('hel')
  .or()
  .titleWordsAnyStartsWith('welco')
  .or()
  .titleWordsAnyStartsWith('howd')
  .findAll();

我也需要 .endsWith()

必须滴! 我们使用一个技巧来实现 .endsWith() 匹配:

class Post {
    int? id;

    late String title;

    @Index(type: IndexType.value, caseSensitive: false)
    List<String> get revTitleWords {
        return Isar.splitWords(title).map((word) => word.reversed).toList();
    }
}

不要忘记反转查找目标(的字符串):

final posts = await isar.posts
  .where()
  .revTitleWordsAnyStartsWith('lcome'.reversed)
  .findAll();

词干算法

不幸的是,索引不支持包含匹配(其它数据库也一样)。 但是有一些替代方案值得一看。 这高度依赖于你选择择的使用场景。 例如,索引单词词干代替整个单词。

词干算法是语言规范化的处理过程,经过这种处理,一个单词的多种形式可以减少为一个常用形式,例如:

connection
connections
connective          --->   connect
connected
connecting

受欢迎的示例是 波特词干分析算法 和 雪球词干算法

也有一些高级形式如 词形还原

拼音算法

拼音算法  是将单词的发音作为索引的算法。 换句话说,它允许你搜索和查找目标相似发音的单词。

大多数的拼音算法只支持单个语言。

探测法

探测法是使用英语中的发音声音索引名字的拼音算法。 目标是将同音异义词编码为相同的表现,这样即使它们在拼写上有细微不同也可以匹配。 这是非常易懂的算法,它有多种改进版本。

使用该算法,"Robert" 和 "Rupert" 会返回相同的字符串 "R163" , 但"Rubin" 返回 "R150""Ashcraft" 和 "Ashcroft" 都会返回 "A261"

双元音

双元音 拼音编码算法是该算法的第二代。 它对原来的元音算法进行了大量的基本设计改进。

双元音试图解释斯拉夫语、日耳曼语、凯尔特语、希腊语、法语、意大利语、西班牙语、中文和其他起源语言的英语中的无数不规则性。