Найти дубликат внутри массива без $unwind

У меня есть коллекция пользователей ниже

[{
    "_id": 1,
    "adds": ["111", "222", "333", "111"]
}, {
    "_id": 2,
    "adds": ["555", "666", "777", "555"]
}, {
    "_id": 3,
    "adds": ["888", "999", "000", "888"]
}]

Мне нужно найти дубликаты внутри массива adds

Ожидаемый результат должен быть

[{
    "_id": 1,
    "adds": ["111"]
}, {
    "_id": 2,
    "adds": [ "555"]
}, {
    "_id": 3,
    "adds": ["888"]
}]

Я пробовал использовать множество операторов $setUnion, $setDifference, но ни один из них не помог.

Пожалуйста, помогите!!!


person Dark Knight    schedule 16.10.2018    source источник
comment
Почему вы пытаетесь избежать $unwind? $unwind/$group/$match/$project будет простым подходом для сравнения с ответами, опубликованными до сих пор. Кроме того, какую конкретную версию сервера MongoDB вы используете?   -  person Stennie    schedule 10.12.2018
comment
@Stennie Я использую последнюю версию mongodb 4.0. Потому что $unwind вызывает некоторые проблемы с производительностью. Найти дубликаты внутри массива не составляет большого труда. Для этого должен быть какой-то оператор агрегации? Не так ли? Кстати спасибо за ответ.   -  person Dark Knight    schedule 10.12.2018
comment
@Stennie Есть комментарии? ;-)   -  person Dark Knight    schedule 10.12.2018
comment
Как и в MongoDB 4.0, нет сокращенного оператора агрегации для фильтрации массива, чтобы найти только дубликаты. Существует несколько разных подходов для достижения этого результата с использованием существующих операторов, но вам придется провести бенчмаркинг, чтобы сравнить производительность для вашего варианта использования. Если это обычная потребность, вы можете рассмотреть возможность настройки модели данных, чтобы сделать запросы более эффективными. Например, вместо массива значений у вас может быть массив объектов со счетчиками, которые вы увеличиваете при добавлении к массиву: [{"111": 2, "222": 1, "333": 1}].   -  person Stennie    schedule 29.12.2018
comment
Вы также можете предложить функцию для нового оператора в системе отслеживания проблем MongoDB Jira (проект: SERVER, компонент: Aggregation Framework). Это вряд ли поможет вам в краткосрочной перспективе, но если другие заинтересуются той же функцией, она может появиться в будущем выпуске MongoDB.   -  person Stennie    schedule 29.12.2018
comment
Спасибо, @Stennie. Я открыл одну jira для этого jira.mongodb.org/browse/SERVER-38805.   -  person Dark Knight    schedule 02.01.2019


Ответы (3)


Вы можете использовать $range для создания массивов чисел. от 1 до n, где n — это $размер из adds. Затем вы можете «перебрать» эти числа и проверить, есть ли adds в index ($arrayElemAt) существует где-то до index, если да, то его следует рассматривать как дубликат. Вы можете использовать $indexOfArray, чтобы проверить, существует ли элемент в массиве, указав 0 и index в качестве диапазона поиска.

Тогда вам просто нужно использовать $project и $map для замены индексов фактическими элементами. Вы также можете добавить $setUnion, чтобы избежать повторяющихся дубликатов. в конечном наборе результатов.

db.users.aggregate([
    {
        $addFields: {
            duplicates: {
                $filter: {
                    input: { $range: [ 1, { $size: "$adds" } ] },
                    as: "index",
                    cond: {
                        $ne: [ { $indexOfArray: [ "$adds", { $arrayElemAt: [ "$adds", "$$index" ]  }, 0, "$$index" ] }, -1 ]
                    }
                }
            }
        }
    },
    {
        $project: {
            _id: 1,
            adds: {
                $setUnion: [ { $map: { input: "$duplicates", as: "d", in: { $arrayElemAt: [ "$adds", "$$d" ] } } }, [] ]
            }
        }
    }
])

Отпечатки:

{ "_id" : 1, "adds" : [ "111" ] }
{ "_id" : 2, "adds" : [ "555" ] }
{ "_id" : 3, "adds" : [ "888" ] }
person mickl    schedule 16.10.2018
comment
здорово здорово здорово. Потрясающий Микл - person Dark Knight; 16.10.2018
comment
@mickl - похоже, это не работает, когда вы повторяете несколько значений. ex добавляет: [111, 222, 333,333, 111] - person s7vr; 16.10.2018
comment
Пожалуйста, обратите внимание, что вам не нужен $slice, так как $indexOfArray поддерживает параметр startIndex. Кроме того, два этапа $addFields и $project можно объединить в один. - person dnickless; 16.10.2018
comment
Изменил мой ответ, спасибо @dnickless, это сделало его немного короче. Я по-прежнему оставлю два этапа, так как считаю, что это более читабельно, чем вложенная карта/фильтр. - person mickl; 17.10.2018

Вот еще одна версия, которую вы можете сравнить с точки зрения производительности:

db.users.aggregate({
  $project:{
    "adds":{
      $reduce:{
        "input":{$range:[0,{$size:"$adds"}]}, // loop variable from 0 to max. index of $adds array
      //"input":{$range:[0,{$subtract:[{$size:"$adds"},1]}]}, // this would be enough but looks more complicated
        "initialValue":[],
        "in":{
            $let:{
              "vars":{
                "curr": { $arrayElemAt: [ "$adds", "$$this"] } // the element we're looking at
              },
              "in":{
                // if there is another identical element after the current one then we have a duplicate
                $cond:[
                  {$ne:[{$indexOfArray:["$adds","$$curr",{$add:["$$this",1]}]},-1]},
                  {$setUnion:["$$value",["$$curr"]]}, // combine duplicates found so far with new duplicate
                  "$$value" // continue with current value
                ]
              }
            }
        }
      }
    }
  }
})

Логика основана на переменной цикла, которую мы получаем через $range оператор. Эта переменная цикла обеспечивает последовательный доступ к массиву adds. Для каждого элемента, который мы просматриваем, мы проверяем, есть ли другой идентичный элемент после текущего индекса. Если да, то у нас есть дубликат, иначе нет.

person dnickless    schedule 16.10.2018

Вы можете попробовать агрегацию ниже. Идея состоит в том, чтобы собрать различные значения, перебрать значения и проверить, присутствует ли значение в массиве adds; если присутствует, сохраните значение, иначе игнорируйте значение.

db.users.aggregate({
  "$project":{
    "adds":{
      "$reduce":{
        "input":{"$setUnion":["$adds",[]]},
        "initialValue":[],
        "in":{
          "$concatArrays":[
            "$$value",
            {"$let":{
              "vars":{
                "match":{
                  "$filter":{"input":"$adds","as":"a","cond":{"$eq":["$$a","$$this"]}}
                }},
                "in":{
                  "$cond":[{"$gt":[{"$size":"$$match"},1]},["$$this"],[]]
                }
            }}
          ]
        }
      }
    }
  }
})
person s7vr    schedule 16.10.2018
comment
Так, наверное, будет быстрее: db.users.aggregate({ "$project":{ "adds":{ "$reduce":{ "input":"$adds", "initialValue":[], "in":{ "$let":{ "vars":{ "match":{ "$filter":{"input":"$adds","as":"a","cond":{"$eq":["$$a","$$this"]}} }}, "in":{ "$cond":[{"$gt":[{"$size":"$$match"},1]},{"$setUnion":["$$value",["$$this"]]},"$$value"] } } } } } } }) - person dnickless; 16.10.2018