Использование map/reduce для отображения свойств в коллекции

Обновление: продолжение MongoDB Получить имена всех ключей в коллекции.

Как указано Kristina, можно использовать карту/сокращение Mongodb для отображения ключей в коллекции:

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 + different, мы получим:

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

Но мы не получим бар и ключи baaar, которые вложены в структуру данных. Возникает вопрос: как получить все ключи, независимо от их уровня глубины? В идеале мне бы хотелось, чтобы script дошел до всех уровней глубины, создавая такой вывод, как:

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

Заранее благодарю вас!

Ответ 1

ОК, это немного сложнее, потому что вам нужно будет использовать некоторую рекурсию.

Чтобы сделать рекурсию, вам нужно будет сохранить некоторые функции на сервере.

Шаг 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; }

Шаг 3: запустите отображение карты и просмотрите результаты

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 (по яйцам и типу)

Шаг 4: Некоторые возможные исправления

Для проблемы # 1 исправление относительно просто. Просто измените функцию map. Измените это:

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

:

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

Проблема № 2 немного более рискованная. Вы хотели, чтобы все ключи и технически "egg.0" были действительным ключом. Вы можете изменить m_sub, чтобы игнорировать такие числовые клавиши. Но также легко увидеть ситуацию, в которой это происходит. Скажем, у вас есть ассоциативный массив внутри обычного массива, тогда вы хотите, чтобы появилось "0". Я оставлю все это решение до вас.

Ответ 2

С вдохновением от Gates VP и Kristina я создал инструмент с открытым исходным кодом под названием Variety, который делает именно это: https://github.com/variety/variety

Надеюсь, вы найдете это полезным. Дайте мне знать, если у вас есть вопросы или какие-либо проблемы с его использованием.

Ответ 3

как простая функция;

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))
    )
  })
})