MongoDB中的文本索引

00:00/00:00

MongoDB支持在字符串内容上执行文本检索的查询操作。如果需要执行文本检索的话,MongoDB使用文本索引及$text操作符。PS:给大家推荐一首初中的时候最喜欢的歌:可米小子的青春纪念册。

注意:视图不支持文本检索。

示例

下面的例子展示了如何构建一个文本索引,并且给定唯一的文本字段,使用它来查找咖啡店。

使用下面的文档创建一个集合stores:

db.stores.insert(
   [
     { _id: 1, name: "Java Hut", description: "Coffee and cakes" },
     { _id: 2, name: "Burger Buns", description: "Gourmet hamburgers" },
     { _id: 3, name: "Coffee Shop", description: "Just coffee" },
     { _id: 4, name: "Clothes Clothes Clothes", description: "Discount clothing" },
     { _id: 5, name: "Java Shopping", description: "Indonesian goods" }
   ]
)

文本索引

MongoDB提供了文本索引以支持在字符串内容上的文本检索查询。 text索引可以包含值为字符串或字符串元素数组的任何字段。

如果想要执行文本检索查询,我们必须在集合上创建一个text索引。一个集合只能有一个文本检索索引,但是该索引可以覆盖多个字段。

例如,我们可以在mongo shell中运行下面的命令来在namedescription字段上进行文本检索:

db.stores.createIndex( { name: "text", description: "text" } )

从3.2版本开始,MongoDB引入了版本3的text索引。新版本索引的主要功能包括:

  • 改进的大小写不敏感
  • 音节不敏感
  • 用于分词的额外分词符

创建文本索引

使用db.collection.createIndex()方法以创建文本索引。如果想要对一个包含一个字符粗或字符串元素的数组的字段进行索引,将字段作为参数,并且在索引文档中指明text字样,如下所示:

db.reviews.createIndex( { comments: "text" } )

我们可以在多个字段上创建text索引。下面的示例在subjectcomments字段上创建了一个文本索引。

db.reviews.createIndex(
   {
     subject: "text",
     comments: "text"
   }
 )

复合索引中可以包含text索引键值与升序/降序键值的组合。

如果想要删除一个text索引,可以使用索引名称进行删除。

db.collection.dropIndex("MyTextIndex")

指定权重

对于text索引,一个索引字段的权重表示:就文本检索分数而言,该字段与其它索引字段重要性的相对大小。

对于文档中的每个索引字段,MongoDB将匹配数目与权重相乘,并且这些结果进行求和。然后MongoDB会使用这个和来计算文档的分数。

对于每个检索字段而言,默认的权重为1。如果想要调整索引字段的权重,可以在db.collection.createIndex()方法中使用weights选项中进行设置。

通配符文本索引

如果想要在多个字段上创建文本索引,我们也可以使用通配符($**)。通过使用通配符文本索引,MongoDB对集合中每个文档中包含字符串数据的每个字段都进行索引。下面的示例使用通配符指定器创建了一个文本索引:

db.collection.createIndex( { "$**": "text" } )

该索引允许对包含字符串内容的所有字段进行文本检索。该索引对于高度非结构化的数据非常有效,特别是在不清楚即时查询过程中应该在文本索引中包含哪些字段的情况下。

通配符文本索引是多字段的文本索引。这样的话,我们可以在索引创建阶段为特定的字段分配权重以控制结果的排序。

通配符文本索引,和其它所有文本索引一样,可以是复合索引的一个部分,例如,下面的示例在a字段上以及通配符声明变量上创建了一个复合索引:

db.collection.createIndex( { a: 1, "$**": "text" } )

和所有的复合文本索引一样,由于a在文本索引键值的前面,如果想要使用该索引执行$text检索,那么这个查询也必须在前面包含一个a的等量匹配条件。

大小写不敏感

从3.2版本开始修改
版本3的文本索引支持土耳其语言的通用C/简单S以及Unicode 8.0字符数据库大写转换的特殊的T大小写转化。

大写转换拓展了大小写的文本索引不敏感性,以包含有音符的字符,例如éÉ以及来自非拉丁字母的字符,例如中欧字母表中的Ии

版本3的文本索引也是非音节敏感的。这样的话,索引并不会对é/É/e/E进行区分。

之前版本的文本索引只对[A-z]大小不敏感。例如,对于只对非音节的拉丁字符大小写不敏感。对于其它所有字符,文本检索的早期版本将它们看作不同的字符。

音节的不敏感

从3.2版本开始修改
从3版本开始,文本检索为非音节敏感的。也就是说,索引并不会区分包含音节标记的字符和它们对应的非标记字符,例如é/É/e。具体说来,文本索引将字符根据Unicode 8.0字符数据库Prop列表进行了音节的分类。

版本3的文本索引也是对带有音节的大小写不敏感的。这样的话,索引就不会区分é/É/e/E

之前版本的文本索引将不同音节的字符看作是不同的字符。

分词符

从3.2版本开始修改
对于分词,版本3的文本索引使用的分词符号类别包括:Unicode 8.0 字符数据库Prop列表中的逗号、连字符、模式语义、引用标记、终止标记以及空格等。

例如,如果给定一个字符串:“Il a dit qu'il «était le meilleur joueur du monde”, 文本索引将会把«,»以及空格作为分隔符。

之前版本的索引会将«作为单词«était的一部分,将»作为单词monde»的一部分。

索引条目

文本索引将索引字段中的单词进行分词用于索引条目。文本索引对集合中的每个文档的每个索引字段中的每个独特的单词存储一个索引条目。该索引使用简单的特定语言前缀词干。

支持的语言和停用词

MongoDB支持多种语言的文本检索。文本索引会丢弃掉特定语言的停用词(例如,英语中的theandaand等)并且使用简单的针对特定语言的后缀词干提取。

如果我们将语言值设置为none,那么文本索引将会使用简单的分词,而不是用任何停用词和词干提取。

sparse性质

文本索引默认为稀疏索引,忽略sparse: true选项。如果一个文档缺少文本索引字段(或者该字段为null或空数组),MongoDB将不会为该文档在文本索引中增加条目。在插入过程中,MongoDB会插入该文档,但是不会将其加入到文本索引中。

对于一个包含文本索引和其它类型键值的复合索引,只有文本索引字段决定该索引是否引用一个文档。其它键值不会决定索引是否应用该文档。

$text操作符

使用$text查询操作符在一个集合上使用文本索引执行文本检索。

$text可以使用空格或者大部分表达符号作为分隔符对字符串进行分词,然后对检索字符串中的所有单位执行一个逻辑的OR操作。

例如,我们可以使用下面的查询来找到包含coffee/shop/java列表中任一术语的所有商店:

db.stores.find( { $text: { $search: "java coffee shop" } } )

精确词组

我们也可以 通过将词组放在一个双引号中进行精确的词组检索。例如,下面的命令将会查找所有包含javacoffee shop的文档:

db.stores.find( { $text: { $search: "java \"coffee shop\"" } } )

术语排除

如果想要排除一个单词,我们可以在前面增加一个-字符。例如,想要找到所有包含java或者shop但是不包含coffee的商店,使用下面的命令:

db.stores.find( { $text: { $search: "java shop -coffee" } } )

排序

MongoDB默认以未排列的顺序返回结果。但是,文本检索查询将会对每个文档都计算一个相对分数,用来表示文档与查询的匹配程度。

如果需要将结果以相关分数的顺序进行排列,我们必须显式将文本分数字段的$meta进行映射,并且在该字段上进行排序。

db.stores.find(
   { $text: { $search: "java coffee shop" } },
   { score: { $meta: "textScore" } }
).sort( { score: { $meta: "textScore" } } ) 

文本检索也可以在聚合管道中使用。

支持的语言

MongoDB支持对多种语言的支持。其中,MongoDB 企业版支持中文检索,使用的是Rosette语义平台的文本检索基础技术。

聚合框架

使用聚合框架时,使用$match$text表达式来执行一个文本检索查询。如果需要根据相关性分数对结果进行排序,在$sort阶段使用一个$meta聚合操作符。

从2.6版本之后

限制

聚合管道中的文本索引有以下几个限制:

  • 包含$text$match阶段必须是管道的第一个阶段
  • 文本操作符只能在该阶段出现一次
  • 文本操作符表达式不能出现在$or或者是$nor表达式中。
  • 文本检索默认不按照匹配分数的顺序返回匹配的文档。在$sort阶段使用$meta聚合表达式。

文本评分

$text操作符会给在索引字段上包含检索条目的每个文档都分配一个分数。该分数表示该文档与给定文本索引查询的相关性。这个分数是$sort管道定义的一个部分,也可以作为映射表达式的一个部分。{$meta: "textScore" }表达式提供了$text操作处理中的信息。

示例
下面的示例假定articles集合上在subject字段上有一个文本索引:

db.articles.createIndex( { subject: "text" } )

计算文章中包含某个单词的总阅读数

下面的聚合在$match阶段检索了cake单词,然后在$group阶段计算了匹配文档的总阅读数。

db.articles.aggregate(
   [
     { $match: { $text: { $search: "cake" } } },
     { $group: { _id: null, views: { $sum: "$views" } } }
   ]
)

根据文本检索分数返回排序结果

如果我们想要根据文本检索分数进行排序的话,可以在$sort阶段包含一个$meta表达式。下面的示例匹配cake或者tea单词,并且根据textScore降序进行返回,最后只返回结果集中的title字段:

db.articles.aggregate(
   [{ $match: { $text: { $search: "cake tea" } } },
     { $sort: { score: { $meta: "textScore" } } },
     { $project: { title: 1, _id: 0 } }
   ]
)

指定的元数据决定了排列顺序。例如,TextScore元数据降序排列。

文本评分上的匹配

textScore元数据可以用于包含$text阶段$match阶段后面的映射、排序以及条件。

下面的示例匹配了caketea单词,将titlescore字段进行了排序,最后只返回了score大于1.0的文档。

db.articles.aggregate(
   [
     { $match: { $text: { $search: "cake tea" } } },
     { $project: { title: 1, _id: 0, score: { $meta: "textScore" } } },
     { $match: { score: { $gt: 1.0 } } }
   ]
)

指定文本检索的语言

下面的聚合在$match阶段检索了包含单词saber但是不包含单词claro的西班牙文档,并且在$group阶段计算了匹配文档的总浏览数。

db.articles.aggregate(
   [
     { $match: { $text: { $search: "saber -claro", $language: "es" } } },
     { $group: { _id: null, views: { $sum: "$views" } } }
   ]
)

限制

每个集合中只能有一个文本索引。

一个集合中最多只能有一个文本索引。

文本检索和提示

如果查询中包含$text查询表达式,我们就不能使用hint()

文本检索和排序

排序操作不能从文本索引中获取排列顺序,及时是复合文本索引也一样。例如,排序操作不能使用文本索引中的顺序。

复合索引

一个复合索引可能包含一个文本索引与一个升序/降序索引键值的组合。然而,这些复合索引有以下限制:

  • 一个复合文本索引不能包含其它特殊的索引类型,例如:多键值索引或者地理空间索引字段。
  • 如果复合文本索引中包含先于文本索引键值的索引键,如果需要执行$text检索的话,查询前缀必须包含前面这些键的等式匹配条件

删除文本索引

如果需要删除一个文本索引,将索引的名称传递给db.collection.dropIndex()方法。我们可以通过运行`db.collections.getIndexex()方法得到索引的名称。

存储需求及性能成本

文本索引有以下存储需求及性能成本:

  • 文本索引有可能非常庞大。它们对于每个插入的文档中的每个索引字段中的每个独特的提取词干之后的单词都会包含一个索引条目。
  • 创建一个文本索引与创建一个大型多键值索引非常相似,相对与在相同的数据上创建一个简单、有序的索引花费的时间要长。
  • 当在一个已有的集合中创建一个大型文本索引时,保证拥有一个足够高的文件描述符打开限制。
  • 文本索引将会影响插入吞吐,因为MongoDB必须对每个新输入源文档中的每个索引字段上的独特的提取词干之后的单词增加一个索引条目。
  • 此外,文本索引不会存储文档中单词的词组或近义词信息。因此,当整个集合都放在内存中时,词组的查询将会变得更高效。

文本检索支持

文本索引支持$text查询 操作。它有以下几种语义:
从3.2版本开始修改

{
  $text:
    {
    “$search: <string>,
      $language: <string>,
      $caseSensitive: <boolean>,
      $diacriticSensitive: <boolean>
    }
}

$text操作符接受以下字段的文本查询文档:

字段 类型 描述
$search 字符串 MongoDB解析的单词字符串,被用于查询文本索引。MongoDB对这些单词执行逻辑或操作,除非将其指定为数组。
$language 字符串 可选,指定搜索停用词列表以及词干提取和分词规则的语言。如果没有指定,检索将会使用索引的默认语言。如果将该值设置为none,那么文本检索就会使用没有停用词列表和词干提取的简单分词。
$caseSensitive 布尔型 可选,布尔型标记来启用或禁止大小写敏感检索。默认为false
$diacriticSensitive 布尔型 可选,布尔型标记来启用或禁止音节敏感。默认为false之前版本的文本检索本质上就是音节敏感的,不能被设置为非音节敏感的。因此,该选项对早期版本的文本检索无效。

总结

在数据库领域,感觉MongoDB在文本检索这一块支持还不错,只不过在打分的灵活性方面还是有待加强。目前看到的资料都只是支持给不同的字段赋予不同的权重,没看到可以提供自定义公式的功能(例如,根据某些字段进行衰减)。如果单纯做文本检索这一块的话,可能还是比较推荐Elastic Search,感觉各种功能和接口都比较完善了。但是,如果文本检索只是很小的一个部分,比较希望能够用统一的技术的话,可以选择MongoDB。有利有弊,综合考虑~

打赏

mickey

记录生活,写给几十年后的自己。