Sort by in mongodb field contains an array - mongodb

I have a collection in which I store data in below format I want to apply sort by in below collection.
{
"job_count" : [
ObjectId("58eb607531f78831a8894a3e"),
ObjectId("58eb607531f78831a8894a3e"),
ObjectId("58eb607531f78831a8894a3e")
]
},
{
"job_count" : [
ObjectId("58eb607531f78831a8894a3e")
]
},
{
"job_count" : [
ObjectId("58eb607531f78831a8894a3e"),
ObjectId("58eb607531f78831a8894a3e")
]
}
i want data to be like as below
{
"job_count" : [
ObjectId("58eb607531f78831a8894a3e"),
ObjectId("58eb607531f78831a8894a3e"),
ObjectId("58eb607531f78831a8894a3e")
]
},
{
"job_count" : [
ObjectId("58eb607531f78831a8894a3e"),
ObjectId("58eb607531f78831a8894a3e")
]
},
{
"job_count" : [
ObjectId("58eb607531f78831a8894a3e")
]
}
my complete query is
Jobs.aggregate(
{"$match" : $condArray},
{"$unwind" : { path: "$mytradesmen.hired", preserveNullAndEmptyArrays: true}},
{"$lookup" : {
"from":"users",
"localField":"mytradesmen.hired",
"foreignField":"_id",
"as": "user_details"
}
},
{"$unwind": { path: "$user_details", preserveNullAndEmptyArrays: true}},
{ "$sort" : {"job_count":-1}})
Can anyone help me figure out the query i should modify to get the expected result,
please let me know in case of any further detail required i will edit my question accordingly

Use the $size operator to create an extra field that holds the count of the elements in the array and then $sort on that field:
Jobs.aggregate([
{
"$project": {
"count": { "$size": "$job_count" },
"job_count": 1
}
},
{ "$sort" : { "count": -1 } }
])

Related

Aggregate Lookup with pipeline and match not working mongodb

I have these 2 simple collections:
items:
{
"id" : "111",
"name" : "apple",
"status" : "active"
}
{
"id" : "222",
"name" : "banana",
"status" : "active"
}
inventory:
{
"item_id" : "111",
"qty" : 3,
"branch" : "main"
}
{
"item_id" : "222",
"qty" : 3
}
Now I want to to only return the items with "status" == "active" and with "branch" that exist and is equal to "main" in the inventory collection. I have this code below but it returns all documents, with the second document having an empty "info" array.
db.getCollection('items')
.aggregate([
{$match:{$and:[
{"status":'active'},
{"name":{$exists:true}}
]
}},
{$lookup:{
as:"info",
from:"inventory",
let:{fruitId:"$id"},
pipeline:[
{$match:{
$and:[
{$expr:{$eq:["$item_id","$$fruitId"]}},
{"branch":{$eq:"main"}},
{"branch":{$exists:true}}
]
}
}
]
}}
])
Can anyone give me an idea on how to fix this?
Your code is doing well. I think you only need a $match stage in the last of your pipeline.
db.items.aggregate([
{
$match: {
$and: [
{ "status": "active" },
{ "name": { $exists: true } }
]
}
},
{
$lookup: {
as: "info",
from: "inventory",
let: { fruitId: "$id" },
pipeline: [
{
$match: {
$and: [
{ $expr: { $eq: [ "$item_id", "$$fruitId" ] } },
{ "branch": { $eq: "main" } },
{ "branch": { $exists: true } }
]
}
}
]
}
},
{
"$match": {
"info": { "$ne": [] }
}
}
])
mongoplayground
Query
match
lookup on id/item_id, and match branch with "main" (if it doesn't exists it will be false anyways)
keep only the not empty
*query is almost the same as #YuTing one,but i had written it anyways, so i send it, for the small difference of alternative lookup syntax
Test code here
items.aggregate(
[{"$match":
{"$expr":
{"$and":
[{"$eq":["$status", "active"]},
{"$ne":[{"$type":"$name"}, "missing"]}]}}},
{"$lookup":
{"from":"inventory",
"localField":"id",
"foreignField":"item_id",
"pipeline":[{"$match":{"$expr":{"$eq":["$branch", "main"]}}}],
"as":"inventory"}},
{"$match":{"$expr":{"$ne":["$inventory", []]}}},
{"$unset":["inventory"]}])

MongoDB - Performing an upsert on an array if arrayFilters are not satisfied

I have the following document stored in mongo:
{
"_id" : ObjectId("5d1a08d2329a3c1374f176df"),
"associateID" : "1234567",
"associatePreferences" : [
{
"type" : "NOTIFICATION",
"serviceCode" : "service-code",
"eventCode" : "test-template",
"preferences" : [
"TEXT",
"EMAIL"
]
},
{
"type" : "URGENT_NOTIFICATION",
"serviceCode" : "service-code",
"eventCode" : "test-template",
"preferences" : [
"TEXT"
]
}
]
}
I am basically trying to query one of the elements of the associatePreferences array based off of its type, serviceCode, and eventCode and add a new value to the preferences array. However, if that combination of type, serviceCode, and eventCode is not present, I would like to add a new element to the associatePreferences array with those values. This is my current query:
db.user_communication_preferences.update(
{'associateID':'testassociate'},
{$addToSet:{'associatePreferences.$[element].preferences':"NEW_VALUE"}},
{arrayFilters:[{'element.serviceCode':'service-code-not-present', 'element.eventCode':'event-code-not-present','element.type':'URGENT_NOTIFICATION'}]}
)
This query works if all of the arrayFilters are present in the an element of associatePreferences, but it does not add a new element if it is not present. What am I missing?
You can use aggregation pipeline to check the existence of the element, then append the element to associatePreferences array conditionally. Finally, using the aggregation result to update back your document.
db.user_communication_preferences.aggregate([
{
"$match": {
"associateID": "testassociate"
}
},
{
"$addFields": {
"filteredArray": {
"$filter": {
"input": "$associatePreferences",
"as": "pref",
"cond": {
$and: [
{
$eq: [
"$$pref.type",
"URGENT_NOTIFICATION"
]
},
{
$eq: [
"$$pref.eventCode",
"event-code-not-exists"
]
},
{
$eq: [
"$$pref.serviceCode",
"service-code-not-exists"
]
}
]
}
}
}
}
},
{
$addFields: {
"needAddElement": {
$eq: [
{
"$size": "$filteredArray"
},
0
]
}
}
},
{
"$addFields": {
"associatePreferences": {
"$concatArrays": [
"$associatePreferences",
{
"$cond": {
"if": {
$eq: [
"$needAddElement",
true
]
},
"then": [
{
"type": "URGENT_NOTIFICATION",
"serviceCode": "service-code-not-exists",
"eventCode": "event-code-not-exists",
"preferences": [
"TEXT"
]
}
],
"else": []
}
}
]
}
}
}
]).forEach(result){
db.user_communication_preferences.update({
_id : result._id
}, {
$set: {
"associatePreferences" : result.associatePreferences
}
})
}

$elemMatch against two Array elements if one fails

A bit odd but this is what I am looking for.
I have an array as follow:
Document 1:
Items: [
{
"ZipCode": "11111",
"ZipCode4" "1234"
}
Document 2:
Items: [
{
"ZipCode": "11111",
"ZipCode4" "0000"
}
I would like to use a single query, and send a filter on ZipCode = 1111 && ZipCode4 = 4321, if this fails, the query should look for ZipCode = 1111 && ZipCode4: 0000
Is there a way to do this in a single query ? or do I need to make 2 calls to my database ?
For matching both data set (11111/4321) and (11111/0000), you can use $or and $and with $elemMatch like the following :
db.test.find({
$or: [{
$and: [{
"Items": {
$elemMatch: { "ZipCode": "11111" }
}
}, {
"Items": {
$elemMatch: { "ZipCode4": "4321" }
}
}]
}, {
$and: [{
"Items": {
$elemMatch: { "ZipCode": "11111" }
}
}, {
"Items": {
$elemMatch: { "ZipCode4": "0000" }
}
}]
}]
})
As you want conditional staging, this is not possible but we can get closer to it like this :
db.test.aggregate([{
$match: {
$or: [{
$and: [{ "Items.ZipCode": "11111" }, { "Items.ZipCode4": "4321" }]
}, {
$and: [{ "Items.ZipCode": "11111" }, { "Items.ZipCode4": "0000" }]
}]
}
}, {
$project: {
Items: 1,
match: {
"$map": {
"input": "$Items",
"as": "val",
"in": {
"$cond": [
{ $and: [{ "$eq": ["$$val.ZipCode", "11111"] }, { "$eq": ["$$val.ZipCode4", "4321"] }] },
true,
false
]
}
}
}
}
}, {
$unwind: "$match"
}, {
$group: {
_id: "$match",
data: {
$push: {
_id: "$_id",
Items: "$Items"
}
}
}
}])
The first $match is for selecting only the items we need
The $project will build a new field that check if this items is from the 1st set of data (11111/4321) or the 2nd set of data (11111/0000).
The $unwind is used to remove the array generated by $map.
The $group group by set of data
So in the end you will have an output like the following :
{ "_id" : true, "data" : [ { "_id" : ObjectId("58af69ac594b51730a394972"), "Items" : [ { "ZipCode" : "11111", "ZipCode4" : "4321" } ] }, { "_id" : ObjectId("58af69ac594b51730a394974"), "Items" : [ { "ZipCode" : "11111", "ZipCode4" : "4321" } ] } ] }
{ "_id" : false, "data" : [ { "_id" : ObjectId("58af69ac594b51730a394971"), "Items" : [ { "ZipCode" : "11111", "ZipCode4" : "0000" } ] } ] }
Your application logic can check if there is _id:true in this output array, just take the corresponding data field for _id:true. If there is _id:false in this object take the corresponding data field for _id:false.
In the last $group, you can also use $addToSet to builds 2 field data1 & data2 for both type of data set but this will be painful to use as it will add null object to the array for each one of the opposite type :
"$addToSet": {
"$cond": [
{ "$eq": ["$_id", true] },
"$data",
null
]
}
Here is a gist

How can I select document with array items containing in values array?

I have collection in mongodb (3.0):
{
_id: 1,
m: [{_id:11, _t: 'type1'},
{_id:12, _t: 'type2'},
{_id:13, _t: 'type3'}]
},
{
_id: 2,
m: [{_id:21, _t: 'type1'},
{_id:22, _t: 'type21'},
{_id:23, _t: 'type3'}]
}
I want to find documents with m attributes where m._t containing ['type1', 'type2'].
Like this:
{
_id: 1,
m: [{_id:11, _t: 'type1'},
{_id:12, _t: 'type2'}]
},
{
_id: 2,
m: [{_id:21, _t: 'type1'}]
}
I tried to use $ and $elemMatch, but couldn't get required result.
How to do it, using find()?
Help me, please! Thanks!
Because the $elemMatch operator limits the contents of the m array field from the query results to contain only the first element matching the $elemMatch condition, the following will only return the an array with the first matching elements
{
"_id" : 11,
"_t" : "type1"
}
and
{
"_id" : 21,
"_t" : "type1"
}
Query using $elemMatch projection:
db.collection.find(
{
"m._t": {
"$in": ["type1", "type2"]
}
},
{
"m": {
"$elemMatch": {
"_t": {
"$in": ["type1", "type2"]
}
}
}
}
)
Result:
/* 0 */
{
"_id" : 1,
"m" : [
{
"_id" : 11,
"_t" : "type1"
}
]
}
/* 1 */
{
"_id" : 2,
"m" : [
{
"_id" : 21,
"_t" : "type1"
}
]
}
One approach you can take is the aggregation framework, where your pipeline would consist of a $match operator, similar to the find query above to filter the initial stream of documents. The next pipeline step would be the crucial $unwind operator that "splits" the array elements to be further streamlined with another $match operator and then the final $group pipeline to restore the original data structure by using the accumulator operator $push.
The following illustrates this path:
db.collection.aggregate([
{
"$match": {
"m._t": {
"$in": ["type1", "type2"]
}
}
},
{
"$unwind": "$m"
},
{
"$match": {
"m._t": {
"$in": ["type1", "type2"]
}
}
},
{
"$group": {
"_id": "$_id",
"m": {
"$push": "$m"
}
}
}
])
Sample Output:
/* 0 */
{
"result" : [
{
"_id" : 2,
"m" : [
{
"_id" : 21,
"_t" : "type1"
}
]
},
{
"_id" : 1,
"m" : [
{
"_id" : 11,
"_t" : "type1"
},
{
"_id" : 12,
"_t" : "type2"
}
]
}
],
"ok" : 1
}
To get your "filtered" result, the $redact with the aggregation pipeline is the fastest way:
db.junk.aggregate([
{ "$match": { "m._t": { "$in": ["type1", "type2"] } } },
{ "$redact": {
"$cond": {
"if": {
"$or": [
{ "$eq": [ { "$ifNull": ["$_t", "type1"] }, "type1" ] },
{ "$eq": [ { "$ifNull": ["$_t", "type2"] }, "type2" ] }
],
},
"then": "$$DESCEND",
"else": "$$PRUNE"
}
}}
])
The $redact operator sets up a logical filter for the document that can also traverse into the array levels. Note that this is matching on _t at all levels of the document, so make sure there are no other elements sharing this name.
The query uses $in for selection just as the logical filter uses $or. Anything that does not match, gets "pruned".
{
"_id" : 1,
"m" : [
{
"_id" : 11,
"_t" : "type1"
},
{
"_id" : 12,
"_t" : "type2"
}
]
}
{
"_id" : 2,
"m" : [ { "_id" : 21, "_t" : "type1" } ]
}
Short and sweet and simple.
A bit more cumbersome, but a reasonably safer is to use this construct with $map and $setDifference to filter results:
db.junk.aggregate([
{ "$match": { "m._t": { "$in": ["type1", "type2"] } } },
{ "$project": {
"m": {
"$setDifference": [
{ "$map": {
"input": "$m",
"as": "el",
"in": {
"$cond": {
"if": {
"$or": [
{ "$eq": [ "$$el._t", "type1" ] },
{ "$eq": [ "$$el._t", "type2" ] }
]
},
"then": "$$el",
"else": false
}
}
}},
[false]
]
}
}}
])
The $map evaluates the conditions against each element and the $setDifference removes any of those condtions that returned false rather than the array content. Very similar to the $cond in redact above, but it is just working specifically with the one array and not the whole document.
In future MongoDB releases ( currently available in development releases ) there will be the $filter operator, which is very simple to follow:
db.junk.aggregate([
{ "$match": { "m._t": { "$in": ["type1", "type2"] } } },
{ "$project": {
"m": {
"$filter": {
"input": "$m",
"as": "el",
"cond": {
"$or": [
{ "$eq": [ "$$el._t", "type1" ] },
{ "$eq": [ "$$el._t", "type2" ] }
]
}
}
}
}}
])
And that will simply remove any array element that does not match the specified conditions.
If you want to filter array content on the server, the aggregation framework is the way to do it.

Check if an element appears in an array during the projection stage of a mongo aggregation pipeline

I've got a collection of mongo documents like -
{
"_id" : "c959e4d6-961d-4043-ade6-2f93aa055e11",
"events" : [
"clickOut"
"showHoverAd",
"closeHoverAd"
]
}
{
"_id" : "d0dcb2be-f8bc-45cd-8337-d89a16063b08",
"events" : [
"zoom",
"pan"
]
}
{
"_id" : "9179b26e-e45c-48ab-93f6-e73b8ebe559b",
"events" : [
"clickOut"
]
}
{
"_id" : "db0b82ad-7a33-4ce8-9117-f6ecf041d0d9",
"events" : [
"adjustStars",
"adjustPrice",
"closeHoverAd",
"showHoverAd"
]
}
I'm trying to use a projection stage in an aggregation pipeline to identify if a particular string appears in the events field.
db.events.aggreate([
{$project: {
session: '$_id',
clickedOut: {
$cond: [{$elemMatch: {'$events':'clickOut'}},true,false]
}
}}
])
I'm getting an error - exception: invalid operator '$elemMatch'.
I want my output documents to look like -
{
"session" : "c959e4d6-961d-4043-ade6-2f93aa055e11",
"clickedOut" : false
}
{
"session" : "d0dcb2be-f8bc-45cd-8337-d89a16063b08",
"clickedOut" : true
}
But I can't seem to find a way of doing it. I've tried using $in and $all or simply
$cond: {'$events':'clickOut'}
but I'm not getting anywhere.
Use the following aggregation:
db.events.aggregate([
{
"$unwind": "$events"
},
{
"$project": {
"_id": 0,
"session": "$_id",
"clickedOut": {
"$cond": [ { "$eq": [ "$events", "clickOut" ] }, 1, 0 ]
}
}
},
{
"$group": {
"_id": "$session",
"count": {
"$sum": "$clickedOut"
}
}
},
{
"$project": {
"_id": 0,
"session": "$_id",
"clickedOut": {
"$cond": [ { "$eq": [ "$count", 1 ] }, true, false ]
}
}
},
]);
Output:
/* 1 */
{
"result" : [
{
"session" : "db0b82ad-7a33-4ce8-9117-f6ecf041d0d9",
"clickedOut" : false
},
{
"session" : "9179b26e-e45c-48ab-93f6-e73b8ebe559b",
"clickedOut" : true
},
{
"session" : "d0dcb2be-f8bc-45cd-8337-d89a16063b08",
"clickedOut" : false
},
{
"session" : "c959e4d6-961d-4043-ade6-2f93aa055e11",
"clickedOut" : true
}
],
"ok" : 1
}