This question already has answers here:
Retrieve only the queried element in an object array in MongoDB collection
(18 answers)
Closed 4 years ago.
Could you please help me to write some sort of aggregation query using mongodb.
I have next data structure.
[
{
id: 1,
shouldPay: true,
users: [
{
id: 100,
items: [{...}],
tags: ['a', 'b', 'c', 'd']
},
{
id: 100,
items: [{...}],
tags: ['b', 'c', 'd']
},
{
id: 100,
items: [{...}],
tags: ['c', 'd']
}
],
}
]
In result I want to get something like that:
[
{
id: 1,
shouldPay: true,
user: {
id: 100,
items: [{...}],
tags: ['a', 'b', 'c', 'd']
}
}
]
The main idea is to select a specific user that has "a" letter or list of letters ['a', 'b'] in tags.
You can use below aggregation
Use $match at the starting of the pipeline to filter out the documents which don't contain "a" and "b" in tags array. And then use $filter with $setIsSubset to filter out the nested array.
$arrayELemAt to return the specified element from the array.
db.collection.aggregate([
{ "$match": { "users.tags": { "$in": ["a", "b"] }}},
{ "$project": {
"users": {
"$arrayElemAt": [
{ "$filter": {
"input": "$users",
"cond": { "$setIsSubset": [["a", "b"], "$$this.tags"] }
}},
0
]
}
}}
])
You need to use $unwind along with $filter:
db.collection.aggregate([
{
$unwind: "$users"
},
{
$match: {
"users.tags": "a"
}
}
])
Result:
[
{
"_id": ObjectId("5a934e000102030405000000"),
"id": 1,
"shouldPay": true,
"users": {
"id": 100,
"tags": [
"a",
"b",
"c",
"d"
]
}
}
]
Related
I am struggling with retrieving nested document in root.
below i have a schema inside it there is a array of tasks in which objects are present,
in those objects there is again an array of assigned objects and in those objects i have solution array.
now i want to merge all solutions in one array and get that solution array in root od document.
Schema -
{
_id: 1,
tasks: [
{
_id: 1,
assigned: [
{
_id: 1,
solutions: [
{
_id: 1,
name: 'solution 1',
},
{
_id: 2,
name: 'solution 2',
},
],
},
{
_id: 2,
solutions: [
{
_id: 1,
name: 'solution 1',
},
{
_id: 2,
name: 'solution 2',
},
],
},
],
},
],
};
I want to merge all solutions to a single array based on some condition and set that array into new field in root of collection.
const order = this.orderModel
.aggregate([
{ $match: { _id: orderId, student: studentId } },
{
$addFields: {
solutions: {
$map: {
input: '$tasks.assigned.solutions',
as: 's',
in: '$$s',
},
},
},
},
])
.exec();
output i am getting -
"solutions": [
[
[
{
"id": 1,
"name": "solution 1",
}
],
[
{
"_id": 2,
"name": "solution 2"
}
]
]
],
Maybe something like this:
db.collection.aggregate([
{
"$project": {
"solutions": {
"$reduce": {
"input": "$tasks",
"initialValue": [],
"in": {
"$concatArrays": [
"$$value",
{
$reduce: {
input: "$$this.assigned",
initialValue: [],
in: {
$concatArrays: [
"$$value",
"$$this.solutions"
]
}
}
}
]
}
}
}
}
}
])
Explained:
Use $project and two nested $reduce/$concatArrays to join the "solutions" array objects under the new array field "solutions" in the document root.
Playground
if you want to filter based on some condition you can replace "$$this.solutions" with:
{
$filter: {
input: "$$this.solutions",
cond: { $eq: ["$$this._id", 1] }
}
}
will filter only documents with _id:1
see example here
Let's say I have those documents below:
[
{
array : ['a', 'b' , 'c'],
},
{
array : ['b', 'd' , 'e'],
},
{
array : ['d', 'e' , 'f'],
},
]
and input array for query:
["b","d","e","f"]
Expected output:
['b', 'd' , 'e'],['d', 'e' , 'f']
Which query can I use to do that?
And how to filter which element is not in the document?
Expected result:
[
{
array : ['b', 'd' , 'e'],
missingElement : ['f']
},
{
array : ['d', 'e' , 'f'],
missingElement : ['b']
},
]
$expr - Allow to use aggregation operator.
1.1. $eq - Compare the result from 1.1.1 and 1.1.2 are equal.
1.1.1. $size - Get the size of array field.
1.1.2. $size - Get the size of array from the result 1.1.2.1.
1.1.2.1. $setIntersection - Intersect array field and input array, return the intersected value(s) in array.
db.collection.find({
$expr: {
$eq: [
{
$size: "$array"
},
{
$size: {
$setIntersection: [
"$array",
[
"b",
"d",
"e",
"f"
]
]
}
}
]
}
})
Sample Mongo Playground
Updated
For Aggregation query to find missing element(s):
$match - Filter the documents (as explained in the first answer for $expr).
$project - Decorate the output documents. For missingElement field, you need $filter operator to find each value in the input array does not exist ($not and $in) in the array.
db.collection.aggregate([
{
$match: {
$expr: {
$eq: [
{
$size: "$array"
},
{
$size: {
$setIntersection: [
"$array",
[
"b",
"d",
"e",
"f"
]
]
}
}
]
}
}
},
{
$project: {
array: 1,
missingElement: {
$filter: {
input: [
"b",
"d",
"e",
"f"
],
cond: {
$not: {
$in: [
"$$this",
"$array"
]
}
}
}
}
}
}
])
Sample Mongo Playground (Aggregation query)
I want to get all items (or better ids of items) that have missing tags.
If a child item misses the tag of its parent it should be outputted.
The documents looks like this:
[
{
id: 1,
tags: [
"a",
"b"
],
childs: [
2,
3
]
},
{
id: 2,
tags: [
"a"
],
childs: []
},
{
id: 3,
tags: [],
childs: []
},
{
id: 4,
tags: [
"c"
],
childs: [
5
]
},
{
id: 5,
tags: [
"c"
],
childs: []
},
{
id: 6,
tags: [
"b"
],
childs: [
5
]
},
{
id: 7,
tags: [],
childs: []
},
]
Now I want to search by a tag name to get items with missing tag.
The desired result should look like this:
Check for tag "a":
{
id: 3,
...
}
or
{
ids: [3]
}
Check for tag "b":
{
id: 2,
...
},
{
id: 3,
...
}
or
{
ids: [2, 3]
}
I tried aggregation with the lookup and pipeline function, but did not got it working.
[{
$match: {
tags: "a",
childs: {
$ne: []
}
}
}, {
$lookup: {
from: 'collection1',
localField: 'childs',
foreignField: 'id',
as: 'childs_items',
pipeline: [{
$matching : {
"tags": {
$nin: "a"
}
}
}]
}
}]
What would be the best approach?
EDIT: changed document example tags of last two docs to "c"
EDIT2: added example data
Might be there will be other easy ways but, this is your corrected query,
$match conditions as usual,
$lookup with pipeline, define variable for childs to access inside lookup
$match conditions, childs match or not, tags not equal to specified character
$project to create ids array, using $reduce
db.collection.aggregate([
{
$match: {
tags: "b", // add your search
childs: { $ne: [] }
}
},
{
$lookup: {
from: "collection",
let: { childs: "$childs" },
as: "ids",
pipeline: [
{
$match: {
$expr: { $in: ["$id", "$$childs"] },
tags: { $ne: "b" } // add your search
}
}
]
}
},
{
$project: {
id: 1,
ids: {
$reduce: {
input: "$ids",
initialValue: [],
in: { $concatArrays: ["$$value", ["$$this.id"]] }
}
}
}
}
])
Playground
Your last edit, you can use $unwind and then $group after $lookup in above example, remove $project stage,
{ $unwind: "$ids" },
{
$group: {
_id: null,
ids: { $push: "$ids.id" }
}
}
Playground
I have a sample data like this:
[
{ objectId: 1, user: 1, phones: [1, 2], emails: ['a'] },
{ objectId: 2, user: 1, phones: [1, 5], emails: ['a', 'f'] },
{ objectId: 3, user: 1, phones: [8, 9], emails: ['f', 'g'] },
{ objectId: 4, user: 1, phones: [10], emails: ['h'] },
{ objectId: 5, user: 2, phones: [1, 2, 3], emails: ['aa', 'bb', cc'] },
]
Now I need to merge all related rows into one on these conditions:
Have same user
Have at least either one common phone or email
So output something like this:
[
{ objectId: 1, user: 1, phones: [1, 2, 5, 8, 9], emails: ['a', 'f', 'g'] },
{ objectId: 4, user: 1, phones: [10], emails: ['h'] },
{ objectId: 5, user: 2, phones: [1, 2, 3], emails: ['aa', 'bb', cc'] },
]
This is what I have came up with so far:
[
{
$unwind: {
path: "$phones",
preserveNullAndEmptyArrays: true
}
},
{
$group: {
_id: {
user: "$user",
phone: "$phones"
},
objectIds: {
$addToSet: "$_id"
},
emailsList: {
$push: "$emails"
},
user: { $first: "$user" },
phones: {
$first: "$phones"
}
}
},
{
"$addFields": {
"emails": {
"$reduce": {
"input": "$emailsList",
"initialValue": [],
"in": { "$setUnion": ["$$value", "$$this"] }
}
}
}
},
{
"$project": {
"emailsList": 0
}
},
{
$unwind: {
path: "$emails",
preserveNullAndEmptyArrays: true
}
},
{
$group: {
_id: {
user: "$user",
phone: "$emails"
},
objectIdsList: {
$push: "$objectIds"
}
}
},
{
"$project": {
"mergedObjectIds": {
"$reduce": {
"input": "$objectIdsList",
"initialValue": [],
"in": { "$setUnion": ["$$value", "$$this"] }
}
}
}
}
]
And then we have a list of objectIds need to be merged in, then I will merge it all in application code. So is there anyway I can do that in aggregation framework alone, or pipe the result of this aggregate in to the next one
Unless I'm missing something, these are just the "sets" for each user. So simply unwind both arrays and accumulate via $addToSet for each of "phones" and "emails":
db.collection.aggregate([
{ "$unwind": "$phones" },
{ "$unwind": "$emails" },
{ "$group": {
"_id": "$user",
"phones": { "$addToSet": "$phones" },
"emails": { "$addToSet": "$emails" }
}}
])
Which returns:
{ "_id" : 2, "phones" : [ 3, 2, 1 ], "emails" : [ "cc", "bb", "aa" ] }
{ "_id" : 1, "phones" : [ 9, 1, 2, 5, 8 ], "emails" : [ "g", "f", "a" ] }
A "set" is not really considered to be "ordered", so if you expect a certain order then you need to sort elsewhere, and probably best in the client.
Any "unique" id's don't really apply here. If anything you would use a different accumulator like $min or $max, or maybe $first depending on what you want, however the only relevant details I see here is the "user" for grouping and the other accumulated "set" values.
Even though unwinding multiple arrays produces a "cartesian product" of the other values, it really does not matter when everything being pulled out is as "distinct" values anyway. This typically only matters where you need to "count" elements, and that is something your output is not looking for in the question.
I have a large set of documents that may have two arrays or one of the two. I want to merge them in a $project.
I am currently using $concatArrays but as the documentation says it returns null when one of the arrays is null. I can figure out how to add a condition statement in there that will either return the $concatArrays or what ever array is in there.
Example
I have:
{_id: 1, array1: ['a', 'b', 'b'], array2: ['e', 'e']}
{_id: 2, array1: ['a', 'b', 'b']}
{_id: 3, array2: ['e', 'e']}
I want:
{_id: 1, combinedArray: ['a','b', 'b', 'e', 'e']}
{_id: 2, combinedArray: ['a','b', 'b']}
{_id: 3, combinedArray: ['e', 'e']}
I tried:
$project: {
combinedArray: { '$concatArrays': [ '$array1', '$array2' ] }
}
//output (unexpected result):
{_id: 1, combinedArray: ['a','b', 'b', 'e', 'e']}
{_id: 2, combinedArray: null}
{_id: 3, combinedArray: null}
I also tried:
$project: {
combinedArray: { '$setUnion': [ '$array1', '$array2' ] }
}
//output (unexpected result):
{_id: 1, combinedArray: ['a','b', 'e']}
{_id: 2, combinedArray: ['a','b']}
{_id: 3, combinedArray: ['e']}
As documentation for $concatArrays says
If any argument resolves to a value of null or refers to a field that
is missing, $concatArrays returns null.
So we need to be sure that we are not passing arguments which refer to a missing field or null. You can do that with $ifNull operator:
Evaluates an expression and returns the value of the expression if the
expression evaluates to a non-null value. If the expression evaluates
to a null value, including instances of undefined values or missing
fields, returns the value of the replacement expression.
So just return empty array if filed expression will not evaluate to non-null value:
db.collection.aggregate([
{$project: {
combinedArray: { '$concatArrays': [
{$ifNull: ['$array1', []]},
{$ifNull: ['$array2', []]}
] }
}
}
])
You can easily achieve this with the $ifNull operator:
db.arr.aggregate([
{
$project:{
combinedArray:{
$concatArrays:[
{
$ifNull:[
"$array1",
[]
]
},
{
$ifNull:[
"$array2",
[]
]
}
]
}
}
}
])
output:
{ "_id" : 1, "combinedArray" : [ "a", "b", "b", "e", "e" ] }
{ "_id" : 2, "combinedArray" : [ "a", "b", "b" ] }
{ "_id" : 3, "combinedArray" : [ "e", "e" ] }
I tried to do this with nested $cond, answer with $ifNull is better, but still posting my answer.
db.getCollection('test').aggregate( [{
$project: {
combinedArray: { $cond: [
{ $and: [ { $isArray: ["$array1"] }, { $isArray: ["$array2"] } ] },
{ '$concatArrays': [ '$array1', '$array2' ] },
{ $cond: [
{ $isArray: ["$array1"] },
"$array1",
"$array2"
] }
] }
}
}] )