projection for nested array in mongo and node - node.js

hi everyone i am trying to project the cities which are belongs to particular state by taking the country_code and state_code
[
{
'$match': {
'iso2': 'IN'
}
}, {
'$project': {
'states': {
'$slice': [
'$states.cities.name', 1, 1
]
}
}
}
]
when i tried this i am getting the result but is there any better way to do it
tq

I would do it as follow:
db.collection.aggregate([
{
"$match": {
"iso2": "IN",
"states.state_code": "AG"
}
},
{
$addFields: {
states: {
"$filter": {
"input": "$states",
"as": "state",
"cond": {
"$eq": [
"$$state.state_code",
"AG"
]
}
}
}
}
},
{
$project: {
cities: "$states.cities.name"
}
},
{
$unwind: "$cities"
}
])
Explained:
Match only documents having iso2=IN and states.state_code=AG
( For this stage is good if you have index on at least {iso2:1} or compound index on {iso2:1,states.state_code:1} )
Filter only the states with state_code=AG
$project the names only
$unwind to flatten first array.
playground

Related

Mongoose - How to query field in the last object of an array of objects

I have MongoDB documents structured like this:
{
"_id": "5d8b987f9f8b9f9c8c8b9f9",
"targetsList": [
{
"target": "user",
"statusList": [
{
"date": "2018-01-01",
"type": "OK"
},
{
"date": "2018-01-02",
"type": "FAILD"
}
]
}
]
}
And I want to count all documents that in their "targetList" array, there is an object with "target"=="user" - and also that object conatin on the last element of its "statusList" array, an object with "type" != "FAILD".
Any ideas on how to implement this kind of query?
Mongo playground:
https://mongoplayground.net/p/3bCoHRnh-KQ
In this example, I expected the count to be 1, because only the second object meets the conditions.
An aggregation pipeline
1st step - Filtering out where "targetsList.target": "user"
2nd step - $unwind on targetsList to get it out of array
3rd step - getting the last element of the targetsList.statusList array using $arrayElemAt
4th step - getting the results where that last element is not FAILD
5th step - getting the count
demo - you can try removing parts of the pipeline to see what the intermediate results are
db.collection.aggregate([
{
$match: {
"targetsList.target": "user"
}
},
{
$unwind: "$targetsList"
},
{
$project: {
"targetsList.statusList": {
$arrayElemAt: [
"$targetsList.statusList",
-1
]
},
}
},
{
$match: {
"targetsList.statusList.type": {
$ne: "FAILD"
}
}
},
{
$count: "withoutFailedInLastElemCount"
}
])
Unless it's crucial that the element be the last index, this should work for your case.
db.collection.find({
"targetsList.statusList.type": {
$in: [
"FAILD"
]
}
})
This will retrieve documents where the type value is FAILD. To invert this you can swap $in for $nin.
Updated playground here
Here's another way to do it with a leading monster "$match".
db.collection.aggregate([
{
"$match": {
"targetsList.target": "user",
"$expr": {
"$reduce": {
"input": "$targetsList",
"initialValue": false,
"in": {
"$or": [
"$$value",
{
"$ne": [
{
"$last": "$$this.statusList.type"
},
"FAILD"
]
}
]
}
}
}
}
},
{
"$count": "noFailedLastCount"
}
])
Try it on mongoplayground.net.

How to sum values of third level nested array of objects across all documents in MongoDB?

I have a mongoose document having the following Schema:
Products
{
"section":"",
"category":"Food & Drink",
"sub_category":"Main Dish",
"product_code":"ST",
"title":"Steak",
"description":"Served with sauted vegetables",
"tags":[
],
"warranty":"None",
"product_variants":[
{
"variant_code":"ST1",
"variant_title":"Rib Eye",
"images":[
],
"status":"Active",
"variant_details":[
{
"size":"6oz",
"local_price":800,
"local_discount":"0",
"foreign_price":0,
"foreign_discount":"0",
"inventory":[
{
"branch_id":{
},
"quantity":94
}
]
},
{
"size":"10oz",
"local_price":1000,
"local_discount":"0",
"foreign_price":0,
"foreign_discount":"0",
"inventory":[
{
"branch_id":{
},
"quantity":147
}
]
},
{
"size":"12oz",
"local_price":1200,
"local_discount":"0",
"foreign_price":0,
"foreign_discount":"0",
"inventory":[
{
"branch_id":{
},
"quantity":199
}
]
}
]
}
]
}
The above document shows only one object in the product_variants field but please note that there could be several objects as well. I need to sum the quantity for each size and product variant.
How would I do that using aggregate function? I am using mongoose in node js environment.
Query
(its based on the last comment in the previous answer, similar query but multiplies that quantity with the local price)
Test code here
db.collection.aggregate([
{
"$unwind": "$product_variants"
},
{
"$unwind": "$product_variants.variant_details"
},
{
"$unwind": "$product_variants.variant_details.inventory"
},
{
"$set": {
"total_local_price": {
"$multiply": [
"$product_variants.variant_details.inventory.quantity",
"$product_variants.variant_details.local_price"
]
}
}
},
{
$group: {
_id: null, // or "$_id" if you want only for 1 document
total_qty: {
$sum: "$total_local_price"
}
}
}
])
You can use this aggregation query:
Fisrt $project to get only the quantity values. It generates the following output:
"array": [
[
[
94
],
[
147
],
[
199
]
]
So next step is to use $unwind three times to flat the array.
And $group by _id using $sum
yourModel.aggregate([{
"$project": {
"array": "$product_variants.variant_details.inventory.quantity"
}
},
{
"$unwind": "$array"
},
{
"$unwind": "$array"
},
{
"$unwind": "$array"
},
{
"$group": {
"_id": "$_id",
"size": {
"$sum": "$array"
}
}
}])
Example here
Edit
As Takis _ suggested into the comments if you want to get all values from your entire collection (not only for each document) you can $group using null as this example

How to find array greater than document size in MongoDB

I have schema like below
[
{
id:"111"
tags:[222,333,444,555]
},
{
id: "222"
tags:[312,345,534]
},
{
id:"333"
tags:[111,222,333,444,555]
},
]
I want to find all documents where tags array size is greater than document size returned by $match in aggregation pipeline, so in above Ex. the number of documents are 3 so i want to return all documents having tags array size greater that 3
[
{
id:"111"
tags:[222,333,444,555]
},
{
id:"333"
tags:[111,222,333,444,555]
},
]
I am using aggregation pipeline to process other info, I am stuck at how to have store document size so that i can find all tags greater than document size
below is query which i am using, i want to do it in aggregation and in one call
.aggregate([
{
"$match":{
"ids":{
"$in":[
"111",
"222",
"333"
]
}
}
})]
Facet helps you to solve this problem.
$facet helps to categorize the incoming documents. We use totalDoc for counting the document and allDocuments for getting all the documents
$arrayElemAt helps to get the first object from totalDoc where we already know that only one object should be inside the totalDoc. Because when we group it, we use _id:null
$unwind helps to de-structure the allDocuments array
Here is the code
db.collection.aggregate([
{
$facet: {
totalDoc: [
{
$group: {
_id: null,
count: {
$sum: 1
}
}
}
],
allDocuments: [
{
$project: {
tags: 1
}
}
]
}
},
{
$addFields: {
totalDoc: {
"$arrayElemAt": [
"$totalDoc",
0
]
}
}
},
{
$unwind: "$allDocuments"
},
{
$addFields: {
sizeGtDoc: {
$gt: [
{
$size: "$allDocuments.tags"
},
"$totalDoc.count"
]
}
}
},
{
$match: {
sizeGtDoc: true
}
},
{
"$replaceRoot": {
"newRoot": "$allDocuments"
}
}
])
Working Mongo playground
You can try,
$match you condition
$group by null and make root array of documents and get count of root documents in count
$unwind deconstruct root array
$match tags size and count greater than or not using $expr expression match
$replaceRoot to replace root object in root
db.collection.aggregate([
{ $match: { id: { $in: ["111", "222", "333"] } } },
{
$group: {
_id: null,
root: { $push: "$$ROOT" },
count: { $sum: 1 }
}
},
{ $unwind: "$root" },
{ $match: { $expr: { $gt: [{ $size: "$root.tags" }, "$count"] } } },
{ $replaceRoot: { newRoot: "$root" } }
])
Playground
Second option:
first 2 stages $match and $group both are same as like above query,
$project to filter root array match condition if tags size and count greater than or not, this will return filtered root array
$unwind deconstruct root array
$replaceRoot replace root object to root
db.collection.aggregate([
{ $match: { id: { $in: ["111", "222", "333"] } } },
{
$group: {
_id: null,
root: { $push: "$$ROOT" },
count: { $sum: 1 }
}
},
{
$project: {
root: {
$filter: {
input: "$root",
cond: { $gt: [{ $size: "$$this.tags" }, "$count"] }
}
}
}
},
{ $unwind: "$root" },
{ $replaceRoot: { newRoot: "$root" } }
])
Playground
You can skip $unwind and $replaceRoot stages if you want because this query always return one document in root, so you can easily access like this result[0]['root'], you can save 2 stages processing and execution time.
You could use $facet to get two streams i.e. one with the filtered documents and the counts using $count. The resulting streams can then
be aggregated further with a $filter as follows to get the desired result
db.getCollection('collection').aggregate([
{ '$facet': {
'counts': [
{ '$match': { 'id': { '$in': ['111', '222', '333'] } } },
{ '$count': "numberOfMatches" }
],
'docs': [
{ '$match': { 'id': { '$in': ['111', '222', '333'] } } },
]
} },
{ '$project': {
'result': {
'$filter': {
'input': '$docs',
'cond': {
'$gt': [
{ '$size': '$$this.tags' },
{ '$arrayElemAt': ['$counts.numberOfMatches', 0] }
]
}
}
}
} }
])

Mongodb lookup array of elements with combined result

So these are my two documents
Order document:
{
"_id":"02a33b9a-284c-4869-885e-d46981fdd679",
"context":{
"products":[
{
"id": "e68fc86a-b4ad-4588-b182-ae9ee3db25e4",
"version": "2020-03-14T13:18:41.296+00:00"
}
],
},
}
Product document:
{
"_id":"e68fc86a-b4ad-4588-b182-ae9ee3db25e4",
"context":{
"name": "My Product",
"image": "someimage"
},
}
So I'm trying to do a lookup for a products in order document, but the result should contain combined fields, like so:
"products":[
{
"_id": "e68fc86a-b4ad-4588-b182-ae9ee3db25e4",
"version": "2020-03-14T13:18:41.296+00:00",
"name": "My Product",
"image": "someimage"
}
],
Not sure how to do this, should I do it outside of the lookup, or inside? This is my aggregation
Orders.aggregate([
{
"$lookup":{
"from":"products",
"let":{
"products":"$context.products"
},
"pipeline":[
{
"$match":{
"$expr":{
"$in":[
"$_id",
"$$products.id"
]
}
}
},
{
"$project":{
"_id":0,
"id":1,
"name":"$context.name"
}
}
],
"as":"mergedProducts"
}
},
{
"$project":{
"context":"$context",
"mergedProducts":"$mergedProducts"
}
},
]);
You need to run that mapping outside of $lookup by running $map along with $arrayElemAt to get single pair from both arrays and then apply $mergeObjects to get one object as a result:
db.Order.aggregate([
{
$lookup: {
from: "products",
localField: "context.products.id",
foreignField: "_id",
as: "productDetails"
}
},
{
$addFields: {
productDetails: {
$map: {
input: "$productDetails",
in: {
_id: "$$this._id",
name: "$$this.context.name"
}
}
}
}
},
{
$project: {
_id: 1,
"context.products": {
$map: {
input: "$context.products",
as: "prod",
in: {
$mergeObjects: [
"$$prod",
{ $arrayElemAt: [ { $filter: { input: "$productDetails", cond: { $eq: [ "$$this._id", "$$prod.id" ] } } }, 0 ] }
]
}
}
}
}
}
])
Mongo Playground
The goals of the last step is to take take two arrays: products and productDetails (the output of $lookup) and find matches between them. We know there's always one match so we can get only one item $arrayElemAt 0. As an output of $map there will be single array containing "merged" documents.

Query by data already in the object

I'm writing a query that gets data from "coll2" based on data that is inside "coll1".
Coll1 has the following data structure:
{
"_id": "asdf",
"name": "John",
"bags": [
{
"type": "typ1",
"size": "siz1"
},
{
"type": "typ2",
"size": "siz2"
}
]
}
Coll2 has the following data structure:
{
_id: "qwer",
coll1Name: "John",
types: ["typ1", "typ3"],
sizes: ["siz1", "siz4"]
}
{
_id: "zxcv",
coll1Name: "John",
types: ["typ2", "typ3"],
sizes: ["siz1", "siz2"]
}
{
_id: "fghj",
coll1Name: "John",
types: ["typ2", "typ3"],
sizes: ["siz1", "siz4"]
}
I want to get all the documents in coll2 that have the same Type+Size combo as in coll1 using the $lookup stage of the aggregation pipeline. I understand that this can be achieved by using the $lookup pipeline and $expr but I cant seem to figure out how to dynamically make a query to pass into the $match stage.
The output I would like to get for the above data would be:
{
_id: "qwer",
coll1Name: "John",
types: ["typ1", "typ3"],
sizes: ["siz1", "siz4"]
}
{
_id: "zxcv",
coll1Name: "John",
types: ["typ2", "typ3"],
sizes: ["siz1", "siz2"]
}
You can use $lookup to get the data from Col2. Then you need to check if there's any element in Col2 ($anyElemenTrue) that matches with Col1. $map and $in can be used here. Then you just need to $unwind and promote Col2 to root level using $replaceRoot
db.Col1.aggregate([
{
$lookup: {
from: "Col2",
localField: "name",
foreignField: "coll1Name",
as: "Col2"
}
},
{
$project: {
Col2: {
$filter: {
input: "$Col2",
as: "c2",
cond: {
$anyElementTrue: {
$map: {
input: "$bags",
as: "b",
in: {
$and: [
{ $in: [ "$$b.type", "$$c2.types" ] },
{ $in: [ "$$b.size", "$$c2.sizes" ] },
]
}
}
}
}
}
}
}
},
{
$unwind: "$Col2"
},
{
$replaceRoot: {
newRoot: "$Col2"
}
}
])
You are correct in your approach to use $lookup with the pipeline field to filter the input documents in the $match pipeline
The $expr expression should typically follow
"$expr": {
"$and": [
{ "$eq": [ "$name", "$$coll1_name" ] },
{ "$setEquals": [ "$bags.type", "$$types" ] },
{ "$setEquals": [ "$bags.size", "$$sizes" ] }
]
}
where the first match expression in the $and conditional { "$eq": [ "$name", "$$coll1_name" ] } checks to see if the name field in coll1 collection matches the coll1Name field in the input documents from coll2.
Of course the fields from coll2 should be defined in a variable in the pipeline with the let field for the $lookup pipeline to access them.
The other match filters are basically checking if the arrays are equal where "$bags.type" from coll1 resolves to an array of types i.e. [ "typ1", "typ3" ] for example.
On getting the output field from $lookup which happens to be an array, you can filter the documents in coll2 on that array field where there can be some empty lists as a resul of the above $lookup pipeline $match filter:
{ "$match": { "coll1Data.0": { "$exists": true } } }
Overall your aggregate pipeline operation would be as follows:
db.getCollection('coll2').aggregate([
{ "$lookup" : {
"from": "coll1",
"let": { "coll1_name": "$coll1Name", "types": "$types", "sizes": "$sizes" },
"pipeline": [
{ "$match": {
"$expr": {
"$and": [
{ "$eq": [ "$name", "$$coll1_name" ] },
{ "$setEquals": [ "$bags.type", "$$types" ] },
{ "$setEquals": [ "$bags.size", "$$sizes" ] }
]
}
} }
],
"as": "coll1Data"
} },
{ "$match": { "coll1Data.0": { "$exists": true } } },
{ "$project": { "coll1Data": 0 } }
])

Resources