Mongo ranking results - mongodb

I have a collection like
db.books.insertMany([
{"products" : [{"name": "name1", "ids": [4, 5, 6]}], "author" : "Dante", "shelf": "a" },
{ "products" : [{"name": "name1", "ids": [4, 5]}], "author" : "Homer", "shelf": "a" },
{ "products" : [{"name": "name1", "ids": [2]}], "author" : "Dante", "shelf": "b" },
])
and I want to retrieve all documents where "shelf" is 'a'
and sort by 2 conditions:
1 - by Author
2 - documents where products.ids not contains 6 should be the first.
Could anyone help?

You can try this query:
First $match the shelf value with "a".
Then create an auxiliar value where will be true if 6 not exists into products.ids, otherwise false.
Then $sort by values you want.
And use $project to remove the auxiliar value.
db.collection.aggregate([
{
"$match": {"shelf": "a"}
},
{
"$set": {
"rank": {
"$eq": [
{
"$filter": {
"input": "$products",
"cond": {"$in": [6,"$$this.ids"]}
}
},[]
]
}
}
},
{
"$sort": {
"rank": -1,
"author": 1
}
},
{
"$project": {"rank": 0}
}
])
Example here

Here is a variation that sorts more granularly on author+"not containing 6".
db.foo.aggregate([
{$match: {shelf:'a'}}
,{$unwind: '$products'}
,{$addFields: {sortMarker: {$cond: [
{$in: [6, '$products.ids']},
"Z", // THEN make sortMarker at the end
"A" // ELSE make sortMarker at the start
]}
}}
,{$sort: {'author':1, 'sortMarker':1}}
]);
which given this input set:
{"products" : [
{"name": "name3", "ids": [6, 7]},
{"name": "name2", "ids": [4, 5]}
],
"author" : "Homer",
"shelf": "a" },
{"products" : [
{"name": "name1", "ids": [4, 5, 6]},
{"name": "name4", "ids": [9]},
{"name": "name7", "ids": [9,6]},
{"name": "name7", "ids": [10]}
],
"author" : "Dante",
"shelf": "a"},
{ "products" : [
{"name": "name1", "ids": [2]}
], "author" : "Dante",
"shelf": "b"}
yields this result:
{
"_id" : 1,
"products" : {
"name" : "name4",
"ids" : [
9
]
},
"author" : "Dante",
"shelf" : "a",
"sortMarker" : "A"
}
{
"_id" : 1,
"products" : {
"name" : "name7",
"ids" : [
10
]
},
"author" : "Dante",
"shelf" : "a",
"sortMarker" : "A"
}
{
"_id" : 1,
"products" : {
"name" : "name1",
"ids" : [
4,
5,
6
]
},
"author" : "Dante",
"shelf" : "a",
"sortMarker" : "Z"
}
{
"_id" : 1,
"products" : {
"name" : "name7",
"ids" : [
9,
6
]
},
"author" : "Dante",
"shelf" : "a",
"sortMarker" : "Z"
}
{
"_id" : 0,
"products" : {
"name" : "name2",
"ids" : [
4,
5
]
},
"author" : "Homer",
"shelf" : "a",
"sortMarker" : "A"
}
{
"_id" : 0,
"products" : {
"name" : "name3",
"ids" : [
6,
7
]
},
"author" : "Homer",
"shelf" : "a",
"sortMarker" : "Z"
}
Optionally, this stage can be added after the $sort:
{$group: {_id: '$author', products: {$push: '$products'}}}
And this will bring the sorted "not containing 6 then containing 6" items together again as an array packaged by author; the $push retains the order. Note we need only need author in _id because the match was for one shelf. If more than one shelf is in the match, then we would need:
{$group: {_id: {author:'$author',shelf:'$shelf'}, products: {$push: '$products'}}}

Related

Convert array of objects to array of strings in mongodb

I am looking at the following documentation.
The following inserts documents into a collection classes.
db.classes.insertMany( [
{ _id: 1, title: "Reading is ...", enrollmentlist: [ "giraffe2", "pandabear", "artie" ], days: ["M", "W", "F"] },
{ _id: 2, title: "But Writing ...", enrollmentlist: [ "giraffe1", "artie" ], days: ["T", "F"] }
] )
And the members collection:
db.members.insertMany( [
{ _id: 1, name: "artie", joined: new Date("2016-05-01"), status: "A" },
{ _id: 2, name: "giraffe", joined: new Date("2017-05-01"), status: "D" },
{ _id: 3, name: "giraffe1", joined: new Date("2017-10-01"), status: "A" },
{ _id: 4, name: "panda", joined: new Date("2018-10-11"), status: "A" },
{ _id: 5, name: "pandabear", joined: new Date("2018-12-01"), status: "A" },
{ _id: 6, name: "giraffe2", joined: new Date("2018-12-01"), status: "D" }
] )
They use the following aggregation to join the two collections on the array field, enrollmentlist.
db.classes.aggregate( [
{
$lookup:
{
from: "members",
localField: "enrollmentlist",
foreignField: "name",
as: "enrollee_info"
}
}
] )
Which returns the following:
{
"_id" : 1,
"title" : "Reading is ...",
"enrollmentlist" : [ "giraffe2", "pandabear", "artie" ],
"days" : [ "M", "W", "F" ],
"enrollee_info" : [
{ "_id" : 1, "name" : "artie", "joined" : ISODate("2016-05-01T00:00:00Z"), "status" : "A" },
{ "_id" : 5, "name" : "pandabear", "joined" : ISODate("2018-12-01T00:00:00Z"), "status" : "A" },
{ "_id" : 6, "name" : "giraffe2", "joined" : ISODate("2018-12-01T00:00:00Z"), "status" : "D" }
]
}
{
"_id" : 2,
"title" : "But Writing ...",
"enrollmentlist" : [ "giraffe1", "artie" ],
"days" : [ "T", "F" ],
"enrollee_info" : [
{ "_id" : 1, "name" : "artie", "joined" : ISODate("2016-05-01T00:00:00Z"), "status" : "A" },
{ "_id" : 3, "name" : "giraffe1", "joined" : ISODate("2017-10-01T00:00:00Z"), "status" : "A" }
]
}
How can I reduce enrolle_info just to be an array of strings with all of the names?
This is the result I am after:
{
"_id" : 1,
"title" : "Reading is ...",
"enrollmentlist" : [ "giraffe2", "pandabear", "artie" ],
"days" : [ "M", "W", "F" ],
"enrollee_info" : [
"artie",
"pandabear"
"giraffe2"
]
}
{
"_id" : 2,
"title" : "But Writing ...",
"enrollmentlist" : [ "giraffe1", "artie" ],
"days" : [ "T", "F" ],
"enrollee_info" : [
"artie",
"giraffe1"
]
}
I have also looked into using multiple joins by introducing the pipeline field inside the $lookup operation. I am able to use a $project to get an array just with {"name": "example"} but I am not sure how to remove the "name". I have tried using an {"$unwind": "$enrollee_info.name"} but that does not give me what I want. Do I need to introduce another stage to my aggregation pipeline after I do the join?
It seems that I was over complicating this. I was able to achieve my desired result by doing the following:
db.classes.aggregate( [
{
$lookup:
{
from: "members",
localField: "enrollmentlist",
foreignField: "name",
as: "enrollee_info"
}
},
{
$project:
{
"_id": 1,
"title": 1,
"days": 1,
"enrollee_names": "$enrollee_info.name"
}
}
] )
Result:
[
{
"id": 1,
"title": "Reading is ...",
"days": [
"M",
"W",
"F"
],
"names": [
"artie",
"pandabear",
"giraffe2"
]
},
{
"id": 2,
"title": "But Writing ...",
"days": [
"T",
"F"
],
"names": [
"artie",
"giraffe1"
]
}
]

Limit the output of each bucket in `$bucketAuto` aggregation stage of MongoDB

Consider a collection user with the following documents:
{"id": 1, "name": "John", "designation": "customer"}
{"id": 2, "name": "Alison", "designation": "manager"}
{"id": 3, "name": "Sam", "designation": "customer"}
{"id": 4, "name": "George", "designation": "salesperson"}
{"id": 5, "name": "Will", "designation": "salesperson"}
{"id": 6, "name": "Daffney", "designation": "customer"}
{"id": 7, "name": "Julie", "designation": "salesperson"}
{"id": 8, "name": "Elliot", "designation": "customer"}
{"id": 9, "name": "Bruno", "designation": "customer"}
{"id": 10, "name": "Omar", "designation": "customer"}
{"id": 11, "name": "Sid", "designation": "customer"}
{"id": 12, "name": "Nelson", "designation": "manager"}
In the following operation, input documents are grouped into three buckets according to the values in the designation field:
db.users.aggregate([
{
"$bucketAuto": {
groupBy: "$designation",
buckets: 5,
output: {
"count": { $sum: 1 },
"users" : {
$push: {
"name": "$name"
},
}
}
}
}
])
Following are the results of this operation:
/* 1 */
{
"_id" : {"min" : "customer", "max" : "manager"},
"count" : 7.0,
"users" : [
{"name" : "John"},
{"name" : "Sam"},
{"name" : "Daffney"},
{"name" : "Elliot"},
{"name" : "Bruno"},
{"name" : "Omar"},
{"name" : "Sid"}
]
}
/* 2 */
{
"_id" : {"min" : "manager", "max" : "salesperson"},
"count" : 2.0,
"users" : [
{"name" : "Nelson"},
{"name" : "Alison"}
]
}
/* 3 */
{
"_id" : {"min" : "salesperson", "max" : "salesperson"},
"count" : 3.0,
"users" : [
{"name" : "George"},
{"name" : "Will"},
{"name" : "Julie"}
]
}
What I wanted to do was limit the number of results in the "users" attribute of the resulting documents to 2, something like this:
/* 1 */
{
"_id" : {"min" : "customer", "max" : "manager"},
"count" : 2.0,
"users" : [
{"name" : "John"},
{"name" : "Sam"}
]
}
/* 2 */
{
"_id" : {"min" : "manager", "max" : "salesperson"},
"count" : 2.0,
"users" : [
{"name" : "Nelson"},
{"name" : "Alison"}
]
}
/* 3 */
{
"_id" : {"min" : "salesperson", "max" : "salesperson"},
"count" : 2.0,
"users" : [
{"name" : "George"},
{"name" : "Will"}
]
}
Is there some way I can do that?
I am not sure it is possible with $bucketAuto, but you can try $slice to get limited elements from array and $size to get number of element in array,
add this stage after $bucketAuto stage,
{
$addFields: {
users: { $slice: ["$users", 2] }
}
},
{
$addFields: {
count: { $size: "$users" }
}
}
Playground

How do I use mongodb $lookup with a nested array?

I'm trying to perform a search query which would combine two collections.
Collection 1 is called stock collection.
{ "fruit_id" : "1", "name" : "apples", "stock": 100 }
{ "fruit_id" : "2", "name" : "oranges", "stock": 50 }
{ "fruit_id" : "3", "name" : "plums", "stock": 60 }
Collection 2 is called orders collection
{ "order_id" : "001", "ordered_fruits":
[
{"fruit_id" : "1", "name" : "apples", "ordered" : 5},
{"fruit_id" : "3", "name" : "plums", "ordered" : 20}
]
}
{ "order_id" : "002", "ordered_fruits":
[
{"fruit_id" : "2", "name" : "oranges", "ordered" : 30},
{"fruit_id" : "3", "name" : "plums", "ordered" : 20}
]
}
I am trying to code a query that returns the stock collection with an additional key pair which represents the total amount of ordered fruits.
{ "fruit_id" : "1", "name" : "apples", "stock": 100, "ordered": 5 }
{ "fruit_id" : "2", "name" : "oranges", "stock": 50, "ordered": 30 }
{ "fruit_id" : "3", "name" : "plums", "stock": 60, "ordered": 40 }
I have tried to use $lookup from the aggregation framework but it becomes complicated with nested arrays. I'm pretty stuck now.
You can use below aggregation
db.stock.aggregate([
{ "$lookup": {
"from": "orders",
"let": { "fruitId": "$fruit_id" },
"pipeline": [
{ "$match": { "$expr": { "$in": ["$$fruitId", "$ordered_fruits.fruit_id"] }}},
{ "$unwind": "$ordered_fruits" },
{ "$match": { "$expr": { "$eq": ["$ordered_fruits.fruit_id", "$$fruitId"] }}}
],
"as": "orders"
}},
{ "$project": {
"ordered": { "$sum": "$orders.ordered_fruits.ordered" },
"fruit_id" : 1, "name" : 1, "stock": 1
}}
])
MongoPlayground

How to aggregate sum in MongoDB according to the data in the array to get a total count?

I have data in the form:
{
"_id": 1,
"array": [{
"who": "jon",
"whom": "max",
"point": 2,
"description": "la-la"
}]
} {
"_id": 2,
"array": [{
"Id": "jon",
"ToId": "peter",
"point": 3,
"description": "la-la"
}]
} {
"_id": 3,
"array": [{
"Id": "peter",
"ToId": "max",
"point": 2,
"description": "la-la"
}]
} {
"_id": 4,
"array": [{
"Id": "max",
"ToId": "peter",
"point": 3,
"description": "la-la"
}]
}
I took the data from the array command:
db.game.aggregate([{$project: {_id: false, array: true}}, {$unwind: "$array"}])
{ "array" : { "Id" : "jon", "ToId" : "max", "pointCount" : 2, "description" : "la-la" } }
{ "array" : { "Id" : "jon", "ToId" : "peter", "pointCount" : 3, "description" : "la-la" } }
{ "array" : { "Id" : "peter", "ToId" : "max", "pointCount" : 2, "description" : "la-la" } }
{ "array" : { "Id" : "max", "ToId" : "peter", "pointCount" : 3, "description" : "la-la" } }
How to get the data in a format
{"ToId" : "peter", "pointCount" : 6}
{"ToId" : "max", "pointCount" : 4}
Thank you.
Your data from array command is different from the input. If you have the given input, the following query will work.
db.game.aggregate([
{$unwind: "$array"},
{$group:{"_id" : "$array.ToId", "pointCount": {$sum:"$array.point"}}},
{$project: {_id: 0, "ToId":"$_id", "pointCount": 1}}
])

Query mutiple fields from a nested document of MongoDB

I am using MongoDB 3.0.5. I have a collection like
db.getCollection('mytest').insert([
{_id: 1, "records": [{"Name": "Joe", "Salary": 70000, "Department": "IT"}]},
{_id: 2, "records": [{"Name": "Henry", "Salary": 80000, "Department": "Sales"}, {"Name": "Jake", "Salary": 40000, "Department": "Sales"}]},
{_id: 3, "records": [{"Name": "Sam", "Salary": 90000, "Department": "IT"}, {"Name": "Tom", "Salary": 50000, "Department": "Sales"}]},
{_id: 4, "records":[{"Name": "Janice", "Salary": 80000, "Department": "Finance"}, {"Name": "Kale", "Salary": 95000, "Department": "IT"}]}
])
Now I could query a single field, say like people in the IT department -
> db.getCollection('mytest').find({"records.Department": {"$in": ["IT"]}}, {"records.$": 1})
{ "_id" : 1, "records" : [ { "Name" : "Joe", "Salary" : 70000, "Department" : "IT" } ] }
{ "_id" : 3, "records" : [ { "Name" : "Sam", "Salary" : 90000, "Department" : "IT" } ] }
{ "_id" : 4, "records" : [ { "Name" : "Kale", "Salary" : 95000, "Department" : "IT" } ] }
I really want to find the salaries in the departments of Finance and IT.
But the query with more than one fields returns part of the desired results -
> db.getCollection('mytest').find({"records.Department": {"$in": ["IT", "Finance"]}}, {"records.$": 1})
{ "_id" : 1, "records" : [ { "Name" : "Joe", "Salary" : 70000, "Department" : "IT" } ] }
{ "_id" : 3, "records" : [ { "Name" : "Sam", "Salary" : 90000, "Department" : "IT" } ] }
{ "_id" : 4, "records" : [ { "Name" : "Janice", "Salary" : 80000, "Department" : "Finance" } ] }
I wonder if somebody could help with it. Thanks.
You can do it using mongodb $filter operator. Try the following code:
db.getCollection('mytest').aggregate([
{ $match: {'records.Department': {$in: ['IT', 'Finance']}}},
{ $project: {
departments: {
$filter: {
input: '$records',
as: 'record',
cond: {
$or: [
{$eq: ['$$record.Department', "IT"]},
{$eq: ['$$record.Department', "Finance"]}
]
}
}
}
}
}
])
For your data I got the following result:
{ "_id" : 1, "departments" : [ { "Name" : "Joe", "Salary" : 70000, "Department": "IT" } ] }
{ "_id" : 3, "departments" : [ { "Name" : "Sam", "Salary" : 90000, "Department": "IT" } ] }
{ "_id" : 4, "departments" : [ { "Name" : "Janice", "Salary" : 80000, "Department" : "Finance" }, { "Name" : "Kale", "Salary" : 95000, "Department" : "IT" } ] }