Списки MongoDB - получите каждый N-й элемент

У меня есть схема 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" : [ 
      ...
    ]
  }
]

где количество экземпляров для каждого элемента может быть довольно большим.

Я иногда хочу получить только образец данных, т.е. получить каждый третий экземпляр или каждый 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)?

Ответ 1

Вам может понравиться такой подход с использованием агрегации $lookup. И, вероятно, самый удобный и быстрый способ без какой-либо уловки агрегации.

Создайте коллекцию Names со следующей схемой

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

а затем коллекция Instances имеющих родительский идентификатор как "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 aggregation 3.6 вы можете использовать $sample внутри pipeline $lookup для случайного получения каждого N-го элемента.

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

Вы можете проверить это здесь

Ответ 2

Похоже, что ваш вопрос четко задан "получить каждый n-й экземпляр", что кажется довольно ясным вопросом.

Операции запроса, такие как .find() могут действительно возвращать документ "как есть", за исключением общего поля "выделение" в проекции и таких операторов, как оператор позиционного $ match или $elemMatch которые допускают единичный сопоставленный элемент массива.

Конечно, есть $slice, но это только позволяет "выбор диапазона" в массиве, поэтому снова не применяется.

"Единственными" вещами, которые могут изменить результат на сервере, являются .aggregate() и .mapReduce(). Первый не очень хорошо "играет" с "нарезкой" массивов, по крайней мере, с помощью "n" элементов. Однако, поскольку аргументы "function()" mapReduce основаны на логике JavaScript, у вас есть немного больше возможностей для игры.

Для аналитических процессов и для аналитических целей "только", затем просто отфильтруйте содержимое массива с помощью mapReduce, используя .filter():

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", но если это только для анализа/анализа, то в этой концепции нет ничего плохого. Конечно, вывод не "точно", как структурирован ваш документ, но он настолько близок к факсимиле, насколько может получить mapReduce.

Другое предложение, которое я вижу здесь, требует создания новой коллекции со всеми "денормализованными" элементами и вставки "индекса" из массива как части уникального _id _id. Это может привести к тому, что вы можете запросить напрямую, например, "каждый n-й элемент", который вам все равно придется сделать:

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

Поэтому потренируйтесь и укажите значение индекса "каждого n-го элемента", чтобы получить "каждый n-й элемент". Так что, похоже, это не решает проблему, которая была задана.

Если форма вывода кажется более желательной для ваших целей "тестирования", то лучшим последующим запросом по этим результатам будет использование конвейера агрегации с $redact

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

Это, по крайней мере, использует "логическое условие", почти такое же, как то, что применялось с .filter() раньше, чтобы просто выбрать элементы "n-го индекса" без перечисления всех возможных значений индекса в качестве аргумента запроса.

Ответ 3

К сожалению, с помощью структуры агрегации это невозможно, поскольку для этого потребуется опция с $unwind, чтобы испустить индекс/позиция массива, из которых в настоящее время агрегация не может обрабатываться. Для этого здесь есть открытый JIRA билет 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(){}

Затем вы можете запустить следующую mapReduce функцию в своей коллекции:

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

И затем вы можете запросить коллекцию результатов в geta list/array каждого N-го элемента массива экземпляров с помощью map():

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

Ответ 4

Нет необходимости $unwind здесь. Вы можете использовать $push с $arrayElemAt для проецирования значения массива по запрошенному индексу внутри агрегации $group.

Что-то вроде

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

Ответ 5

Вы можете использовать ниже агрегации:

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

$ range генерирует список индексов. Третий параметр представляет ненулевой шаг. Для N = 2 это будет [0,2,4,6...], для N = 3 он вернет [0,3,6,9...] и так далее. Затем вы можете использовать $ map для получения соответствующих элементов из массива instances.

Ответ 6

Или с помощью всего лишь блока поиска:

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

Возвращает обещание, которое затем() вы можете вызвать для получения данных по модулю N.