使用map/reduce将集合中的属性映射

27

更新:MongoDB获取集合所有键名的方法 的后续跟进。

正如Kristina所指出的,可以使用Mongodb的map/reduce方法来列出集合中的键名:

db.things.insert( { type : ['dog', 'cat'] } );
db.things.insert( { egg : ['cat'] } );
db.things.insert( { type :  [] }); 
db.things.insert( { hello : []  } );

mr = db.runCommand({"mapreduce" : "things",
"map" : function() {
    for (var key in this) { emit(key, null); }
},  
"reduce" : function(key, stuff) { 
   return null;
}}) 

db[mr.result].distinct("_id")

//output: [ "_id", "egg", "hello", "type" ]
只要我们只想获得位于第一层深度的键,这个方法就可以正常工作。但是,它将无法检索位于更深层次的键。如果我们添加一个新记录:
db.things.insert({foo: {bar: {baaar: true}}})

如果我们再次运行上面的 map-reduce + distinct 代码片段,我们将得到:

[ "_id", "egg", "foo", "hello", "type" ] 

但是我们无法获取嵌套在数据结构中的barbaaar键。问题是:我该如何检索所有键,无论它们的深度如何?理想情况下,我希望脚本可以遍历所有深度级别,生成如下输出:

["_id","egg","foo","foo.bar","foo.bar.baaar","hello","type"]      

提前感谢您!

4个回答

27

好的,这要复杂一些,因为您需要使用一些递归。

为了实现递归,您需要能够将一些函数存储在服务器上。

步骤1:定义一些函数并将它们放在服务器端

isArray = function (v) {
  return v && typeof v === 'object' && typeof v.length === 'number' && !(v.propertyIsEnumerable('length'));
}

m_sub = function(base, value){
  for(var key in value) {
    emit(base + "." + key, null);
    if( isArray(value[key]) || typeof value[key] == 'object'){
      m_sub(base + "." + key, value[key]);
    }
  }
}

db.system.js.save( { _id : "isArray", value : isArray } );
db.system.js.save( { _id : "m_sub", value : m_sub } );

步骤2:定义映射和减少函数

map = function(){
  for(var key in this) {
    emit(key, null);
    if( isArray(this[key]) || typeof this[key] == 'object'){
      m_sub(key, this[key]);
    }
  }
}

reduce = function(key, stuff){ return null; }

步骤三:运行MapReduce并查看结果

mr = db.runCommand({"mapreduce" : "things", "map" : map, "reduce" : reduce,"out": "things" + "_keys"});
db[mr.result].distinct("_id");

你将获得以下结果:

["_id", "_id.isObjectId", "_id.str", "_id.tojson", "egg", "egg.0", "foo", "foo.bar", "foo.bar.baaaar", "hello", "type", "type.0", "type.1"]

这里有一个明显的问题,我们添加了一些意外的字段: 1. _id 数据 2. .0(在 egg 和 type 上)

步骤 4:一些可能的修复方法

对于问题#1,解决方法相对容易。只需修改map函数。将此更改为:

emit(base + "." + key, null); if( isArray...

变成这个:

if(key != "_id") { emit(base + "." + key, null); if( isArray... }

问题#2有点棘手。您想要所有键,而从技术上讲,“egg.0”一个有效的键。您可以修改m_sub以忽略这些数字键。但也很容易看出会出现反效果的情况。假设您在普通数组中有一个关联数组,那么您希望“0”出现。其余解决方案由您自行决定。


感谢盖茨!我也发现了另一种解决方案(但不涉及使用map/reduce),在这里得到了解释:http://groups.google.com/group/mongodb-user/browse_thread/thread/3e10a4b409dd6cb4/ccc9de1fafafe37e?lnk=gst&q=list+keys+depth#ccc9de1fafafe37e - Andrea Fiore
请查看新代码。步骤3的第一部分是指步骤2中所命名的函数。 - Gates VP
因此,这个答案的日期是2010年6月,很可能你需要添加新的out参数,这在当时并不存在。坦白地说,你可能不想使用M/R来完成这个任务,因为它可能可以使用更好的聚合框架来完成。 - Gates VP
2
你真是太棒了,即使在2017年也是如此! - Orelsanpls

8
带着 Gates VP 和 Kristina 的回答作为灵感,我创建了一个名为 Variety 的开源工具,可以做到这一点:https://github.com/variety/variety。希望您会发现它很有用。如果您有问题或使用过程中遇到任何问题,请告诉我。

11
我们需要一个开源工具来查询“模式”,这有点令人遗憾。我认为人们选择MongoDB的原因是:首先,使用其适用于真正的“文档”存储(在高度嵌套或结构无法预先知道的意义下,或者查询性质是这样的,出于某种原因只有MongoDB才是正确的选择),其次,作为懒惰或初级开发人员避免使用更强制性结构(通常是关系型)数据库系统的一种方式。在我的经验中,后者非常普遍,特别是在初创公司中。仅仅因为JSON容易并不意味着它是正确的。 - Adam Donahue

1
我解决了盖茨提出的问题#2,其中例如data.0, data.1, data.2被返回。尽管如上所述这些是有效的键,但为了展示目的,我想要摆脱它们。我通过快速编辑m_sub函数来解决了这个问题,如下所示。
const m_sub = function (base, value) {
for (var key in value) {
    if(key != "_id" && isNaN(key)){
        emit(base + "." + key, null);
        if (isArray(value[key]) || typeof value[key] == 'object') {
            m_sub(base + "." + key, value[key]);
        }
    }
}

这个更改还实现了针对问题#1的上述解决方案,唯一的更改是在第一个if语句中,我将其更改为:
if(key != "_id")

使用 isNaN(x) 函数来实现这个功能:
if(key != "_id" && isNaN(key))

希望这能对某些人有所帮助,如果此解决方案存在问题,请提供反馈!

0
作为一个简单的函数;
const getProps = (db, collection) => new Promise((resolve, reject) => {
  db
  .collection(collection)
  .mapReduce(function() {
    for (var key in this) { emit(key, null) }
  }, (prev, next) => null, {
    out: collection + '_keys'
  }, (err, collection_props) => {
    if (err) reject(err)

    collection_props
    .find()
    .toArray()
    .then(
      props => resolve(props.map(({_id}) => _id))
    )
  })
})

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接