3.1. 查询服务器协议

一个 查询服务器 是一个外部进程,它通过一个简单的自定义 JSON 协议在 stdin/stdout 上与 CouchDB 通信。它用于处理所有设计函数调用:viewsshowslistsfiltersupdatesvalidate_doc_update

CouchDB 通过 stdin/stdout 与查询服务器进程通信,使用以换行符结尾的 JSON 消息。发送到查询服务器的消息始终是 array 类型,并遵循模式 [<command>, <*arguments>]\n

注意

在文档示例中,我们省略了尾部的 \n 以提高可读性。此外,示例包含格式化的 JSON 值,而实际数据以紧凑模式传输,不包含格式化空格。

3.1.1. reset

命令:

reset

参数:

查询服务器状态 (可选)

返回值:

true

这将重置查询服务器的状态,并使其忘记所有先前的输入。如果适用,这是运行垃圾回收的时机。

CouchDB 发送

["reset"]

查询服务器回答

true

要设置新的查询服务器状态,使用第二个参数和对象数据。

CouchDB 发送

["reset", {"reduce_limit": true, "timeout": 5000}]

查询服务器回答

true

3.1.2. add_lib

命令:

add_lib

参数:

通过 views/lib 路径指定的 CommonJS 库对象

返回值:

true

CommonJS 库添加到查询服务器状态,以便在 map 函数中进一步使用。

CouchDB 发送

[
    "add_lib",
    {
        "utils": "exports.MAGIC = 42;"
    }
]

查询服务器回答

true

注意

此库不应有任何副作用,也不应跟踪其自身状态,否则如果出现问题,您将需要进行大量的调试工作。请记住,完全索引重建是一个繁重的操作,这是修复共享状态错误的唯一方法。

3.1.3. add_fun

命令:

add_fun

参数:

映射函数源代码。

返回值:

true

在创建或更新视图时,这是查询服务器接收视图函数以进行评估的方式。查询服务器应解析、编译和评估它接收的函数,使其以后可调用。如果失败,查询服务器将返回错误。CouchDB 可以在发送任何文档之前存储多个函数。

CouchDB 发送

[
    "add_fun",
    "function(doc) { if(doc.score > 50) emit(null, {'player_name': doc.name}); }"
]

查询服务器回答

true

3.1.4. map_doc

命令:

map_doc

参数:

文档对象

返回值:

每个应用的 函数 的键值对数组

当视图函数存储在查询服务器中时,CouchDB 开始发送数据库中的所有文档,一次一个。查询服务器依次调用先前存储的函数,并使用文档,并存储其结果。当所有函数都被调用后,结果将作为 JSON 字符串返回。

CouchDB 发送

[
    "map_doc",
    {
        "_id": "8877AFF9789988EE",
        "_rev": "3-235256484",
        "name": "John Smith",
        "score": 60
    }
]

如果上面的函数是存储的唯一函数,则查询服务器回答

[
    [
        [null, {"player_name": "John Smith"}]
    ]
]

也就是说,一个数组,其中包含给定文档的每个函数的结果。

如果要从视图中排除文档,则数组应为空。

CouchDB 发送

[
    "map_doc",
    {
        "_id": "9590AEB4585637FE",
        "_rev": "1-674684684",
        "name": "Jane Parker",
        "score": 43
    }
]

查询服务器回答

[[]]

3.1.5. reduce

命令:

reduce

参数:
  • 归约函数源代码

  • 映射函数 结果的数组,其中每个项目以格式 [[key, id-of-doc], value] 表示

返回值:

包含一对值的数组:true 和另一个包含归约结果的数组

如果视图定义了归约函数,则 CouchDB 将进入归约阶段。查询服务器将收到一个归约函数列表和一些映射结果,它可以在这些结果上应用这些函数。

CouchDB 发送

[
    "reduce",
    [
        "function(k, v) { return sum(v); }"
    ],
    [
        [[1, "699b524273605d5d3e9d4fd0ff2cb272"], 10],
        [[2, "c081d0f69c13d2ce2050d684c7ba2843"], 20],
        [[null, "foobar"], 3]
    ]
]

查询服务器回答

[
    true,
    [33]
]

请注意,即使视图服务器以 [[key, id-of-doc], value] 的形式接收映射结果,函数也可能以不同的形式接收它们。例如,JavaScript 查询服务器在键列表和值列表上应用函数。

3.1.6. rereduce

命令:

rereduce

参数:
  • 归约函数源代码

  • 值列表

在构建视图时,CouchDB 将直接将归约步骤应用于映射步骤的输出,并将重新归约步骤应用于先前归约步骤的输出。

CouchDB 将发送一个归约函数列表和一个值列表,其中没有键或文档 ID 到重新归约步骤。

CouchDB 发送

[
    "rereduce",
    [
        "function(k, v, r) { return sum(v); }"
    ],
    [
        33,
        55,
        66
    ]
]

查询服务器回答

[
    true,
    [154]
]

3.1.7. ddoc

命令:

ddoc

参数:

对象数组。

  • 第一阶段(ddoc 初始化)

    • "new"

    • 设计文档 _id

    • 设计文档对象

  • 第二阶段(设计函数执行)

    • 设计文档 _id

    • 作为对象键数组的函数路径

    • 函数参数数组

返回值:
  • 第一阶段(ddoc 初始化):true

  • 第二阶段(设计函数执行):取决于执行的函数的自定义对象

此命令分两个阶段执行:ddoc 注册和 设计函数 执行。

在第一阶段,CouchDB 将完整的 design document 内容发送到查询服务器,以便它通过 _id 值将其缓存,以便在以后执行函数。

为此,CouchDB 发送

[
    "ddoc",
    "new",
    "_design/temp",
    {
        "_id": "_design/temp",
        "_rev": "8-d7379de23a751dc2a19e5638a7bbc5cc",
        "language": "javascript",
        "shows": {
            "request": "function(doc,req){ return {json: req}; }",
            "hello": "function(doc,req){ return {body: 'Hello, ' + (doc || {})._id + '!'}; }"
        }
    }
]

查询服务器回答

true

此后,design document 将准备好为第二阶段中的子命令提供服务。

注意

每个 ddoc 子命令都是根 design document 键,因此它们实际上不是子命令,而是 JSON 路径的第一个元素,这些元素可以被处理和处理。

子命令执行的模式是通用的

["ddoc", <design_doc_id>, [<subcommand>, <funcname>], [<argument1>, <argument2>, ...]]

3.1.7.1. shows

警告

Show 函数在 CouchDB 3.0 中已弃用,将在 CouchDB 4.0 中删除。

命令:

ddoc

子命令:

shows

参数:
  • 文档对象或 null(如果请求中未指定文档 id

  • 请求对象

返回值:

包含两个元素的数组

执行 show 函数

Couchdb 发送

[
    "ddoc",
    "_design/temp",
    [
        "shows",
        "doc"
    ],
    [
        null,
        {
            "info": {
                "db_name": "test",
                "doc_count": 8,
                "doc_del_count": 0,
                "update_seq": 105,
                "purge_seq": 0,
                "compact_running": false,
                "sizes": {
                  "active": 1535048,
                  "disk": 15818856,
                  "external": 15515850
                },
                "instance_start_time": "1359952188595857",
                "disk_format_version": 6,
                "committed_update_seq": 105
            },
            "id": null,
            "uuid": "169cb4cc82427cc7322cb4463d0021bb",
            "method": "GET",
            "requested_path": [
                "api",
                "_design",
                "temp",
                "_show",
                "request"
            ],
            "path": [
                "api",
                "_design",
                "temp",
                "_show",
                "request"
            ],
            "raw_path": "/api/_design/temp/_show/request",
            "query": {},
            "headers": {
                "Accept": "*/*",
                "Host": "localhost:5984",
                "User-Agent": "curl/7.26.0"
            },
            "body": "undefined",
            "peer": "127.0.0.1",
            "form": {},
            "cookie": {},
            "userCtx": {
                "db": "api",
                "name": null,
                "roles": [
                    "_admin"
                ]
            },
            "secObj": {}
        }
    ]
]

查询服务器发送

[
    "resp",
    {
        "body": "Hello, undefined!"
    }
]

3.1.7.2. lists

警告

List 函数在 CouchDB 3.0 中已弃用,将在 CouchDB 4.0 中删除。

命令:

ddoc

子命令:

lists

参数:
返回值:

数组。有关详细信息,请参见下文。

执行 list 函数

list 函数的通信协议有点复杂,因此让我们使用一个示例来说明。

假设我们有一个视图函数,它发出 id-rev

function(doc) {
    emit(doc._id, doc._rev);
}

我们想用 list 函数模拟 _all_docs JSON 响应。我们 list 函数的第一个版本如下所示

function(head, req){
    start({'headers': {'Content-Type': 'application/json'}});
    var resp = head;
    var rows = [];
    while(row=getRow()){
        rows.push(row);
    }
    resp.rows = rows;
    return toJSON(resp);
}

在 list 函数执行期间的整个通信会话可以分为三个部分

  1. 初始化

    list 函数返回的第一个对象是一个数组,其结构如下

    ["start", <chunks>, <headers>]
    

    其中 <chunks> 是一个将发送到客户端的文本块数组,而 <headers> 是一个包含响应 HTTP 标头的对象。

    此消息从查询服务器发送到 CouchDB,在 start() 调用中,该调用初始化对客户端的 HTTP 响应。

    [
        "start",
        [],
        {
            "headers": {
                "Content-Type": "application/json"
            }
        }
    ]
    

    在此之后,列表函数可能会开始处理视图行。

  2. 视图处理

    由于视图结果可能非常大,因此最好不要在单个命令中传递所有行。相反,CouchDB 可以逐行将视图行发送到查询服务器,从而允许将视图处理和输出生成作为流进行处理。

    CouchDB 发送一个包含视图行数据的特殊数组。

    [
        "list_row",
        {
            "id": "0cb42c267fe32d4b56b3500bc503e030",
            "key": "0cb42c267fe32d4b56b3500bc503e030",
            "value": "1-967a00dff5e02add41819138abb3284d"
        }
    ]
    

    如果查询服务器对此有要返回的内容,它将返回一个数组,该数组在头部包含一个 "chunks" 项,在尾部包含一个数据数组。在本例中,它没有要返回的内容,因此响应将为

    [
      "chunks",
      []
    ]
    

    当没有更多视图行要处理时,CouchDB 会发送一个 list_end 消息,表示没有更多数据要发送。

    ["list_end"]
    
  3. 完成

    通信过程的最后阶段是返回列表尾部:最后一个数据块。在此之后,列表函数的处理将完成,客户端将收到完整的响应。

    对于我们的示例,最后一条消息是

    [
        "end",
        [
            "{\"total_rows\":2,\"offset\":0,\"rows\":[{\"id\":\"0cb42c267fe32d4b56b3500bc503e030\",\"key\":\"0cb42c267fe32d4b56b3500bc503e030\",\"value\":\"1-967a00dff5e02add41819138abb3284d\"},{\"id\":\"431926a69504bde41851eb3c18a27b1f\",\"key\":\"431926a69504bde41851eb3c18a27b1f\",\"value\":\"1-967a00dff5e02add41819138abb3284d\"}]}"
        ]
    ]
    

在本例中,我们从查询服务器返回了结果,该结果包含在单个消息中。对于少量行来说,这没问题,但对于大型数据集来说,例如包含数百万个文档或数百万个视图行的数据集,这将不可接受。

让我们修复我们的列表函数,并查看通信中的变化。

function(head, req){
    start({'headers': {'Content-Type': 'application/json'}});
    send('{');
    send('"total_rows":' + toJSON(head.total_rows) + ',');
    send('"offset":' + toJSON(head.offset) + ',');
    send('"rows":[');
    if (row=getRow()){
        send(toJSON(row));
    }
    while(row=getRow()){
        send(',' + toJSON(row));
    }
    send(']');
    return '}';
}

“等等,什么?” - 你可能想问。是的,我们将通过字符串块手动构建 JSON 响应,但让我们看一下日志。

[Wed, 24 Jul 2013 05:45:30 GMT] [debug] [<0.19191.1>] OS Process #Port<0.4444> Output :: ["start",["{","\"total_rows\":2,","\"offset\":0,","\"rows\":["],{"headers":{"Content-Type":"application/json"}}]
[Wed, 24 Jul 2013 05:45:30 GMT] [info] [<0.18963.1>] 127.0.0.1 - - GET /blog/_design/post/_list/index/all_docs 200
[Wed, 24 Jul 2013 05:45:30 GMT] [debug] [<0.19191.1>] OS Process #Port<0.4444> Input  :: ["list_row",{"id":"0cb42c267fe32d4b56b3500bc503e030","key":"0cb42c267fe32d4b56b3500bc503e030","value":"1-967a00dff5e02add41819138abb3284d"}]
[Wed, 24 Jul 2013 05:45:30 GMT] [debug] [<0.19191.1>] OS Process #Port<0.4444> Output :: ["chunks",["{\"id\":\"0cb42c267fe32d4b56b3500bc503e030\",\"key\":\"0cb42c267fe32d4b56b3500bc503e030\",\"value\":\"1-967a00dff5e02add41819138abb3284d\"}"]]
[Wed, 24 Jul 2013 05:45:30 GMT] [debug] [<0.19191.1>] OS Process #Port<0.4444> Input  :: ["list_row",{"id":"431926a69504bde41851eb3c18a27b1f","key":"431926a69504bde41851eb3c18a27b1f","value":"1-967a00dff5e02add41819138abb3284d"}]
[Wed, 24 Jul 2013 05:45:30 GMT] [debug] [<0.19191.1>] OS Process #Port<0.4444> Output :: ["chunks",[",{\"id\":\"431926a69504bde41851eb3c18a27b1f\",\"key\":\"431926a69504bde41851eb3c18a27b1f\",\"value\":\"1-967a00dff5e02add41819138abb3284d\"}"]]
[Wed, 24 Jul 2013 05:45:30 GMT] [debug] [<0.19191.1>] OS Process #Port<0.4444> Input  :: ["list_end"]
[Wed, 24 Jul 2013 05:45:30 GMT] [debug] [<0.19191.1>] OS Process #Port<0.4444> Output :: ["end",["]","}"]]

请注意,现在查询服务器通过轻量级块发送响应,如果我们的通信过程非常慢,客户端将看到响应数据如何出现在他们的屏幕上。逐块显示,无需等待完整的结果,就像他们之前对我们的列表函数所做的那样。

3.1.7.3. updates

命令:

ddoc

子命令:

更新

参数:
  • 文档对象或 null(如果请求中未指定文档 id)。

  • 请求对象

返回值:

包含三个元素的数组。

  • "up"

  • 文档对象或 null(如果不需要存储任何内容)。

  • 响应对象

执行 更新函数

CouchDB 发送

[
    "ddoc",
    "_design/id",
    [
        "updates",
        "nothing"
    ],
    [
        null,
        {
            "info": {
                "db_name": "test",
                "doc_count": 5,
                "doc_del_count": 0,
                "update_seq": 16,
                "purge_seq": 0,
                "compact_running": false,
                "sizes": {
                  "active": 7979745,
                  "disk": 8056936,
                  "external": 8024930
                },
                "instance_start_time": "1374612186131612",
                "disk_format_version": 6,
                "committed_update_seq": 16
            },
            "id": null,
            "uuid": "7b695cb34a03df0316c15ab529002e69",
            "method": "POST",
            "requested_path": [
                "test",
                "_design",
                "1139",
                "_update",
                "nothing"
            ],
            "path": [
                "test",
                "_design",
                "1139",
                "_update",
                "nothing"
            ],
            "raw_path": "/test/_design/1139/_update/nothing",
            "query": {},
            "headers": {
                "Accept": "*/*",
                "Accept-Encoding": "identity, gzip, deflate, compress",
                "Content-Length": "0",
                "Host": "localhost:5984"
            },
            "body": "",
            "peer": "127.0.0.1",
            "form": {},
            "cookie": {},
            "userCtx": {
                "db": "test",
                "name": null,
                "roles": [
                    "_admin"
                ]
            },
            "secObj": {}
        }
    ]
]

查询服务器回答

[
    "up",
    null,
    {"body": "document id wasn't provided"}
]

或者在更新成功的情况下

[
    "up",
    {
        "_id": "7b695cb34a03df0316c15ab529002e69",
        "hello": "world!"
    },
    {"body": "document was updated"}
]

3.1.7.4. filters

命令:

ddoc

子命令:

过滤器

参数:
返回值:

包含两个元素的数组。

  • true

  • 与输入文档顺序相同的布尔值数组。

执行 过滤器函数

CouchDB 发送

[
    "ddoc",
    "_design/test",
    [
        "filters",
        "random"
    ],
    [
        [
            {
                "_id": "431926a69504bde41851eb3c18a27b1f",
                "_rev": "1-967a00dff5e02add41819138abb3284d",
                "_revisions": {
                    "start": 1,
                    "ids": [
                        "967a00dff5e02add41819138abb3284d"
                    ]
                }
            },
            {
                "_id": "0cb42c267fe32d4b56b3500bc503e030",
                "_rev": "1-967a00dff5e02add41819138abb3284d",
                "_revisions": {
                    "start": 1,
                    "ids": [
                        "967a00dff5e02add41819138abb3284d"
                    ]
                }
            }
        ],
        {
            "info": {
                "db_name": "test",
                "doc_count": 5,
                "doc_del_count": 0,
                "update_seq": 19,
                "purge_seq": 0,
                "compact_running": false,
                "sizes": {
                  "active": 7979745,
                  "disk": 8056936,
                  "external": 8024930
                },
                "instance_start_time": "1374612186131612",
                "disk_format_version": 6,
                "committed_update_seq": 19
            },
            "id": null,
            "uuid": "7b695cb34a03df0316c15ab529023a81",
            "method": "GET",
            "requested_path": [
                "test",
                "_changes?filter=test",
                "random"
            ],
            "path": [
                "test",
                "_changes"
            ],
            "raw_path": "/test/_changes?filter=test/random",
            "query": {
                "filter": "test/random"
            },
            "headers": {
                "Accept": "application/json",
                "Accept-Encoding": "identity, gzip, deflate, compress",
                "Content-Length": "0",
                "Content-Type": "application/json; charset=utf-8",
                "Host": "localhost:5984"
            },
            "body": "",
            "peer": "127.0.0.1",
            "form": {},
            "cookie": {},
            "userCtx": {
                "db": "test",
                "name": null,
                "roles": [
                    "_admin"
                ]
            },
            "secObj": {}
        }
    ]
]

查询服务器回答

[
    true,
    [
        true,
        false
    ]
]

3.1.7.5. views

命令:

ddoc

子命令:

视图

参数:

文档对象数组。

返回值:

包含两个元素的数组。

  • true

  • 与输入文档顺序相同的布尔值数组。

1.2 版中的新增功能。

执行 视图函数,以代替过滤器。

过滤器 命令的作用相同。

3.1.7.6. validate_doc_update

命令:

ddoc

子命令:

验证文档更新

参数:
返回值:

1

执行 验证函数

CouchDB 发送

[
    "ddoc",
    "_design/id",
    ["validate_doc_update"],
    [
        {
            "_id": "docid",
            "_rev": "2-e0165f450f6c89dc6b071c075dde3c4d",
            "score": 10
        },
        {
            "_id": "docid",
            "_rev": "1-9f798c6ad72a406afdbf470b9eea8375",
            "score": 4
        },
        {
            "name": "Mike",
            "roles": ["player"]
        },
        {
            "admins": {},
            "members": []
        }
    ]
]

查询服务器回答

1

注意

虽然此命令的唯一有效响应是 true,但为了防止保存文档,查询服务器需要引发错误:forbiddenunauthorized;这些错误将分别转换为正确的 HTTP 403HTTP 401 响应。

3.1.7.7. rewrites

命令:

ddoc

子命令:

重写

参数:
返回值:

1

执行 重写函数

CouchDB 发送

[
    "ddoc",
    "_design/id",
    ["rewrites"],
    [
        {
            "method": "POST",
            "requested_path": [
                "test",
                "_design",
                "1139",
                "_update",
                "nothing"
            ],
            "path": [
                "test",
                "_design",
                "1139",
                "_update",
                "nothing"
            ],
            "raw_path": "/test/_design/1139/_update/nothing",
            "query": {},
            "headers": {
                "Accept": "*/*",
                "Accept-Encoding": "identity, gzip, deflate, compress",
                "Content-Length": "0",
                "Host": "localhost:5984"
            },
            "body": "",
            "peer": "127.0.0.1",
            "cookie": {},
            "userCtx": {
                "db": "test",
                "name": null,
                "roles": [
                    "_admin"
                ]
            },
            "secObj": {}
        }
    ]
]

查询服务器回答

[
    "ok",
    {
        "path": "some/path",
        "query": {"key1": "value1", "key2": "value2"},
        "method": "METHOD",
        "headers": {"Header1": "value1", "Header2": "value2"},
        "body": ""
    }
]

或者在直接响应的情况下

[
    "ok",
    {
        "headers": {"Content-Type": "text/plain"},
        "body": "Welcome!",
        "code": 200
    }
]

或者用于立即重定向

[
    "ok",
    {
        "headers": {"Location": "http://example.com/path/"},
        "code": 302
    }
]

3.1.8. 返回错误

当出现问题时,查询服务器可以通过发送特殊消息来响应接收到的命令,从而通知 CouchDB。

错误消息会阻止进一步的命令执行,并将错误描述返回给 CouchDB。错误在逻辑上分为两组。

  • 常见错误。这些错误只会中断当前的查询服务器命令,并将错误信息返回给 CouchDB 实例,不会终止查询服务器进程。

  • 致命错误。致命错误表示无法恢复的状况。例如,如果您的设计函数无法导入第三方模块,最好将此类错误视为致命错误,并终止整个进程。

3.1.8.1. error

要引发错误,查询服务器应响应

["error", "error_name", "reason why"]

"error_name" 有助于按类型对问题进行分类,例如,如果它是 "value_error",则表示数据不正确,"not_found" 表示缺少资源,"type_error" 表示数据类型不正确。

"reason why" 以人类可读的方式解释了问题所在,以及如何解决问题。

例如,对不存在的文档调用 更新函数 可能会产生以下错误消息。

["error", "not_found", "Update function requires existent document"]

3.1.8.2. forbidden

forbidden 错误被 验证文档更新函数 广泛使用,以停止进一步的函数处理,并防止存储新的文档修订版。由于这实际上不是错误,而是对用户操作的断言,因此 CouchDB 不会在 “error” 级别记录它,而是返回 HTTP 403 Forbidden 响应,其中包含错误信息对象。

要引发此错误,查询服务器应响应

{"forbidden": "reason why"}

3.1.8.3. unauthorized

unauthorized 错误在很大程度上与 forbidden 错误类似,但含义是请先授权。这种细微的差别有助于最终用户了解他们可以采取哪些措施来解决问题。与 forbidden 类似,CouchDB 不会在 “error” 级别记录它,而是返回 HTTP 401 Unauthorized 响应,其中包含错误信息对象。

要引发此错误,查询服务器应响应

{"unauthorized": "reason why"}

3.1.9. 日志记录

查询服务器可以随时发送一些信息,这些信息将保存在 CouchDB 的日志文件中。这是通过在单独的行上发送包含单个参数的特殊 log 对象来完成的。

["log", "some message"]

CouchDB 不会响应,而是将接收到的消息写入日志文件。

[Sun, 13 Feb 2009 23:31:30 GMT] [info] [<0.72.0>] Query Server Log Message: some message

这些消息仅在 info level 级别记录。