Mongo DB project elements of an array into a single array - mongodb

I have a collection in my DB where each item looks like this :
{
"_id" : ObjectId("XXXXXX"),
"name" : "foo",
"products" : [{
"name" : "bar",
"code" : 123
},{
"name" : "foo",
"code" : 321
}]
}
And I want to get from a query a single array containing : [123, 321] from a specific collection.
This is what I have attempted :
db.getCollection('catalog').aggregate({
"$project": {
"products": {
"$map": {
"input": "$products",
"as" : "item",
"in" : "$$item.code"
}
}
}
})
But it fails and it stats "Last stage is undefined".
What am I doing wrong?

You can get a output like this, { "products" : [ 123, 321 ] }, using the following aggregation. Note the usage of the $reduce instead of the $map array operator.
db.collection.aggregate([
{
$project: {
_id: 0,
products: {
$reduce: {
input: "$products",
initialValue: [],
in: {
$concatArrays: [ "$$value", ["$$this.code"] ]
}
}
}
}
}
])

Related

Retrieve only queried element from a single document in mongoDB

I have a collection like below :
`{
"topics" : [
{
"id" : "2",
"name" : "Test1",
"owner" : [
"123"
]
},
{
"id" : "3",
"name" : "Test2",
"owner" : [
"123",
"456"
]
}
]
}`
As, this data is in single document, and I want only matching elements based on their owner, I am using below query ( using filter in aggregation ), but I am getting 0 matching elements.
Query :
Thanks in advance...!!
db.getCollection('topics').aggregate([
{"$match":{"topics.owner":{"$in":["123","456"]}}},
{"$project":{
"topics":{
"$filter":{
"input":"$topics",
"as":"topic",
"cond": {"$in": ["$$topic.owner",["123","456"]]}
}},
"_id":0
}}
])
This query should produce below output :
{
"topics" : [
{
"id" : "1",
"name" : "Test1",
"owner" : ["123"]
},
{
"id" : "2",
"name" : "Test2",
"owner" : ["123","456"]
}
]
}
As the topic.owner is an array, you can't use $in directly as this compares whether the array is within in an array.
Instead, you should do as below:
$filter - Filter the document in the topics array.
1.1. $gt - Compare the result from 1.1.1 is greater than 0.
1.1.1. $size - Get the size of the array from the result 1.1.1.1.
1.1.1.1. $setIntersection - Intersect the topic.owner array with the input array.
{
"$project": {
"topics": {
"$filter": {
"input": "$topics",
"as": "topic",
"cond": {
$gt: [
{
$size: {
$setIntersection: [
"$$topic.owner",
[
"123",
"456"
]
]
}
},
0
]
}
}
},
"_id": 0
}
}
Demo # Mongo Playground
db.getCollection('topics').aggregate([
{"$unwind":"$topics"},
{"$addFields":{
"rest":{"$or":[{"$in":["12z3","$topics.owner"]},{"$in":["456","$topics.owner"]}]}
}},
{"$match":{
"rest":true
}},
{"$group":{
"_id":"$_id",
"topics":{"$push":"$topics"}
}}
])

How to get the data from 2 different collections using lookup and how to add some condition in lookup to get the data based on some condition?

I have two collections as shown below:
First collection:
[{
"UID" : 3398,
"name" : "test"
}]
Second collection:
[{
"ques_id" : "q1",
"ri_score" : 5.3,
"uc_id" : 3398
},
{
"ques_id" : "q2",
"ri_score" : 5.3,
"uc_id" : 3398
}]
The query which I am using is given below:
db.first_collection.aggregate([
{
$lookup: {
from: "second_collection",
localField: "UID",
foreignField: "uc_id",
as: "second_collection_docs"
}
},
{
$project: {
"UID": 1,
"name": 1,
"second_collection_docs.ri_score": 1
}
}
])
The above query returns the data as shown below:
{
"UID" : 3394,
"name" : "test",
"second_collection_docs" : [{
"ri_score" : "5.3"
},
{
"ri_score" : "5.3"
}]
}
In the second collection data it is giving the same ri_score 2 times but I want it only one time. The expected result is shown below:
{
"UID" : 3394,
"name" : "test",
"second_collection_docs" : [{
"ri_score" : "5.3"
}]
}
Is there any solution for this so that I can get the expected result? Thank you in advance.
Maybe this could work using $addFields and $reduce:
db.first_collection.aggregate([
{}, //lookup stage
{}, // project stage
//Above the same query you have and here insert this stage
{
"$addFields": {
"values": {
"$reduce": {
"input": "$values",
"initialValue": [],
"in": {
"$cond": [
{
"$in": [
"$$this.ri_score",
"$$value.ri_score"
]
},
"$$value",
{
$concatArrays: [
"$$value",
[
"$$this"
]
]
}
]
}
}
}
}
}
])
Basically this query add the field values which is an array with objects that ri_score is not repeated.
Check this example where I've added another object with different ri_score and check if it works as you expected.
Also an example with your values is here. As you can see, ri_score is not duplicated.

Project multiple documents from a field's value using aggregation framework

Consider this document
{
"_id" : ObjectId("56d06614070b7f2b117b23db"),
"name" : "joe",
"value" : 3,
}
I need to get the following aggregation result :
[{
"name" : "joe",
},
{
"name" : "joe",
},
{
"name" : "joe",
}]
Do you have an idea on how to do that with aggregation framework ?
You can use $range:
db.collection.aggregate([
{ "$project": {
"_id": 0,
"name": {
"$map": {
"input": { "$range": [0,"$value"] },
"in": "$name"
}
}
}},
{ "$unwind": "$name" }
])
That essentially supplies [0,1,2] as in input array to $map, from which you can emit each value of "name" repeated. So the "$value" field in the document is being used as the upper bound or "length" of the array to produce.
Not really any other option if your MongoDB does not support that, other than you simply transform on the cursor instead.

$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

Filter array in subdocument array field

I am trying to fetch an element from an array in the MongoDB. I think the aggregation filter is the right one to apply. But I tried million times already, I still cannot find where is the problem. Could you give me hand?
MongoDB sample data:
{
"_id" : 12,
"items" : [
{
"columns" : [
{
"title" : "hhh",
"value" : 10
},
{
"title" : "hahaha",
"value" : 20
}
]
},
{
"columns" : [
{
"title" : "hiii",
"value" : 50
}
]
}
]
}
My solution:
db.myCollection.aggregate([
{
$project: {
items: {
$filter: {
input: "$items",
as: "item",
cond: { $eq: [ "$$item.columns.title", "hahaha" ]}
}
}
}
}
]).pretty()
My result:
{
"_id" : 15,
"items" : [
{
"columns" : [ ]
},
{
"columns" : [ ]
}
]
}
Expected result:
{
"_id" : 15,
"items" : [
{
"columns" : [
{
"title" : "hahaha",
"value" : 20
}
]
},
{
"columns" : []
}
]
}
I have checked the Mongo reference:
https://docs.mongodb.com/manual/reference/operator/aggregation/filter/#example
MongoDB version:3.4.1
Testing environment: Mongo Shell
You need to use the $map array operator to $filter the sub array in your subdocument. Also you should do this in the $addFields aggregation pipeline stage to automatically include all others fields in the query result if you need them.
You can also replace the $addFields stage with $project as you were doing but in this case, you will need to explicitly include all other fields.
let value = "hahaha";
db.coll.aggregate([
{
"$addFields": {
"items": {
"$map": {
"input": "$items",
"as": "item",
"in": {
"columns": {
"$filter": {
"input": "$$item.columns",
"as": "elt",
"cond": { "$eq": [ "$$elt.title", value ] }
}
}
}
}
}
}
}
])