Получить только запрошенный элемент в массиве объектов в коллекции MongoDB

Предположим, в моей коллекции есть следующие документы:

{  
   "_id":ObjectId("562e7c594c12942f08fe4192"),
   "shapes":[  
      {  
         "shape":"square",
         "color":"blue"
      },
      {  
         "shape":"circle",
         "color":"red"
      }
   ]
},
{  
   "_id":ObjectId("562e7c594c12942f08fe4193"),
   "shapes":[  
      {  
         "shape":"square",
         "color":"black"
      },
      {  
         "shape":"circle",
         "color":"green"
      }
   ]
}

Сделайте запрос:

db.test.find({"shapes.color": "red"}, {"shapes.color": 1})

Or

db.test.find({shapes: {"$elemMatch": {color: "red"}}}, {"shapes.color": 1})

Возвращает совпавший документ (Document 1), но всегда со ВСЕМИ элементами массива в shapes:

{ "shapes": 
  [
    {"shape": "square", "color": "blue"},
    {"shape": "circle", "color": "red"}
  ] 
}

Однако я бы хотел получить документ (Document 1) только с массивом, содержащим color=red:

{ "shapes": 
  [
    {"shape": "circle", "color": "red"}
  ] 
}

Как я могу это сделать?


person Sebtm    schedule 21.10.2010    source источник


Ответы (14)


Новый оператор проекции $elemMatch MongoDB 2.2 предоставляет еще один способ изменения возвращенного документа. чтобы содержать только первый совпавший shapes элемент:

db.test.find(
    {"shapes.color": "red"}, 
    {_id: 0, shapes: {$elemMatch: {color: "red"}}});

Возврат:

{"shapes" : [{"shape": "circle", "color": "red"}]}

В 2.2 вы также можете сделать это с помощью $ projection operator , где $ в имени поля объекта проекции представляет индекс первого совпадающего элемента массива поля из запроса. Следующее возвращает те же результаты, что и выше:

db.test.find({"shapes.color": "red"}, {_id: 0, 'shapes.$': 1});

Обновление MongoDB 3.2

Начиная с выпуска 3.2, вы можете использовать новый _8 _ для фильтрации массива во время проецирования, преимущество которого состоит в том, что он включает все совпадения, а не только первое.

db.test.aggregate([
    // Get just the docs that contain a shapes element where color is 'red'
    {$match: {'shapes.color': 'red'}},
    {$project: {
        shapes: {$filter: {
            input: '$shapes',
            as: 'shape',
            cond: {$eq: ['$$shape.color', 'red']}
        }},
        _id: 0
    }}
])

Полученные результаты:

[ 
    {
        "shapes" : [ 
            {
                "shape" : "circle",
                "color" : "red"
            }
        ]
    }
]
person JohnnyHK    schedule 03.09.2012
comment
любое решение, если я хочу, чтобы он возвращал все элементы, которые соответствуют ему, а не только первый? - person Steve Ng; 25.12.2013
comment
Боюсь, я использую Mongo 3.0.X :-( - person charliebrownie; 10.01.2016
comment
@charliebrownie Затем используйте один из других ответов, в которых используется aggregate. - person JohnnyHK; 11.01.2016
comment
этот запрос возвращает только фигуры массива и не возвращает другие поля. Кто-нибудь знает, как вернуть и другие поля? - person Mark Thien; 29.07.2016
comment
Это тоже работает: db.test.find({}, {shapes: {$elemMatch: {color: "red"}}}); - person Paul; 14.07.2017
comment
@johnnyhk Как использовать его с массивом, предположим, мне нужно вернуть фигуры, цвет которых соответствует [красный, зеленый, синий]? - person iit2011081; 13.10.2017
comment
@ user3542450 Лучше всего опубликовать это как новый вопрос, а не обсуждать его здесь. - person JohnnyHK; 13.10.2017
comment
@ iit2011081 Я ответил на ваш вопрос, пожалуйста, посмотрите. Спасибо - person ashishSober; 28.02.2020
comment
Это ошибка: $$ shape.color? двойной $$ в условии фильтра $. - person Augusto Jara; 18.05.2021

Новая структура агрегации в MongoDB 2.2+ предоставляет альтернативу Map / Reduce. Оператор $unwind можно использовать для разделения вашего shapes массива на поток документов, которые можно соответствует:

db.test.aggregate(
  // Start with a $match pipeline which can take advantage of an index and limit documents processed
  { $match : {
     "shapes.color": "red"
  }},
  { $unwind : "$shapes" },
  { $match : {
     "shapes.color": "red"
  }}
)

Результаты в:

{
    "result" : [
        {
            "_id" : ObjectId("504425059b7c9fa7ec92beec"),
            "shapes" : {
                "shape" : "circle",
                "color" : "red"
            }
        }
    ],
    "ok" : 1
}
person Stennie    schedule 03.09.2012
comment
@JohnnyHK: В этом случае $elemMatch - другой вариант. Я попал сюда с помощью вопроса группы Google где $ elemMatch не будет работать, потому что он возвращает только первое совпадение для каждого документа. - person Stennie; 03.09.2012
comment
Спасибо, я не знал об этом ограничении, так что это хорошо. Извините за удаление моего комментария, на который вы отвечаете, я решил вместо этого опубликовать другой ответ и не хотел вводить людей в заблуждение. - person JohnnyHK; 03.09.2012
comment
@JohnnyHK: Не беспокойтесь, теперь есть три полезных ответа на этот вопрос ;-) - person Stennie; 03.09.2012
comment
Для других искателей в дополнение к этому я также попытался добавить { $project : { shapes : 1 } } - который, похоже, работал и был бы полезен, если бы прилагаемые документы были большими, а вы просто хотели просмотреть значения shapes ключей. - person user1063287; 04.12.2014
comment
агрегировать $ undiwnd-формы в начале агрегации - это безумно высокие затраты в больших базах данных, очень-очень не рекомендуется - person Pikachu; 28.01.2015
comment
@calmbird Вы правы; это упрощенный пример. Для большого набора данных вам нужно начать с индексированного $match запроса или использовать $elemMatch с обычным find (), если первого совпадающего элемента достаточно. - person Stennie; 28.01.2015
comment
@calmbird Я обновил пример, включив в него начальную стадию $ match. Если вас интересует более эффективная функция, я бы посмотрел / поддержал SERVER-6612: Поддержка проектирования несколько значений массива в проекции, например спецификатора проекции $ elemMatch в системе отслеживания проблем MongoDB. - person Stennie; 28.01.2015
comment
Замечательно! Спасибо. Я только что задал об этом вопрос, но отказался: P - person Pikachu; 28.01.2015

Внимание! Этот ответ предоставляет решение, которое было актуально в то время, до того как были представлены новые функции MongoDB 2.2 и выше. См. Другие ответы, если вы используете более новую версию MongoDB.

Параметр селектора поля ограничен полными свойствами. Его нельзя использовать для выбора части массива, только для всего массива. Я пробовал использовать позиционный оператор $, но этого не произошло. Работа.

Самый простой способ - просто отфильтровать фигуры в клиенте.

Если вам действительно нужен правильный вывод непосредственно из MongoDB, вы можете использовать map-reduce для фильтрации фигур.

function map() {
  filteredShapes = [];

  this.shapes.forEach(function (s) {
    if (s.color === "red") {
      filteredShapes.push(s);
    }
  });

  emit(this._id, { shapes: filteredShapes });
}

function reduce(key, values) {
  return values[0];
}

res = db.test.mapReduce(map, reduce, { query: { "shapes.color": "red" } })

db[res.result].find()
person Niels van der Rest    schedule 21.10.2010

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

db.test.aggregate([
    { $match: { 
         shapes: { $elemMatch: {color: "red"} } 
    }},
    { $redact : {
         $cond: {
             if: { $or : [{ $eq: ["$color","red"] }, { $not : "$color" }]},
             then: "$$DESCEND",
             else: "$$PRUNE"
         }
    }}]);

$redact "ограничивает содержимое документов на основе информации, хранящейся в самих документах". Таким образом, он будет работать только внутри документа. Он в основном сканирует ваш документ сверху вниз и проверяет, соответствует ли он вашему if условию, которое находится в $cond, если есть совпадение, он либо сохранит содержимое ($$DESCEND), либо удалит ($$PRUNE).

В приведенном выше примере сначала $match возвращает весь массив shapes, а $ redact сокращает его до ожидаемого результата.

Обратите внимание, что {$not:"$color"} необходимо, потому что он также будет сканировать верхний документ, и если $redact не найдет поле color на верхнем уровне, он вернет false, который может удалить весь документ, который нам не нужен.

person anvarik    schedule 04.06.2014
comment
идеальный ответ. Как вы упомянули, $ unwind потребляет много оперативной памяти. Так будет лучше по сравнению. - person manojpt; 21.04.2015
comment
Я сомневаюсь. В этом примере shape - это массив. Будет ли $ redact сканировать все объекты в массиве фигур ?? Насколько это будет хорошо с точки зрения производительности ?? - person manojpt; 23.04.2015
comment
не все, а результат вашего первого матча. Вот почему вы указываете $match в качестве первого агрегированного этапа. - person anvarik; 23.04.2015
comment
оккк .. если индекс создан на цветном поле, даже тогда он будет сканировать все объекты в массиве фигур ??? Какой может быть эффективный способ сопоставления нескольких объектов в массиве ??? - person manojpt; 24.04.2015
comment
@anvarik, если я правильно понял, нет способа использовать индекс, определенный в поле массива, для ускорения поиска совпадающих элементов массива: и $ unwind, и $ redact должны сканировать весь массив, но $ redact использует гораздо меньше оперативной памяти, потому что он работает с одним документом вместо создания одной копии документа для каждого элемента массива. Я прав или что-то упускаю? - person Cec; 24.09.2015
comment
Блестяще! Я не понимаю, как здесь работает $ eq. Я оставил его изначально, и это не сработало для меня. Каким-то образом он просматривает массив форм, чтобы найти совпадение, но запрос никогда не указывает, какой массив искать. Например, если в документах были формы и, например, размеры; будет ли $ eq искать совпадения в обоих массивах? $ Redact просто ищет что-нибудь в документе, которое соответствует условию 'if'? - person Onosa; 30.12.2015

Лучше сделать запрос в соответствующем элементе массива, используя $slice, полезно ли возвращать значимый объект в массиве.

db.test.find({"shapes.color" : "blue"}, {"shapes.$" : 1})

$slice полезно, когда вы знаете индекс элемента, но иногда вам нужно, чтобы какой-либо элемент массива соответствовал вашим критериям. Вы можете вернуть соответствующий элемент с помощью оператора $.

person Narendran    schedule 18.09.2014

 db.getCollection('aj').find({"shapes.color":"red"},{"shapes.$":1})

ВЫХОДЫ

{

   "shapes" : [ 
       {
           "shape" : "circle",
           "color" : "red"
       }
   ]
}
person Viral Patel    schedule 07.12.2016
comment
спасибо за запрос, но он просто возвращает первый, даже если условие совпадает для нескольких элементов в массиве, какие-либо предложения? - person Genius; 11.03.2021

Синтаксис поиска в mongodb:

    db.<collection name>.find(query, projection);

и второй запрос, который вы написали, то есть

    db.test.find(
    {shapes: {"$elemMatch": {color: "red"}}}, 
    {"shapes.color":1})

в этом случае вы использовали оператор $elemMatch в части запроса, тогда как если вы используете этот оператор в части проекции, вы получите желаемый результат. Вы можете записать свой запрос как

     db.users.find(
     {"shapes.color":"red"},
     {_id:0, shapes: {$elemMatch : {color: "red"}}})

Это даст вам желаемый результат.

person Vicky    schedule 22.12.2013
comment
У меня это работает. Однако похоже, что "shapes.color":"red" в параметре запроса (первый параметр метода поиска) не требуется. Вы можете заменить его на {} и получить те же результаты. - person Erik Olson; 10.05.2014
comment
@ErikOlson Ваше предложение верно в приведенном выше случае, когда нам нужно найти все документы красного цвета и применить проекцию только к ним. Но допустим, если кому-то требуется узнать весь документ, имеющий синий цвет, но он должен возвращать только те элементы этого массива фигур, которые имеют красный цвет. В этом случае на вышеуказанный запрос может ссылаться и кто-то другой. - person Vicky; 11.05.2014
comment
Это кажется самым простым, но я не могу заставить его работать. Он возвращает только первый соответствующий вложенный документ. - person newman; 30.08.2015
comment
populate не работает, почему? - person Mahmood Hussain; 17.02.2021
comment
@MahmoodHussain Этому ответу почти 7 лет, поэтому может быть проблема с версией. Можете ли вы проверить последнюю документацию. Я постараюсь запустить аналогичный метод на последней версии и поделиться своими выводами. Можете объяснить, чего именно вы пытаетесь достичь? - person Vicky; 17.02.2021
comment
@Vicky Patient.find( { user: req.user._id, _id: req.params.patientId, "tests.test": req.params.testId, }, { "tests.$": 1, name: 1, } ) .populate({ path: "tests", populate: { path: "test", model: "Test", }, }) .exec((err, patient) => { if (err || !patient) { return res.status(404).send({ error: { message: err } }); } return res.send({ patient }); }); Но при заполнении возникает ошибка - person Mahmood Hussain; 18.02.2021
comment
Привет @MahmoodHussain, это не похоже на собственный запрос mongodb. Вы используете Mongoose или любую другую библиотеку для запроса mongo. Этот вопрос был только для собственного запроса mongo. Пожалуйста, предоставьте более подробную информацию о вашей реализации и, возможно, поделитесь структурой объекта. Вы можете создать здесь новый вопрос о переполнении стека со всеми подробностями, и мы можем помочь вам лучше там. - person Vicky; 18.02.2021
comment
@MahmoodHussain Я могу посоветовать добавить имя модели после первого вызова метода заполнения, .populate ({path: tests, ‹-------------------------------- ДОБАВИТЬ ИМЯ МОДЕЛИ ЗДЕСЬ ›››, заполнить: {path: test, model: Test,. ... - person Vicky; 18.02.2021

Спасибо JohnnyHK.

Здесь я просто хочу добавить более сложное использование.

// Document 
{ 
"_id" : 1
"shapes" : [
  {"shape" : "square",  "color" : "red"},
  {"shape" : "circle",  "color" : "green"}
  ] 
} 

{ 
"_id" : 2
"shapes" : [
  {"shape" : "square",  "color" : "red"},
  {"shape" : "circle",  "color" : "green"}
  ] 
} 


// The Query   
db.contents.find({
    "_id" : ObjectId(1),
    "shapes.color":"red"
},{
    "_id": 0,
    "shapes" :{
       "$elemMatch":{
           "color" : "red"
       } 
    }
}) 


//And the Result

{"shapes":[
    {
       "shape" : "square",
       "color" : "red"
    }
]}
person Eddy    schedule 23.03.2014

Вам просто нужно запустить запрос

db.test.find(
{"shapes.color": "red"}, 
{shapes: {$elemMatch: {color: "red"}}});

вывод этого запроса

{
    "_id" : ObjectId("562e7c594c12942f08fe4192"),
    "shapes" : [ 
        {"shape" : "circle", "color" : "red"}
    ]
}

как вы и ожидали, он даст точное поле из массива, которое соответствует цвету: 'красный'.

person Vaibhav Patil    schedule 17.09.2016

Наряду с $project будет более подходящим, чтобы другие разумные совпадающие элементы были объединены вместе с другими элементами в документе.

db.test.aggregate(
  { "$unwind" : "$shapes" },
  { "$match" : { "shapes.color": "red" } },
  { 
    "$project": {
      "_id":1,
      "item":1
    }
  }
)
person shakthydoss    schedule 09.02.2013
comment
не могли бы вы описать, что это достигается с помощью набора входов и выходов? - person Alexander Mills; 24.11.2015

Точно так же вы можете найти для нескольких

db.getCollection('localData').aggregate([
    // Get just the docs that contain a shapes element where color is 'red'
  {$match: {'shapes.color': {$in : ['red','yellow'] } }},
  {$project: {
     shapes: {$filter: {
        input: '$shapes',
        as: 'shape',
        cond: {$in: ['$$shape.color', ['red', 'yellow']]}
     }}
  }}
])
person ashishSober    schedule 28.02.2020
comment
Этот ответ действительно является предпочтительным способом 4.x: $match, чтобы сократить пространство, затем $filter, чтобы сохранить то, что вы хотите, перезаписав поле ввода (используйте вывод $filter в поле shapes до $project обратно на shapes. Примечание о стиле: лучше не использовать имя поля в качестве аргумента as, потому что это может впоследствии привести к путанице с $$shape и $shape. Я предпочитаю zz как поле as, потому что оно действительно выделяется. - person Buzz Moschetti; 03.03.2020

Используйте функцию агрегирования и $project, чтобы получить конкретное поле объекта в документе

db.getCollection('geolocations').aggregate([ { $project : { geolocation : 1} } ])

результат:

{
    "_id" : ObjectId("5e3ee15968879c0d5942464b"),
    "geolocation" : [ 
        {
            "_id" : ObjectId("5e3ee3ee68879c0d5942465e"),
            "latitude" : 12.9718313,
            "longitude" : 77.593551,
            "country" : "India",
            "city" : "Chennai",
            "zipcode" : "560001",
            "streetName" : "Sidney Road",
            "countryCode" : "in",
            "ip" : "116.75.115.248",
            "date" : ISODate("2020-02-08T16:38:06.584Z")
        }
    ]
}
person KARTHIKEYAN.A    schedule 09.02.2020

Хотя этот вопрос был задан 9,6 лет назад, он очень помог многим людям, и я был одним из них. Спасибо всем за все ваши вопросы, подсказки и ответы. Взяв один из ответов здесь ... Я обнаружил, что следующий метод также можно использовать для проецирования других полей в родительском документе. Это может быть полезно для кого-то.

Для следующего документа необходимо было выяснить, настроена ли для сотрудника (emp # 7839) история отпусков на 2020 год. История отпусков реализована как встроенный документ в родительский документ Employee.

db.employees.find( {"leave_history.calendar_year": 2020}, 
    {leave_history: {$elemMatch: {calendar_year: 2020}},empno:true,ename:true}).pretty()


{
        "_id" : ObjectId("5e907ad23997181dde06e8fc"),
        "empno" : 7839,
        "ename" : "KING",
        "mgrno" : 0,
        "hiredate" : "1990-05-09",
        "sal" : 100000,
        "deptno" : {
                "_id" : ObjectId("5e9065f53997181dde06e8f8")
        },
        "username" : "none",
        "password" : "none",
        "is_admin" : "N",
        "is_approver" : "Y",
        "is_manager" : "Y",
        "user_role" : "AP",
        "admin_approval_received" : "Y",
        "active" : "Y",
        "created_date" : "2020-04-10",
        "updated_date" : "2020-04-10",
        "application_usage_log" : [
                {
                        "logged_in_as" : "AP",
                        "log_in_date" : "2020-04-10"
                },
                {
                        "logged_in_as" : "EM",
                        "log_in_date" : ISODate("2020-04-16T07:28:11.959Z")
                }
        ],
        "leave_history" : [
                {
                        "calendar_year" : 2020,
                        "pl_used" : 0,
                        "cl_used" : 0,
                        "sl_used" : 0
                },
                {
                        "calendar_year" : 2021,
                        "pl_used" : 0,
                        "cl_used" : 0,
                        "sl_used" : 0
                }
        ]
}
person Ali    schedule 16.04.2020

person    schedule
comment
Добро пожаловать в Stack Overflow! Спасибо за фрагмент кода, который может оказать некоторую немедленную помощь. Правильное объяснение значительно улучшило бы его долгосрочную ценность, объяснив, почему это хорошее решение проблемы, и сделает его более полезным для будущих читателей с другими подобными вопросами. Измените свой ответ, чтобы добавить пояснения, включая сделанные вами предположения. - person sepehr; 25.10.2018