CouchDB 视图为每个 "group" 返回一个元素
A CouchDB view returning one element for each per "group"
我的数据库中有很多这样的文档:
{
"_id": "7fa2e319f3b908818d1c6eda9205fc6f",
"_rev": "3-9db3d8cc45c9a45b35c3981011e77bb5",
"Guid": "2d69ba2e-972e-4659-8d3f-35f660229b6d",
"CompanyId": "foo",
"Date": "2021-02-12T08:59:48Z",
"Author": "Me",
...
}
CompanyId
键出现在所有文档中,并且多个文档具有相同的 CompanyId
值。
我正在寻找一种方法来为每个不同的 CompanyId
值获取具有最新 Date
的文档。换句话说,我想要一个文档数组,每个文档都有不同的 CompanyId
值。
这是一对map/reduce:
function(doc) {
emit(doc.CompanyId, {doc: doc, date: Date.parse(doc.Date)});
}
function(keys, values, rereduce) {
var maxDate = 0;
var maxDoc = '';
for(i = 0; i<values.length; i++) {
if(values[i].date>maxDate) {
maxDate=values[i].date;
maxDoc=values[i].doc;
}
}
return {date: maxDate}
}
如果我将 group=true
传递给我的查询,上面的 map/reduce 对会成功 returns 按公司 ID 列出的最新文档的日期:
{
"rows": [
{
"key": "a-testagency-556049-0897",
"value": {
"date": 1613123486000
}
},
{
"key": "a-testagency-556677-1317",
"value": {
"date": 1613123435000
}
}
]
}
但我想要的不仅仅是结果中的日期,我还想要文档本身,所以我将最后一行更改为:
return {date: maxDate, doc: maxDoc}
不幸的是,这会引发如下错误:
"Reduce output must shrink more rapidly: input size: 37325 output size: 37233"
是不是有些公司id只有一个匹配的文件?
如果我再次将 return 行更改为更小的内容:
return {date: maxDate, companyId: maxDoc.Author}
然后查询现在 returns null
作为值而不是日期和作者。
最后的代码片段使用 pouchDB 演示了一种(在几种方法中)实现预期结果的方法。这是一个非常简单的解决方案,但结果是从 2 个请求而不是 1 个请求中获得的。
要了解其工作原理,请查看 CouchDB 文档中的 Reduce/Rereduce,特别是关于 rereduce
的内容。此处的深入写作只是对该文档的重述。
这是演示片段中的设计文档。
{
"_id": "_design/SO-66173759",
"views": {
"most_recent": {
"map": `function (doc) {
if(doc.CompanyId && doc.Date) {
emit([doc.CompanyId,doc.Date],doc._id);
}
}`,
"reduce": `function(keys,values,rereduce) {
return values[0];
}`
}
}
}
emit 生成复杂键 [companyId, Date],value = document _id。例如:
[Acme Amalgamated,2012-07-26T18:57:12.409Z] 7fb19b37-5baa-4c78-b122-fd0bc67253d0
[Acme Amalgamated,2013-08-16T02:53:12.062Z] 93d480a5-a666-4fca-b4a4-c1ba7935de92
[Acme Amalgamated,2013-08-23T22:12:14.401Z] 96a236b1-50d6-4b7e-af45-d05432ca7847
[Acme Amalgamated,2015-05-20T13:17:21.500Z] ad55f6e7-6c61-4793-a75c-9601debd5eaf
[Acme Amalgamated,2015-12-17T09:18:33.741Z] cf1a0844-aa54-42e2-a9d6-16e80f5c420a
[Acme Amalgamated,2016-08-24T04:01:15.551Z] 0658a417-901f-4cc7-a005-c10c72d2720c
[Acme Amalgamated,2020-06-19T07:58:44.680Z] 56126238-496a-475e-9001-8a4a86e055a3 **
[Cyberdyne Systems,2012-02-02T17:07:30.692Z] d8a2b649-10b0-467b-b7e1-304b13f7560a
[Cyberdyne Systems,2012-05-31T17:47:15.607Z] 64699ddb-e8f5-449f-b1cb-804ccddbbfb2
[Cyberdyne Systems,2012-07-11T13:08:00.879Z] 1aa285f4-dc4b-49e4-9f0b-331b3d552dad
[Cyberdyne Systems,2014-08-12T22:43:47.651Z] f5597fb0-e37e-4a62-8ef2-244140b0439b
[Cyberdyne Systems,2015-03-25T04:54:40.459Z] c80f58d3-9fb1-466f-9fd2-2b3f86331fc3
[Cyberdyne Systems,2017-08-13T02:42:49.530Z] c2b2d636-b444-4216-9b0b-e982ea467e51 **
[Lorem Ipsum Inc.,2010-01-26T21:31:34.752Z] d6e2b66d-286f-48b9-b678-7afeca2c3c01
[Lorem Ipsum Inc.,2012-07-04T02:29:20.509Z] c7d528fe-ce4f-4c35-a054-39f726962b4a
[Lorem Ipsum Inc.,2012-10-26T01:21:11.765Z] 773af027-1fdd-4f41-b1b1-8d5169871684
[Lorem Ipsum Inc.,2012-12-15T22:27:12.999Z] 78d60df3-aed6-4cab-8071-4d08ccfd0184
[Lorem Ipsum Inc.,2014-11-04T02:03:49.328Z] 098f600c-8059-44d5-a526-b67f0bbeb609
[Lorem Ipsum Inc.,2017-04-30T14:54:17.967Z] 639ddd9c-9a41-42fd-b1be-7fea8eb4f3ec
[Lorem Ipsum Inc.,2017-11-21T10:45:53.152Z] ee2a7445-276d-4e9c-9f9d-1fa76d87741f **
** companyId 组中的最新文档
通常,发出 document._id 是多余的,因为视图查询 return 文档 _id - 但在这种情况下这样做是有充分理由的,这很明显稍后.
密切注意索引的排序方式;在每个公司组中,最近的日期是 companyId 组的 last 条目。
现在是简洁的 reduce 函数
function(keys,values,rereduce) { return values[0]; }
使用以下参数会产生令人满意的结果
{
reduce: true,
group_level: 1,
descending: true
}
descending
属性 是这里的杀手,因为 reduce
函数将以 倒序 接收键和值;通过 returning 第 0 个值元素,reduce
将始终根据日期字段生成最新的文档 ID,即使 rereduce = true
.
鉴于前面的示例视图,使用上述参数访问视图将 return 以下 key/value 行
[Lorem Ipsum Inc.] ee2a7445-276d-4e9c-9f9d-1fa76d87741f
[Cyberdyne Systems] c2b2d636-b444-4216-9b0b-e982ea467e51
[Acme Amalgamated] 56126238-496a-475e-9001-8a4a86e055a3
它就在那里 - 使用具有以下参数的值(文档 ID)调用 _all_docs
{
include_docs: true,
keys: [
"ee2a7445-276d-4e9c-9f9d-1fa76d87741f",
"c2b2d636-b444-4216-9b0b-e982ea467e51",
"56126238-496a-475e-9001-8a4a86e055a3"
]
}
将 return 日期最近的文件,按 companyId 区分。
这是一个演示片段。该代码生成 20 个随机文档并生成压缩视图数据。
重要
一定要了解整个索引都会被扫描以寻找不受约束的 reduce。如果您的索引包含数百万个文档,那么可能需要采用更复杂的方法,例如利用 start/end 键或涉及 _changes 提要的有趣内容。 YMMV.
async function view_reduce() {
let result = await db.query('SO-66173759/most_recent', {
reduce: true,
group_level: 1,
descending: true
});
// show
gel('view_reduce').innerText = result.rows.map(row => `${row.key}\t${row.value}`).join('\n');
return result;
}
async function showMostRecentDocs() {
// Use results from reduce to get documents
let result = await view_reduce();
// The result row values are document ids; use allDocs to fetch the docs
result = await db.allDocs({
include_docs: true,
keys: result.rows.map(row => row.value)
});
//show
gel('view_most_recent').innerText = result.rows.map(row => [row.doc.CompanyId, row.doc.Date, row.doc.Author].join('\t')).join('\n');
}
async function showViewKeyValues() {
let result = await db.query('SO-66173759/most_recent', {
reduce: false,
include_docs: false
});
//show
gel('view_key_value').innerText =
result.rows.map(row => `[${row.key}]\t${row.value}`).join('\n');
}
async function showViewDocs() {
let result = await db.query('SO-66173759/most_recent', {
reduce: false,
include_docs: true
});
//show
gel('view_docs').innerText = result.rows.map(row => [row.doc.CompanyId, row.doc.Date, row.doc.Author].join('\t')).join('\n');
}
function getDocsToInstall(count) {
const sourceDocs = [{
"CompanyId": "Acme Amalgamated",
"Author": "Wile E. Coyote",
},
{
"CompanyId": "Acme Amalgamated",
"Author": "Road R. Unner",
},
{
"CompanyId": "Lorem Ipsum Inc.",
"Author": "Caesar Augustus",
},
{
"CompanyId": "Lorem Ipsum Inc.",
"Author": "Marcus Aurelius",
},
{
"CompanyId": "Cyberdyne Systems",
"Author": "Miles Dyson",
}
];
// design document
const ddoc = {
"_id": "_design/SO-66173759",
"views": {
"most_recent": {
"map": `function (doc) {
if(doc.CompanyId && doc.Date) {
emit([doc.CompanyId,doc.Date],doc._id);
}
}`,
"reduce": `function(keys,values,rereduce) {
return values[0];
}`
}
}
};
// create a set of random documents.
let docs = new Array(count);
const dateSeed = [new Date(2010, 0, 1), new Date(), 0, 24];
while (count--) {
let doc = Object.assign({}, sourceDocs[Math.random() * sourceDocs.length | 0]);
doc.Date = randomDate(...dateSeed).toISOString();
docs[count] = doc;
}
docs.push(ddoc);
return docs;
}
const db = new PouchDB('SO-66173759', {
adapter: 'memory'
});
// install docs and show view in various forms.
(async() => {
await db.bulkDocs(getDocsToInstall(20));
await showViewDocs();
await showViewKeyValues();
return showMostRecentDocs();
})();
const gel = id => document.getElementById(id);
/*
*/
function randomDate(start, end, startHour, endHour) {
var date = new Date(+start + Math.random() * (end - start));
var hour = startHour + Math.random() * (endHour - startHour) | 0;
date.setHours(hour);
return date;
}
<script src="https://cdn.jsdelivr.net/npm/pouchdb@7.1.1/dist/pouchdb.min.js"></script>
<script src="https://github.com/pouchdb/pouchdb/releases/download/7.1.1/pouchdb.memory.min.js"></script>
<div>View: distinct, most recent</div>
<pre id='view_most_recent'></pre>
<hr/>
<div>View: reduce result</div>
<pre id='view_reduce'></pre>
<hr/>
<div>View: key/value</div>
<pre id='view_key_value'></pre>
<hr/>
<div>View: docs</div>
<pre id='view_docs'></pre>
FWIW
不要将文档存储为值!这是非常低效的。尽管宇宙中某个地方可能存在一个很好的理由,但我从未遇到过这样做的正当理由。
我的数据库中有很多这样的文档:
{
"_id": "7fa2e319f3b908818d1c6eda9205fc6f",
"_rev": "3-9db3d8cc45c9a45b35c3981011e77bb5",
"Guid": "2d69ba2e-972e-4659-8d3f-35f660229b6d",
"CompanyId": "foo",
"Date": "2021-02-12T08:59:48Z",
"Author": "Me",
...
}
CompanyId
键出现在所有文档中,并且多个文档具有相同的 CompanyId
值。
我正在寻找一种方法来为每个不同的 CompanyId
值获取具有最新 Date
的文档。换句话说,我想要一个文档数组,每个文档都有不同的 CompanyId
值。
这是一对map/reduce:
function(doc) {
emit(doc.CompanyId, {doc: doc, date: Date.parse(doc.Date)});
}
function(keys, values, rereduce) {
var maxDate = 0;
var maxDoc = '';
for(i = 0; i<values.length; i++) {
if(values[i].date>maxDate) {
maxDate=values[i].date;
maxDoc=values[i].doc;
}
}
return {date: maxDate}
}
如果我将 group=true
传递给我的查询,上面的 map/reduce 对会成功 returns 按公司 ID 列出的最新文档的日期:
{
"rows": [
{
"key": "a-testagency-556049-0897",
"value": {
"date": 1613123486000
}
},
{
"key": "a-testagency-556677-1317",
"value": {
"date": 1613123435000
}
}
]
}
但我想要的不仅仅是结果中的日期,我还想要文档本身,所以我将最后一行更改为:
return {date: maxDate, doc: maxDoc}
不幸的是,这会引发如下错误:
"Reduce output must shrink more rapidly: input size: 37325 output size: 37233"
是不是有些公司id只有一个匹配的文件?
如果我再次将 return 行更改为更小的内容:
return {date: maxDate, companyId: maxDoc.Author}
然后查询现在 returns null
作为值而不是日期和作者。
最后的代码片段使用 pouchDB 演示了一种(在几种方法中)实现预期结果的方法。这是一个非常简单的解决方案,但结果是从 2 个请求而不是 1 个请求中获得的。
要了解其工作原理,请查看 CouchDB 文档中的 Reduce/Rereduce,特别是关于 rereduce
的内容。此处的深入写作只是对该文档的重述。
这是演示片段中的设计文档。
{
"_id": "_design/SO-66173759",
"views": {
"most_recent": {
"map": `function (doc) {
if(doc.CompanyId && doc.Date) {
emit([doc.CompanyId,doc.Date],doc._id);
}
}`,
"reduce": `function(keys,values,rereduce) {
return values[0];
}`
}
}
}
emit 生成复杂键 [companyId, Date],value = document _id。例如:
[Acme Amalgamated,2012-07-26T18:57:12.409Z] 7fb19b37-5baa-4c78-b122-fd0bc67253d0
[Acme Amalgamated,2013-08-16T02:53:12.062Z] 93d480a5-a666-4fca-b4a4-c1ba7935de92
[Acme Amalgamated,2013-08-23T22:12:14.401Z] 96a236b1-50d6-4b7e-af45-d05432ca7847
[Acme Amalgamated,2015-05-20T13:17:21.500Z] ad55f6e7-6c61-4793-a75c-9601debd5eaf
[Acme Amalgamated,2015-12-17T09:18:33.741Z] cf1a0844-aa54-42e2-a9d6-16e80f5c420a
[Acme Amalgamated,2016-08-24T04:01:15.551Z] 0658a417-901f-4cc7-a005-c10c72d2720c
[Acme Amalgamated,2020-06-19T07:58:44.680Z] 56126238-496a-475e-9001-8a4a86e055a3 **
[Cyberdyne Systems,2012-02-02T17:07:30.692Z] d8a2b649-10b0-467b-b7e1-304b13f7560a
[Cyberdyne Systems,2012-05-31T17:47:15.607Z] 64699ddb-e8f5-449f-b1cb-804ccddbbfb2
[Cyberdyne Systems,2012-07-11T13:08:00.879Z] 1aa285f4-dc4b-49e4-9f0b-331b3d552dad
[Cyberdyne Systems,2014-08-12T22:43:47.651Z] f5597fb0-e37e-4a62-8ef2-244140b0439b
[Cyberdyne Systems,2015-03-25T04:54:40.459Z] c80f58d3-9fb1-466f-9fd2-2b3f86331fc3
[Cyberdyne Systems,2017-08-13T02:42:49.530Z] c2b2d636-b444-4216-9b0b-e982ea467e51 **
[Lorem Ipsum Inc.,2010-01-26T21:31:34.752Z] d6e2b66d-286f-48b9-b678-7afeca2c3c01
[Lorem Ipsum Inc.,2012-07-04T02:29:20.509Z] c7d528fe-ce4f-4c35-a054-39f726962b4a
[Lorem Ipsum Inc.,2012-10-26T01:21:11.765Z] 773af027-1fdd-4f41-b1b1-8d5169871684
[Lorem Ipsum Inc.,2012-12-15T22:27:12.999Z] 78d60df3-aed6-4cab-8071-4d08ccfd0184
[Lorem Ipsum Inc.,2014-11-04T02:03:49.328Z] 098f600c-8059-44d5-a526-b67f0bbeb609
[Lorem Ipsum Inc.,2017-04-30T14:54:17.967Z] 639ddd9c-9a41-42fd-b1be-7fea8eb4f3ec
[Lorem Ipsum Inc.,2017-11-21T10:45:53.152Z] ee2a7445-276d-4e9c-9f9d-1fa76d87741f **
** companyId 组中的最新文档
通常,发出 document._id 是多余的,因为视图查询 return 文档 _id - 但在这种情况下这样做是有充分理由的,这很明显稍后.
密切注意索引的排序方式;在每个公司组中,最近的日期是 companyId 组的 last 条目。
现在是简洁的 reduce 函数
function(keys,values,rereduce) { return values[0]; }
使用以下参数会产生令人满意的结果
{
reduce: true,
group_level: 1,
descending: true
}
descending
属性 是这里的杀手,因为 reduce
函数将以 倒序 接收键和值;通过 returning 第 0 个值元素,reduce
将始终根据日期字段生成最新的文档 ID,即使 rereduce = true
.
鉴于前面的示例视图,使用上述参数访问视图将 return 以下 key/value 行
[Lorem Ipsum Inc.] ee2a7445-276d-4e9c-9f9d-1fa76d87741f
[Cyberdyne Systems] c2b2d636-b444-4216-9b0b-e982ea467e51
[Acme Amalgamated] 56126238-496a-475e-9001-8a4a86e055a3
它就在那里 - 使用具有以下参数的值(文档 ID)调用 _all_docs
{
include_docs: true,
keys: [
"ee2a7445-276d-4e9c-9f9d-1fa76d87741f",
"c2b2d636-b444-4216-9b0b-e982ea467e51",
"56126238-496a-475e-9001-8a4a86e055a3"
]
}
将 return 日期最近的文件,按 companyId 区分。
这是一个演示片段。该代码生成 20 个随机文档并生成压缩视图数据。
重要
一定要了解整个索引都会被扫描以寻找不受约束的 reduce。如果您的索引包含数百万个文档,那么可能需要采用更复杂的方法,例如利用 start/end 键或涉及 _changes 提要的有趣内容。 YMMV.
async function view_reduce() {
let result = await db.query('SO-66173759/most_recent', {
reduce: true,
group_level: 1,
descending: true
});
// show
gel('view_reduce').innerText = result.rows.map(row => `${row.key}\t${row.value}`).join('\n');
return result;
}
async function showMostRecentDocs() {
// Use results from reduce to get documents
let result = await view_reduce();
// The result row values are document ids; use allDocs to fetch the docs
result = await db.allDocs({
include_docs: true,
keys: result.rows.map(row => row.value)
});
//show
gel('view_most_recent').innerText = result.rows.map(row => [row.doc.CompanyId, row.doc.Date, row.doc.Author].join('\t')).join('\n');
}
async function showViewKeyValues() {
let result = await db.query('SO-66173759/most_recent', {
reduce: false,
include_docs: false
});
//show
gel('view_key_value').innerText =
result.rows.map(row => `[${row.key}]\t${row.value}`).join('\n');
}
async function showViewDocs() {
let result = await db.query('SO-66173759/most_recent', {
reduce: false,
include_docs: true
});
//show
gel('view_docs').innerText = result.rows.map(row => [row.doc.CompanyId, row.doc.Date, row.doc.Author].join('\t')).join('\n');
}
function getDocsToInstall(count) {
const sourceDocs = [{
"CompanyId": "Acme Amalgamated",
"Author": "Wile E. Coyote",
},
{
"CompanyId": "Acme Amalgamated",
"Author": "Road R. Unner",
},
{
"CompanyId": "Lorem Ipsum Inc.",
"Author": "Caesar Augustus",
},
{
"CompanyId": "Lorem Ipsum Inc.",
"Author": "Marcus Aurelius",
},
{
"CompanyId": "Cyberdyne Systems",
"Author": "Miles Dyson",
}
];
// design document
const ddoc = {
"_id": "_design/SO-66173759",
"views": {
"most_recent": {
"map": `function (doc) {
if(doc.CompanyId && doc.Date) {
emit([doc.CompanyId,doc.Date],doc._id);
}
}`,
"reduce": `function(keys,values,rereduce) {
return values[0];
}`
}
}
};
// create a set of random documents.
let docs = new Array(count);
const dateSeed = [new Date(2010, 0, 1), new Date(), 0, 24];
while (count--) {
let doc = Object.assign({}, sourceDocs[Math.random() * sourceDocs.length | 0]);
doc.Date = randomDate(...dateSeed).toISOString();
docs[count] = doc;
}
docs.push(ddoc);
return docs;
}
const db = new PouchDB('SO-66173759', {
adapter: 'memory'
});
// install docs and show view in various forms.
(async() => {
await db.bulkDocs(getDocsToInstall(20));
await showViewDocs();
await showViewKeyValues();
return showMostRecentDocs();
})();
const gel = id => document.getElementById(id);
/*
*/
function randomDate(start, end, startHour, endHour) {
var date = new Date(+start + Math.random() * (end - start));
var hour = startHour + Math.random() * (endHour - startHour) | 0;
date.setHours(hour);
return date;
}
<script src="https://cdn.jsdelivr.net/npm/pouchdb@7.1.1/dist/pouchdb.min.js"></script>
<script src="https://github.com/pouchdb/pouchdb/releases/download/7.1.1/pouchdb.memory.min.js"></script>
<div>View: distinct, most recent</div>
<pre id='view_most_recent'></pre>
<hr/>
<div>View: reduce result</div>
<pre id='view_reduce'></pre>
<hr/>
<div>View: key/value</div>
<pre id='view_key_value'></pre>
<hr/>
<div>View: docs</div>
<pre id='view_docs'></pre>
FWIW
不要将文档存储为值!这是非常低效的。尽管宇宙中某个地方可能存在一个很好的理由,但我从未遇到过这样做的正当理由。