concat array fields of all matching documents mongodb - mongodb

I have below document structure in mongodb:
{name: String, location: [String]}
Example documents are:
{name: "XYZ", location: ["A","B","C","D"]},
{name: "XYZ", location: ["M","N"]},
{name: "ABC", location: ["P","Q","R","S"]}
I want to write a query that when searches for a specific name, concats all location arrays of resulting documents. For example, If I search for name XYZ, I should get:
{name:"XYZ",location:["A","B","C","D","M","N"]}
I guess this is possible using aggregation that might use $unwind operator, but I am unable to frame the query.
Please help me to frame the query.
Thanks!

$match - Filter document(s).
$group - Group by name. Add the location array into the location field via $push. It results in the location with the value of the nested array.
$project - Decorate the output document. With the $reduce operator transforms the original location array which is a nested array to be flattened by combining arrays into one via $concatArrays.
db.collection.aggregate([
{
$match: {
name: "XYZ"
}
},
{
$group: {
_id: "$name",
location: {
$push: "$location"
}
}
},
{
$project: {
_id: 0,
name: "$_id",
location: {
$reduce: {
input: "$location",
initialValue: [],
in: {
$concatArrays: [
"$$value",
"$$this"
]
}
}
}
}
}
])
Demo # Mongo Playground

This should do the trick:
Match the required docs. Unwind the location array. Group by name, and project the necessary output.
db.collection.aggregate([
{
"$match": {
name: "XYZ"
}
},
{
"$unwind": "$location"
},
{
"$group": {
"_id": "$name",
"location": {
"$push": "$location"
}
}
},
{
"$project": {
name: "$_id",
location: 1,
_id: 0
}
}
])
Playground link.

Related

MongoDB: Project only fields of a specific type

Is there a way in MongoDB to project all fields of a document that have a specific type?
For example if I have the following document:
{
_id: 5dde4c55c6c36b3bb4f5ad30,
name: "Peter",
age: 45,
division: "marketing"
}
I would like to say: Return only the fields of type string. This way I would end up with:
{
_id: 5dde4c55c6c36b3bb4f5ad30,
name: "Peter",
division: "marketing"
}
You can use $type to check the type of field,
$reduce input $$ROOT object as array using $objectToArray
check condition if field value type is string then concat with initialValue and return
return value will be array we need to convert it to array using $arrayToObject
$replaceWith will replace root to new returned object
db.collection.aggregate([
{
$replaceWith: {
$arrayToObject: {
$reduce: {
input: { $objectToArray: "$$ROOT" },
initialValue: [],
in: {
$concatArrays: [
"$$value",
{
$cond: [
{ $eq: [{ $type: "$$this.v" }, "string"] },
["$$this"],
[]
]
}
]
}
}
}
}
}
])
Playground

mongodb: sort nested array using dynamic parmeter of field name

Assume in an aggregation pipeline one of the steps produces the following results:
{
customer: "WN",
sort_category: "category_a",
locations: [
{
city: "Elkana",
category_a: 11904.0,
category_b: 74.0,
category_c: 657.0,
},
{
city: "Haifa",
category_a: 20.0,
category_b: 841.0,
category_c: 0,
},
{
city" : "Jerusalem",
category_a: 451.0,
category_b: 45.0,
category_c: 712.0,
}
]
}
{
...
}
The next step is to sort the list of the nested objects of each document in the collection.
The list of the nested objects should be sorted by dynamic parameter containing the field name.
For example - the list of locations should be sorted by the value of category_a.
category_a is parmeter given in sort_category field.
Here is a solution without use $function:
db.tests.aggregate([
{$unwind:"$locations"},
{$project: {
_id: 1,
customer: 1,
sort_category: 1,
locations: 1,
locationsKV:{$objectToArray:"$locations"}
}
},
{$unwind:"$locationsKV"},
{$project:{
_id: 1,
customer: 1,
sort_category: 1,
locations: 1,
locationsKV: 1,
category: {
$cond:[{$eq: ["$sort_category","$locationsKV.k"]},
"$locationsKV.v", 0]},
agg: {
$cond: [{$eq: ["$sort_category","$locationsKV.k"]}, true, false]
}
}
},
{$match: {agg: true}},
{$sort: {category: 1}},
{$group: {
_id: "$_id",
customer: {$first: "$customer"},
sort_category: {$first: "$sort_category"},
locations: {$push: "$locations"}
}
},
{$project:{ _id: 0}}
])
You can try custom way,
$addFields will add one copy field named category of key sort_category inside every object in locations array using $map and $reduce
$unwind deconstruct locations array
$sort by category field that we have added in locations array
$project to remove category field
$group by _id and reconstruct locations array
$project to remove _id field
db.collection.aggregate([
{
$addFields: {
locations: {
$map: {
input: "$locations",
as: "l",
in: {
$mergeObjects: [
"$$l",
{
category: {
$reduce: {
input: { $objectToArray: "$$l" },
initialValue: null,
in: {
$cond: [{ $eq: ["$$this.k", "$sort_category"] }, "$$this.v", "$$value"]
}
}
}
}
]
}
}
}
}
},
{ $unwind: "$locations" },
{ $sort: { "locations.category": 1 } },
{ $project: { "locations.category": 0 } },
{
$group: {
_id: "$_id",
customer: { $first: "$customer" },
sort_category: { $first: "$sort_category" },
locations: { $push: "$locations" }
}
},
{ $project: { _id: 0 } }
])
Playground
If you are planing to upgrade your MongoDB to v4.4 or also this will helpful for others, The $function is a option for custom operation and user defined operation.
There are 3 properties:
body The function definition. You can specify the function definition as either BSON type Code or String, define our own function using function(){, we have passed locations array and sort_category that is dynamic field, the function logic is sort by descending order
args Arguments passed to the function body
lang The language used in the body. You must specify lang: "js"
db.collection.aggregate([
{
$addFields: {
locations: {
$function: {
body: function(locations, sort_category){
return locations.sort(function(a, b){
// DESCENDING ORDER
return b[sort_category] - a[sort_category]
// ASCENDING ORDER
// return a[sort_category] - b[sort_category]
})
},
args: ["$locations", "$sort_category"],
lang: "js"
}
}
}
}
])
For more guidelines about restrictions and considerations Refer.

return match item only from array of object mongoose

[
{
item: "journal",
instock: [
{
warehouse: "A",
qty: 5,
items: null
},
{
warehouse: "C",
qty: 15,
items: [
{
name: "alexa",
age: 26
},
{
name: "Shawn",
age: 26
}
]
}
]
}
]
db.collection.find({
"instock.items": {
$elemMatch: {
name: "alexa"
}
}
})
This returns whole items array where as i just want items array with one item {name: 'alexa', age: 26}
Playground Link : https://mongoplayground.net/p/0gB4hNswA6U
You can use the .aggregate(pipeline) function.
Your code will look like:
db.collection.aggregate([{
$unwind: {
path: "$instock"
}
}, {
$unwind: {
path: "$instock.items"
}
}, {
$replaceRoot: {
newRoot: "$instock.items"
}
}, {
$match: {
name: "alexa"
}
}])
Commands used in this pipeline:
$unwind - deconstructs an array of items into multiple documents which all contain the original fields of the original documents except for the unwinded field which now have a value of all the deconstructed objects in the array.
$replaceRoot - takes the inner object referenced on newRoot and puts it as the document.
$match - a way to filter the list of documents you ended up with by some condition. Basically the first argument in the .find() function.
For more information about aggregation visit MongoDB's website:
Aggregation
$unwind
$replaceRoot
$match
EDIT
The wanted result was to get single item arrays as a response, to achieve that you can simply remove the $replaceRoot stage in the pipeline.
Final pipeline:
db.collection.aggregate([{
$unwind: {
path: "$instock"
}
}, {
$unwind: {
path: "$instock.items"
}
}, {
$match: {
"instock.items.name": "alexa"
}
}])
You have to use $elemMatch in projection section:
db.collection.find({
// whatever your query is
}, {
"instock.items": {
$elemMatch: {
name: "alexa"
}
}
});
UPDATE
Here is a good example of this usage. From the official mongodb documentation.
An alternative approach where $filter is the key at the project stage.
db.collection.aggregate([
{
$match: {
"instock.items.name": "alexa"
}
},
{
$unwind: "$instock"
},
{
$project: {
"item": "$item",
qty: "$instock.qty",
warehouse: "$instock.warehouse",
items: {
$filter: {
input: "$instock.items",
as: "item",
cond: {
$eq: [
"$$item.name",
"alexa"
]
}
}
}
}
},
{
$group: {
_id: "$_id",
instock: {
$push: "$$ROOT"
}
}
}
])
The idea is to:
have $match as top level filter
then $unwind instock array items to prepare for the $filter
Use $project for rest of the fields as they are, and use $filter on items array field
Finally $group them back since $unwind was used previously.
Play link

How do you convert an array of ObjectIds into an array of embedded documents with a field containing the original array element value

I have a collection of documents where one of the fields is currently an array of ObjectId items.
{
_id: ObjectId(...),
user: "jdoe",
docs: [
ObjectId(1),
ObjectId(2),
...
]
}
{
_id: ObjectId(...),
user: "jsmith",
docs: [
ObjectId(3),
ObjectId(4),
...
]
}
How can I update all of the documents in my collection to convert the docs field into an array of objects that contain a "docID" field equal to the original element value?
For example, I'd want my documents to end up looking like:
{
_id: ObjectId(...),
user: "jdoe",
docs: [
{ docID: ObjectId(1) },
{ docID: ObjectId(2) },
...
]
}
{
_id: ObjectId(...),
user: "jsmith",
docs: [
{ docID: ObjectId(3)},
{ docID: ObjectId(4)},
...
]
}
I'm hoping there is a command that I can run from the shell such as:
db.getCollection('myCollection').update(
{},
{
$set: {
'docs.$[]: { docID: '$$VALUE'}
}
},
{multi: true }
);
But I can't figure out how to reference the original value of the element.
Update:
I'm marking #mickl with the correct answer since it got me on the correct track. Below is the final aggregate that I ended up with which only changes the docs field if it is an array of object IDs, otherwise the existing value is left as-is, including documents that don't have a docs field.
db.getCollection('myCollection').aggregate([
{ $addFields: {
'docs': { $cond: {
if : { $eq: [{ $type: { $arrayElemAt: [ '$docs', 0]} }, "objectId"]},
then: { $map: {
input: '$docs',
in: { tocID: '$$this'}
}},
else : '$docs'
}}
}},
{ $out: "myCollection" }
])
You can use $map to reshape your data and $out to replace existing collection with aggregation result:
db.col.aggregate([
{
$addFields: {
docs: {
$map: {
input: "$docs",
in: { docID: "$$this" }
}
}
}
},
{ $out: "col" }
])

Get original document field as part of aggregate result

I am wanting to get all of the document fields in my aggregate results but as soon as I use $group they are gone. Using $project allows me to readd whatever fields I have defined in $group but no luck on getting the other fields:
var doc = {
_id: '123',
name: 'Bob',
comments: [],
attendances: [{
answer: 'yes'
}, {
answer: 'no'
}]
}
aggregate({
$unwind: '$attendances'
}, {
$match: {
"attendances.answer": { $ne:"no" }
}
}, {
$group: {
_id: '$_id',
attendances: { $sum: 1 },
comments: { $sum: { $size: { $ifNull: [ "$comments", [] ] }}}
}
}, {
$project: {
comments: 1,
}
}
This results in:
[{
_id: 5317b771b6504bd4a32395be,
comments: 12
},{
_id: 53349213cb41af00009a94d0,
comments: 0
}]
How do I get 'name' in there? I have tried adding to $group as:
name: '$name'
as well as in $project:
name: 1
But neither will work
You can't project fields that are removed during the $group operation.
Since you are grouping by the original document _id and there will only be one name value, you can preserve the name field using $first:
db.sample.aggregate(
{ $group: {
_id: '$_id',
comments: { $sum: { $size: { $ifNull: [ "$comments", [] ] }}},
name: { $first: "$name" }
}}
)
Example output would be:
{ "_id" : "123", "comments" : 0, "name" : "Bob" }
If you are grouping by criteria where there could be multiple values to preserve, you should either $push to an array in the $group or use $addToSet if you only want unique names.
Projecting all the fields
If you are using MongoDB 2.6 and want to get all of the original document fields (not just name) without listing them individually you can use the aggregation variable $$ROOT in place of a specific field name.