如何在MongoDB中执行全文搜索
作为 Write for DOnations 计划的一部分,作者选择了 Open Internet/Free Speech Fund 来接受捐赠。
介绍
通过搜索精确匹配、使用大于或小于比较或使用正则表达式来过滤数据的 MongoDB 查询在许多情况下都可以很好地工作。 但是,这些方法在过滤包含丰富文本数据的字段时存在不足。
想象一下,您在网络搜索引擎中输入了“咖啡配方”,但它只返回包含该短语的页面。 在这种情况下,您可能找不到您正在寻找的确切内容,因为大多数流行的咖啡食谱网站可能不包含确切的短语“咖啡食谱”。 但是,如果您将该短语输入到真正的搜索引擎中,您可能会找到标题为“很棒的咖啡饮品(附食谱!)”或“您可以在家制作的咖啡店饮品和零食”之类的页面。 在这些示例中,存在“咖啡”一词,但标题包含“食谱”一词的另一种形式或完全排除它。
这种将文本匹配到搜索查询的灵活性对于专门搜索文本数据的全文搜索引擎来说是典型的。 有多种专门用于此类应用程序的开源工具正在使用中,ElasticSearch 是一个特别受欢迎的选择。 但是,对于不需要在专用搜索引擎中找到的强大搜索功能的场景,一些通用数据库管理系统提供了自己的全文搜索功能。
在本教程中,您将通过示例学习如何在 MongoDB 中创建文本索引,并使用它来根据常见的全文搜索查询和过滤器搜索数据库中的文档。
先决条件
要遵循本教程,您将需要:
- 具有
sudo
权限的常规非 root 用户和配置了 UFW 的防火墙的服务器。 本教程使用运行 Ubuntu 20.04 的服务器进行了验证,您可以按照此 Ubuntu 20.04 初始服务器设置教程来准备您的服务器。 - MongoDB 安装在您的服务器上。 要进行此设置,请按照我们关于 如何在 Ubuntu 20.04 上安装 MongoDB 的教程进行操作。
- 通过启用身份验证和创建管理用户来保护服务器的 MongoDB 实例。 要像这样保护 MongoDB,请按照我们关于 如何在 Ubuntu 20.04 上保护 MongoDB 的教程进行操作。
- 熟悉查询 MongoDB 集合和过滤结果。 要了解如何使用 MongoDB 查询,请按照我们关于 如何在 MongoDB 中创建查询的指南进行操作。
注意:有关如何配置服务器、安装 MongoDB 和保护 MongoDB 安装的链接教程参考 Ubuntu 20.04。 本教程专注于 MongoDB 本身,而不是底层操作系统。 只要启用了身份验证,它通常适用于任何 MongoDB 安装,无论操作系统如何。
第 1 步——准备测试数据
为了帮助您了解如何在 MongoDB 中执行全文搜索,此步骤概述了如何打开 MongoDB shell 以连接到本地安装的 MongoDB 实例。 它还解释了如何创建示例集合并将一些示例文档插入其中。 此示例数据将在本指南中的命令和示例中使用,以帮助解释如何使用 MongoDB 搜索文本数据。
要创建此示例集合,请以您的管理用户身份连接到 MongoDB shell。 本教程遵循先决条件 MongoDB 安全教程 的约定,并假设此管理用户名为 AdminSammy,其身份验证数据库为 admin
。 如果不同,请务必在以下命令中更改这些详细信息以反映您自己的设置:
mongo -u AdminSammy -p --authenticationDatabase admin
输入您在安装期间设置的密码以访问 shell。 提供密码后,您的提示将变为大于号:
注意: 在新连接上,MongoDB shell 默认会连接到 test
数据库。 您可以安全地使用此数据库来试验 MongoDB 和 MongoDB shell。
或者,您可以切换到另一个数据库来运行本教程中给出的所有示例命令。 要切换到另一个数据库,请运行 use
命令,后跟数据库名称:
use database_name
要了解全文搜索如何应用于 MongoDB 中的文档,您需要一组可以过滤的文档。 本指南将使用一系列示例文档,其中包括几种不同类型的咖啡饮品的名称和描述。 这些文档将与以下描述古巴咖啡饮料的示例文档具有相同的格式:
示例 Cafecito 文档
{ "name": "Cafecito", "description": "A sweet and rich Cuban hot coffee made by topping an espresso shot with a thick sugar cream foam." }
该文档包含两个字段:咖啡饮料的 name
和更长的 description
,它提供有关饮料及其成分的一些背景信息。
在 MongoDB shell 中运行以下 insertMany()
方法,创建一个名为 recipes
的集合,同时向其中插入五个示例文档:
db.recipes.insertMany([ {"name": "Cafecito", "description": "A sweet and rich Cuban hot coffee made by topping an espresso shot with a thick sugar cream foam."}, {"name": "New Orleans Coffee", "description": "Cafe Noir from New Orleans is a spiced, nutty coffee made with chicory."}, {"name": "Affogato", "description": "An Italian sweet dessert coffee made with fresh-brewed espresso and vanilla ice cream."}, {"name": "Maple Latte", "description": "A wintertime classic made with espresso and steamed milk and sweetened with some maple syrup."}, {"name": "Pumpkin Spice Latte", "description": "It wouldn't be autumn without pumpkin spice lattes made with espresso, steamed milk, cinnamon spices, and pumpkin puree."} ])
此方法将返回分配给新插入对象的对象标识符列表:
Output{ "acknowledged" : true, "insertedIds" : [ ObjectId("61895d2787f246b334ece911"), ObjectId("61895d2787f246b334ece912"), ObjectId("61895d2787f246b334ece913"), ObjectId("61895d2787f246b334ece914"), ObjectId("61895d2787f246b334ece915") ] }
您可以通过在不带参数的 recipes
集合上运行 find()
方法来验证文档是否已正确插入。 这将检索集合中的每个文档:
db.recipes.find()
Output{ "_id" : ObjectId("61895d2787f246b334ece911"), "name" : "Cafecito", "description" : "A sweet and rich Cuban hot coffee made by topping an espresso shot with a thick sugar cream foam." } . . .
有了示例数据,您就可以开始学习如何使用 MongoDB 的全文搜索功能了。
第 2 步 — 创建文本索引
要开始使用 MongoDB 的全文搜索功能,您必须在集合上创建文本索引。 Indexes 是特殊的数据结构,它仅将来自每个文档的一小部分数据存储在与文档本身分开的集合中。 MongoDB中有几种用户可以创建的索引,它们都有助于数据库在查询集合时优化搜索性能。
然而, 文本索引 是一种特殊类型的索引,用于进一步方便搜索包含文本数据的字段。 当用户创建文本索引时,MongoDB 会自动从搜索中删除任何特定于语言的 停用词 。 这意味着 MongoDB 将忽略给定语言的最常用词(在英语中,诸如“a”、“an”、“the”或“this”之类的词)。
MongoDB 还将在搜索中实现 suffix-stemming 的形式。 这涉及到 MongoDB 识别搜索词的根部分,并将该根的其他语法形式(通过添加常见的后缀创建,如“-ing”、“-ed”或“-er”)视为等同于搜索的目的。
由于这些和其他特性,MongoDB 可以更灵活地支持用自然语言编写的查询并提供更好的结果。
注意: 本教程主要关注英文文本,但MongoDB在使用全文搜索和文本索引时支持多种语言。 想了解更多MongoDB支持的语言,请参考支持语言的官方文档。
您只能为任何给定的 MongoDB 集合创建一个文本索引,但可以使用多个字段创建索引。 在我们的示例集合中,每个文档的 name
和 description
字段中都存储了有用的文本。 为这两个字段创建文本索引可能很有用。
运行以下 createIndex()
方法,将为两个字段创建文本索引:
db.recipes.createIndex({ "name": "text", "description": "text" });
对于 name
和 description
这两个字段中的每一个,索引类型设置为 text
,告诉 MongoDB 根据这些字段创建一个为全文搜索量身定制的文本索引字段。 输出将确认索引创建:
Output{ "createdCollectionAutomatically" : false, "numIndexesBefore" : 1, "numIndexesAfter" : 2, "ok" : 1 }
现在您已经创建了索引,您可以使用它向数据库发出全文搜索查询。 在下一步中,您将学习如何执行包含单个和多个单词的查询。
第 3 步 — 搜索一个或多个单词
也许最常见的搜索问题是查找包含一个或多个单词的文档。
通常,用户希望搜索引擎能够灵活地确定给定的搜索词应该出现在哪里。 例如,如果您要使用任何流行的网络搜索引擎并输入“咖啡甜辣”,您可能不会期望结果会以确切的顺序包含这三个单词。 您更有可能期望一个包含“咖啡”、“甜”和“辣”字样的网页列表,但不一定彼此相邻。
这也是 MongoDB 在使用文本索引时处理典型搜索查询的方式。 此步骤通过几个示例概述了 MongoDB 如何解释搜索查询。
首先,假设您想搜索配方中含有香料的咖啡饮料,因此您可以使用以下命令单独搜索单词 spiced
:
db.recipes.find({ $text: { $search: "spiced" } });
请注意,使用全文搜索时的语法与常规查询略有不同。 单个字段名称(例如 name
或 description
)不会出现在过滤器文档中。 相反,该查询使用 $text
运算符,告诉 MongoDB 此查询打算使用您之前创建的文本索引。 您不需要比这更具体,因为您可能还记得,一个集合可能只有一个文本索引。 此过滤器的嵌入文档内部是 $search
运算符,将搜索查询作为其值。 在此示例中,查询是一个单词:spiced
。
运行此命令后,MongoDB 会生成以下文档列表:
Output{ "_id" : ObjectId("61895d2787f246b334ece915"), "name" : "Pumpkin Spice Latte", "description" : "It wouldn't be autumn without pumpkin spice lattes made with espresso, steamed milk, cinnamon spices, and pumpkin puree." } { "_id" : ObjectId("61895d2787f246b334ece912"), "name" : "New Orleans Coffee", "description" : "Cafe Noir from New Orleans is a spiced, nutty coffee made with chicory." }
结果集中有两个文档,它们都包含类似于搜索查询的单词。 虽然 New Orleans Coffee
文档的描述中确实包含单词 spiced
,但 Pumpkin Spice Late
文档没有。
无论如何,由于 MongoDB 使用了词干提取,这个查询仍然返回了它。 MongoDB 将单词 spiced
剥离为仅 spice
,在索引中查找 spice
并对其进行词干化。 因此,Pumpkin Spice Late
文档中的词 spice
和 spices
成功匹配了搜索查询,即使您没有专门搜索这两个词。
现在,假设您特别喜欢浓缩咖啡饮料。 尝试使用两个词查询 spiced espresso
来查找文档,以查找以浓缩咖啡为基础的辛辣咖啡。
db.recipes.find({ $text: { $search: "spiced espresso" } });
这次的结果列表比之前更长:
Output{ "_id" : ObjectId("61895d2787f246b334ece914"), "name" : "Maple Latte", "description" : "A wintertime classic made with espresso and steamed milk and sweetened with some maple syrup." } { "_id" : ObjectId("61895d2787f246b334ece913"), "name" : "Affogato", "description" : "An Italian sweet dessert coffee made with fresh-brewed espresso and vanilla ice cream." } { "_id" : ObjectId("61895d2787f246b334ece911"), "name" : "Cafecito", "description" : "A sweet and rich Cuban hot coffee made by topping an espresso shot with a thick sugar cream foam." } { "_id" : ObjectId("61895d2787f246b334ece915"), "name" : "Pumpkin Spice Latte", "description" : "It wouldn't be autumn without pumpkin spice lattes made with espresso, steamed milk, cinnamon spices, and pumpkin puree." } { "_id" : ObjectId("61895d2787f246b334ece912"), "name" : "New Orleans Coffee", "description" : "Cafe Noir from New Orleans is a spiced, nutty coffee made with chicory." }
在搜索查询中使用多个单词时,MongoDB 执行逻辑 OR
操作,因此文档只需匹配表达式的一部分即可包含在结果集中。 结果包含包含 spiced
和 espresso
或仅包含任一术语的文档。 请注意,单词不一定需要彼此靠近,只要它们出现在文档中的某个位置即可。
注意: 如果您尝试对未定义文本索引的集合执行任何全文搜索查询,MongoDB 将返回错误消息:
Error messageError: error: { "ok" : 0, "errmsg" : "text index required for $text query", "code" : 27, "codeName" : "IndexNotFound" }
在这一步中,您学习了如何使用一个或多个单词作为文本搜索查询,MongoDB 如何使用逻辑 OR
操作连接多个单词,以及 MongoDB 如何执行词干提取。 接下来,您将在文本搜索查询中使用完整的短语,并开始使用排除来进一步缩小搜索结果的范围。
第 4 步 — 搜索完整短语并使用排除项
查找单个单词可能会返回太多结果,或者结果可能不够精确。 在此步骤中,您将使用短语搜索和排除来更精确地控制搜索结果。
假设你爱吃甜食,外面很热,加冰淇淋的咖啡听起来很不错。 尝试使用前面概述的基本搜索查询查找冰淇淋咖啡:
db.recipes.find({ $text: { $search: "ice cream" } });
数据库将返回两个咖啡配方:
Output{ "_id" : ObjectId("61895d2787f246b334ece913"), "name" : "Affogato", "description" : "An Italian sweet dessert coffee made with fresh-brewed espresso and vanilla ice cream." } { "_id" : ObjectId("61895d2787f246b334ece911"), "name" : "Cafecito", "description" : "A sweet and rich Cuban hot coffee made by topping an espresso shot with a thick sugar cream foam." }
虽然 Affogato
文档符合您的期望,但 Cafecito
不是用冰淇淋制成的。 搜索引擎使用逻辑 OR
操作接受第二个结果,只是因为描述中出现了单词 cream
。
要告诉 MongoDB 您正在寻找 ice cream
作为一个完整的短语而不是两个单独的词,请使用以下查询:
db.recipes.find({ $text: { $search: "\"ice cream\"" } });
请注意短语周围的每个双引号前面的反斜杠:\"ice cream\"
。 您正在执行的搜索查询是 "ice cream"
,双引号表示应该完全匹配的短语。 反斜杠 (\
) 转义双引号,因此它们不会被视为 JSON 语法的一部分,因为它们可能出现在 $search
运算符值内。
这一次,MongoDB 返回一个结果:
Output{ "_id" : ObjectId("61895d2787f246b334ece913"), "name" : "Affogato", "description" : "An Italian sweet dessert coffee made with fresh-brewed espresso and vanilla ice cream." }
该文档与搜索词完全匹配,仅 cream
和 ice
都不足以算作匹配。
另一个有用的全文搜索功能是排除修饰符。 为了说明这是如何工作的,首先运行以下查询以获取基于 espresso 的集合中所有咖啡饮品的列表:
db.recipes.find({ $text: { $search: "espresso" } });
此查询返回四个文档:
Output{ "_id" : ObjectId("61895d2787f246b334ece914"), "name" : "Maple Latte", "description" : "A wintertime classic made with espresso and steamed milk and sweetened with some maple syrup." } { "_id" : ObjectId("61895d2787f246b334ece913"), "name" : "Affogato", "description" : "An Italian sweet dessert coffee made with fresh-brewed espresso and vanilla ice cream." } { "_id" : ObjectId("61895d2787f246b334ece915"), "name" : "Pumpkin Spice Latte", "description" : "It wouldn't be autumn without pumpkin spice lattes made with espresso, steamed milk, cinnamon spices, and pumpkin puree." } { "_id" : ObjectId("61895d2787f246b334ece911"), "name" : "Cafecito", "description" : "A sweet and rich Cuban hot coffee made by topping an espresso shot with a thick sugar cream foam." }
请注意,其中两种饮料与牛奶一起供应,但假设您想要不含牛奶的饮料。 这是排除可以派上用场的情况。 在单个查询中,您可以通过在要排除的单词或短语前面加上减号 (-
) 来将要出现在结果中的单词与要排除的单词连接起来。
例如,假设您运行以下查询来查找不含牛奶的浓缩咖啡:
db.recipes.find({ $text: { $search: "espresso -milk" } });
使用此查询,两个文档将从先前返回的结果中排除:
Output{ "_id" : ObjectId("61895d2787f246b334ece913"), "name" : "Affogato", "description" : "An Italian sweet dessert coffee made with fresh-brewed espresso and vanilla ice cream." } { "_id" : ObjectId("61895d2787f246b334ece911"), "name" : "Cafecito", "description" : "A sweet and rich Cuban hot coffee made by topping an espresso shot with a thick sugar cream foam." }
您还可以排除完整的短语。 要搜索不含冰淇淋的咖啡,您可以在搜索查询中包含 -"ice cream"
。 同样,您需要使用反斜杠转义双引号,如下所示:
db.recipes.find({ $text: { $search: "espresso -\"ice cream\"" } });
Output{ "_id" : ObjectId("61d48c31a285f8250c8dd5e6"), "name" : "Maple Latte", "description" : "A wintertime classic made with espresso and steamed milk and sweetened with some maple syrup." } { "_id" : ObjectId("61d48c31a285f8250c8dd5e7"), "name" : "Pumpkin Spice Latte", "description" : "It wouldn't be autumn without pumpkin spice lattes made with espresso, steamed milk, cinnamon spices, and pumpkin puree." } { "_id" : ObjectId("61d48c31a285f8250c8dd5e3"), "name" : "Cafecito", "description" : "A sweet and rich Cuban hot coffee made by topping an espresso shot with a thick sugar cream foam." }
现在您已经了解了如何根据由多个单词组成的短语过滤文档以及如何从搜索结果中排除某些单词和短语,您可以熟悉 MongoDB 的全文搜索评分。
第 5 步 — 对结果进行评分并按分数排序
当一个查询,尤其是一个复杂的查询,返回多个结果时,一些文档可能比其他文档更匹配。 例如,当您寻找 spiced espresso
饮料时,那些既有香料又有浓缩咖啡的饮料比没有香料或不以浓缩咖啡为基础的饮料更合适。
全文搜索引擎通常为搜索结果分配相关性分数,表明它们与搜索查询的匹配程度。 MongoDB 也这样做,但默认情况下搜索相关性是不可见的。
再次搜索 spiced espresso
,但这次让 MongoDB 也返回每个结果的搜索相关性分数。 为此,您可以在查询过滤器文档之后添加一个投影:
db.recipes.find( { $text: { $search: "spiced espresso" } }, { score: { $meta: "textScore" } } )
投影 { score: { $meta: "textScore" } }
使用 $meta
运算符,这是一种特殊的投影,可以从返回的文档中返回特定的元数据。 此示例返回文档的 textScore
元数据,这是 MongoDB 全文搜索引擎的内置功能,包含搜索相关性分数。
执行查询后,返回的文档将包含一个名为 score
的新字段,如过滤器文档中指定的那样:
Output{ "_id" : ObjectId("61895d2787f246b334ece913"), "name" : "Affogato", "description" : "An Italian sweet dessert coffee made with fresh-brewed espresso and vanilla ice cream.", "score" : 0.5454545454545454 } { "_id" : ObjectId("61895d2787f246b334ece911"), "name" : "Cafecito", "description" : "A sweet and rich Cuban hot coffee made by topping an espresso shot with a thick sugar cream foam.", "score" : 0.5384615384615384 } { "_id" : ObjectId("61895d2787f246b334ece914"), "name" : "Maple Latte", "description" : "A wintertime classic made with espresso and steamed milk and sweetened with some maple syrup.", "score" : 0.55 } { "_id" : ObjectId("61895d2787f246b334ece912"), "name" : "New Orleans Coffee", "description" : "Cafe Noir from New Orleans is a spiced, nutty coffee made with chicory.", "score" : 0.5454545454545454 } { "_id" : ObjectId("61895d2787f246b334ece915"), "name" : "Pumpkin Spice Latte", "description" : "It wouldn't be autumn without pumpkin spice lattes made with espresso, steamed milk, cinnamon spices, and pumpkin puree.", "score" : 2.0705128205128203 }
请注意 Pumpkin Spice Latte
的得分要高多少,这是唯一一种同时包含 spiced
和 espresso
的咖啡饮料。 根据 MongoDB 的相关性分数,它是与该查询最相关的文档。 但是,默认情况下,不会按相关性顺序返回结果。
要改变这一点,您可以在查询中添加 sort()
子句,如下所示:
db.recipes.find( { $text: { $search: "spiced espresso" } }, { score: { $meta: "textScore" } } ).sort( { score: { $meta: "textScore" } } );
排序文档的语法与投影的语法相同。 现在,文档列表是相同的,但它们的顺序不同:
Output{ "_id" : ObjectId("61895d2787f246b334ece915"), "name" : "Pumpkin Spice Latte", "description" : "It wouldn't be autumn without pumpkin spice lattes made with espresso, steamed milk, cinnamon spices, and pumpkin puree.", "score" : 2.0705128205128203 } { "_id" : ObjectId("61895d2787f246b334ece914"), "name" : "Maple Latte", "description" : "A wintertime classic made with espresso and steamed milk and sweetened with some maple syrup.", "score" : 0.55 } { "_id" : ObjectId("61895d2787f246b334ece913"), "name" : "Affogato", "description" : "An Italian sweet dessert coffee made with fresh-brewed espresso and vanilla ice cream.", "score" : 0.5454545454545454 } { "_id" : ObjectId("61895d2787f246b334ece912"), "name" : "New Orleans Coffee", "description" : "Cafe Noir from New Orleans is a spiced, nutty coffee made with chicory.", "score" : 0.5454545454545454 } { "_id" : ObjectId("61895d2787f246b334ece911"), "name" : "Cafecito", "description" : "A sweet and rich Cuban hot coffee made by topping an espresso shot with a thick sugar cream foam.", "score" : 0.5384615384615384 }
Pumpkin Spice Latte
文档显示为第一个结果,因为它具有最高的相关性分数。
根据相关性分数对结果进行排序可能会有所帮助。 对于包含多个单词的查询尤其如此,其中最合适的文档通常包含多个搜索词,而不太相关的文档可能只包含一个。
结论
通过学习本教程,您已经熟悉了 MongoDB 的全文搜索功能。 您创建了一个文本索引并使用单个和多个单词、完整短语和排除项编写了文本搜索查询。 您还评估了返回文档的相关性分数,并对搜索结果进行了排序以首先显示最相关的结果。 虽然 MongoDB 的全文搜索功能可能不如某些专用搜索引擎强大,但它们足以应对许多用例。
请注意,在单个文本索引中还有更多搜索查询修饰符(例如区分大小写和变音符号以及对多种语言的支持)。 这些可用于更强大的场景以支持文本搜索应用程序。 有关 MongoDB 全文搜索功能及其使用方法的更多信息,我们鼓励您查看官方 官方 MongoDB 文档 。