MongoDB-权威指南-三-

145 阅读45分钟

MongoDB 权威指南(三)

原文:The Definitive Guide to Mongodb

协议:CC BY-NC-SA 4.0

七、Python 和 MongoDB

Abstract

Python 是比较容易学习和掌握的编程语言之一。如果你相对来说是编程新手,这是一门非常好的语言。如果你已经非常熟悉编程,你会更快地掌握它。

Python 是比较容易学习和掌握的编程语言之一。如果你相对来说是编程新手,这是一门非常好的语言。如果你已经非常熟悉编程,你会更快地掌握它。

Python 可以用来快速开发应用,同时确保代码本身保持良好的可读性。记住这一点,本章将向您展示如何通过 Python 驱动程序(也称为 PyMongo 驱动程序;本章将交替使用这两个术语)。

首先,您将看到Connection()函数,它使您能够建立到数据库的连接。其次,你将学习如何写文档或字典,以及如何插入它们。第三,您将学习如何使用 Python 驱动程序使用find()find_one()命令来检索文档。这两个命令都可以选择使用一组丰富的查询修饰符来缩小搜索范围,使查询更容易实现。第四,您将了解用于执行更新的各种操作符。最后,您将了解如何使用 PyMongo 在文档甚至数据库级别删除数据。作为额外的收获,您将学习如何使用DBRef模块来引用存储在其他地方的数据。

我们开始吧。

Note

在这一章中,你会看到许多实际的代码例子来说明所讨论的概念。代码本身前面会有三个大于号(>>>),表示该命令是用 Python shell 编写的。查询代码将以粗体显示,结果输出将以明文呈现。

使用 Python 处理文档

如前几章所述,MongoDB 使用 BSON 风格的文档,PHP 使用关联数组。同样,Python 也有它所谓的字典。如果你已经玩过 MongoDB 控制台,我们相信你一定会爱上 Python。毕竟,语法是如此的相似,以至于语言语法的学习曲线可以忽略不计。

我们已经在前一章中介绍了 MongoDB 文档的结构,所以这里不再重复。相反,让我们来看看文档在 Python shell 中是什么样子的:

item = {

"Type" : "Laptop"

"ItemNumber" : "1234EXD"

"Status" : "In use"

"Location" : {

"Department" : "Development"

"Building" : "2B"

"Floor" : 12

"Desk" : 120101

"Owner" : "Anderson, Thomas"

}

"Tags" : ["Laptop","Development","In Use"]

}

虽然您应该记住 Python 术语词典,但在大多数情况下,本章将引用它的 MongoDB 等价物 document。毕竟,大多数时候,我们将使用 MongoDB 文档。

使用 PyMongo 模块

Python 驱动程序使用模块。您可以像对待 PHP 驱动程序中的类一样对待这些类。PyMongo 驱动程序中的每个模块负责一组操作。以下每个任务都有一个单独的模块(还有很多):建立连接、使用数据库、利用集合、操作光标、使用DBRef模块、转换ObjectId以及运行服务器端 JavaScript 代码。

本章将带您了解使用 PyMongo 驱动程序所需的最基本也是最有用的操作。一步一步地,您将学习如何通过简单易懂的代码片段使用命令,您可以将这些代码片段直接复制并粘贴到 Python shell(或脚本)中。从这里开始,这是管理 MongoDB 数据库的第一步。

连接和断开

建立到数据库的连接需要首先导入 PyMongo 驱动程序的MongoClient模块,这使您能够建立连接。在 shell 中键入以下语句来加载MongoClient模块:

>>> from pymongo import MongoClient

一旦您的 MongoDB 服务启动并运行(如果您希望连接,这是强制性的),您就可以通过调用MongoClient()函数建立到服务的连接。

如果没有给出额外的参数,该函数假设您想要连接到本地主机上的服务(本地主机的默认端口号是 27017)。下面一行建立了连接:

>>> c = MongoClient()

您可以看到连接通过 MongoDB 服务 shell 进入。一旦建立了连接,就可以使用c字典来引用该连接,就像在 shell 中使用db和在 PHP 中使用$c一样。接下来,选择您想要使用的数据库,将该数据库存储在db字典下。您可以像在 MongoDB shell 中那样做——在下面的例子中,您使用了inventory数据库:

>>> db = c.inventory

>>> db

Database(Connection('localhost', 27017), u'inventory')

本例中的输出显示您已经连接到本地主机,并且正在使用inventory数据库。

现在已经选择了数据库,您可以用完全相同的方式选择您的 MongoDB 集合。因为您已经在db字典下存储了数据库名称,所以您可以使用它来选择集合的名称;在这种情况下称为items:

>>> collection = db.items

插入数据

剩下的工作就是通过将文档存储在字典中来定义它。让我们以前面的例子为例,将它插入到 shell 中:

>>> item = {

... "Type" : "Laptop"

... "ItemNumber" : "1234EXD"

... "Status" : "In use"

... "Location" : {

... "Department" : "Development"

... "Building" : "2B"

... "Floor" : 12

... "Desk" : 120101

... "Owner" : "Anderson, Thomas"

... }

... "Tags" : ["Laptop","Development","In Use"]

... }

一旦定义了文档,就可以使用 MongoDB shell 中提供的相同的insert()函数来插入它:

>>> collection.insert(item)

ObjectId('4c57207b4abffe0e0c000000')

这就是全部内容:您定义文档并使用insert()函数插入它。

在插入文档时,还有一个更有趣的技巧可以利用:同时插入多个文档。您可以通过在单个字典中指定两个文档,然后插入该文档来实现这一点。结果将返回两个ObjectId值;请仔细注意在以下示例中如何使用括号:

>>> two = [{

... "Type" : "Laptop"

... "ItemNumber" : "2345FDX"

... "Status" : "In use"

... "Location" : {

... "Department" : "Development"

... "Building" : "2B"

... "Floor" : 12

... "Desk" : 120102

... "Owner" : "Smith, Simon"

... }

... "Tags" : ["Laptop","Development","In Use"]

... }

... {

... "Type" : "Laptop"

... "ItemNumber" : "3456TFS"

... "Status" : "In use"

... "Location" : {

... "Department" : "Development"

... "Building" : "2B"

... "Floor" : 12

... "Desk" : 120103

... "Owner" : "Walker, Jan"

... }

... "Tags" : ["Laptop","Development","In Use"]

... }]

>>> collection.insert(two)

[ObjectId('4c57234c4abffe0e0c000001'), ObjectId('4c57234c4abffe0e0c000002')]

查找您的数据

PyMongo 提供了两个函数来查找您的数据:find_one(),它在您的集合中查找符合指定标准的单个文档;和find(),它可以根据提供的参数找到多个文档(如果不指定任何参数,find()匹配集合中的所有文档)。让我们看一些例子。

查找单个文档

正如刚才提到的,您使用find_one()函数来查找单个文档。该函数类似于 MongoDB shell 中的findOne()函数,因此掌握它的工作原理应该不会对您构成太大的挑战。默认情况下,如果在没有任何参数的情况下执行,该函数将返回集合中的第一个文档,如下例所示:

>>> collection.find_one()

{

u'Status': u'In use'

u'Tags': [u'Laptop', u'Development', u'In Use']

u'ItemNumber': u'1234EXD'

u'Location':{

u'Department': u'Development'

u'Building': u'2B'

u'Floor': 12

u'Owner': u'Anderson, Thomas'

u'Desk': 120101

}

u'_id': ObjectId('4c57207b4abffe0e0c000000')

u'Type': u'Laptop'

}

您可以指定附加参数,以确保返回的第一个文档与您的查询相匹配。用于find()功能的每个参数也可用于find_one(),尽管limit参数将被忽略。需要编写查询参数,就像在 shell 中定义它们一样;也就是说,您需要指定一个键及其值(或多个值)。例如,假设您想要查找一个文档,其ItemNumber的值为3456TFS,并且您不想返回该文档的 _ id。以下查询实现了这一点,并返回如下所示的输出:

>>> collection.find_one({"ItemNumber" : "3456TFS"} ,fields={'_id' : False})

{

u'Status': u'In use'

u'Tags': [u'Laptop', u'Development', u'In Use']

u'ItemNumber': u'3456TFS'

u'Location': {

u'Department': u'Development'

u'Building': u'2B'

u'Floor': 12

u'Owner': u'Walker, Jan'

u'Desk': 120103

}

u'Type': u'Laptop'

}

Note

Python 是区分大小写的。因此,truefalse并不等同于TrueFalse

如果搜索条件对于一个文档来说比较常见,您还可以指定附加的查询运算符。例如,假设查询{"Department" : "Development"},它将返回多个结果。我们马上就会看到这样一个例子。然而,首先让我们确定如何返回多个文档,而不仅仅是一个。这可能和你想象的有点不一样。

查找多个文档

您需要使用find()函数来返回多个文档。到目前为止,您可能已经在 MongoDB 中使用过这个命令很多次了,所以您可能对它已经相当熟悉了。Python 中的概念是一样的:在括号中指定查询参数来查找指定的信息。

然而,将结果返回到屏幕上的工作方式有所不同。就像在 PHP 和 shell 中工作一样,查询一组文档会返回一个游标实例。然而,与在 shell 中输入不同,您不能简单地输入db.items.find()来显示所有结果。相反,您需要使用光标检索所有文档。以下示例显示了如何显示来自items集合的所有文档(注意,您之前定义了collection来匹配集合的名称;为了清楚起见,省略了结果):

>>> for doc in collection.find():

... doc

...

请密切注意单词doc前的缩进。如果不使用这个缩进,那么将显示一个错误消息,说明没有出现预期的缩进块。Python 的优势之一是使用这种缩进方法作为块分隔符,因为这种方法可以保持代码有序。请放心,您会相对较快地习惯这种 Pythonic 编码惯例。但是,如果您碰巧忘记了缩进,您将会看到一条类似如下的错误消息:

File "<stdin>", line 2

doc

^

IndentationError: expected an indented block

接下来,让我们看看如何使用find()函数指定查询操作符。为此使用的方法与本书前面提到的方法相同:

>>> for doc in collection.find({"Location.Owner" : "Walker, Jan"}):

... doc

...

{

u'Status': u'In use'

u'Tags': [u'Laptop', u'Development', u'In Use']

u'ItemNumber': u'3456TFS'

u'Location': {

u'Department': u'Development'

u'Building': u'2B'

u'Floor': 12

u'Owner': u'Walker, Jan'

u'Desk': 120103

}

u'_id': ObjectId('4c57234c4abffe0e0c000002')

u'Type': u'Laptop'

}

使用点符号

点符号用于搜索嵌入对象中的匹配元素。前面的代码片段显示了如何做到这一点的示例。使用这种技术时,您只需在嵌入对象中指定一个项的键名来搜索它,如下例所示:

>>> for doc in collection.find({"Location.Department" : "Development"}):

... doc

...

本示例返回任何设置了Development部门的文档。当在一个简单的数组中搜索信息时(例如,应用的标签),您只需要填写任何匹配的标签:

>>> for doc in collection.find({"Tags" : "Laptop"}):

... doc

...

返回字段

如果您的文档相对较大,并且您不希望返回存储在文档中的所有键/值信息,那么您可以在find()函数中包含一个额外的参数,以指定只有特定的一组字段需要使用True返回,或者使用False保持隐藏。您可以通过提供fields参数来实现这一点,然后在搜索条件后提供一个字段名列表。请注意,在_id字段之外,您不能在一个查询中混合使用TrueFalse

以下示例仅返回当前所有者的姓名、物品编号和对象 ID(这将始终被返回,除非您明确告诉 MongoDB 不要返回):

>>> for doc in collection.find({'Status' : 'In use'}, fields={'ItemNumber' : True, 'Location.Owner' : True}):

... doc

...

{

u'ItemNumber': u'1234EXD'

u'_id': ObjectId('4c57207b4abffe0e0c000000')

u'Location': {

u'Owner': u'Anderson, Thomas'

}

}

{

u'ItemNumber': u'2345FDX'

u'_id': ObjectId('4c57234c4abffe0e0c000001')

u'Location': {

u'Owner': u'Smith, Simon'

}

}

{

u'ItemNumber': u'3456TFS'

u'_id': ObjectId('4c57234c4abffe0e0c000002')

u'Location': {

u'Owner': u'Walker, Jan'

}

}

我想你会同意这种指定标准的方法非常方便。

用 sort()、limit()和 skip()简化查询

sort()limit()skip()函数使得实现查询更加容易。单独来看,这些功能都有其用途,但是将它们结合起来会使它们变得更好、更强大。您可以使用sort()功能按特定键对结果进行排序;用于限制返回结果总数的limit()函数;和skip()函数,在返回匹配查询的剩余文档之前跳过找到的前 n 个项目。

让我们看一组单独的例子,从sort()函数开始。为了节省空间,下面的示例包含另一个参数,以确保只返回几个字段:

>>> for doc in collection.find ({'Status' : 'In use'}

... fields={'ItemNumber' : True, 'Location.Owner' : True}).sort(‘ItemNumber’):

... doc

...

{

u'ItemNumber': u'1234EXD'

u'_id': ObjectId('4c57207b4abffe0e0c000000')

u'Location': {

u'Owner': u'Anderson, Thomas'

}

}

{

u'ItemNumber': u'2345FDX'

u'_id': ObjectId('4c57234c4abffe0e0c000001')

u'Location': {

u'Owner': u'Smith, Simon'

}

}

{

u'ItemNumber': u'3456TFS'

u'_id': ObjectId('4c57234c4abffe0e0c000002')

u'Location': {

u'Owner': u'Walker, Jan'

}

}

接下来,我们来看看limit()函数的运行情况。在这种情况下,您告诉函数只返回它在集合中找到的前两个条目中的ItemNumber(注意,在这个例子中没有指定搜索条件):

>>> for doc in collection.find({}, {"ItemNumber" : "true"}).limit(2):

... doc

...

{u'ItemNumber': u'1234EXD', u'_id': ObjectId('4c57207b4abffe0e0c000000')}

{u'ItemNumber': u'2345FDX', u'_id': ObjectId('4c57234c4abffe0e0c000001')}

在返回一组文档之前,您可以使用skip()功能跳过一些项目,如下例所示:

>>> for doc in collection.find({}, {"ItemNumber" : "true"}).skip(2):

... doc

...

{u'ItemNumber': u'3456TFS', u'_id': ObjectId('4c57234c4abffe0e0c000002')}

您也可以结合使用这三种功能,只选择找到的一定数量的项目,同时指定要跳过的特定数量的项目,并对它们进行排序:

>>> for doc in collection.find( {'Status' : 'In use'}

... fields={'ItemNumber' : True, 'Location.Owner' : True}).limit(2).skip(1).sort 'ItemNumber'):

... doc

...

{

u'ItemNumber': u'2345FDX'

u'_id': ObjectId('4c57234c4abffe0e0c000001')

u'Location': {

u'Owner': u'Smith, Simon'

}

}

{

u'ItemNumber': u'3456TFS'

u'_id': ObjectId('4c57234c4abffe0e0c000002')

u'Location': {

u'Owner': u'Walker, Jan'

}

}

您刚才所做的——限制返回的结果并跳过一定数量的项目——通常被称为分页。你可以用一种稍微简单一点的方式用$slice操作符来完成,这将在本章的后面讨论。

聚合查询

如前所述,MongoDB 附带了一套强大的聚合工具(关于这些工具的更多信息,请参见第 4 章)。您可以在 Python 驱动程序中使用所有这些工具。这些工具使得使用count()函数对数据进行计数成为可能;使用distinct()函数获得没有重复的不同值的列表;最后但同样重要的是,使用map_reduce()功能对数据进行分组,并对结果进行批量处理,或者只是进行计数。

这组命令,单独或一起使用,使您能够有效地查询您需要知道的信息,而不是别的。

除了这些基本的聚合命令,PyMongo 驱动程序还包括聚合框架。这个强大的功能将允许您计算聚合值,而无需使用通常过于复杂的 map/reduce(或 MapReduce)框架。

使用 count()计数项目

如果您只想计算符合条件的项目总数,您可以使用count()功能。该函数不像find()函数那样返回所有信息;相反,它返回一个整数值,其中包含找到的项目总数。

让我们看一些简单的例子。我们可以从返回整个集合中的文档总数开始,不指定任何标准:

>>> collection.count()

3

您还可以更精确地指定这些计数查询,如下例所示:

>>> collection.find({"Status" : "In use", "Location.Owner" : "Walker, Jan"}).count()

1

当您只需要快速计算符合您的标准的文档总数时,count()功能会非常有用。

使用 distinct()对唯一项目进行计数

count()函数是获得返回项目总数的一个很好的方法。但是,有时您可能会意外地将重复项添加到您的集合中,因为您只是忘记了删除或更改一个旧文档,并且您希望获得没有重复项的准确计数。这就是distinct()函数可以帮你的地方。该函数确保只返回唯一的项目。让我们通过向集合中添加另一个项目来设置一个示例,但是使用之前使用的ItemNumber:

>>> dup = ( {

"ItemNumber" : "2345FDX"

"Status" : "Not used"

"Type" : "Laptop"

"Location" : {

"Department" : "Storage"

"Building" : "1A"

}

"Tags" : ["Not used","Laptop","Storage"]

} )

>>> collection.insert(dup)

ObjectId('4c592eb84abffe0e0c000004')

当您在此时使用count()功能时,唯一项目的数量将不正确:

>>> collection.find({}).count()

4

相反,您可以使用distinct()函数来确保忽略任何重复项:

>>> collection.distinct("ItemNumber")

[u'1234EXD', u'2345FDX', u'3456TFS']

使用聚合框架对数据进行分组

聚合框架是一个计算聚合值的好工具,不需要使用 MapReduce。虽然 MapReduce 非常强大——并且可用于 PyMongo 驱动程序——但是聚合框架同样可以完成大多数工作,而且性能更好。为了演示这一点,aggregate()函数中最强大的管道操作符之一$group将用于按照标签对之前添加的文档进行分组,并使用$sum聚合表达式对其进行计数。让我们看一个例子:

>>> collection.aggregate([

... {'$unwind' : '$Tags'}

... {'$group' : {'_id' : '$Tags', 'Totals' : {'$sum' : 1}}}

... ])

首先,aggregate()函数使用$unwind管道操作符从文档的'$Tags数组(注意其名称中的强制$)创建一个标记文档流。接下来,调用$group管道操作符,使用它的值作为它的'_id'和总计数,为每个唯一标签创建一个单独的行——使用$group$sum表达式计算'Totals'值。结果输出如下所示:

{

u'ok': 1.0

u'result': [

{u'_id': u'Laptop', u'Totals': 4}

{u'_id': u'In Use', u'Totals': 3}

{u'_id': u'Development', u'Totals': 3}

{u'_id': u'Storage', u'Totals': 1}

{u'_id': u'Not used', u'Totals': 1}

]

}

输出准确返回所请求的信息。但是,如果我们希望按'Totals''对输出进行排序呢?这可以通过简单地添加另一个管道操作符$sort来实现。但是,在这样做之前,我们需要导入 SON 模块:

>>> from bson.son import SON

现在,我们可以根据'Totals'值按降序(-1)对结果进行排序,如下所示:

>>> collection.aggregate([

... {'$unwind' : '$Tags'}

... {'$group' : {'_id' : '$Tags', 'Totals' : {'$sum' : 1}}}

... {'$sort' : SON([('Totals', -1)])}

... ])

这将返回以下结果,以降序排列:

{

u'ok': 1.0

u'result': [

{u'_id': u'Laptop', u'Totals': 4}

{u'_id': u'In Use', u'Totals': 3}

{u'_id': u'Development', u'Totals': 3}

{u'_id': u'Storage', u'Totals': 1}

{u'_id': u'Not used', u'Totals': 1}

]

}

除了$sum管道表达式之外,$group管道操作符还支持各种其他表达式,下面列出了其中一些:

  • $push:创建并返回一个数组,其中包含在它的组中找到的所有值。
  • $addToSet:创建并返回一个数组,其中包含在该组中找到的所有唯一值。
  • $first:仅返回其组中找到的第一个值。
  • $last:仅返回其组中找到的最后一个值。
  • $max:返回其组中找到的最大值。
  • $min:返回其组中找到的最小值。
  • $avg:返回其所在组的平均值。

在这个例子中,您已经看到了$group$unwind$sort管道操作符,但是还有许多更强大的管道操作符,比如$geoNear操作符。聚合框架及其操作符将在第 4、6 和 8 章中详细讨论。

使用提示()指定索引

您可以使用hint()函数来指定查询数据时应该使用哪个索引。使用此函数可以帮助您提高查询的性能。在 Python 中,hint()函数也在光标上执行。但是,您应该记住,您在 Python 中指定的提示名称需要与您传递给create_index()函数的名称相同。

在下一个示例中,您将首先创建一个索引,然后搜索指定该索引的数据。然而,在可以按升序排序之前,您需要使用import()函数来导入ASCENDING方法。最后,您需要执行create_index()功能:

>>> from pymongo import ASCENDING

>>> collection.create_index([("ItemNumber", ASCENDING)])

u'ItemNumber_1'

>>> for doc in collection.find({"Location.Owner" : "Walker, Jan"}) .hint([("ItemNumber", ASCENDING)]):

... doc

...

{

u'Status': u'In use'

u'Tags': [u'Laptop', u'Development', u'In Use']

u'ItemNumber': u'3456TFS'

u'Location': {

u'Department': u'Development'

u'Building': u'2B'

u'Floor': 12

u'Owner': u'Walker, Jan'

u'Desk': 120103

}

u'_id': ObjectId('4c57234c4abffe0e0c000002')

u'Type': u'Laptop'

}

当您的集合不断增长时,使用索引可以帮助您显著提高性能速度(有关性能调优的更多详细信息,请参见第 10 章)。

用条件运算符细化查询

您可以使用条件运算符来优化您的查询。MongoDB 包括六个以上可通过 PyMongo 访问的条件操作符;这些和你在前面章节中看到的条件操作符是一样的。下面几节将带您浏览 Python 中可用的条件操作符,以及如何使用它们来优化 Python 中的 MongoDB 查询。

使用ltlt、gt、ltelte 和gte 运算符

让我们从查看$lt$gt$lte$gte条件操作符开始。您可以使用$lt操作符来搜索任何小于 n 的数字信息。该操作符只接受一个参数:数字n,它指定了限制。下面的示例查找书桌号小于 120102 的所有条目。请注意,不包括比较值本身:

>>> for doc in collection.find({"Location.Desk" : {"$lt" : 120102} }):

... doc

...

{

u'Status': u'In use'

u'Tags': [u'Laptop', u'Development', u'In Use']

u'ItemNumber': u'1234EXD'

u'Location': {

u'Department': u'Development'

u'Building': u'2B'

u'Floor': 12

u'Owner': u'Anderson, Thomas'

u'Desk': 120101

}

u'_id': ObjectId('4c57207b4abffe0e0c000000')

u'Type': u'Laptop'

}

同样,您可以使用$gt操作符来查找任何值高于所提供的比较值的项目。再次注意,不包括比较值本身:

>>> for doc in collection.find({"Location.Desk" : {"$gt" : 120102} }):

... doc

...

{

u'Status': u'In use'

u'Tags': [u'Laptop', u'Development', u'In Use']

u'ItemNumber': u'3456TFS'

u'Location': {

u'Department': u'Development'

u'Building': u'2B'

u'Floor': 12

u'Owner': u'Walker, Jan'

u'Desk': 120103

}

u'_id': ObjectId('4c57234c4abffe0e0c000002')

u'Type': u'Laptop'

}

如果您想在结果中包含比较值,那么您可以使用$lte$gte操作符分别查找任何小于或等于 n 或大于或等于 n 的值。以下示例说明了如何使用这些运算符:

>>> for doc in collection.find({"Location.Desk" : {"$lte" : 120102} }):

... doc

...

{

u'Status': u'In use'

u'Tags': [u'Laptop', u'Development', u'In Use']

u'ItemNumber': u'1234EXD'

u'Location': {

u'Department': u'Development'

u'Building': u'2B'

u'Floor': 12

u'Owner': u'Anderson, Thomas'

u'Desk': 120101

}

u'_id': ObjectId('4c57207b4abffe0e0c000000')

u'Type': u'Laptop'

}

{

u'Status': u'In use'

u'Tags': [u'Laptop', u'Development', u'In Use']

u'ItemNumber': u'2345FDX'

u'Location': {

u'Department': u'Development'

u'Building': u'2B'

u'Floor': 12

u'Owner': u'Smith, Simon'

u'Desk': 120102

}

u'_id': ObjectId('4c57234c4abffe0e0c000001')

u'Type': u'Laptop'

}

>>> for doc in collection.find({"Location.Desk" : {"$gte" : 120102} }):

... doc

...

{

u'Status': u'In use'

u'Tags': [u'Laptop', u'Development', u'In Use']

u'ItemNumber': u'2345FDX'

u'Location': {

u'Department': u'Development'

u'Building': u'2B'

u'Floor': 12

u'Owner': u'Smith, Simon'

u'Desk': 120102

}

u'_id': ObjectId('4c57234c4abffe0e0c000001')

u'Type': u'Laptop'

}

{

u'Status': u'In use'

u'Tags': [u'Laptop', u'Development', u'In Use']

u'ItemNumber': u'3456TFS'

u'Location': {

u'Department': u'Development'

u'Building': u'2B'

u'Floor': 12

u'Owner': u'Walker, Jan'

u'Desk': 120103

}

u'_id': ObjectId('4c57234c4abffe0e0c000002')

u'Type': u'Laptop'

}

搜索与$ne 不匹配的值

您可以使用$ne(不等于)运算符来搜索集合中不符合指定标准的任何文档。该操作符需要一个参数,即文档不应该具有的键和值信息,以使结果返回匹配:

>>> collection.find({"Status" : {"$ne" : "In use"}}).count()

1

用$in 指定匹配数组

$in操作符允许您指定一个可能匹配的数组。

例如,假设您只寻找两种开发计算机:not used或带有Development。还假设您希望将结果限制为两项,只返回ItemNumber:

>>> for doc in collection.find({"Tags" : {"$in" : ["Not used","Development"]}} , fields={"ItemNumber":"true"}).limit(2):

... doc

...

{u'ItemNumber': u'1234EXD', u'_id': ObjectId('4c57207b4abffe0e0c000000')}

{u'ItemNumber': u'2345FDX', u'_id': ObjectId('4c57234c4abffe0e0c000001')}

使用$nin 对匹配数组进行指定

使用$nin操作符和使用$in操作符完全一样;不同之处在于,该操作符排除了与给定数组中指定的任何值相匹配的任何文档。例如,以下查询查找Development部门当前未使用的任何项目:

>>> for doc in collection.find({"Tags" : {"$nin" : ["Development"]}}, fields={"ItemNumber": True}):

... doc

...

{u'ItemNumber': u'2345FDX', u'_id': ObjectId('4c592eb84abffe0e0c000004')}

查找与数组值匹配的文档

$in操作符可以用来查找与数组中指定的任何值相匹配的任何文档,而$all操作符可以让您查找与数组中指定的所有值相匹配的任何文档。完成此操作的语法看起来完全相同:

>>> for doc in collection.find({"Tags" : {"$all" : ["Storage","Not used"]}}, fields={"ItemNumber":"true"}):

... doc

...

{u'ItemNumber': u'2345FDX', u'_id': ObjectId('4c592eb84abffe0e0c000004')}

指定多个表达式与$或匹配

您可以使用$or操作符来指定一个文档可以拥有的多个值,其中至少有一个值必须为真,才符合匹配条件。这大致类似于$in操作符;不同之处在于$or操作符允许您指定键和值。您还可以将$or操作符与另一个键/值对结合使用。我们来看几个例子。

本示例将位置设置为Storage或所有者设置为Anderson, Thomas的所有文档返回到:

>>> for doc in collection.find({"$or" : [ { "Location.Department" : "Storage" }

... { "Location.Owner" : "Anderson, Thomas"} ] } ):

... doc

...

您也可以将上述代码与另一个键/值对结合使用,如下例所示:

>>> for doc in collection.find({ "Location.Building" : "2B", "$or" : [ { "Location.Department" : "Storage" }

... { "Location.Owner" : "Anderson, Thomas"} ] } ):

... doc

...

$or操作符基本上允许您同时进行两个搜索,并合并结果输出,即使单个搜索彼此没有任何共同点。另外,$or子句是并行执行的,每个子句可能使用不同的索引。

用$slice 从数组中检索项目

您可以使用$slice操作符从文档的给定数组中检索一定数量的项目。该运算符提供类似于skip()limit()功能的功能;区别在于这两个函数作用于整个文档,而$slice操作符作用于单个文档中的数组。

在查看示例之前,让我们添加一个新文档,以便更好地了解这个操作符。假设你的公司疯狂地痴迷于跟踪它的椅子库存,跟踪它们可能去的任何地方。自然地,每把椅子都有它曾经所属的桌子的历史。$slice示例操作符非常适合跟踪这种库存。

首先添加以下文档:

>>> chair = ({

... "Status" : "Not used"

... "Tags" : ["Chair","Not used","Storage"]

... "ItemNumber" : "6789SID"

... "Location" : {

... "Department" : "Storage"

... "Building" : "2B"

... }

... "PreviousLocation" :

... [ "120100","120101","120102","120103","120104","120105"

... "120106","120107","120108","120109","120110" ]

... })

>>> collection.insert(chair)

ObjectId('4c5973554abffe0e0c000005')

现在假设您想要查看上一个示例中返回的椅子的所有可用信息,但有一点需要注意:您不想查看所有以前的位置信息,而只想查看它所属的前三张桌子:

>>> collection.find_one({'ItemNumber' : '6789SID'}, {'PreviousLocation' : {'$slice' : 3} })

{

u'Status': u'Not used'

u'PreviousLocation': [u'120100', u'120101', u'120102']

u'Tags': [u'Chair', u'Not used', u'Storage']

u'ItemNumber': u'6789SID'

u'Location': {

u'Department': u'Storage'

u'Building': u'2B'

}

u'_id': ObjectId('4c5973554abffe0e0c000005')

}

类似地,通过使整数值为负,可以看到它最近的三个位置:

>>> collection.find_one({'ItemNumber' : '6789SID'}, {'PreviousLocation' : {'$slice' : -3} })

{

u'Status': u'Not used'

u'PreviousLocation': [u'120108', u'120109', u'120110']

u'Tags': [u'Chair', u'Not used', u'Storage']

u'ItemNumber': u'6789SID'

u'Location': {

u'Department': u'Storage'

u'Building': u'2B'

}

u'_id': ObjectId('4c5973554abffe0e0c000005')

}

或者,您可以跳过椅子的前五个位置,将返回的结果数量限制为三个(特别注意这里的括号):

>>> collection.find_one({'ItemNumber' : '6789SID'}, {'PreviousLocation' : {'$slice' : [5, 3] } })

{

u'Status': u'Not used'

u'PreviousLocation': [u'120105', u'120106', u'120107']

u'Tags': [u'Chair', u'Not used', u'Storage']

u'ItemNumber': u'6789SID'

u'Location': {

u'Department': u'Storage'

u'Building': u'2B'

}

u'_id': ObjectId('4c5973554abffe0e0c000005')

}

你大概明白了。这个例子可能看起来有点不寻常,但库存控制系统经常转向非正统;并且$slice操作者天生擅长帮助你考虑不寻常或复杂的情况。例如,$slice操作符可能是实现网站评论区分页系统的一个特别有效的工具。

使用正则表达式进行搜索

进行搜索的一个有用工具是正则表达式。Python 的默认正则表达式模块叫做re。使用re模块执行搜索需要首先加载该模块,如下例所示:

>>> import re

加载模块后,您可以在搜索条件的value字段中指定正则表达式查询。下面的例子显示了如何搜索任何文档,其中ItemNumber的值包含一个4(为了简单起见,这个例子只返回ItemNumber)中的值:

>>> for doc in collection.find({'ItemNumber' : re.compile('4')}, {'ItemNumber' : True}):

... doc

...

{u'ItemNumber': u'1234EXD', u'_id': ObjectId('4c57207b4abffe0e0c000000')}

{u'ItemNumber': u'2345FDX', u'_id': ObjectId('4c57234c4abffe0e0c000001')}

{u'ItemNumber': u'2345FDX', u'_id': ObjectId('4c592eb84abffe0e0c000004')}

{u'ItemNumber': u'3456TFS', u'_id': ObjectId('4c57234c4abffe0e0c000002')}

您可以进一步定义正则表达式。在这个阶段,您的查询是区分大小写的,它将匹配任何在值ItemNumber中有一个4的文档,不管它的位置如何。但是,假设您想要查找一个文档,其中的ItemNumber值以FS结尾,前面是一个未知值,并且在FS之后不能包含任何附加数据:

>>> for doc in collection.find({'ItemNumber' : re.compile('.FS$')},fields={'ItemNumber' : True}):

... doc

...

{u'ItemNumber': u'3456TFS', u'_id': ObjectId('4c57234c4abffe0e0c000002')}

您也可以使用find()以不区分大小写的方式搜索信息,但首先您必须添加另一个函数,如下例所示:

>>> for doc in collection.find({'Location.Owner' : re.compile('^anderson. ', re.IGNORECASE)}

... fields={'ItemNumber' : True, 'Location.Owner' : True}):

... doc

...

{

u'ItemNumber': u'1234EXD'

u'_id': ObjectId('4c57207b4abffe0e0c000000')

u'Location': {

u'Owner': u'Anderson, Thomas'

}

}

只要使用得当,正则表达式可能是一个非常强大的工具。关于re模块如何工作及其包含的功能的更多细节,请参考 http://docs.python.org/library/re.html 的模块官方文档。

修改数据

到目前为止,您已经学习了如何使用 Python 中的条件运算符和正则表达式来查询数据库中的信息。在下一节中,我们将研究如何使用 Python 来修改集合中的现有数据。我们可以使用 Python 以几种不同的方式来完成这项任务。接下来的几节将在前面使用的查询操作符的基础上寻找与您的修改相匹配的文档。在一些情况下,您可能需要跳回到本章的前面部分来温习使用查询操作符的特定方面——但这是学习过程中的正常部分,它将巩固到目前为止所学的课程。

更新您的数据

您使用 Python 的update()函数的方式与您在 MongoDB shell 或 PHP 驱动程序中使用同名函数的方式没有太大区别。在这种情况下,您需要提供两个强制参数来更新您的数据:argdocarg参数指定用于匹配文档的键/值信息,而doc参数包含更新的信息。您还可以提供几个可选参数来指定您选项。以下列表涵盖了 Python 更新信息的选项列表,包括它们的作用:

  • upsert (optional):如果设置为True,则执行一次上插。
  • manipulate(可选):如果设置为True,表示在使用 SONManipulator 的所有实例执行更新之前,将对文档进行操作。更多信息,请参考子 _ 操纵器文档( http://api.mongodb.org/python/current/api/pymongo/son_manipulator.html )。
  • check_keys(可选):如果设置为Trueupdate()将检查文档中是否有任何键以受限字符“$”或“.”开头替换 arg 时。
  • multi(可选):如果设置为True,则更新任何匹配的文档,而不仅仅是它找到的第一个文档(默认操作)。建议您总是将此设置为TrueFalse,而不是依赖默认行为(这在将来总是会改变)。
  • w (optional):如果设置为 0,更新操作将不被确认。使用副本集时,w也可以设置为 n,确保主服务器在成功复制到 n 个节点时确认更新操作。也可以设置为majority—一个保留字符串,以确保大多数副本节点将确认更新,或者设置为特定的标记,以确保标记的节点将确认更新。该选项默认为 1,表示确认更新操作。
  • wtimeout (optional):用于指定服务器等待接收确认的时间(毫秒)。默认为 10000。
  • j (optional):如果设置为True,该布尔选项将在指示更新成功之前强制将数据写入日志。默认为False
  • fsync (optional):如果设置为True,该布尔选项会在返回成功之前将数据同步到磁盘。如果该选项被设置为True,则意味着w被设置为0,即使它没有被设置。

如果在更新文档时没有指定任何修改符操作符,那么默认情况下,文档中的所有信息将被替换为您在doc参数中插入的任何数据。最好避免依赖默认行为;相反,您应该使用前面提到的操作符来显式地指定您想要的更新(您将很快学习如何做到这一点)。

通过查看一个不使用任何条件操作符的例子,您可以明白为什么最好在update()命令中使用条件操作符:

// Define the updated data

>>> update = ( {

"Type" : "Chair"

"Status" : "In use"

"Tags" : ["Chair","In use","Marketing"]

"ItemNumber" : "6789SID"

"Location" : {

"Department" : "Marketing"

"Building" : "2B"

"DeskNumber" : 131131

"Owner" : "Martin, Lisa"

}

} )

// Now, perform the update

>>> collection.update({"ItemNumber" : "6789SID"}, update)

// Inspect the result of the update

>>> collection.find_one({"Type" : "Chair"})

{

u'Status': u'In use'

u'Tags': [u'Chair', u'In use', u'Marketing']

u'ItemNumber': u'6789SID'

u'Location': {

u'Department': u'Marketing'

u'Building': u'2B'

u'DeskNumber': 131131

u'Owner': u'Martin, Lisa'

}

u'_id': ObjectId('4c5973554abffe0e0c000005')

u'Type': u'Chair'

}

这个例子的一个大缺点是它有点长,并且只更新了几个字段。接下来,我们将看看修饰符操作符可以用来完成什么。

修饰运算符

第 4 章详细介绍了 MongoDB shell 如何包含大量的修饰符操作符,您可以使用这些操作符更容易地操作数据,而无需重写整个文档来更改单个字段的值(正如您在前面的示例中所做的那样)。

修饰运算符允许您做任何事情,从更改文档中的一个现有值,到插入整个数组,到从数组中指定的多个项目中删除所有条目。作为一个组,这些操作符使得修改数据变得容易。现在让我们来看看操作符是做什么的,以及如何使用它们。

用$inc 增加一个整数值

您使用$inc运算符将文档中的整数值增加给定的数字 n。以下示例显示了如何将Location.Desknumber的整数值增加 20:

>>> collection.update({"ItemNumber" : "6789SID"}, {"$inc" : {"Location.DeskNumber" : 20}})

接下来,检查更新是否按预期工作:

>>> collection.find_one({"Type" : "Chair"}, fields={"Location" : "True"})

{

u'_id': ObjectId('4c5973554abffe0e0c000005')

u'Location': {

u'Department': u'Marketing'

u'Building': u'2B'

u'Owner': u'Martin, Lisa'

u'DeskNumber': 131151

}

}

注意,$inc操作符只作用于整数值(即数值),而不能作用于任何字符串值,甚至不能作用于作为字符串添加的数值(例如,"123" vs. 123)。

用$set 更改现有值

您可以使用$set操作符来更改任何匹配文档中的现有值。这是一个您会经常使用的运算符。下一个示例更改当前匹配键/值"Location.Department / Development"的任何项目中"Building"的值。

首先使用$set执行更新,确保所有文档都已更新:

>>> collection.update({"Location.Department" : "Development"}

... {"$set" : {"Location.Building" : "3B"} }

... multi = True )

接下来,使用find_one()命令确认一切顺利:

>>> collection.find_one({"Location.Department" : "Development"}, fields={"Location.Building" : True})

{

u'_id': ObjectId('4c57207b4abffe0e0c000000')

u'Location': {u'Building': u'3B'}

}

删除未设置$的键/值字段

同样,使用$unset操作符从文档中删除一个键/值字段,如下例所示:

>>> collection.update({"Status" : "Not used", "ItemNumber" : "2345FDX"}

... {"$unset" : {"Location.Building" : 1 } } )

接下来,使用find_one()命令确认一切顺利:

>>> collection.find_one({"Status" : "Not used", "ItemNumber" : "2345FDX"}, fields={"Location" : True})

{

u'_id': ObjectId('4c592eb84abffe0e0c000004')

u'Location': {u'Department': u'Storage'}

}

用$push 向数组中添加值

假设数组存在,$push操作符允许您向数组添加一个值。如果数组不存在,将使用指定的值创建数组。

Warning

如果您使用$push来更新一个不是数组的现有字段,将会出现一个错误消息。

现在,您已经准备好向一个已经存在的数组添加一个值,并确认是否一切顺利。首先,执行更新:

>>> collection.update({"Location.Owner" : "Anderson, Thomas"}

... {"$push" : {"Tags" : "Anderson"} }, multi = True )

现在,执行find_one()来确认更新是否顺利:

>>> collection.find_one({"Location.Owner" : "Anderson, Thomas"}, fields={"Tags" : "True"})

{

u'_id': ObjectId('4c57207b4abffe0e0c000000')

u'Tags': [u'Laptop', u'Development', u'In Use', u'Anderson']

}

pushpush 和each 将多个值添加到数组中

$push操作符也可以用来将多个值一次添加到一个现有数组中。这可以通过添加$each修改器来实现。这里,同样的规则也适用:数组必须已经存在,否则您将收到一个错误。以下示例结合使用$each修饰符和$正则表达式来执行搜索;这使您能够将更改应用于所有匹配的查询:

>>> collection.update({ "Location.Owner" : re.compile("^Walker,") }

... { '$push' : { 'Tags' : { '$each' : ['Walker','Warranty'] } } } )

接下来,执行find_one()来查看是否一切顺利:

>>> collection.find_one({"Location.Owner" : re.compile("^Walker,")}, fields={"Tags" : True})

{

u'_id': ObjectId('4c57234c4abffe0e0c000002')

u'Tags': [u'Laptop', u'Development', u'In Use', u'Walker', u'Warranty']

}

用$addToSet 向现有数组添加值

$addToSet操作符还允许您向现有数组添加一个值。不同之处在于,这个方法在尝试更新之前检查数组是否已经存在(操作符$push不检查这个条件)。

该运算符只接受一个附加值;然而,知道您可以将$addToSet$each操作符结合使用也很好。我们来看两个例子。首先,让我们使用$addToSet操作符对任何匹配"Type : Chair"的对象执行更新,然后使用find_one()函数检查是否一切顺利:

>>> collection.update({"Type" : "Chair"}, {"$addToSet" : {"Tags" : "Warranty"} }, multi = True)

>>> collection.find_one({"Type" : "Chair"}, {"Tags" : "True"})

{

u'_id': ObjectId('4c5973554abffe0e0c000005')

u'Tags': [u'Chair', u'In use', u'Marketing', u'Warranty']

}

您也可以使用$each语句来添加多个标签。请注意,您使用正则表达式来执行此搜索。此外,列表中的一个标签已经被预先添加;好在不会再加了,因为这是$addToSet专门阻止的:

// Use the $each operator to add multiple tags, including one that was already added

>>> collection.update({"Type" : "Chair", "Location.Owner" : re.compile("^Martin,")}

... {"$addToSet" : { "Tags" : {"$each" : ["Martin","Warranty","Chair","In use"] } } } )

现在是检查一切是否顺利的时候了;具体来说,您希望验证重复的Warranty标记没有被再次添加:

>>> collection.find_one({"Type" : "Chair", "Location.Owner" : re.compile("^Martin,")}, fields={"Tags" : True})

{

u'_id': ObjectId('4c5973554abffe0e0c000005')

u'Tags': [u'Chair', u'In use', u'Marketing', u'Warranty', u'Martin']

}

用$pop 从数组中移除元素

到目前为止,您已经看到了如何使用update()函数向现有文档添加值。现在让我们反过来看看如何删除数据。我们将从$pop操作符开始。

该运算符允许您删除数组中的第一个或最后一个值,但不能删除这两个值之间的任何内容。以下示例从找到的第一个符合"Type" : "Chair"条件的文档中删除Tags数组中的第一个值;然后,该示例使用find_one()命令来确认更新是否一切顺利:

>>> collection.update({"Type" : "Chair"}, {"$pop" : {"Tags" : -1}})

>>> collection.find_one({"Type" : "Chair"}, fields={"Tags" : True})

{

u'_id': ObjectId('4c5973554abffe0e0c000005')

u'Tags': [u'In use', u'Marketing', u'Warranty', u'Martin']

}

相反,给Tags数组一个正值会删除数组中的最后一个匹配项,如下例所示:

>>> collection.update({"Type" : "Chair"}, {"$pop" : {"Tags" : 1}})

接下来,再次执行find_one()功能以确认一切顺利:

>>> collection.find_one({"Type" : "Chair"}, fields={"Tags" : True})

{

u'_id': ObjectId('4c5973554abffe0e0c000005')

u'Tags': [u'In use', u'Marketing', u'Warranty']

}

使用$pull 删除特定值

$pull操作符允许您从数组中删除特定值的每次出现,而不管该值出现了多少次;只要值不变,就会被删除。

我们来看一个例子。首先使用$push操作符将值为Double的相同标签添加到Tags数组中:

>>> collection.update({"Type" : "Chair"}, {"$push" : {"Tags" : "Double"} }, multi = False )

>>> collection.update({"Type" : "Chair"}, {"$push" : {"Tags" : "Double"} }, multi = False )

接下来,通过执行find_one()命令,确保标记被添加了两次。一旦确认标签存在两次,使用$pull操作符删除标签的两个实例:

>>> collection.find_one({"Type" : "Chair"}, fields={"Tags" : True})

{

u'_id': ObjectId('4c5973554abffe0e0c000005')

u'Tags': [u'In use', u'Marketing', u'Warranty', u'Double', u'Double']

}

>>> collection.update({"Type" : "Chair"}, {"$pull" : {"Tags" : "Double"} }, multi = False)

为了确认一切顺利,再次执行find_one()命令,这次确保结果不再列出Double标签:

>>> collection.find_one({"Type" : "Chair"}, fields={"Tags" : True})

{

u'_id': ObjectId('4c5973554abffe0e0c000005')

u'Tags': [u'In use', u'Marketing', u'Warranty']

}

您可以使用$pullAll操作符来执行相同的操作;与$pull不同的是$pullAll让你移除多个标签。再来看一个例子。首先,您需要再次将多个项目添加到Tags数组中,并确认它们已经被添加:

>>> collection.update({"Type" : "Chair"}, {"$addToSet" : { "Tags" : {"$each" : ["Bacon","Spam"] } } } )

>>> collection.find_one({"Type" : "Chair"}, fields={"Tags" : True})

{

u'_id': ObjectId('4c5973554abffe0e0c000005')

u'Tags': [u'In use', u'Marketing', u'Warranty', u'Bacon', u'Spam']

}

现在您可以使用$pullAll操作符来删除多个标签。下面的示例显示了如何使用此运算符;该示例还在之后立即执行一个find_one()命令,以确认BaconSpam标签已经被删除:

>>> collection.update({"Type" : "Chair"}, {"$pullAll" : {"Tags" : ["Bacon","Spam"] } }, multi = False)

>>> collection.find_one({"Type" : "Chair"}, fields={"Tags" : "True"})

{

u'_id': ObjectId('4c5973554abffe0e0c000005')

u'Tags': [u'In use', u'Marketing', u'Warranty']

}

使用 save()快速保存文档

您可以使用save()功能通过 upsert 方法快速添加文档。为此,您还必须定义_id字段的值。如果您要保存的文档已经存在,它将被更新;如果它不存在,将被创建。

让我们看一个保存名为Desktop的文档的例子。首先通过在 shell 中键入一个标识符来指定文档,然后可以用save()函数保存它。一旦保存成功,执行save()函数将从文档中返回ObjectId:

>>> Desktop = ( {

"Status" : "In use"

"Tags" : ["Desktop","In use","Marketing","Warranty"]

"ItemNumber" : "4532FOO"

"Location" : {

"Department" : "Marketing"

"Building" : "2B"

"Desknumber" : 131131

"Owner" : "Martin, Lisa"

}

} )

>>> collection.save(Desktop)

ObjectId('4c5ddbe24abffe0f34000001')

现在假设您意识到忘记在文档中指定一个键/值对。通过定义文档的名称,后跟方括号中的关键字,然后包括所需的内容,可以很容易地将这些信息添加到文档中。一旦你这样做了,你可以通过简单地再次保存整个文档来执行 upsert 这样做将再次从文档中返回ObjectId:

>>> Desktop[ "Type" ] = "Desktop"

>>> collection.save(Desktop)

ObjectId('4c5ddbe24abffe0f34000001')

如您所见,返回的ObjectId的值没有改变。

原子地修改文档

您可以使用find_and_modify()函数自动修改文档并返回结果。find_and_modify()函数只能用来更新一个文档——仅此而已。您还应该记住,默认情况下,返回的文档不会包含所做的修改;获取这些信息需要您指定一个附加参数,new: True

find_and_modify()函数可以与多个参数一起使用,您必须包括update参数或remove参数。以下列表涵盖了所有可用的参数,解释了它们是什么以及它们的作用:

  • query:指定查询的过滤器。如果没有指定,那么集合中的所有文档都将被视为可能的候选文档,之后它遇到的第一个文档将被更新或删除。
  • update:指定用于更新文档的信息。请注意,前面指定的任何修改操作符都可以用于此目的。
  • upsert:如果设置为True,则执行一次上插。
  • sort:按照指定的顺序对匹配的文档进行排序。
  • full_response:如果设置为True,返回整个响应对象。
  • remove:如果设置为True,则删除第一个匹配的文档。
  • new:如果设置为True,则返回更新的文档,而不是选择的文档。然而,这不是默认设置,有时可能会有点混乱。
  • fields:指定您希望看到返回的字段,而不是整个文档。这与find()功能的工作原理相同。注意,_id字段将总是被返回,除非被显式禁用。

让参数发挥作用

你知道参数的作用。现在是时候将它们与find_and_modify()函数结合使用了。首先使用find_and_modify()搜索任何具有"Type" : "Desktop"的键/值对的文档,然后通过设置一个额外的键/值对"Status" : "In repair"来更新每个匹配查询的文档。最后,您希望确保 MongoDB 返回更新的文档,而不是匹配查询的旧文档:

>>> collection.find_and_modify(query={"Type" : "Desktop"}

... update={'$set' : {'Status' : 'In repair'} }, new=True )

{

u'ok': 1.0

u'value': {

u'Status': u'In repair'

u'Tags': [u'Desktop', u'In use', u'Marketing', u'Warranty']

u'ItemNumber': u'4532FOO'

u'Location': {

u'Department': u'Marketing'

u'Building': u'2B'

u'Owner': u'Martin, Lisa'

u'Desknumber': 131131

}

u'_id': ObjectId('4c5dda114abffe0f34000000')

u'Type': u'Desktop'

}

}

让我们看另一个例子。这一次,您将使用find_and_modify()删除一个文档;在这种情况下,输出将显示删除了哪个文档:

>>> collection.find_and_modify(query={'Type' : 'Desktop'}

... sort={'ItemNumber' : -1}, remove=True) {

u'ok': 1.0

u'value': {

u'Status': u'In use'

u'Tags': [u'Desktop', u'In use', u'Marketing', u'Warranty']

u'ItemNumber': u'4532FOO'

u'Location': {

u'Department': u'Marketing'

u'Building': u'2B'

u'Owner': u'Martin, Lisa'

u'Desknumber': 131131

}

u'_id': ObjectId('4c5ddbe24abffe0f34000001')

u'Type': u'Desktop'

}

}

删除数据

大多数情况下,您将使用 Python 驱动程序来添加或修改数据。然而,了解如何删除数据也很重要。Python 驱动程序提供了几种删除数据的方法。首先,您可以使用remove()函数从集合中删除单个文档。其次,您可以使用drop()drop_collection()功能删除整个收藏。最后,您可以使用drop_database()函数删除整个数据库。(看起来你不太可能经常使用这个功能!)

然而,我们将更仔细地看看这些函数,看看所有这些函数的例子。

让我们从查看remove()函数开始。此函数允许您指定一个参数作为参数,用于在当前集合中查找和删除任何匹配的文档。在这个例子中,您使用remove()函数删除每个具有"Status" : "In use"的键/值对的文档;之后,使用find_one()功能确认结果:

>>> collection.remove({"Status" : "In use"})

>>> collection.find_one({"Status" : "In use"})

>>>

您需要小心使用这个函数指定什么样的标准。通常,您应该首先执行一个find(),这样您就可以确切地看到哪些文档将被删除。或者,您可以使用ObjectId删除一个项目。

如果您厌倦了整个集合,您可以使用drop()drop_collection()函数来删除它。两个函数的工作方式相同(实际上,一个只是另一个的别名);具体来说,两者都只需要一个参数,即集合的名称:

>>> db.items.drop()

最后(因为其潜在的破坏性),函数drop_database()使您能够删除整个数据库。您可以使用Connection模块调用这个函数,如下例所示:

>>> c.drop_database("inventory")

在两个文档之间创建链接

数据库引用(DBRefs)是在 PyMongo 中通过 DBRef()函数实现的,它是 DBRef 模块的一部分,可用于在位于不同位置的两个文档之间创建链接。这是一个字段存储约定,可用于应用逻辑。例如,您可以为所有雇员创建一个集合,为所有项目创建另一个集合,然后使用DBRef()函数创建雇员和项目位置之间的引用,而不是为每个项目手动键入它们。

Note

MongoDB 无法确保 DBRefs 有效。因此,在删除可能由 DBRefs 链接的文档时应小心谨慎。

您可能还记得前面的章节,您可以通过两种方式引用数据。首先,您可以添加一个简单的引用(手动引用),它使用一个文档中的_id字段来存储另一个文档中对它的引用。第二,您可以使用DBRef模块,它带来了比手动引用更多的选项。

让我们先创建一个手动引用。从保存文档开始。例如,假设您想要将某个人的信息保存到特定的集合中。下面的例子定义了一个jan字典,并将其保存到people集合中,以获取一个ObjectId:

>>> jan = {

... "First Name" : "Jan"

... "Last Name" : "Walker"

... "Display Name" : "Walker, Jan"

... "Department" : "Development"

... "Building" : "2B"

... "Floor" : 12

... "Desk" : 120103

... "E-Mail" : "``jw@example.com/

... }

>>> people = db.people

>>> people.insert(jan)

ObjectId('4c5e5f104abffe0f34000002')

添加项目并取回其 ID 后,可以使用此信息将项目链接到另一个集合中的另一个文档:

>>> laptop = {

... "Type" : "Laptop"

... "Status" : "In use"

... "ItemNumber" : "12345ABC"

... "Tags" : ["Warranty","In use","Laptop"]

... "Owner" : jan[ "_id" ]

... }

>>> items = db.items

>>> items.insert(laptop)

ObjectId('4c5e6f6b4abffe0f34000003')

现在假设你想找出主人的信息。在这种情况下,您所要做的就是查询在Owner字段中给出的ObjectId;显然,只有当您知道数据存储在哪个集合中时,这才是可能的。

但是假设你不知道这些信息存储在哪里。正是为了处理这样的场景,创建了DBRef()函数。即使您不知道哪个集合包含原始数据,也可以使用这个函数,这样在搜索信息时就不必担心集合名称了。

DBRef()函数有三个参数;它可以接受第四个参数,您可以用它来指定附加的关键字参数。以下是三个主要论点的列表,以及它们能让你做什么:

  • collection(必填):指定原始数据所在的集合(例如people)。
  • id(必填):指定需要引用的单据的_id值。
  • database(可选):指定要引用的数据库的名称。

在使用DBRef函数之前必须加载DBRef模块,所以让我们在进一步操作之前加载模块:

>>> from bson.dbref import DBRef

至此,您已经准备好查看一个DBRef()函数的实际例子。在下面的示例中,您将一个人插入到people集合中,并将一个项目添加到items集合中,使用DBRef来引用所有者:

>>> mike = {

... "First Name" : "Mike"

... "Last Name" : "Wazowski"

... "Display Name" : "Wazowski, Mike"

... "Department" : "Entertainment"

... "Building" : "2B"

... "Floor" : 10

... "Desk" : 120789

... "E-Mail" : "mw@monsters.inc"

... }

>>> people.save(mike)

ObjectId('4c5e73714abffe0f34000004')

在这一点上,没有发生任何有趣的事情。是的,您添加了一个文档,但是您没有添加对它的引用。但是,您确实有文档的ObjectId,所以现在您可以将下一个文档添加到集合中,然后使用DBRef()owner字段指向之前插入的文档的值。特别注意DBRef()函数的语法;特别是,您应该注意到给出的第一个参数是您先前指定的文档所在的集合的名称,而第二个参数只不过是对mike字典中的_id键的引用:

>>> laptop = {

... "Type" : "Laptop"

... "Status" : "In use"

... "ItemNumber" : "2345DEF"

... "Tags" : ["Warranty","In use","Laptop"]

... "Owner" : DBRef('people', mike[ "_id" ])

... }

>>> items.save(laptop)

ObjectId('4c5e740a4abffe0f34000005')

您可能已经注意到,这段代码与您用来创建手动引用的代码没有太大的不同。但是,我们建议您在需要引用特定信息时使用 DBRefs,而不是嵌入它。采用这种方法还意味着无论何时查询引用的信息,都不需要查找集合的名称。

检索信息

你知道如何用DBRef()引用信息;现在让我们假设您想要检索前面引用的信息。您可以使用 Python 驱动程序的dereference()函数来实现这一点。您所需要做的就是将先前指定的包含引用信息的字段定义为一个参数,然后按 Return 键。

为了进行演示,让我们从头到尾看一遍从一个文档到另一个文档引用和检索信息的过程。让我们首先找到包含引用数据的文档,然后检索该文档进行显示。第一步是创建一个查询,查找包含引用信息的随机文档:

>>> items.find_one({"ItemNumber" : "2345DEF"})

{

u'Status': u'In use'

u'Tags': [u'Warranty', u'In use', u'Laptop']

u'ItemNumber': u'2345DEF'

u'Owner': DBRef(u'people', ObjectId('4c5e73714abffe0f34000004'))

u'_id': ObjectId('4c5e740a4abffe0f34000005')

u'Type': u'Laptop'

}

接下来,您希望将此项存储在一个person字典下:

>>> person = items.find_one({"ItemNumber" : "2345DEF"})

此时,您可以使用dereference()函数将Owner字段解引用到person["Owner"]字段作为参数。这是可能的,因为Owner字段链接到您想要检索的数据:

>>> db.dereference(person["Owner"])

{

u'Building': u'2B'

u'Floor': 10

u'Last Name': u'Wazowski'

u'Desk': 120789

u'E-Mail': u'mw@monsters.inc'

u'First Name': u'Mike'

u'Display Name': u'Wazowski, Mike'

u'Department': u'Entertainment'

u'_id': ObjectId('4c5e73714abffe0f34000004')

}

那还不算太糟!这个例子要说明的一点是,DBRefs 技术为存储您想要引用的数据提供了一种很好的方式。此外,它允许在指定集合和数据库名称时有一定的灵活性。如果您想保持数据库整洁,您会发现自己经常使用这个特性,尤其是在数据确实不应该嵌入的情况下。

摘要

在这一章中,我们探索了如何将 MongoDB 的 Python 驱动程序(PyMongo)用于最常用的操作。一路上,我们讲述了如何搜索、存储、更新和删除数据。

我们还研究了如何使用两种方法引用另一个集合中包含的文档:手动引用和 DBRefs。当观察这些方法时,我们已经看到它们的语法非常相似,但是 DBRefs 方法在功能方面提供了更多的健壮性,所以在大多数情况下它是更好的。

下一章将深入探讨 MongoDB 更高级的查询方法。

八、高级查询

Abstract

到目前为止,这几章已经介绍了根据给定的标准查找一个或一系列文档的大多数基本查询机制。有许多机制可以找到给定的文档,并将它们返回到您的应用中,以便进行处理。但是有时这些普通的查询机制不能满足需要,您希望对集合中的大多数或所有文档执行复杂的操作。当需要这种查询或操作时,许多开发者要么遍历集合中的所有文档,要么编写一系列要按顺序执行的查询来执行必要的计算。虽然这是一种有效的做事方式,但是编写和维护起来很麻烦,而且效率很低。正是由于这些原因,MongoDB 提供了一些高级的查询机制,您可以使用它们来最大限度地利用您的数据。我们将在本章研究的高级 MongoDB 特性是全文搜索、聚合框架和 MapReduce 框架。

到目前为止,这几章已经介绍了根据给定的标准查找一个或一系列文档的大多数基本查询机制。有许多机制可以找到给定的文档,并将它们返回到您的应用中,以便进行处理。但是有时这些普通的查询机制不能满足需要,您希望对集合中的大多数或所有文档执行复杂的操作。当需要这种查询或操作时,许多开发者要么遍历集合中的所有文档,要么编写一系列要按顺序执行的查询来执行必要的计算。虽然这是一种有效的做事方式,但是编写和维护起来很麻烦,而且效率很低。正是由于这些原因,MongoDB 提供了一些高级的查询机制,您可以使用它们来最大限度地利用您的数据。我们将在本章研究的高级 MongoDB 特性是全文搜索、聚合框架和 MapReduce 框架。

全文搜索是 MongoDB 中最受欢迎的特性之一。它表示能够在 MongoDB 中创建专门的文本索引,然后在这些索引上执行文本搜索,以定位包含匹配文本元素的文档。MongoDB 全文搜索特性不仅仅是简单的字符串匹配,还包括基于您为文档选择的语言的全词干方法,这是一个非常强大的工具,用于对文档执行语言查询。这个最近引入的特性在 MongoDB 的 2.4 版本中被标记为“实验性的”,因为开发团队仍在努力改进它,这意味着您必须手动激活它才能在您的 MongoDB 环境中使用。

本章将介绍的第二个特性是 MongoDB 聚合框架。在第 4 章和第 6 章中介绍了这个特性,它提供了一整套查询特性,可以让你迭代选择的文档,或者所有的文档,收集或者操作信息。然后,这些查询函数被安排到一个操作管道中,在您的集合上一个接一个地执行这些操作,以从您的查询中收集信息。

我们将讨论的第三个也是最后一个特性叫做 MapReduce,对于那些使用过 Hadoop 的人来说,这个特性听起来很熟悉。MapReduce 是一种强大的机制,它利用 MongoDB 内置的 JavaScript 引擎来实时执行抽象代码。这是一个非常强大的工具,它使用了两个 JavaScript 函数,一个用于映射数据,另一个用于从映射的数据中转换和提取信息。

在本章中要记住的最重要的事情可能是,这些是真正的高级特性,如果它们被误用,可能会给 MongoDB 节点带来严重的性能问题,所以只要有可能,在将它们部署到重要系统之前,应该在测试环境中测试这些特性。

文本搜索

MongoDB 的文本搜索首先创建一个全文索引,并指定您希望被索引的字段,以方便文本搜索。这个文本索引将检查集合中的每个文档,并对每个文本字符串进行标记和词干处理。这个记号化和词干化的过程包括将文本分解成记号,这些记号在概念上接近单词。然后,MongoDB 对每个令牌进行词干分析,以找到该令牌的根概念。例如,假设分解一个字符串到达 token fishing。然后这个标记被追溯到词根 fish,因此 MongoDB 为该文档创建了一个 fish 索引条目。同样的标记化和词干化过程也适用于用户输入的搜索参数,以执行给定的文本搜索。然后将这些参数与每个文档进行比较,并计算相关性分数。然后根据用户的分数将文档返回给用户。

您可能想知道如何对像 the 或 it 这样的单词进行词干处理,如果文档不是英文的会怎么样。答案是这些词和类似的词不会被词干化,MongoDB 文本搜索支持许多语言。

MongoDB 文本搜索引擎是为 MongoDB,Inc .团队编写的用于文本数据检索的专有引擎。MongoDB 文本搜索还利用了 Snowball 字符串处理语言,该语言支持词干提取和停用词,即那些不需要词干提取的词,因为它们在索引或搜索方面不代表任何有价值的概念。

从这一点来看,MongoDB 的文本搜索非常复杂,并且设计得尽可能灵活和准确。

文本搜索的成本和限制

从您所了解的文本搜索功能可以想象,使用 MongoDB 文本搜索会有一些相关的成本。首先,它将未来文档的文档存储分配更改为 usePowerOf2Sizes 选项,该选项指示 MongoDB 分配存储以更有效地重用空闲空间。第二,文本索引很大,根据您存储的文档数量和每个索引字段中的标记数量,它会增长得非常快。第三个限制是,在现有文档上构建文本索引非常耗时,并且需要向具有文本索引的字段添加新条目,这也是成本较高的。第四,像 MongoDB 中的所有东西一样,文本索引在 RAM 中工作得更好。最后,由于文本索引的复杂性和大小,它们目前被限制为每个集合一个。

启用文本搜索

如前所述,文本搜索是在 MongoDB 2.4 中作为一个实验性或测试版特性引入的。因此,您需要在集群中使用这个特性的每个 MongoDB 实例(如果是分片的话,还有 MongoS)上显式启用文本搜索功能。有三种方法可以启用文本搜索;第一种方法是在用于启动或停止 MongoDB 进程的命令中添加以下选项:

--setParameter textSearchEnabled=true

第二种方法是在 MongoDB 实例的配置文件中添加以下选项:

setParameter = textSearchEnabled=true

让文本搜索在 MongoDB 实例上工作的第三种也是最后一种方法是通过 Mongo shell 运行以下命令:

db.adminCommand({ setParameter: 1, textSearchEnabled : true }

有了这个集合,您现在可以在这个节点上使用 MongoDB 全文搜索特性。

Note

这一功能处于测试阶段并不意味着它不起作用。MongoDB,Inc .团队已经付出了相当大的努力来获得这个特性。通过使用该功能并报告您在 MongoDB,Inc. JIRA (jira.mongodb.org)上遇到的任何问题,您可以帮助他们准备好该功能的正式发布。

到目前为止,您应该已经在 MongoDB 实例上启用了文本搜索特性,并准备好利用它了!让我们看看如何创建文本搜索索引和执行文本搜索。

使用文本搜索

尽管我们已经描述了所有的复杂性,MongoDB 文本搜索非常容易使用;创建文本索引的方式与创建任何其他索引的方式相同。例如,为了在我们的理论博客集合的“内容”元素上创建一个文本索引,我将运行以下代码

db.blog.ensureIndex( { content : "text" } );

仅此而已。MongoDB 将处理剩下的工作,并在您的数据库中插入一个文本索引,所有将来有内容字段的文档都将被处理,并将条目添加到要搜索的文本索引中。但是实际上,仅仅创建一个索引是不够的。我们需要一组合适的文本数据来处理和查询。

加载文本数据

最初,我们计划使用来自 twitter 的实时数据流,但这些文档太难看了,无法使用。因此,我们创建了一个由八个文档组成的小批量文件,模仿 twitter feeds,让文本搜索变得简单起来。

继续将来自twitter.tgz的 MongoDB 数据mongoimport到您的数据库中:

$ mongoimport test.json -d test -c texttest

connected to: 127.0.0.1

Sat Jul 6 17:52:19 imported 8 objects

现在我们已经恢复了数据,如果还没有启用文本索引,请继续:

db.adminCommand({ setParameter: 1, textSearchEnabled : true });

{ "was" : false, "ok" : 1 }

既然我们已经启用了文本索引,我们应该在 twitter 数据上创建一个文本索引。

创建文本索引

对于 twitter 数据,我们关心的部分是text字段,它是推文的正文。要设置文本索引,我们运行以下命令:

use test;

db. texttest.ensureIndex( { body : "text" } );

如果您看到错误消息“text search not enabled”,您需要使用刚才显示的命令来确保文本索引正在运行。现在,如果您查看您的日志,您将看到以下内容,其中显示了正在构建的文本索引:

Sat Jul 6 17:54:16.078 [conn41] build index test.texttest { _fts: "text", _ftsx: 1 }

Sat Jul 6 17:54:16.089 [conn41] build index done. scanned 8 total records. 0.01 secs

我们还可以检查集合的索引:

db.texttest.getIndexes()

[

{

"v" : 1

"key" : {

"_id" : 1

}

"ns" : "test.texttest"

"name" : "_id_"

}

{

"v" : 1

"key" : {

"_fts" : "text"

"_ftsx" : 1

}

"ns" : "test.texttest"

"name" : "body_text"

"weights" : {

"body" : 1

}

"default_language" : "english"

"language_override" : "language"

"textIndexVersion" : 1

}

]

好了,我们已经启用了文本搜索,创建了我们的索引,并确认它在那里;现在让我们运行文本搜索命令。

运行文本搜索命令

在我们使用的 MongoDB 版本中,text命令没有 shell 助手,所以我们使用如下的runCommand语法来执行它:

> db.texttest.runCommand( "text", { search :"fish" } )

该命令将返回任何匹配查询字符串"fish"的文档。在这种情况下,它返回了两个文档。输出显示了相当多的调试信息,以及一个"results"数组;这包括许多文件。这些包括匹配文档和返回的得分的组合,匹配文档作为obj。您可以看到匹配文档的文本部分都包含单词 fish 或 fishing,它们都与我们的查询匹配!同样值得注意的是,MongoDB 文本索引是不区分大小写的,这是执行文本查询时的一个重要考虑因素。

Note

请记住,文本搜索中的所有条目都被标记化和词干化。这意味着像 fishy 或 fishing 这样的词将被归结为 fish。

此外,您可以看到分数,0.75 或 0.666,表明该结果与您的查询的相关性—值越高,匹配越好。您还可以看到查询的统计数据,包括返回的对象数量(2)和花费的时间(112 微秒)。

{

"queryDebugString" : "fish||||||"

"language" : "english"

"results" : [

{

"score" : 0.75

"obj" : {

"_id" : ObjectId("51d7ccb36bc6f959debe5514")

"number" : 1

"body" : "i like fish"

"about" : "food"

}

}

{

"score" : 0.6666666666666666

"obj" : {

"_id" : ObjectId("51d7ccb36bc6f959debe5516")

"number" : 3

"body" : "i like to go fishing"

"about" : "recreation"

}

}

]

"stats" : {

"nscanned" : 2

"nscannedObjects" : 0

"n" : 2

"nfound" : 2

"timeMicros" : 112

}

"ok" : 1

}

现在让我们来看看其他一些可以用来增强文本查询的文本搜索特性。

过滤文本查询

我们可以做的第一件事是过滤文本查询。为了细化我们的鱼查询,假设我们只想要引用鱼作为食物的文档,而不是任何匹配“钓鱼”活动的文档。为了添加这个额外的参数,我们使用filter选项并提供一个带有普通查询的文档。因此,为了找到我们的鱼作为食物,我们运行以下:

> db.texttest.runCommand( "text", { search : "fish", filter : { about : "food" } })

{

"queryDebugString" : "fish||||||"

"language" : "english"

"results" : [

{

"score" : 0.75

"obj" : {

"_id" : ObjectId("51d7ccb36bc6f959debe5514")

"number" : 1

"body" : "i like fish"

"about" : "food"

}

}

]

"stats" : {

"nscanned" : 2

"nscannedObjects" : 2

"n" : 1

"nfound" : 1

"timeMicros" : 101

}

"ok" : 1

}

那太完美了;我们只返回了我们想要的一个项目,没有得到不相关的“钓鱼”文档。注意,nScannednscannedObjects的值是 2,这表示该查询从索引中扫描了两个文档(nScanned),然后必须检索这两个文档来检查它们的内容(nScannedObjects)以返回一个匹配的文档(n)。现在我们来看另一个例子。

更复杂的文本搜索

首先运行下面的查询,它将返回两个文档。为了简洁起见,结果被削减到只有文本字段。

db.texttest.runCommand( "text", { search : "cook" })

"body" : "i want to cook dinner"

"body" : "i am to cooking lunch"

如你所见,我们有两份文件,都是关于做饭的。假设我们想从搜索中排除午餐,只返回晚餐。我们可以通过添加–lunch来将文本lunch从我们的搜索中排除。

> db.texttest.runCommand( "text", { search : "cook -lunch" })

{

"queryDebugString" : "cook||lunch||||"

"language" : "english"

"results" : [

{

"score" : 0.6666666666666666

"obj" : {

"_id" : ObjectId("51d7ccb36bc6f959debe5518")

"number" : 5

"body" : "i want to cook dinner"

"about" : "activities"

}

}

]

"stats" : {

"nscanned" : 2

"nscannedObjects" : 0

"n" : 1

"nfound" : 1

"timeMicros" : 150

}

"ok" : 1

}

首先请注意,queryDebugString包含了cooklunch,因为这是我们正在使用的搜索词。还要注意,扫描了两个条目,但只返回了一个。搜索的工作方式是首先找到所有匹配项,然后排除不匹配项。

人们可能会发现最后一个有价值的搜索功能是字符串搜索,它可以用来匹配特定的单词或短语,而无需词干。目前,我们个人搜索的所有元素都被标记化,然后被词干化,每个词都被评估。以下面的查询为例:

> db.texttest.runCommand( "text", { search : "mongodb text search" })

{

"queryDebugString" : "mongodb|search|text||||||"

"language" : "english"

"results" : [

{

"score" : 3.875

"obj" : {

"_id" : ObjectId("51d7ccb36bc6f959debe551a")

"number" : 7

"body" : "i like mongodb text search"

"about" : "food"

}

}

{

"score" : 3.8000000000000003

"obj" : {

"_id" : ObjectId("51d7ccb36bc6f959debe551b")

"number" : 8

"body" : "mongodb has a new text search feature"

"about" : "food"

}

}

]

"stats" : {

"nscanned" : 6

"nscannedObjects" : 0

"n" : 2

"nfound" : 2

"timeMicros" : 537

}

"ok" : 1

}

您可以在queryDebugString中看到每个元素都被评估和查询。您还可以看到,该查询评估并找到了两个文档。现在,请注意当我们运行带有转义引号的相同查询以使其成为字符串文字时的区别:

> db.texttest.runCommand( "text", { search : "\"mongodb text search\"" })

{

"queryDebugString" : "mongodb|search|text||||mongodb text search||"

"language" : "english"

"results" : [

{

"score" : 3.875

"obj" : {

"_id" : ObjectId("51d7ccb36bc6f959debe551a")

"number" : 7

"body" : "i like mongodb text search"

"about" : "food"

}

}

]

"stats" : {

"nscanned" : 6

"nscannedObjects" : 0

"n" : 1

"nfound" : 1

"timeMicros" : 134

}

"ok" : 1

}

您可以看到只返回了一个文档,这个文档实际上包含有问题的文本。您还可以看到,在queryDebugString中,最后一个元素是字符串本身,而不仅仅是三个标记化和词干化的元素。

附加选项

除了我们到目前为止已经讨论过的,还有三个选项可以添加到text函数中。第一个是limit,限制返回的文档数量。它可以按如下方式使用:

> db.texttest.runCommand( "text", { search :"fish", limit : 1 } )

第二个选项是project,它允许您设置将作为查询结果显示的字段。此选项获取一个描述您希望显示哪些字段的文档,0 表示关闭,1 表示打开。默认情况下,指定此选项时,除了_id打开之外,所有元素都关闭。

> db.texttest.runCommand( "text", { search :"fish", project : { _id : 0, body : 1 } } )

第三个也是最后一个选项是language,它允许您指定文本搜索将使用哪种语言。如果没有指定语言,则使用索引的默认语言。语言必须全部用小写字母指定。可以按如下方式调用它:

> db.texttest.runCommand( "text", { search :"fish", lagnuage : "french" } )

目前,文本搜索支持以下语言。

  • 丹麦的
  • 荷兰人
  • 英语
  • 芬兰人的
  • 法语
  • 德国人
  • 匈牙利的
  • 意大利的
  • 挪威的
  • 葡萄牙语
  • 罗马尼亚的
  • 俄语
  • 西班牙语
  • 瑞典的
  • 土耳其的

有关 MongoDB 文本搜索目前支持的更多细节,请参见页面。mongodb。org/manual/reference/command/text/

其他语言的文本索引

我们最初在前面创建了一个简单的文本索引,以便开始我们的文本工作。但是,您可以使用许多其他技术来使您的文本索引更适合您的工作负载。您可能还记得,单词如何词干化的逻辑会根据 MongoDB 用来执行它的语言而改变。默认情况下,所有的索引都是用英语创建的,但是这并不适合很多人,因为他们的数据可能不是英语的,因此语言的规则是不同的。您可以在每个查询中指定要使用的语言,但是当您知道正在使用哪种语言时,这并不十分友好。您可以通过将该选项添加到索引创建中来指定默认语言:

db. texttest.ensureIndex( { content : "text" }, { default_language : "french" } );

这将创建一个以法语为默认语言的文本索引。现在请记住,每个集合只能有一个文本索引,因此在创建这个索引之前,需要删除任何其他索引。

但是如果我们在一个集合中有多种语言呢?文本索引特性提供了一个解决方案,但是它要求您用正确的语言标记所有文档。您可能认为由 MongoDB 来确定给定文档是哪种语言会更好,但是没有编程方式来进行精确的语言匹配。相反,MongoDB 允许您处理指定自己语言的文档。例如,以下面四个文档为例:

{ _id : 1, content : "cheese", lingvo : "english" }

{ _id : 2, content : "fromage", lingvo: "french" }

{ _id : 3, content : "queso", lingvo: "spanish" }

{ _id : 4, content : "ost", lingvo: "swedish" }

它们包括四种语言(在lingvo字段中),如果我们保留任何一种默认语言,那么我们需要指定我们将在其中搜索的语言。因为我们已经指定了给定内容的语言,所以我们可以将该字段用作语言覆盖,并且将使用给定的语言而不是默认语言。我们可以用它创建一个索引,如下所示:

db.texttest.ensureIndex( { content : "text" }, { language_override : "lingvo" } );

因此,这些文档的默认语言将是所提供的语言,任何缺少lingvo字段的文档都将使用默认索引,在本例中为英语。

带文本索引的复合索引

虽然在一个集合中只能有一个文本索引,但是可以让文本索引覆盖文档中多个字段,甚至所有字段。您可以像对普通索引一样指定额外的字段。假设我们想索引内容和任何评论;我们可以这样做。现在,我们可以在这两个字段中进行文本搜索。

db.texttest.ensureIndex( { content : "text", comments : "text" });

您甚至可能希望为文档中的所有字段创建文本索引。MongoDB 有一个通配符说明符,可以用来引用所有文档的所有文本元素;符号是"$**"。如果您希望将它指定为您的文本索引的形式,您将需要为您的文档添加名称的索引选项。这样,自动生成的名称将不会被使用,也不会因为字段太长而导致索引问题。索引的最大长度是 121 个字符,包括集合、数据库和要索引的字段的名称。

Note

强烈建议您为任何包含文本字段的复合索引指定一个名称,以避免名称长度导致的问题。

这为我们提供了以下语法,用于在texttest集合中所有文档的所有字符串元素上创建名为alltextindex的文本索引:

db.texttest.ensureIndex( { "$**": "text" }, { name: "alltextindex" } )

使用复合文本索引可以做的下一件事是为该索引上的不同文本字段指定权重。要做到这一点,可以向要索引的每个字段添加高于默认值 1 的权重值。然后,这些值将以 N:1 的比率增加给定索引结果的重要性。以下面的索引为例:

db.texttest.ensureIndex( { content : "text", comments : "text"}, { weights : { content: 10, comments: 5, } } );

这个索引意味着文档的content部分将比comments值优先 10:5 倍。任何其他字段的默认权重为 1,相比之下,评论的权重为 5,内容的权重为 10。您还可以结合权重和通配符文本搜索参数来对特定字段进行加权。

Note

请注意,如果您有太多的文本索引,您将得到一个“太多的文本索引”错误。如果发生这种情况,您应该删除一个现有的文本索引,以便创建新的文本索引。

除了使用其他文本字段创建复合索引之外,还可以使用其他非文本字段创建复合索引。您可以像添加任何其他索引一样构建这些索引,如下例所示:

db.texttest.ensureIndex( { content : "text", username : 1 });

该命令在文档的content部分创建一个文本索引,在username部分创建一个普通索引。这在使用filter参数时特别有用,因为过滤器实际上是对所有使用的子文档的查询。这些也需要从索引中或通过阅读文档本身来读取。让我们看看前面的例子:

db.texttest.runCommand( "text", { search : "fish", filter : { about : "food" } })

给定这个查询的过滤器,我们将需要索引文档的about部分;否则,每一个理论上匹配的文档都需要完全阅读,然后进行验证,这是一个代价高昂的过程。但是,如果我们按如下方式建立索引,我们可以通过这样的索引来避免这些读取,这样的索引包含了about元素:

db.texttest.ensureIndex( { about : 1, content : "text" });

现在让我们再次运行find命令:

> db.texttest.runCommand( "text", { search : "fish", filter : { about : "food" } })

{

"queryDebugString" : "fish||||||"

"language" : "english"

"results" : [

{

"score" : 0.75

"obj" : {

"_id" : ObjectId("51d7ccb36bc6f959debe5514")

"number" : 1

"body" : "i like fish"

"about" : "food"

}

}

]

"stats" : {

"nscanned" : 1

"nscannedObjects" : 0

"n" : 1

"nfound" : 1

"timeMicros" : 95

}

"ok" : 1

}

您可以看到没有扫描的对象,这应该会提高查询的整体效率。有了这些选项,你应该能够为你的文本搜索带来真正的灵活性和力量。

现在,您应该已经看到了 MongoDB 最新搜索特性的巨大威力,并且应该已经掌握了从文本搜索中获得真正力量的知识。

聚合框架

MongoDB 中的聚合框架表示对集合中的所有数据执行选择操作的能力。这是通过创建一个聚合操作管道来实现的,这些操作将首先对数据按顺序执行,然后每个后续操作将对上一个操作的结果执行。熟悉 Linux 或 Unix shell 的人会认为这构成了操作的 shell 管道。

在聚合框架中有大量的操作符,它们可以作为聚合的一部分来收集数据。在这里,我们将介绍一些高级管道操作符,并通过一些例子来演示如何使用它们。这意味着我们将涵盖以下运营商:

  • $group
  • $limit
  • $match
  • $sort
  • $unwind
  • $project
  • $skip

关于全套操作符的更多细节,请查看聚合文档,可从 http://docs.mongodb.org/manual/aggregation/ 获得。我们已经创建了一个示例集合,您可以使用它来测试一些聚合命令。使用以下命令提取归档文件:

$ tar -xvf test.tgz

x test/

x test/aggregation.bson

x test/aggregation.metadata.json

x test/mapreduce.bson

x test/mapreduce.metadata.json

接下来要做的是运行mongorestore命令来恢复测试数据库:

$ mongorestore test

connected to: 127.0.0.1

Sun Jul 21 19:26:21.342 test/aggregation.bson

Sun Jul 21 19:26:21.342 going into namespace [test.aggregation]

1000 objects found

Sun Jul 21 19:26:21.350 Creating index: { key: { _id: 1 }, ns: "test.aggregation", name: "_id_" }

Sun Jul 21 19:26:21.688 test/mapreduce.bson

Sun Jul 21 19:26:21.689 going into namespace [test.mapreduce]

1000 objects found

Sun Jul 21 19:26:21.695 Creating index: { key: { _id: 1 }, ns: "test.mapreduce", name: "_id_" }

既然我们已经有了要处理的数据集合,我们需要看看如何运行聚合命令以及如何构建聚合管道。为了运行聚合查询,我们使用aggregate命令,并为它提供一个包含管道的文档。对于我们的测试,我们将使用各种管道文档运行以下聚合命令:

> db.aggregation.aggregate({pipeline document})

所以,事不宜迟,让我们开始研究我们的聚合示例。

$组

$group命令顾名思义就是这样做的;它将文档分组在一起,因此您可以创建结果的集合。让我们首先创建一个简单的 group 命令,它将列出我们的“aggregation”集合中所有不同的颜色。首先,我们创建一个_id文档,该文档将列出我们想要分组的集合中的所有元素。因此,我们用$group命令开始我们的管道文档,并向其中添加我们的_id文档:

{ $group : { _id : "$color" } }

现在你可以看到我们有了"$color"_id值。注意color这个名字前面有一个$的标志;这表明该元素是对我们文档中某个字段的引用。这为我们提供了基本的文档结构,所以让我们执行聚合:

> db.aggregation.aggregate( { $group : { _id : "$color" } } )

{

"result" : [

{

"_id" : "red"

}

{

"_id" : "maroon"

}

...

{

"_id" : "grey"

}

{

"_id" : "blue"

}

]

"ok" : 1

}

美元总数

$group操作符的结果中,你可以看到我们的结果堆栈中有许多不同的颜色。结果是一个元素数组,其中包含许多文档,每个文档的_id值是文档的"color"字段中的一种颜色。这实际上并没有告诉我们太多,所以让我们扩展一下我们使用$group命令所做的事情。我们可以使用$sum操作符为我们的组添加一个计数,它可以为找到的值的每个实例增加一个值。为此,我们通过为新字段提供一个名称以及它的值应该是什么来为我们的$group命令添加一个额外的值。在这种情况下,我们需要一个名为"count"的字段,因为它代表每种颜色出现的次数;它的值是{$sum : 1},这意味着我们希望为每个文档创建一个总和,并且每次增加 1。这为我们提供了以下文档:

{ $group : { _id : "$color", count : { $sum : 1 } }

让我们用这个新文档来运行我们的聚合:

> db.aggregation.aggregate({ $group : { _id : "$color", count : { $sum : 1 } } }

{

"result" : [

{

"_id" : "red"

"count" : 90

}

{

"_id" : "maroon"

"count" : 91

}

...

{

"_id" : "grey"

"count" : 91

}

{

"_id" : "blue"

"count" : 91

}

]

"ok" : 1

}

现在你可以看到每种颜色出现的频率。我们可以通过向_id文档添加额外的元素来进一步扩展我们正在分组的内容。假设我们想要找到由"color""transport"组成的组。为此,我们可以将_id更改为包含项目子文档的文档,如下所示:

{ $group : { _id : { color: "$color", transport: "$transport"} , count : { $sum : 1 } } }

如果我们运行这个,我们会得到一个大约 50 个元素长的结果,太长了,无法在这里显示。对此有一个解决方案,那就是$limit操作符。

美元限额

$limit操作符是我们将合作的下一个管道操作符。顾名思义,$limit用于限制返回结果的数量。在我们的例子中,我们希望使现有管道的结果更易于管理,所以让我们给结果增加一个 5 的限制。为了增加这个限制,我们需要将我们的一个文档转换成一组管道文档。

[

{ $group : { _id : { color: "$color", transport: "$transport"} , count : { $sum : 1 } } }

{ $limit : 5 }

]

这将为我们提供以下结果:

> db.aggregation.aggregate( [ { $group : { _id : { color: "$color", transport: "$transport"} , count : { $sum : 1 } } }, { $limit : 5 } ] )

{

"result" : [

{

"_id" : {

"color" : "maroon"

"transport" : "motorbike"

}

"count" : 18

}

{

"_id" : {

"color" : "orange"

"transport" : "autombile"

}

"count" : 18

}

{

"_id" : {

"color" : "green"

"transport" : "train"

}

"count" : 18

}

{

"_id" : {

"color" : "purple"

"transport" : "train"

}

"count" : 18

}

{

"_id" : {

"color" : "grey"

"transport" : "plane"

}

"count" : 18

}

]

"ok" : 1

}

您现在可以看到添加到_id的传输元素中的额外字段,我们已经将结果限制为只有五个。现在,您应该看到我们如何从多个操作符构建管道,以从我们的集合中提取数据聚合信息。

$匹配

我们要查看的下一个操作符是$match,它用于有效地返回聚合管道中普通 MongoDB 查询的结果。$match操作符最好用在管道的开始,以限制最初放入管道的文档数量;通过限制处理的文档数量,我们显著降低了性能开销。例如,假设我们只想对那些num值大于 500 的文档执行管道操作。我们可以使用查询{ num : { $gt : 500 } }返回所有符合这个标准的文档。如果我们将这个查询作为一个$match添加到我们现有的聚合中,我们会得到以下结果:

[

{ $match : { num : { $gt : 500 } } }

{ $group : { _id : { color: "$color", transport: "$transport"} , count : { $sum : 1 } } }

{ $limit : 5 }

]

这将返回以下结果:

{

"result" : [

{

"_id" : {

"color" : "white"

"transport" : "boat"

}

"count" : 9

}

{

"_id" : {

"color" : "black"

"transport" : "motorbike"

}

"count" : 9

}

{

"_id" : {

"color" : "maroon"

"transport" : "train"

}

"count" : 9

}

{

"_id" : {

"color" : "blue"

"transport" : "autombile"

}

"count" : 9

}

{

"_id" : {

"color" : "green"

"transport" : "autombile"

}

"count" : 9

}

]

"ok" : 1

}

您会注意到,返回的结果几乎与前面的示例完全不同。这是因为文档的创建顺序已经改变。因此,当我们运行这个查询时,我们限制了输出,删除了之前输出的原始文档。您还会看到,我们的计数是先前结果的一半。这是因为我们已经将潜在的数据集缩减到原来的一半。如果我们希望返回的结果保持一致,我们需要调用另一个管道操作符$sort

$排序

正如您刚才看到的,$limit命令可以改变结果中返回的文档,因为它反映了执行聚合时文档最初输出的顺序。随着$sort命令的出现,这个问题可以得到解决。我们只需要在提供限制之前对特定字段进行排序,以便返回相同的有限结果集。$sort的语法与普通查询相同;您可以指定要排序的文档,正数表示升序,负数表示降序。为了展示这是如何工作的,让我们在有和没有匹配以及限制为 1 的情况下运行我们的查询。您将会看到,在$limit之前使用$sort,我们可以以相同的顺序返回文档。

这给了我们对

[

{ $group : { _id : { color: "$color", transport: "$transport"} , count : { $sum : 1 } } }

{ $sort : { _id :1 } }

{ $limit : 5 }

]

该查询的结果是:

{

"result" : [

{

"_id" : {

"color" : "black"

"transport" : "autombile"

}

"count" : 18

}

]

"ok" : 1

}

第二个查询如下所示:

[

{ $match : { num : { $gt : 500 } } }

{ $group : { _id : { color: "$color", transport: "$transport"} , count : { $sum : 1 } } }

{ $sort : { _id :1 } }

{ $limit : 1 }

]

该查询的结果是

{

"result" : [

{

"_id" : {

"color" : "black"

"transport" : "autombile"

}

"count" : 9

}

]

"ok" : 1

}

您会注意到两个查询现在包含相同的文档,它们只是在计数上有所不同。这意味着我们的排序已经在限制之前被应用,并允许我们得到一致的结果。这些操作符应该让您了解到,通过构建一个操作符管道来操纵事物,直到我们得到想要的结果,您可以驱动多大的力量。

$展开

我们要看的下一个操作符是$unwind。这需要一个数组,并为每个数组元素将每个元素拆分到一个新文档中(在内存中,而不是添加到您的集合中)。与创建 shell 管道一样,理解$unwind操作符输出内容的最佳方式就是自己运行它并评估输出。让我们来看看$unwind的结果:

db.aggregation.aggregate({ $unwind : "$vegetables" });

{

"result" : [

{

"_id" : ObjectId("51de841747f3a410e3000001")

"num" : 1

"color" : "blue"

"transport" : "train"

"fruits" : [

"orange"

"banana"

"kiwi"

]

"vegetables" : "corn"

}

{

"_id" : ObjectId("51de841747f3a410e3000001")

"num" : 1

"color" : "blue"

"transport" : "train"

"fruits" : [

"orange"

"banana"

"kiwi"

]

"vegetables" : "brocoli"

}

{

"_id" : ObjectId("51de841747f3a410e3000001")

"num" : 1

"color" : "blue"

"transport" : "train"

"fruits" : [

"orange"

"banana"

"kiwi"

]

"vegetables" : "potato"

}

...

]

"ok" : 1

}

现在,我们的结果数组中有 3000 个文档,每个文档都有自己的蔬菜和原始源文档的其余部分!你可以看到我们可以用$unwind做的事情的威力,以及如何用一个非常大的巨型文档集合给自己找麻烦。请始终记住,如果您首先运行匹配,那么在运行其他更密集的聚合操作之前,您可以减少要处理的对象的数量。

$项目

下一个操作符$project用于限制字段或重命名作为文档一部分返回的字段。这就像可以在find命令上设置的字段限制参数一样。这是减少聚合返回的多余字段的最佳方式。假设我们只想查看每个文档中的水果和蔬菜;我们可以提供一个文档,显示我们希望显示(或不显示)哪些元素,就像我们添加到我们的find命令中一样。举以下例子:

[

{ $unwind : "$vegetables" }

{ $project : { _id: 0, fruits:1, vegetables:1 } }

]

该投影返回以下结果:

{

"result" : [

{

"fruits" : [

"orange"

"banana"

"kiwi"

]

"vegetables" : "corn"

}

{

"fruits" : [

"orange"

"banana"

"kiwi"

]

"vegetables" : "brocoli"

}

{

"fruits" : [

"orange"

"banana"

"kiwi"

]

"vegetables" : "potato"

}

...

]

"ok" : 1

}

这比以前好,因为现在我们的文档没有以前那么大了。但是更好的办法是减少归还文件的数量。我们的下一个操作员会帮你的。

$跳过

$skip是与$limit操作符互补的管道操作符,但是它不是将结果限制在前 X 个文档,而是跳过前 X 个文档并返回所有其他剩余的文档。我们可以用它来减少归还文件的数量。如果我们用值 2995 将它添加到前面的查询中,我们将只返回五个结果。这将为我们提供以下查询:

[

{ $unwind : "$vegetables" }

{ $project : { _id: 0, fruits:1, vegetables:1 } }

{ $skip : 2995 }

]

结果是

{

"result" : [

{

"fruits" : [

"kiwi"

"pear"

"lemon"

]

"vegetables" : "pumpkin"

}

{

"fruits" : [

"kiwi"

"pear"

"lemon"

]

"vegetables" : "mushroom"

}

{

"fruits" : [

"pear"

"lemon"

"cherry"

]

"vegetables" : "pumpkin"

}

{

"fruits" : [

"pear"

"lemon"

"cherry"

]

"vegetables" : "mushroom"

}

{

"fruits" : [

"pear"

"lemon"

"cherry"

]

"vegetables" : "capsicum"

}

]

"ok" : 1

}

这就是如何使用$skip操作符来减少返回的条目数。您还可以使用互补的$limit操作符以同样的方式限制结果的数量,甚至可以将它们组合起来,在一个集合的中间挑选出一定数量的结果。假设我们想要 3000 个条目的数据集的结果 1500–1510。我们可以提供 1500 的$skip值和 10 的$limit,这将只返回我们想要的 10 个结果。

我们已经讨论了 MongoDB 聚合框架中的一些顶级管道操作符。有许多较小的运算符可以在顶级管道运算符中用作管道表达式。其中包括一些地理函数、数学函数(如平均值、第一名和最后一名)以及一些日期/时间和其他操作。所有这些都可以用来组合执行聚合操作,就像我们已经讨论过的那样。请记住,管道中的每个操作都将根据上一个操作的结果来执行,您可以输出并单步执行它们来创建您想要的结果。

数据处理

MapReduce 是 MongoDB 中最复杂的查询机制之一。它通过两个 JavaScript 函数来工作,mapreduce。这两个函数是完全由用户定义的,这给了您难以置信的灵活性!几个简短的例子将展示一些你可以用 MapReduce 做的事情。

MapReduce 的工作原理

在我们深入例子之前,最好先了解一下什么是 Map/Reduce 以及它是如何工作的。在 MongoDB 的 MapReduce 实现中,我们向给定的集合发出一个专门的查询,然后来自该查询的所有匹配文档都被输入到我们的map函数中。这个map函数被设计用来生成键/值对。然后,任何具有多个值的键集合都被输入到reduce函数,该函数返回输入数据的聚合结果。在这之后,还有一个可选的步骤,可以通过一个finalize函数完成数据的完美呈现。

设置测试文档

首先,我们需要设置一些文档来进行测试。我们已经创建了一个mapreduce集合,它是您之前恢复的test数据库的一部分。如果您还没有恢复它,请使用以下命令提取归档文件:

$ tar -xvf test.tgz

x test/

x test/aggregation.bson

x test/aggregation.metadata.json

x test/mapreduce.bson

x test/mapreduce.metadata.json

然后运行mongorestore命令来恢复test数据库:

$ mongorestore test

connected to: 127.0.0.1

Sun Jul 21 19:26:21.342 test/aggregation.bson

Sun Jul 21 19:26:21.342 going into namespace [test.aggregation]

1000 objects found

Sun Jul 21 19:26:21.350 Creating index: { key: { _id: 1 }, ns: "test.aggregation", name: "_id_" }

Sun Jul 21 19:26:21.688 test/mapreduce.bson

Sun Jul 21 19:26:21.689 going into namespace [test.mapreduce]

1000 objects found

Sun Jul 21 19:26:21.695 Creating index: { key: { _id: 1 }, ns: "test.mapreduce", name: "_id_" }

这将为您提供一个使用 MapReduce 的文档集合。首先,让我们看看世界上最简单的地图功能。

使用地图函数

该函数将从mapreduce集合中的每个文档“发出”颜色和num值。这两个字段将以键/值的形式输出,第一个参数(颜色)作为键,第二个参数(数字)作为值。这在开始时很难理解,所以让我们看看执行这个 emit 的简单的map函数:

var map = function() {

emit(this.color, this.num);

};

为了运行 Map/Reduce,我们还需要一个reduce函数,但是在做任何有趣的事情之前,让我们看看空的reduce函数的结果是什么,以了解会发生什么。

var reduce = function(color, numbers) { };

在您的 shell 中输入这两个命令,您就拥有了运行 MapReduce 所需的一切。

您需要提供的最后一件事是 MapReduce 要使用的输出字符串。这个字符串定义了这个 MapReduce 命令的输出应该放在哪里。两个最常见的选项是

  • 收藏
  • 到控制台(内嵌)

对于我们目前的目的,让我们输出到屏幕上,这样我们就可以看到到底发生了什么。为此,我们传递一个带有值为{ inline : 1 }out选项的文档,如下所示:

{ out : { inline : 1 } }

这为我们提供了以下命令:

db.mapreduce.mapReduce(map,reduce,{ out: { inline : 1 } });

结果看起来像这样:

{

"results" : [

{

"_id" : "black"

"value" : null

}

{

"_id" : "blue"

"value" : null

}

{

"_id" : "brown"

"value" : null

}

{

"_id" : "green"

"value" : null

}

{

"_id" : "grey"

"value" : null

}

{

"_id" : "maroon"

"value" : null

}

{

"_id" : "orange"

"value" : null

}

{

"_id" : "purple"

"value" : null

}

{

"_id" : "red"

"value" : null

}

{

"_id" : "white"

"value" : null

}

{

"_id" : "yellow"

"value" : null

}

]

"timeMillis" : 95

"counts" : {

"input" : 1000

"emit" : 1000

"reduce" : 55

"output" : 11

}

"ok" : 1

}

这表明每个“关键”颜色值是单独分离出来的,并且是每个文档的唯一_id值。因为我们没有为每个文档的值部分指定任何内容,所以它被设置为null。我们可以通过为我们想要的 MapReduce 结果添加输出部分来对此进行修改。在这种情况下,我们需要每个函数的概要。要做到这一点,我们可以使用函数来修改我们想要返回的对象,以代替null。在这种情况下,让我们返回每种颜色的所有值的总和。为此,我们可以创建一个函数,该函数将返回传递给reduce函数的每种颜色的所有数字数组的总和。幸运的是,我们可以使用一个叫做Array.sum的便利函数来对一个数组的所有值求和。这为我们提供了以下reduce功能:

var reduce = function(color, numbers) {

return Array.sum(numbers);

};

太好了。除了我们的内联输出,我们还可以让 MapReduce 写入一个集合;为此,我们只需用我们希望输出到的集合的名称替换那个{ inline : 1 }。所以让我们输出到一个叫做mrresult的集合。这为我们提供了以下命令:

db.mapreduce.mapReduce(map,reduce,{ out: "mrresult" });

当用我们新的reduce函数执行时,它给出了以下结果:

{

"result" : "mrresult"

"timeMillis" : 111

"counts" : {

"input" : 1000

"emit" : 1000

"reduce" : 55

"output" : 11

}

"ok" : 1

}

如果您现在想要查看文档结果,您需要从mrresult集合中查询它们,如下所示:

> db.mrresult.findOne();

{ "_id" : "black", "value" : 45318 }

现在我们有了一个基本的工作系统,我们可以得到更高级的!

高级 MapReduce

假设我们想要的不是所有值的总和,而是平均值!这变得更加困难,因为我们需要添加另一个变量——我们拥有的对象数量!但是我们如何从map函数中传递两个变量呢?毕竟,emit 只接受两个参数。我们可以进行各种各样的“欺骗”;我们返回一个 JSON 文档,它可以有任意多的字段!因此,让我们扩展我们原来的 map 函数,返回一个包含颜色值和计数器值的文档。首先,我们将文档定义为一个新变量,填充 JSON 文档,然后发出该文档。

var map = function() {

var value = {

num : this.num

count : 1

};

emit(this.color, value);

};

请注意,我们将计数器值设置为 1,以便对每个文档只计数一次!现在来看一下reduce函数。它需要处理我们之前创建的一系列有价值的文档。最后要注意的是,我们需要在我们的reduce函数的return函数中返回相同的值,这些值是在我们的map函数中创建并发送给我们的 emit 的。

Note

你也可以通过使用包含所有数字的数组的长度来完成我们在这里做的所有事情。但是通过这种方式,您可以看到更多关于 MapReduce 的功能。

为了处理这个数组,我们创建了一个简单的for循环,数组的长度,我们迭代每个成员,并将每个文档的numcount添加到新的返回变量reduceValue中。现在我们简单地返回这个值,我们有我们的结果。

var reduce = function(color, val ) {

reduceValue = { num : 0, count : 0};

for (var i = 0; i < val.length; i++) {

reduceValue.num += val[i].num;

reduceValue.count += val[i].count;

}

return reduceValue;

};

此时,你应该想知道这是如何得到我们的平均值的。我们有计数和数量,但没有实际的平均值!如果你再次运行 MapReduce,你可以看到自己的结果。现在,请注意,每次输出到一个集合时,MapReduce 都会在写入之前删除该集合!对我们来说,这是一件好事,因为我们只希望这次运行的结果,但它可能会在未来回来困扰你。如果您想合并两者的结果,您可以制作一个类似于{ out : { merge : "mrresult" } }的输出文档。

db.mapreduce.mapReduce(map,reduce,{ out: "mrresult" });

现在让我们快速检查一下这些结果:

> db.mrresult.findOne();

{

"_id" : "black"

"value" : {

"num" : 18381

"count" : 27028

}

}

不,没有平均值。这意味着我们有更多的工作要做,但是如果我们必须返回一个匹配 emit 输入的文档,我们如何计算平均值呢?我们需要第三个函数!MapRreduce 提供了一个名为finalize的函数。这允许您在返回 MapReduce 结果之前进行最后的清理。让我们编写一个函数,它将从reduce获取结果,并为我们计算平均值:

var finalize = function (key, value) {

value.avg = value.num/value.count;

return value;

};

是的,就这么简单。所以现在我们的mapreducefinalize函数都准备好了,我们只需将它们添加到我们的调用中。在最后一个文档中设置了finalize选项;连同out,这给了我们以下命令:

db.mapreduce.mapReduce(map,reduce,{ out: "mrresult", finalize : finalize });

让我们从这里查询一个示例文档:

> db.mrresult.findOne();

{

"_id" : "black"

"value" : {

"num" : 45318

"count" : 91

"avg" : 498

}

}

现在好多了!我们有我们的数字,我们的计数,我们的平均值!

调试 MapReduce

调试 Map/Reduce 是一项相当耗时的任务,但是有几个小技巧可以让你的生活变得更轻松。首先让我们来看看调试一个map。您可以通过用函数重载 emit 来调试map,如下所示:

var emit = function(key, value) {

print("emit results - key: " + key + " value: " + tojson(value));

}

这个emit函数将返回与map函数相同的键和值结果。您可以使用map.apply()和您收集的示例文档来测试一个,如下所示:

> map.apply(db.mapreduce.findOne());

emit results - key: blue value: { "num" : 1, "count" : 1 }

既然您已经知道了对您的map的期望,那么您可以开始调试您的reduce了。你首先需要确认你的mapreduce以相同的格式返回——这很关键。接下来你可以做的是创建一个短数组,里面有一些值,就像传入你的reduce的那些值一样,如下所示:

a = [{ "num" : 1, "count" : 1 },{ "num" : 2, "count" : 1 },{ "num" : 3, "count" : 1 }]

现在可以如下调用reduce。这将允许您查看 emit 返回的值:

>reduce("blue",a);

{ "num" : 6, "count" : 3 }

如果所有其他方法都失败了,并且您对函数内部发生的事情感到困惑,不要忘记您可以使用printjson()函数将任何 JSON 值打印到mongodb日志文件中以供读取。在调试软件时,这总是一个有价值的工具。

摘要

到目前为止,您应该对 MongoDB 中的功能和灵活性有了确切的了解,使用了三个最强大、最灵活的查询系统。通过阅读本章,你应该知道如何使用文本索引在多种语言中执行强大的文本搜索。您应该能够使用 MongoDB 聚合框架创建高度复杂和灵活的聚合。最后,您现在应该能够使用强大的 JavaScript 支持的 MapReduce,这将允许您对数据编写强大的分组和转换。