Отримайте лише запитуваний елемент у масиві об'єктів у колекції MongoDB


377

Припустимо, у моїй колекції є такі документи:

{  
   "_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})

Або

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

Повертає збіглий документ (документ 1) , але завжди з ВСІМ елементами масиву в shapes:

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

Однак я хотів би отримати документ (Документ 1) лише з масиву, який містить color=red:

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

Як я можу це зробити?

Відповіді:


416

Новий $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, ви можете використовувати новий $filterоператор агрегації для фільтрації масиву під час проектування, який має перевагу включати всі збіги, а не лише перший.

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"
            }
        ]
    }
]

15
будь-яке рішення, якщо я хочу, щоб він повертав усі елементи, що відповідають йому, а не лише перший?
Стів Нг

Боюся, я використовую Mongo 3.0.X :-(
charliebrownie

@charliebrownie Потім використовуйте одну з інших відповідей, які використовують aggregate.
JohnnyHK

цей запит повертає масив лише "фігури", а інші поля не повертаються. Хтось знає, як повернути й інші поля?
Марк Тієн

1
Це також працює:db.test.find({}, {shapes: {$elemMatch: {color: "red"}}});
Пол

97

Нова рамка агрегації в 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
}

6
@JohnnyHK: У цьому випадку $elemMatchє інший варіант. Я фактично потрапив сюди через запитання групи Google, де $ elemMatch не працюватиме, оскільки він повертає лише першу відповідність за документ.
Стенні

1
Дякую, я не знав цього обмеження, тому це добре знати. Вибачте за видалення мого коментаря, на який ви відповідаєте, я вирішив надіслати ще одну відповідь, і не хотів плутати людей.
JohnnyHK

3
@JohnnyHK: Не хвилюйтесь, зараз є три корисні відповіді на питання ;-)
Stennie

Для інших шукачів, крім цього, я також спробував додати { $project : { shapes : 1 } }- що, здавалося, працює і було б корисно, якщо документи, що додаються, були великими, і ви просто хотіли переглянути shapesключові значення.
user1063287

2
@calmbird Я оновив приклад, щоб включити початковий етап матчу $. Якщо вас цікавить більш ефективна пропозиція щодо функції, я б спостерігав / доповнював SERVER-6612: Підтримка проектування декількох значень масиву в проекції, як специфікатор проекції $ elemMatch в трекері випуску MongoDB.
Стенні

30

Іншим цікавим способом є використання $ redact , що є однією з нових особливостей агрегації MongoDB 2.6 . Якщо ви використовуєте 2.6, вам не потрібно $ розмотати, що може спричинити проблеми з продуктивністю, якщо у вас є великі масиви.

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що може зняти весь документ, який ми не хочемо.


1
ідеальна відповідь. Як ви вже згадували, $ unind буде споживати багато оперативної пам'яті. Так що це буде краще в порівнянні.
manojpt

У мене є сумніви. У прикладі "фігури" - це масив. Чи буде "$ redact" сканувати всі об'єкти в масиві "фігури" ?? Як це буде добре стосовно продуктивності ??
manojpt

не все, а результат вашого першого матчу. Ось чому ви ставите$match своїм першим етапом сукупності
Анварик

okkk .. якщо індекс, створений у полі "color", навіть тоді він буде сканувати всі об'єкти в масиві "фігури" ??? Який може бути ефективний спосіб зіставлення декількох об'єктів у масиві ???
manojpt

2
Блискуче! Я не розумію, як працює $ eq тут. Я спочатку залишив його, і це не спрацювало для мене. Якось це виглядає в масиві фігур, щоб знайти відповідність, але запит ніколи не вказує, в якому масиві слід шукати. Як би, якщо документи мали форми і, наприклад, розміри; $ eq виглядатиме в обох масивах для матчів? $ Redact просто шукає що-небудь в документі, що відповідає умові "якщо"?
Оноса

30

Застереження: Ця відповідь дає рішення, яке було актуальним на той час , до появи нових можливостей MongoDB 2.2 і новіших версій. Дивіться інші відповіді, якщо ви використовуєте більш нову версію MongoDB.

Параметр вибору поля обмежений повною властивістю. Його не можна використовувати для вибору частини масиву, лише всього масиву. Я спробував скористатися позиційним оператором $ , але це не вийшло.

Найпростіший спосіб - просто фільтрувати фігури в клієнті .

Якщо вам дійсно потрібен правильний висновок безпосередньо з MongoDB, ви можете скористатися зменшенням карт для фільтрування фігур.

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

24

Краще ви можете зробити запит на відповідність елемента масиву, використовуючи $slice, чи корисно повернути значущий об'єкт у масиві.

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

$sliceкорисно, коли ви знаєте індекс елемента, але іноді вам потрібно, який би елемент масиву відповідав вашим критеріям. Ви можете повернути відповідний елемент $оператору.



12

Синтаксис знаходження в 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"}}})

Це дасть бажаний результат.


1
Це працює для мене. Однак виявляється, що "shapes.color":"red"в параметрі запиту (перший параметр методу пошуку) немає необхідності. Ви можете замінити його {}та отримати однакові результати.
Ерік Олсон

2
@ErikOlson Ваша пропозиція є правильною у наведеному вище випадку, де нам потрібно знайти весь документ із червоним кольором та застосувати проекцію лише на них. Але скажімо, якщо хтось вимагає знайти весь документ, який має колір синій, але він повинен повертати лише ті елементи масиву фігур, які мають червоний колір. У цьому випадку на вищезазначений запит також може посилатися хтось інший ..
Vicky

Це здається найпростішим, але я не можу змусити його працювати. Він повертає лише перший відповідний піддокумент.
новачок

8

Завдяки 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"
    }
]}

7

Вам потрібно просто запустити запит

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

Вихід цього запиту є

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

як ви очікували, воно дасть точне поле з масиву, що відповідає кольору: 'червоний'.


3

поряд з $ project буде доречніше інші мудрі відповідні елементи будуть скріплені разом з іншими елементами в документі.

db.test.aggregate(
  { "$unwind" : "$shapes" },
  { "$match" : {
     "shapes.color": "red"
  }},
{"$project":{
"_id":1,
"item":1
}}
)

Ви можете описати, що це здійснюється за допомогою вводу та виводу?
Олександр Міллс

2

Так само ви можете знайти для кратних

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

Ця відповідь справді є кращим 4.x способом: $matchскоротити пробіл, а потім $filterзберегти те, що ви хочете, перезаписавши поле введення (використовуйте вихідний $filterна поле shapesдля $projectповернення до shapes. Примітка стилю: найкраще не використовувати ім'я поля як asаргумент , так як це може призвести до плутанини пізніше $$shapeі $shapeя вважаю за краще. zzяк asполе , тому що він дійсно виділяється.
Базз Moschetti

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

1
Ласкаво просимо до переповнення стека! Дякуємо за фрагмент коду, який може надати деяку обмежену, негайну допомогу. Правильне пояснення значно покращило б його довгострокове значення , описавши, чому це хороше рішення проблеми, та зробило б її кориснішою для майбутніх читачів з іншими подібними питаннями. Відредагуйте свою відповідь, щоб додати пояснення, включаючи зроблені вами припущення.
sepehr

1

Використовуйте функцію агрегації та $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")
        }
    ]
}

0

Хоча питання було задано 9,6 років тому, це було величезною допомогою багатьом людям, я був одним із них. Дякую всім за всі запити, підказки та відповіді. Підбираючи одну з відповідей тут. Я виявив, що наступний метод також може бути використаний для проектування інших полів у батьківському документі. Це може бути корисним для когось.

У наступному документі потрібно було з’ясувати, чи є у працівника (номер № 7839) історія відпустки встановлена ​​на 2020 рік. Історія відпустки реалізована як вбудований документ у батьківському документі працівника.

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
                }
        ]
}
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.