获取每个的唯一键和不同的值列表

时间:2014-03-21 00:42:38

标签: mongodb aggregation-framework

我有一组这样的文件:

输入

[
  { color: "red", size: "small" },
  { color: "blue", size: "small" },
  { color: "red", size: "medium" },
  { color: "green", size: "medium" },
  { color: "black", size: "large" }
];

我想创建一个由每个键组成的集合,以及每个键的不同值:

输出

[
  { name: "color", values: ["red", "blue", "green", "black"] },
  { name: "size", values: ["small" "medium", "large"] }
]

我不知道输入文档的键是什么。

我知道如何分别解决这两个问题:

  1. 按照this answer
  2. 获取任意文档的键
  3. 使用aggregation frameworks $addToSet operator获取每个密钥的不同值列表。
  4. 我想一次性完成这件事。我认为可以在步骤1中使用聚合框架,并将其导入第2步,但可能不会....

    感谢

2 个答案:

答案 0 :(得分:1)

正如我在评论中提到的,如果不事先知道字段名称,除非您愿意考虑不同的架构,否则您无法在一次通过中执行此操作。

以下是一个想法,例如使用不同的架构收集相同的数据,但作为键和值:

{ values : [  {  "k" : "color",  "v" : "red" }, 
              {  "k" : "size",  "v" : "small" } ] }
{ values : [  {  "k" : "color",  "v" : "blue" }, 
              {  "k" : "size",  "v" : "small" } ] }
{ values : [  {  "k" : "color",  "v" : "red" },
              {  "k" : "size",  "v" : "medium" } ] }
{ values : [  {  "k" : "color",  "v" : "green" }, 
              {  "k" : "size",  "v" : "medium" } ] }
{ values : [  {  "k" : "color",  "v" : "black" },
              {  "k" : "size",  "v" : "large" } ] }

汇总是微不足道的,因为它只会对密钥名称进行分组,并使用$addToSet来收集值。

> db.test.aggregate({ $unwind : '$values' }, 
     { $group : 
          {  _id : "$values.k", 
            value: { $addToSet: "$values.v" } } })
{
        "result" : [
                {
                        "_id" : "size",
                        "value" : [
                                "large",
                                "medium",
                                "small"
                        ]
                },
                {
                        "_id" : "color",
                        "value" : [
                                "black",
                                "green",
                                "blue",
                                "red"
                        ]
                }
        ],
        "ok" : 1
}

答案 1 :(得分:0)

我认为这样做的一种方法完全在mapReduce中:

首先是一个映射器:

var mapper = function () {

  for ( var k in this ) {
    if ( k != '_id' )
      emit( { name: k }, this[k] );
  }

};

然后是减速器:

var reducer = function ( key, values ) {

    var unique = [];

    Array.prototype.inArray = function(value) {
        for( var i=0; i < this.length; i++) {
            if ( this[i] == value ) return true;
        }
        return false;
    };

    Array.prototype.addToSet = function(value) {
        if ( this.length == 0 ) {
            this.push(value);
        } else if ( !this.inArray(value) ) {
            this.push(value);
        }
    };

    values.forEach(function(value) {
        unique.addToSet(value);
    });

    return { values: unique };

};

然后运行输出操作:

db.collection.mapReduce(mapper,reducer,{ out: { inline: 1 } })

这给出了“漂亮”的mapReduce样式输出:

{
    "results" : [
            {
                    "_id" : {
                            "name" : "color"
                    },
                    "value" : {
                            "values" : [
                                    "red",
                                    "blue",
                                    "green",
                                    "black"
                            ]
                    }
            },
            {
                    "_id" : {
                            "name" : "size"
                    },
                    "value" : {
                            "values" : [
                                    "small",
                                    "medium",
                                    "large"
                            ]
                    }
            }
    ],
    "timeMillis" : 2,
    "counts" : {
            "input" : 5,
            "emit" : 10,
            "reduce" : 2,
            "output" : 2
    },
    "ok" : 1,
}

只要您可以生成密钥,那么您可以这样构建:

他们列出事情会让事情变得更加困难,但以下内容会出现问题:

db.collection.aggregate([
    { "$group": {
        "_id": false,
        "size": { "$addToSet": "$size" },
        "color": { "$addToSet": "$color" }
    }}
])

结果如下:

{
    "result" : [
            {
                    "_id" : false,
                    "size" : [
                            "large",
                            "medium",
                            "small"
                    ],
                    "color" : [
                            "black",
                            "green",
                            "blue",
                            "red"
                    ]
            }
    ],
    "ok" : 1
}

所以你在一次传递中确实有两个不同的集合。

这样做你如何呈现 是可能的,但只需做更多的工作:

db.collection.aggregate([
    // Project with the "name" as an array of possible
    { "$project": {
          "size": 1,
          "color": 1,
          "name": { "$cond": [ 1, [ "size", "color" ], 0 ] }
    }},

    // Unwind the "name" values. Create duplicates
    { "$unwind": "$name" },

    // Conditionally assign the fields to "value"
    { "$project": {
        "name": 1,
        "value": {"$cond": [
            { "$eq": [ "$name", "size"] },
            "$size",
            "$color"                
        ]}
    }},

    // Group the results by name
    { "$group": {
        "_id": "$name",
        "values": { "$addToSet": "$value" },
    }},

    // Project the fields you want
    { "$project": {
        "_id": 0,
        "name": "$_id",
        "values": 1
    }}
])

这可以为您提供预期的结果。

其中有$cond的“有趣”用法,在将来分配“name”的版本中,应该可以使用$literal运算符替换它。在分配的数组被解开之后,现在有两个的所有内容,但这与后来的$addToSet操作无关。

然后根据匹配的内容有条件地分配“值”。将结果分组到名称上,您有两个按名称键入的文档以及相应的值。

享受。