Ambil hanya elemen yang ditanyakan dalam array objek dalam koleksi MongoDB

377

Misalkan Anda memiliki dokumen-dokumen berikut dalam koleksi saya:

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

Lakukan kueri:

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

Atau

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

Mengembalikan dokumen yang cocok (Dokumen 1) , tetapi selalu dengan SEMUA item array di shapes:

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

Namun, saya ingin mendapatkan dokumen (Dokumen 1) hanya dengan larik yang berisi color=red:

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

Bagaimana saya bisa melakukan ini?

Sebtm
sumber

Jawaban:

416

$elemMatchOperator proyeksi baru MongoDB 2.2 menyediakan cara lain untuk mengubah dokumen yang dikembalikan hanya berisi elemen yang cocok pertamashapes :

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

Pengembalian:

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

Di 2.2 Anda juga bisa melakukan ini menggunakan $ projection operator, di mana $dalam nama bidang objek proyeksi mewakili indeks elemen array yang cocok pertama bidang dari kueri. Berikut ini mengembalikan hasil yang sama seperti di atas:

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

MongoDB 3.2 Pembaruan

Dimulai dengan rilis 3.2, Anda dapat menggunakan $filteroperator agregasi baru untuk memfilter array selama proyeksi, yang memiliki manfaat termasuk semua pertandingan, bukan hanya yang pertama.

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

Hasil:

[ 
    {
        "shapes" : [ 
            {
                "shape" : "circle",
                "color" : "red"
            }
        ]
    }
]
JohnnyHK
sumber
16
solusi apa pun jika saya ingin mengembalikan setiap elemen yang cocok, bukan hanya yang pertama?
Steve Ng
Saya khawatir saya menggunakan Mongo 3.0.X :-(
charliebrownie
@charliebrownie Kemudian gunakan salah satu dari jawaban lain yang digunakan aggregate.
JohnnyHK
kueri ini hanya mengembalikan array "bentuk" dan itu tidak akan mengembalikan bidang lain. Adakah yang tahu cara mengembalikan bidang lain juga?
Mark Thien
1
Ini juga berfungsi:db.test.find({}, {shapes: {$elemMatch: {color: "red"}}});
Paul
97

Kerangka Agregasi baru di MongoDB 2.2+ memberikan alternatif untuk Peta / Mengurangi. The $unwindoperator dapat digunakan untuk memisahkan Anda shapesarray menjadi aliran dokumen yang dapat dicocokkan:

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

Hasil dalam:

{
    "result" : [
        {
            "_id" : ObjectId("504425059b7c9fa7ec92beec"),
            "shapes" : {
                "shape" : "circle",
                "color" : "red"
            }
        }
    ],
    "ok" : 1
}
Stennie
sumber
7
@ JohnnyHK: Dalam hal ini, $elemMatchadalah pilihan lain. Saya benar-benar tiba di sini melalui pertanyaan Grup Google di mana $ elemMatch tidak berfungsi karena hanya mengembalikan kecocokan pertama per dokumen.
Stennie
1
Terima kasih, saya tidak mengetahui batasan itu sehingga itu baik untuk diketahui. Maaf karena menghapus komentar saya yang Anda balas, saya memutuskan untuk mengirim jawaban lain dan tidak ingin membingungkan orang.
JohnnyHK
3
@JohnnyHK: Jangan khawatir, sekarang ada tiga jawaban yang berguna untuk pertanyaan ;-)
Stennie
Untuk pencari lain, selain itu saya juga mencoba menambahkan { $project : { shapes : 1 } }- yang sepertinya berfungsi dan akan membantu jika dokumen yang dilampirkan besar dan Anda hanya ingin melihat nilai shapes-nilai kunci.
user1063287
2
@ calmbird Saya memperbarui contoh untuk memasukkan tahap $ pertandingan awal. Jika Anda tertarik pada saran fitur yang lebih efisien, saya akan menonton / mengungguli SERVER-6612: Mendukung memproyeksikan nilai-nilai larik berganda dalam suatu proyeksi seperti penentu proyeksi $ elemMatch dalam pelacak masalah MongoDB.
Stennie
30

Cara lain yang menarik adalah menggunakan $ redact , yang merupakan salah satu fitur agregasi baru dari MongoDB 2.6 . Jika Anda menggunakan 2.6, Anda tidak perlu $ pelepasan yang dapat menyebabkan masalah kinerja jika Anda memiliki array yang besar.

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

$redact "Membatasi konten dokumen berdasarkan informasi yang tersimpan dalam dokumen itu sendiri" . Jadi itu hanya akan berjalan di dalam dokumen . Ini pada dasarnya memindai dokumen Anda dari atas ke bawah, dan memeriksa apakah itu cocok dengan ifkondisi Anda yang ada $cond, jika ada yang cocok itu akan menjaga konten ( $$DESCEND) atau menghapus ($$PRUNE ).

Pada contoh di atas, pertama $matchmengembalikan keseluruhanshapes array, dan $ redact stripkan ke hasil yang diharapkan.

Catatan yang {$not:"$color"}diperlukan, karena akan memindai dokumen teratas juga, dan jika $redacttidak menemukan colorbidang di tingkat atas ini akan kembali falseyang mungkin menghapus seluruh dokumen yang tidak kita inginkan.

anvarik
sumber
1
jawaban sempurna. Seperti yang Anda sebutkan $ bersantai akan mengkonsumsi banyak RAM. Jadi ini akan lebih baik jika dibandingkan.
manojpt
Saya ragu. Dalam contoh, "bentuk" adalah array. Akankah "$ redact" memindai semua objek dalam array "bentuk" ?? Bagaimana ini akan baik sehubungan dengan kinerja ??
manojpt
tidak semuanya, tetapi hasil pertandingan pertama Anda. Itulah alasan mengapa Anda menempatkan $matchsebagai tahap agregat pertama Anda
anvarik
okkk .. jika indeks dibuat pada bidang "color", itupun akan memindai semua objek dalam array "bentuk" ??? Mana yang bisa menjadi cara efisien untuk mencocokkan beberapa objek dalam sebuah array ???
manojpt
2
Cemerlang! Saya tidak mengerti cara kerja $ eq di sini. Saya meninggalkannya awalnya dan ini tidak berhasil untuk saya. Entah bagaimana, itu terlihat dalam array bentuk untuk menemukan kecocokan, tetapi kueri tidak pernah menentukan array mana yang akan dicari. Seperti, jika dokumen memiliki bentuk dan, misalnya, ukuran; Apakah $ eq akan terlihat di kedua array untuk pertandingan? Apakah $ redact hanya mencari sesuatu di dalam dokumen yang cocok dengan kondisi 'jika'?
Onosa
30

Perhatian: Jawaban ini memberikan solusi yang relevan pada waktu itu , sebelum fitur baru MongoDB 2.2 dan lebih baru diperkenalkan. Lihat jawaban lain jika Anda menggunakan versi MongoDB yang lebih baru.

Parameter pemilih bidang terbatas pada properti lengkap. Itu tidak dapat digunakan untuk memilih bagian dari array, hanya seluruh array. Saya mencoba menggunakan operator posisional $ , tetapi itu tidak berhasil.

Cara termudah adalah dengan hanya menyaring bentuk di klien .

Jika Anda benar - benar membutuhkan output yang benar langsung dari MongoDB, Anda dapat menggunakan pengurangan peta untuk memfilter bentuk.

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()
Niels van der Rest
sumber
24

Lebih baik Anda bisa query dalam elemen array yang cocok menggunakan $sliceitu membantu mengembalikan objek signifikan dalam array.

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

$slicesangat membantu ketika Anda mengetahui indeks elemen, tetapi kadang-kadang Anda ingin mana elemen array yang cocok dengan kriteria Anda. Anda dapat mengembalikan elemen yang cocok dengan $operator.

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

OUTPUT

{

   "shapes" : [ 
       {
           "shape" : "circle",
           "color" : "red"
       }
   ]
}
Viral Patel
sumber
12

Sintaks untuk menemukan di mongodb adalah

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

dan permintaan kedua yang telah Anda tulis, yaitu

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

dalam hal ini Anda telah menggunakan $elemMatchoperator di bagian permintaan, sedangkan jika Anda menggunakan operator ini di bagian proyeksi maka Anda akan mendapatkan hasil yang diinginkan. Anda dapat menuliskan kueri sebagai

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

Ini akan memberi Anda hasil yang diinginkan.

Vicky
sumber
1
Ini bekerja untuk saya. Namun, Tampaknya "shapes.color":"red"dalam parameter kueri (parameter pertama dari metode find) tidak diperlukan. Anda dapat menggantinya dengan {}dan mendapatkan hasil yang sama.
Erik Olson
2
@ErikOlson Saran Anda benar dalam kasus di atas, di mana kita perlu menemukan semua dokumen yang berwarna merah dan untuk menerapkan proyeksi pada mereka saja. Tetapi katakanlah jika seseorang harus mencari tahu semua dokumen yang memiliki warna biru tetapi harus mengembalikan hanya elemen-elemen dari array bentuk yang memiliki warna merah. Dalam hal ini permintaan di atas dapat dirujuk oleh orang lain juga ..
Vicky
Ini tampaknya yang paling mudah, tetapi saya tidak bisa membuatnya bekerja. Hanya mengembalikan subdokumen yang cocok pertama.
pemain baru
8

Terima kasih untuk JohnnyHK .

Di sini saya hanya ingin menambahkan beberapa penggunaan yang lebih kompleks.

// 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"
    }
]}
Eddy
sumber
7

Anda hanya perlu menjalankan kueri

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

output dari kueri ini adalah

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

seperti yang Anda harapkan akan memberikan bidang yang tepat dari array yang cocok dengan warna: 'merah'.

Vaibhav Patil
sumber
3

bersama dengan $ project akan lebih tepat elemen pencocokan bijaksana lainnya akan dipukuli bersama dengan elemen lain dalam dokumen.

db.test.aggregate(
  { "$unwind" : "$shapes" },
  { "$match" : {
     "shapes.color": "red"
  }},
{"$project":{
"_id":1,
"item":1
}}
)
shakthydoss
sumber
dapatkah Anda menjelaskan bahwa ini selesai dengan input dan output yang ditetapkan?
Alexander Mills
2

Anda juga dapat menemukan beberapa

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']]}
     }}
  }}
])
ashishSober
sumber
Jawaban ini memang cara 4.x yang disukai: $matchuntuk mengurangi ruang, kemudian $filteruntuk mempertahankan apa yang Anda inginkan, menimpa bidang input (gunakan output dari $filterbidang shapesuntuk $projectkembali ke shapes. Catatan gaya: sebaiknya tidak menggunakan nama bidang sebagai yang asargumen karena dapat menyebabkan kebingungan kemudian dengan $$shapedan $shapesaya lebih memilih. zzsebagai aslapangan karena benar-benar berdiri.
Buzz Moschetti
1
db.test.find( {"shapes.color": "red"}, {_id: 0})
Poonam Agrawal
sumber
1
Selamat Datang di Stack Overflow! Terima kasih atas cuplikan kode, yang mungkin memberikan bantuan terbatas dan segera. Penjelasan yang tepat akan sangat meningkatkan nilai jangka panjangnya dengan menjelaskan mengapa ini adalah solusi yang baik untuk masalah ini, dan akan membuatnya lebih bermanfaat bagi pembaca masa depan dengan pertanyaan serupa lainnya. Harap edit jawaban Anda untuk menambahkan beberapa penjelasan, termasuk asumsi yang Anda buat.
sepehr
1

Gunakan fungsi agregasi dan $projectuntuk mendapatkan bidang objek tertentu dalam dokumen

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

hasil:

{
    "_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")
        }
    ]
}
KARTHIKEYAN.A
sumber
0

Meskipun pertanyaan itu diajukan 9,6 tahun yang lalu, ini telah sangat membantu banyak orang, saya menjadi salah satu dari mereka. Terima kasih semuanya atas semua pertanyaan, petunjuk, dan jawaban Anda. Mengambil dari salah satu jawaban di sini .. Saya menemukan bahwa metode berikut juga dapat digunakan untuk memproyeksikan bidang lain dalam dokumen induk. Ini mungkin bermanfaat bagi seseorang.

Untuk dokumen berikut, kebutuhannya adalah untuk mengetahui apakah seorang karyawan (emp # 7839) memiliki riwayat cuti yang ditetapkan untuk tahun 2020. Riwayat cuti diimplementasikan sebagai dokumen tertanam di dalam dokumen induk Karyawan.

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
                }
        ]
}
Ali
sumber