MongoDB perform $match with two input array values? - arrays

In MongoDB, I am trying to write a query where I have two input array Bills, Names where the first one contains billids and the second one contains names of the person. Also in reality Bills at index i and Names at index i is the actual document which we want to search in MongoDB.
I want to write a query such that Bills[i] = db_billid && Names[i] = db_name which means I want to return the result where at a particular index both billid and name matches.
I thought of using $in but the thing is I can apply $in in Bills but I don't know at which index that billid is found.
{ $and: [{ billid: { $in: Bills } }, {name: Names[**index at which this bill is found]}] }
Can anyone please help me how can I solve this ??
MongoDB Schema
var transactionsschema = new Schema({
transactionid: {type: String},
billid: {type: String},
name: {type: String}
});
Sample documents in MongoDB
{ _id: XXXXXXXXXXX, transactionid: 1, billid : bnb1234, name: "sudhanshu"}, { _id: XXXXXXXXXXX, transactionid: 2, billid : bnb1235, name: "michael"}, { _id: XXXXXXXXXXX, transactionid: 3, billid : bnb1236, name: "Morgot"}
Sample arrays
Bills = ["bill1", "bill2", "bill3"], Names = ["name1", "name2", "name"]
Edit - If $in can work in array of objects then I can have array of object with keys as billid and name
var arr = [{ billid: "bill1", "name": "name1"}, {billid: "bill2", "name": "name2"}, {billid: "bill3", "name": "name3"}]
But the thing is then how can put below query
{ $and: [{ billid: { $in: arr.Bills } }, {name: arr.Names}] }

Knowing that bills are unique but names may have duplicates you can use $indexOfArray to get index of matching bill and then use that index to compare names array at evaluated index (using $arrayElemAt to retrieve value). Also you have to check if value returned by $indexOfArray is not equal to -1 (no match)
var bills = ["bnb1234", "bnb1235"];
var names = ["sudhanshu", "michael"];
db.col.aggregate([
{
$match: {
$expr: {
$and: [
{ $ne: [{ $indexOfArray: [ bills, "$billid" ] }, -1] },
{ $eq: ["$name", { $arrayElemAt: [ names, { $indexOfArray: [ bills, "$billid" ] } ] }] },
]
}
}
}
])
alternatively $let can be used to avoid duplication:
db.col.aggregate([
{
$match: {
$expr: {
$let: {
vars: { index: { $indexOfArray: [ bills, "$billid" ] } },
in: { $and: [ { $ne: [ "$$index", -1 ] }, { $eq: [ "$name", { $arrayElemAt: [ names, "$$index" ] }] }]}
}
}
}
}
])

Related

Mongodb: check that all the fields of the elements of an array of objects respect a condition

I have a database of a the employees of a company that looks like this:
{
_id: 7698,
name: 'Blake',
job: 'manager',
manager: 7839,
hired: ISODate("1981-05-01T00:00:00.000Z"),
salary: 2850,
department: {name: 'Sales', location: 'Chicago'},
missions: [
{company: 'Mac Donald', location: 'Chicago'},
{company: 'IBM', location: 'Chicago'}
]
}
I have an exercise in which I need to write the MongoDb command that returns all them employees who did all their missions in Chicago. I struggle with the all because I cannot find a way to check that all the locations of the missions array are equal to 'Chicago'.
I was thinking about doing it in two time: first find the total number of missions the employee has and then compare it to the number of mission he has in Chicago (that how I would do in SQL I guess). But I cannot found the number of mission the employee did in Chicago. Here is what I tried:
db.employees.aggregate([
{
$match: { "missions": { $exists: true } }
},
{
$project: {
name: 1,
nbMissionsChicago: {
$sum: {
$cond: [
{
$eq: [{
$getField: {
field: { $literal: "$location" },
input: "$missions"
}
}, "Chicago"]
}, 1, 0
]
}
}
}
}
])
Here is the result :
{ _id: 7698, name: 'Blake', nbMissionsChicago: 0 }
{ _id: 7782, name: 'Clark', nbMissionsChicago: 0 }
{ _id: 8000, name: 'Smith', nbMissionsChicago: 0 }
{ _id: 7902, name: 'Ford', nbMissionsChicago: 0 }
{ _id: 7499, name: 'Allen', nbMissionsChicago: 0 }
{ _id: 7654, name: 'Martin', nbMissionsChicago: 0 }
{ _id: 7900, name: 'James', nbMissionsChicago: 0 }
{ _id: 7369, name: 'Smith', nbMissionsChicago: 0 }
First of all, is there a better method to check that all the locations of the missions array respect the condition? And why does this commands returns only 0 ?
Thanks!
If all you need is the agents who had all their missions in "Chicago" then you don't need an aggregation pipeline for it, specifically the approach of filtering the array as part of the aggregation can't utilize an index and will make performance even worse.
A simple query should suffice here:
db.collection.find({
$and: [
{
"missions": {
$exists: true
}
},
{
"missions.location": {
$not: {
$gt: "Chicago"
}
}
},
{
"missions.location": {
$not: {
$lt: "Chicago"
}
}
}
]
})
Mongo Playground
This way we can build an index on the missions field and utilize it properly, any documents with a different value other then "Chigaco" will not match as they will fail the $gt or $lt comparion.
Note that an empty array also matches the condition, you can change the generic "missions" exists condition key into "missions.0": {$exists: true}, this will also require at least one mission.
You are unable to get the correct result as it is not the correct way to iterate the element in an array field.
Instead, you need to work with $size operator to get the size of an array and the $filter operator to filter the document.
Updated: You can directly compare the filtered array with the original array.
db.employees.aggregate([
{
$match: {
"missions": {
$exists: true
}
}
},
{
$project: {
name: 1,
nbMissionsChicago: {
$eq: [
{
$filter: {
input: "$missions",
cond: {
$eq: [
"$$this.location",
"Chicago"
]
}
}
},
"$missions"
]
}
}
}
])
Demo # Mongo Playground

MongoDB do not return document if condition in lookup fails

I am having this mongo DB query which queries a collection called songs and for each song, returns the respective album associated:
db.songs.aggregate([{
$lookup: {
from: "albums",
let: { album: '$album' },
as: "album",
pipeline: [{
$match: {
$expr: {
$and: [
{ $eq: ['$albumId', '$$album._id'] },
{ $eq: ['$status', 'Draft'] },
]
}
}
}]
}
}])
In the above query, my intention was to return a song only if the album was in Draft status, but in contrast, it returns all songs, and for the ones for which the album is not in Draft, it just returns an empty array inside the lookup. How can I not return the song document at all if the album is not in Draft?
Additionally, is it possible to flatten the results in the document? ie, merge all the fields of albums into the song document?
Once you perform the $lookup you can filter out the documents with an empty array:
{ $match: { album: { $ne: [] } }}
Then there is an example in the MongoDB documentation for the $mergeObjects operator that is very similar to your case. Assuming that each song belongs to one album, put together your aggregation pipeline may look like this:
db.songs.aggregate([
{
$lookup: {
from: "albums",
let: { album: '$album' },
as: "album",
pipeline: [{
$match: {
$expr: {
$and: [
{ $eq: ['$albumId', '$$album._id'] },
{ $eq: ['$status', 'Draft'] },
]
}
}
}]
}
},
{ $match: { album: { $ne: [] } }},
{
$replaceRoot: { newRoot: { $mergeObjects: [ { $arrayElemAt: [ "$album", 0 ] }, "$$ROOT" ] } }
},
{ $project: { album: 0 } }
])
You may want to experiment going in the other direction: find albums in status = Draft then get the songs:
db.album.aggregate([
{$match: {"status":"Draft"}}
,{$lookup: {from: "song",
localField: "album", foreignField: "album",
as: "songs"}}
// songs is now an array of docs. Run $map to turn that into an
// array of just the song title, and overwrite it (think x = x + 1):
,{$addFields: {songs: {$map: {
input: "$songs",
in: "$$this.song"
}} }}
]);
If you have a LOT of material in the song document, you can use the fancier $lookup to cut down the size of the docs in the lookup array -- but you still need the $map to turn it into an array of strings.
db.album.aggregate([
{$match: {"status":"Draft"}}
,{$lookup: {from: "song",
let: { aid: "$album" },
pipeline: [
{$match: {$expr: {$eq:["$album","$$aid"]}}},
{$project: {song:true}}
],
as: "songs"}}
,{$addFields: {songs: {$map: {
input: "$songs",
in: "$$this.song"
}} }}
]);

Looping through array to count in mongodb/mongoose

I have a user schema that contains a value called amputationInfo:
amputationInfo: [
{
type: String,
},
],
Here is an example of what that might look like in the database:
amputationInfo: [
"Double Symes/Boyd",
"Single Above-Elbow"
]
I have a review Schema that allows a user to leave a review, it contains a reference to the user who left it:
user: {
type: mongoose.Schema.ObjectId,
ref: 'User',
require: [true, 'Each review must have an associated user!'],
},
When a user leaves a review, I want to create an aggregate function that looks up the user on the review, finds their amputationInfo, loops through the array and adds up the total amount of users that contain "Double Symes/Boyd", "Single Above-Elbow"
So if we have 3 users and their amputationInfo is as follows:
amputationInfo: [
"Double Symes/Boyd",
"Single Above-Elbow"
]
amputationInfo: [
"Single Above-Elbow"
]
amputationInfo: []
The return from the aggregate function will count each term and add one to the corresponding value and look something like this:
[
{
doubleSymesBoyd: 1,
singleAboveElbow: 2
}
]
Here is what I have tried, but I just don't know enough about mongoDB to solve the issue:
[
{
'$match': {
'prosthetistID': new ObjectId('6126ca6148f34c00189f86f5')
}
}, {
'$lookup': {
'from': 'users',
'localField': 'user',
'foreignField': '_id',
'as': 'userInfo'
}
}, {
'$unwind': {
'path': '$userInfo'
}
}
]
After the $unwind, the resulting object has a userInfo key, that contains an amputationInfo array nested:
You can have following stages
$unwind to deconstruct the array
first $group to get the sum of each category
second $group to push into one document and make it as key value pair
$arrayToObject to get the desired output
$replaceRoot to make the data output into root
Here is the code
db.collection.aggregate([
{ "$unwind": "$userInfo.amputationInfo" },
{
"$group": {
"_id": "$userInfo.amputationInfo",
"count": { "$sum": 1 }
}
},
{
$group: {
_id: null,
data: { $push: {
k: "$_id",
v: "$count"
}
}
}
},
{ $project: { data: { "$arrayToObject": "$data" } } },
{ "$replaceRoot": { "newRoot": "$data" } }
])
Working Mongo playground

Query for returning documents having specific date range number then order them then sort

I have a Song schema that looks like:
const songSchema = new mongoose.Schema({
name: {
type: String,
required: true,
},
URL: {
type: String,
required: true,
},
playedOn: [mongoose.Schema.Types.Date],
});
And in the database I have values such as:
{
"_id" : ObjectId("6013e57d909d5252e869eac0"),
"playedOn" : [
ISODate("2021-01-29T10:41:55.444Z"),
ISODate("2021-01-29T10:41:55.447Z"),
ISODate("2021-01-29T10:41:55.448Z"),
ISODate("2021-01-29T10:41:55.450Z"),
ISODate("2021-01-29T10:41:55.451Z"),
ISODate("2021-01-29T10:41:55.452Z")
],
"name" : "Ariana Grande - Positions",
"URL" : "https://firebasestorage.googleapis.com/v0/b/reactnativeauth-sj.appspot.com/o/003.%20Ariana%20Grande%20-%20positions.mp3?alt=media&token=d206e095-6360-46c4-ad1e-40a9204ca9d0",
"__v" : 1
}
{
"_id" : ObjectId("6013e57d909d5252e869eac1"),
"playedOn" : [
ISODate("2021-01-29T10:41:53.708Z")
],
"name" : "Dua Lipa - Break My Heart",
"URL" : "https://firebasestorage.googleapis.com/v0/b/reactnativeauth-sj.appspot.com/o/005.%20Dua%20Lipa%20-%20Break%20My%20Heart.mp3?alt=media&token=7feea778-761c-4e0e-8840-6f60e7795651",
"__v" : 1
}
I want to first get the songs played most in the last 7 days (say top 10) sorted in descending order by the number of plays, The result be like:
{name: 'Positions', _id: '39s39s', numberOfPlays: 9}
{name: 'Irresistible', _id: '39s39s', numberOfPlays: 5}
{name: 'Closer', _id: '39s39s', numberOfPlays: 3}
I have tried things like:
db.songs.find({playedOn: {elemMatch: {$lt: ISODate()}}}, {_id: 1, playedOn: 1, name: 1}).pretty();
db.songs.find({playedOn: {$lt: ISODate() , $gte: ISODate("2021-01-28T09:20:37.732Z")}}, {_id: 1, playedOn: 1, name: 1}).pretty();
And many different things, but I just can't figure out the query that I should write for the same. I habe also tried using aggregate, but in vain. Please give a direction as to how it can be done.
Using aggregate(),
$match start date and end date in $elemMatch
$filter to get filtered date from array
$size to get size of returned elements from $filter
$sort by numberOfPlays in descending order
$limit to return 10 documents
// start date and end date set your logic as per your requirement
let endDate = new Date();
let startDate = new Date(endDate);
startDate.setDate(startDate.getDate() - 7);
db.songs.aggregate([
{
$match: {
playedOn: {
$elemMatch: {
$gte: startDate,
$lt: endDate
}
}
}
},
{
$project: {
name: 1,
numberOfPlays: {
$size: {
$filter: {
input: "$playedOn",
cond: {
$and: [
{ $gte: ["$$this", startDate] },
{ $lt: ["$$this", endDate] }
]
}
}
}
}
}
},
{ $sort: { numberOfPlays: -1 } },
{ $limit: 10 }
])
Playground
Using find() Starting in MongoDB v4.4, as part of making projection consistent with aggregation’s $project stage,
for limit and sort try .sort("-numberOfPlays").limit(10) end of the query
Playground

MongoDb count percent of document with a certain field present

I have some MongoDb document's(representing orders) and their schema looks roughly like that:
{
id: ObjectID
exchange_order_products: Array
}
The exchange_order_products array is empty if the customer didn't exchange any items he ordered, or if they did, the array will contain an Object for each item exchanged.
I want to get the percent of orders in which the customer didn't exchange anything, e.g. exchange_order_products array is empty.
So basically the formula is the following: (Number Of Orders With At Least One Exchange * 100) / Number of Orders With No Exchanges
I know that I can count the number of orders where the exchange_order_products array is empty like that:
[{$match: {
exchange_order_products: {$exists: true, $size: 0}
}}, {$count: 'count'}]
But how do I simultaneously get the number of all the documents in my collection?
You can use $group and $sum along with $cond to count empty and non-empty ones separately. Then you need $multiply and $divide to calculate the percentage:
db.collection.aggregate([
{
$group: {
_id: null,
empty: { $sum: { $cond: [ { $eq: [ { $size: "$exchange_order_products" }, 0 ] }, 1, 0 ] } },
nonEmpty: { $sum: { $cond: [ { $eq: [ { $size: "$exchange_order_products" }, 0 ] }, 0, 1 ] } },
}
},
{
$project: {
percent: {
$multiply: [
100, { $divide: [ "$nonEmpty", "$empty" ] }
]
}
}
}
])
Mongo Playground

Resources