At the Forge - CouchDB 视图

作者:Reuven M. Lerner

上个月的专栏初步介绍了 CouchDB,一个非关系型开源数据库服务器,现在由 Apache 软件基金会赞助。CouchDB 使用许多与 Web 相关的标准:数据以 JSON 格式存储,通信使用 JSON 和 RESTful 资源进行,函数用 JavaScript 编写。CouchDB 不像其他一些非关系型 (NoSQL) 数据库(如 MongoDB 和 Cassandra)那样快速。但是,CouchDB 的设计宗旨是可靠且易于在多台服务器之间复制——这与关系数据库大相径庭,后者的复制在最好的情况下仍然有点令人恼火。

上个月,我解释了在创建 CouchDB 数据库后,如何使用 curl 实用程序来插入、更新和删除文档。每个“文档”都不过是一个 JSON 对象,这意味着它基本上是一个哈希(一个 Python 字典),然后可能包含任意嵌套级别的数组、哈希和标量值(即字符串和数字)。因此,我可以使用 HTTP 创建一个数据库PUT请求

curl -X PUT https://127.0.0.1:5984/atf

然后,我可以使用 HTTP 向该数据库添加一些文档POST请求

curl -X POST https://127.0.0.1:5984/atf
     -d '{"first_name" : "Atara", "middle_name": "Margalit", 
          "sex":"f", "last_name" : "Lerner-Friedman", 
          "birthday" : "2000-dec-16"}'

curl -X POST https://127.0.0.1:5984/atf
     -d '{"first_name" : "Shikma", "middle_name": "Bruria", 
          "sex":"f", "last_name" : "Lerner-Friedman", 
          "birthday" : "2002-dec-17"}'

curl -X POST https://127.0.0.1:5984/atf
     -d '{"first_name" : "Amotz", "middle_name": "David", 
          "sex":"m", "last_name" : "Lerner-Friedman", 
          "birthday" : "2005-oct-31"}'

然后,我可以通过使用 HTTP 来检查是否有三个文档GET对数据库的请求

bash-3.2# curl -X GET https://127.0.0.1:5984/atf

   {"db_name":"rmltest","doc_count":3,
    "doc_del_count":0,"update_seq":3,"purge_seq":0,
    "compact_running":false,"disk_size":12377,
    "instance_start_time":"1273430793169153","disk_format_version":4}

正如您所见,"doc_count"属性显示此数据库中确实有三个文档。

现在,如果您只有三个文档,查询它们就没有多大意义。但是,如果您有 300 个,甚至 300,000 个文档,您肯定不希望仅仅为了确定哪个是最佳匹配和/或最合适的而迭代它们。

如果您使用的是关系数据库服务器,您将使用 SQL 来检索与特定标准集匹配的行。即使是我今年早些时候介绍过的 MongoDB,也提供了一种类似于 SQL 的查询语言。然而,CouchDB 提供了一个完全不同的查询系统,该系统基于 JavaScript 函数和 MapReduce 范例。CouchDB 的语法需要一些时间才能适应,特别是如果您是编写 JavaScript 函数的新手。然而,一些小函数可以为您提供强大的功能,这(也许)是 CouchDB 成功的秘诀。

CouchDB 视图

我已经解释过,CouchDB 将每个存储的数据项称为“文档”。CouchDB 定义了一种特殊的文档,称为“设计文档”,其中包含一个“视图”——当您想要执行查询时执行的 JavaScript 代码。(设计文档也可能包含“show”函数,这些函数可以对数据的显示方式进行排序或以其他方式修改,但我在本专栏中不会讨论“show”函数。)如果您仅开发数据库或应用程序,您可能希望避免永久视图的开销,而是创建临时视图。临时视图设置起来花费的时间更少,并且更灵活一些,但它们的执行速度要慢得多。

因此,让我们首先创建一个临时视图和一些基本的 JavaScript 视图。对于数据,我使用的是上面输入的关于我的三个孩子的信息。(如果您愿意,可以随意替换有关您自己孩子的信息。)我发现使用 Futon(CouchDB 附带的基于 Web 的管理和维护工具)创建临时视图最容易。只需将您的 Web 浏览器指向运行 CouchDB 的服务器,端口为 5984,然后转到您选择的数据库。然后,从右上角的下拉菜单中选择临时视图。

现在您的屏幕应由两部分组成:在左侧,您有一个简单的 JavaScript 函数,在标题“map”下

function(doc) {
  emit(null, doc);
}

如果您曾经在 Ruby、Python、JavaScript 或 Lisp 等语言中使用过“map”,那么这个函数对您来说可能已经有意义了:您的函数针对文档列表重复调用。如果它产生键值对,则该对将添加到跨所有文档运行的函数的输出中。

例如,示例函数(匿名)将文档作为参数,并返回一个 null 键和文档本身作为值。如果您单击代码下的“运行”,您将获得一组结果:左侧有三个“null”键,右侧有原始文档(及其强制性的 _id 字段)。

当然,您可以修改函数,使其仅输出有关女孩的信息。为此,请编写

function(doc)
{
    if (doc['sex'] == 'f')
    {
        emit(null, doc);
    }
}

请注意,通过使用简单的 if 语句,您可以消除不需要的行。现在,如果您有兴趣获取所有文档,但要进行排序,该怎么办?CouchDB 按键对结果进行排序,这意味着您使用的键不仅对于识别结果文档很有用,而且对于对其进行排序也很有用。例如,您可以按名字对结果进行排序

function(doc) {
        emit(doc.first_name, doc);
}

在我的例子中,这意味着我首先获得我儿子的记录(Amotz),然后是 Atara,然后是 Shikma。在这种特殊情况下,按姓氏排序没有太大帮助,因为他们的姓氏都相同。但是,键可以是任何数据类型,这意味着您甚至可以使用数组按姓氏然后按名字排列项目

function(doc) {
        emit([doc.last_name, doc.first_name], doc);
}

您也可以按生日对它们进行排序

function(doc) {
    emit(doc.birthday, doc)
}

但是,这不一定会产生您想要的效果。“birthday”字段是一个文本字符串,这意味着排序将作为字符串而不是日期完成。(就我孩子的生日而言,排序恰好可以正常工作,但这是一种幸运的巧合,而不是 CouchDB 固有的。)

如果要创建永久视图,有几种方法可以做到这一点。您可以使用 Futon(基于 Web 的)界面,并且可以通过单击临时视图屏幕上的另存为将任何临时视图转换为永久视图。但是另一种方法,如果您要编写复杂的代码,则更灵活一点,是使用 curl 来PUT在服务器上创建一个新的设计文档。此文档包含 JSON,就像 CouchDB 中的所有其他文档一样,但它有许多字段被 CouchDB 特殊处理。这是我的文件,我将其命名为 simpleview.json

{
    "_id" : "_design/example",
    "views": {
        "show_by_birthday": {
            "map" : "function(doc){ emit(doc.birthday, doc) }"
        }
    }
}

然后,我使用 curl 上传了此文件的内容,如下所示

curl -X PUT https://127.0.0.1:5984/atf/_design/simpleview 
 ↪-d @simpleview.json

通过使用 -d 标志和 @ 符号,我可以告诉 curl 从文件而不是命令行上传 JSON。我将其上传到设计文档(您可以从"_design/"在其名称的开头看到),视图名为 simpleview。上传后,我可以使用 Futon 运行它(通过转到菜单项"show_by_birthday"),或者再次使用 curl

curl -X GET https://127.0.0.1:5984/atf/_design/simpleview/
↪_view/show_by_id

查询结果是相同的,无论如何。Futon 以更友好的格式显示它们,但显然程序通过 HTTP 处理 JSON 输出会更容易。如果您想编辑视图,您可以重新通过 curl 上传它,或者使用 Futon 转到视图并编辑 JavaScript 函数。

Reduce

到目前为止,这里的示例都集中在编写“map”函数上,该函数采用文档列表并输出一系列键值对。但是,正如您可能想象的那样,MapReduce 范例有两个部分,第二个部分称为 reduce。其思想是您使用 map 来过滤和转换数据为列表,然后使用 reduce 将该列表转换为更有用的东西,通常是单个值。例如,您可以将 reduce 函数定义如下

function(keys, values, rereduce) {
    return 1;
}

在 Futon 中,这返回一个包含三个文档的列表,其键是生日,值是数字 1。老实说,这没什么意思,因为您可以从“map”请求中完成相同的事情。但是,如果您从 curl 调用相同的查询,您会得到完全不同的结果

{"rows":[
    {"key":null,"value":1}
]}

为什么会有差异?为什么现在只有一行?

答案是 reduce 旨在“reduce”事物。这意味着它不是为每一行返回结果,而是从所有行返回单个结果。reduce 函数实际上可以通过两种方式调用

  • 通常的方式,其中“keys”和“values”表示文档键和值。在这种情况下,“rereduce”参数设置为 false。

  • 对于重新 reduce,“rereduce”参数设置为 true,键为 null,值表示来自“reduce”函数先前部分运行的值。

正如 CouchDB Wiki 所述,这意味着 reduce 函数必须既是可交换的(即,处理参数的顺序无关紧要)又是结合律的(即,执行操作的顺序无关紧要)。这通常(但并非总是)意味着 reduce 函数最终执行加法或乘法,返回对所有文档执行该函数的结果。我发现的一个 reduce 函数示例计算了映射结果的标准差,因此您可以找出文档在至少一个维度上的相似程度。

学习在 CouchDB 中使用 MapReduce 可能需要一些时间。然而,这种范例已经证明了自己大约十年。例如,作为 Google 搜索系统的支柱,MapReduce 已经展示了出色的性能和灵活性。当然,Google 正在使用一些(或某些东西)不是 CouchDB 的东西,但具有类似的界面和范例。

结论

如果您喜欢使用 JavaScript、MVCC、易于复制和 JSON 文档的想法,CouchDB 可能是一个不错的选择。显然,它不如其一些非 SQL 竞争对手(如 MongoDB 和 Cassandra)那样快。但是,CouchDB 内置的(且复杂的)Web 界面、RESTful 通信和 MapReduce 的灵活性都是使用它的充分理由。CouchDB 非常容易设置和使用,为什么不尝试一下呢?即使您最终不使用它,CouchDB 也是学习 MapReduce 并尝试使用它创建一些小函数的好方法。

资源

CouchDB 的主页位于 Apache 项目 (couchdb.apache.org)。在那里,您不仅可以下载软件,还可以阅读文档,从教程到活跃的 Wiki。CouchDB 网站还提供指向各种语言驱动程序的链接,您在使用 CouchDB 时可能会使用这些驱动程序。

如果您对 CouchDB 使用的 JSON 格式感兴趣,您可以在主网站了解更多信息:json.org

最后,关于 CouchDB 的两本好书在过去几个月中出版。《Beginning CouchDB》,作者 Joe Lennon,由 Apress 出版,更适合初学者,但它对 CouchDB、Futon 以及您可能如何使用该系统进行了扎实的介绍。《CouchDB: The Definitive Guide》,作者 J. Chris Anderson、Jan Lehnardt 和 Noah Slater,由 O'Reilly 出版,更高级和内容丰富,但可能不适合非关系数据库的初学者。

有关 CouchDB 的有趣 MapReduce 函数列表,以及关于它们如何工作的解释,请参阅 CouchDB Wiki 上的此页面:wiki.apache.org/couchdb/View_Snippets

Reuven M. Lerner 是一位资深的 Web 开发人员、架构师和培训师。他是西北大学学习科学博士候选人,研究协作在线社区的设计和分析。Reuven 与他的妻子和三个孩子住在以色列的莫迪因。

加载 Disqus 评论