MongoDB 列表 - 获取第 N 个项目

MongoDB lists - get every Nth item

我有一个 Mongodb 大致如下的模式:

[
  {
    "name" : "name1",
    "instances" : [ 
      {
        "value" : 1,
        "date" : ISODate("2015-03-04T00:00:00.000Z")            
      }, 
      {
        "value" : 2,
        "date" : ISODate("2015-04-01T00:00:00.000Z")
      }, 
      {
        "value" : 2.5,
        "date" : ISODate("2015-03-05T00:00:00.000Z")
      },
      ...
    ]
  },
  {
    "name" : "name2",
    "instances" : [ 
      ...
    ]
  }
]

其中每个元素的实例数可能非常大。

有时我只想获取数据样本,即每第 3 个实例获取一次,或每第 10 个实例获取一次......你明白了。

我可以通过获取所有实例并在我的服务器代码中过滤它们来实现这个目标,但我想知道是否有一种方法可以通过使用一些聚合查询来实现。

有什么想法吗?


已更新

假设数据结构像下面@SylvainLeroux 建议的那样是扁平的,即:

[
  {"name": "name1", "value": 1, "date": ISODate("2015-03-04T00:00:00.000Z")},
  {"name": "name2", "value": 5, "date": ISODate("2015-04-04T00:00:00.000Z")},
  {"name": "name1", "value": 2, "date": ISODate("2015-04-01T00:00:00.000Z")},
  {"name": "name1", "value": 2.5, "date": ISODate("2015-03-05T00:00:00.000Z")},
  ...
]

获取第 N 个项目(特定 name)的任务会更容易吗?

不幸的是,使用聚合框架是不可能的,因为这需要一个带有 $unwind to emit an array index/position, of which currently aggregation can't handle. There is an open JIRA ticket for this here SERVER-4588.

的选项

但是,解决方法是使用 MapReduce 但这会带来巨大的性能成本,因为获取数组索引的实际计算是使用嵌入式JavaScript 引擎(很慢),并且仍然有一个全局 JavaScript 锁,它只允许单个 JavaScript 线程一次 运行。

使用 mapReduce,您可以尝试这样的操作:

映射函数:

var map = function(){
    for(var i=0; i < this.instances.length; i++){
        emit(
            { "_id": this._id,  "index": i },
            { "index": i, "value": this.instances[i] }
        );
    }
};

归约函数:

var reduce = function(){}

然后您可以 运行 在 collection 上执行以下 mapReduce 功能:

db.collection.mapReduce( map, reduce, { out : "resultCollection" } );

然后您可以使用 map() 查询结果 collection 以获取实例数组的每个第 N 项的 list/array游标方法:

var thirdInstances = db.resultCollection.find({"_id.index": N})
                                        .map(function(doc){return doc.value.value})

看来你的问题问得很清楚 "get every nth instance" 这看起来确实是一个非常明确的问题。

.find()这样的查询操作实际上只能return文档"as is",投影中的一般字段"selection"和[=23=等运算符除外] 允许单个匹配的数组元素。

当然有 $slice,但这只允许在数组上使用 "range selection",因此同样不适用。

在服务器上可以修改结果的"only"个东西是.aggregate() and .mapReduce()。前者不会以任何方式 "play very well" 与 "slicing" 数组,至少不会 "n" 元素。然而,由于 mapReduce 的 "function()" 参数是基于 JavaScript 的逻辑,因此您有更多的发挥空间。

对于分析过程和分析目的 "only" 然后只需使用 .filter():

通过 mapReduce 过滤数组内容
db.collection.mapReduce(
    function() {
        var id = this._id;
        delete this._id;

        // filter the content of "instances" to every 3rd item only
        this.instances = this.instances.filter(function(el,idx) {
            return ((idx+1) % 3) == 0;
        });
        emit(id,this);
    },
    function() {},
    { "out": { "inline": 1 } } // or output to collection as required
)

此时它实际上只是一个 "JavaScript runner",但如果这只是 anaylsis/testing 那么这个概念通常没有错。当然,输出不是 "exactly" 您的文档的结构,而是 mapReduce 可以得到的最接近传真的结果。

我在这里看到的另一个建议是创建一个包含所有项目 "denormalized" 的新集合,并从数组中插入 "index" 作为 unqique _id 键的一部分。这可能会产生一些你可以直接查询的东西,但对于 "every nth item" 你仍然需要做:

db.resultCollection.find({
     "_id.index": { "$in": [2,5,8,11,14] } // and so on ....
})

所以计算并提供"every nth item"的索引值,以便得到"every nth item"。所以这似乎并没有真正解决所提出的问题。

如果输出形式看起来更适合您的 "testing" 目的,那么对这些结果的更好的后续查询将使用聚合管道,$redact

db.newCollection([
    { "$redact": {
        "$cond": {
            "if": {
                "$eq": [ 
                    { "$mod": [ { "$add": [ "$_id.index", 1] }, 3 ] },
                0 ]
            },
            "then": "$$KEEP",
            "else": "$$PRUNE"
        }
    }}
])

这至少使用了与 .filter() 之前应用的 "logical condition" 相同的 select "nth index" 项目,而没有列出所有可能的索引值查询参数。

或者只用一个查找块:

db.Collection.find({}).then(function(data) {
  var ret = [];
  for (var i = 0, len = data.length; i < len; i++) {
    if (i % 3 === 0 ) {
      ret.push(data[i]);
    }
  }
  return ret;
});

Returns 一个承诺,您可以调用其 then() 来获取第 N 个模数数据。

$unwind is needed here. You can use $push with $arrayElemAt to project the array value at requested index inside $group聚合。

类似

db.colname.aggregate(
[
  {"$group":{
    "_id":null,
    "valuesatNthindex":{"$push":{"$arrayElemAt":["$instances",N]}
    }}
  },
  {"$project":{"valuesatNthindex":1}}
])

您可以使用以下聚合:

db.col.aggregate([
    {
        $project: {
            instances: {
                $map: {
                    input: { $range: [ 0, { $size: "$instances" }, N ] },
                    as: "index",
                    in: { $arrayElemAt: [ "$instances", "$$index" ] }
                }
            }
        }
    }
])

$range generates a list of indexes. Third parameter represents non-zero step. For N = 2 it will be [0,2,4,6...], for N = 3 it will return [0,3,6,9...] and so on. Then you can use $mapinstances 数组中获取相应的项目。

您可能喜欢这种使用 $lookup 聚合的方法。并且可能是最方便快捷的方式,无需任何聚合技巧。

使用以下架构Names创建集合

[
  { "_id": 1, "name": "name1" },
  { "_id": 2, "name": "name2" }
]

然后是 Instances 父 ID 为 "nameId"

的集合
[
  { "nameId": 1, "value" : 1, "date" : ISODate("2015-03-04T00:00:00.000Z") },
  { "nameId": 1, "value" : 2, "date" : ISODate("2015-04-01T00:00:00.000Z") },
  { "nameId": 1, "value" : 3, "date" : ISODate("2015-03-05T00:00:00.000Z") },
  { "nameId": 2, "value" : 7, "date" : ISODate("2015-03-04T00:00:00.000Z") }, 
  { "nameId": 2, "value" : 8, "date" : ISODate("2015-04-01T00:00:00.000Z") }, 
  { "nameId": 2, "value" : 4, "date" : ISODate("2015-03-05T00:00:00.000Z") }
]

现在使用 $lookup 聚合 3.6 语法,您可以使用 $sample$lookup pipeline 中获取每个 Nth随机元素。

db.Names.aggregate([
  { "$lookup": {
    "from": Instances.collection.name,
    "let": { "nameId": "$_id" },
    "pipeline": [
      { "$match": { "$expr": { "$eq": ["$nameId", "$$nameId"] }}},
      { "$sample": { "size": N }}
    ],
    "as": "instances"
  }}
])

你可以测试一下here