Mongodb array projection - mongodb

I have a set of documents in a mongo collection that have a schema like so:
{
"itemIds":{
"12341234-1234-1234-1234-123412341234": true,
"23452345-2345-2354-2345-234523452354": false,
"34563456-3456-3456-3456-345634563456": true
}
}
Because of our move to azure cognitive search, I need to use a mongo projection to get the data to look like this:
{
"itemIds": ["12341234-1234-1234-1234-123412341234",
"34563456-3456-3456-3456-345634563456"]
}
The properties with a false value should be excluded, and the properties with a true value should be converted to array values.

Option 1. Maybe something like this:
db.collection.aggregate([
{
"$addFields": {
"itemIds": {
"$filter": {
"input": {
"$objectToArray": "$itemIds"
},
"as": "item",
"cond": {
$eq: [
"$$item.v",
true
]
}
}
}
}
},
{
$project: {
_id: "$_id",
itemIds: "$itemIds.k"
}
}
])
Explained:
In the addFileds stage convert the object to array to get the key /values in the form k,v so you can filter only the true values.
In the project stage project the _id and itemIds.k values inside the itemIds array
Playground1
Option 2. Faster option via single stage: addFields/$map/$filter:
db.collection.aggregate([
{
"$addFields": {
"itemIds": {
"$map": {
"input": {
"$filter": {
"input": {
"$objectToArray": "$itemIds"
},
"as": "item",
"cond": {
$eq: [
"$$item.v",
true
]
}
}
},
"as": "it",
"in": "$$it.k"
}
}
}
}
])
Explained:
Map the filtered values from ObjectToArray to project in addFields only the necessary array values.
Playground2

Related

Mongodb aggregation convert array of pairs to key and list of values

Trying to condense an array with key value pairs into an array of objects with the key and all the unique values for that key.
I have a structure like:
{
fruits: [
{fruit: apple, type: gaja},
{fruit: apple, type: honey-crisp},
{fruit: apple, type: fuji},
{fruit: cherry, type: black},
{fruit: cherry, type: red},
{fruit: cherry, type: red},
]
}
How can I convert it to:
{
fruits: [
{fruit: apple, types: [gaja, honey-crisp, fuji]},
{fruit: cherry, types: [black, red]}
]
}
Using mongo aggregations I managed to get the first structure from my data using $group and $addToSet. Not sure how to map the array to new object with a key and list of values
Here's another way to do it by using "$reduce". Comments are in the aggregation pipeline.
db.collection.aggregate([
{
"$set": {
// rewrite fruits
"fruits": {
"$reduce": {
"input": "$fruits",
"initialValue": [],
"in": {
"$let": {
"vars": {
// get fruit index in $$value : will be -1 if not there
"idx": {"$indexOfArray": ["$$value.fruit", "$$this.fruit"]}
},
"in": {
"$cond": [
// is fruit not in $$value yet
{"$eq": ["$$idx", -1]},
// new fruit so put in $$value and make "type" an array
{
"$concatArrays": [
"$$value",
[{"$mergeObjects": ["$$this", {"type": ["$$this.type"]}]}]
]
},
// fruit already in $$value, so map $$value with "type" update
{
"$map": {
"input": "$$value",
"as": "val",
"in": {
"$cond": [
// is this array element not the right fruit?
{"$ne": ["$$val.fruit", "$$this.fruit"]},
// nope, leave the element as-is
"$$val",
// this element needs to be updated
{
"$mergeObjects": [
"$$val",
{
"type": {
"$cond": [
// is this "type" already in array?
{"$in": ["$$this.type", "$$val.type"]},
// yes, so leave it as-is
"$$val.type",
// this is a new "type", so add it to array
{"$concatArrays": ["$$val.type", ["$$this.type"]]}
]
}
}
]
}
]
}
}
}
]
}
}
}
}
}
}
}
])
Try it on mongoplayground.net.
Maybe something like this:
db.collection.aggregate([
{
$unwind: "$fruits"
},
{
$group: {
_id: "$fruits.fruit",
type: {
$push: "$fruits.type"
}
}
},
{
$project: {
fruit: "$_id",
type: 1,
_id: 0
}
},
{
$group: {
_id: "",
fruits: {
$push: "$$ROOT"
}
}
}
])
Explained:
Unwind the array
Group to form the type array ( you can use $push or $addToSet in case you need only unique )
Project the necessary fields
Group all documents inside single final one
Playground
Here's another, another way using a multiple "$map" and "$setUnion" to get unique array members.
db.collection.aggregate([
{
"$set": {
// rewrite fruits
"fruits": {
"$map": {
// map over unique fruits
"input": {"$setUnion": "$fruits.fruit"},
"as": "theFruit",
"in": {
// set fruit
"fruit": "$$theFruit",
// "type" are unique elements of fruits.type
// where fruits.fruit == theFruit
"type": {
"$setUnion": {
"$map": {
"input": {
"$filter": {
"input": "$fruits",
"as": "obj",
"cond": {"$eq": ["$$obj.fruit", "$$theFruit"]}
}
},
"in": "$$this.type"
}
}
}
}
}
}
}
}
])
Try it on mongoplayground.net.

Filter query for fetching product based on categories in MERN application?

This is my Product Schema
const product = mongoose.Schema({
name: {
type: String,
},
category:{
type: ObjectId,
ref: Category
}
}
I want to return the product based on filters coming from the front end.
For example: Lets consider there are 2 categories Summer and Winter. When the user wants to filter product by Summer Category an api call would me made to http://localhost:8000/api/products?category=summer
Now my question is, how do I filter because from the frontend I am getting category name and there is ObjectId in Product Schema.
My attempt:
Project.find({category:categoryName})
If I've understood correctly you can try one of these queries:
First one is using pipeline into $lookup to match by category and name in one stage like this:
yourModel.aggregate([
{
"$lookup": {
"from": "category",
"let": {
"category": "$category",
},
"pipeline": [
{
"$match": {
"$expr": {
"$and": [
{
"$eq": [
"$id",
"$$category"
]
},
{
"$eq": [
"$name",
"Summer"
]
}
]
}
}
}
],
"as": "categories"
}
},
{
"$match": {
"categories": {
"$ne": []
}
}
}
])
Example here
Other way is using normal $lookup and then $filter the array returned by join.
yourModel.aggregate([
{
"$lookup": {
"from": "category",
"localField": "category",
"foreignField": "id",
"as": "categories"
}
},
{
"$match": {
"categories": {
"$ne": []
}
}
},
{
"$set": {
"categories": {
"$filter": {
"input": "$categories",
"as": "c",
"cond": {
"$eq": [
"$$c.name",
"Summer"
]
}
}
}
}
}
])
Example here
Note how both queries uses $match: { categories: { $ne : []}}, this is because if the lookup doesn't find any result it returns an empty array. By the way, this stage is optional.
Also you can add $unwind or $arrayElemAt index 0 or whatever to get only one value from the array.
Also, to do it in mongoose you only need to replace "Summer" with your variable.

How to check if a key exists in a mongodb object where the key is a value of some another field in the document while doing aggregation?

First of all I know we can check if a key exists using the dot operator but in my case it is not working and I dont know why.
So far in the aggregation pipeline I have the following records.
{
"my_key":"1234"
"data":{
1234:"abc"
4567:"xyz"
}
}
{
"my_key":"6666"
"data":{
1234:"abc"
4567:"xyz"
}
}
I want to return the document where the my_key value does not exists in the data object. So according to the above example it should return the 2nd document.
I was trying using the $match operator as following but it does not seem to work.
$match :
{
"data.$my_key":{$exists:false}
}
This does not work and I dont get why :(
Is it because the my_key value is a string and the keys in the data object are not strings?
playground
db.collection.aggregate([
{
"$project": {//Reshape the data
"data": {
"$objectToArray": "$data"
},
"my_key": 1
}
},
{
"$unwind": "$data"
},
{
"$match": {//matching
"$expr": {
"$eq": [
"$data.k",
"$my_key"
]
}
}
}
])
Another way
Wihtout unwind
db.collection.aggregate([
{
"$project": {
"data": {
"$objectToArray": "$data"
},
"my_key": 1
}
},
{
$project: {
"output": {
"$map": {
"input": "$data",
"as": "data",
"in": {
"$eq": [
"$$data.k",
"$my_key"
]
}
}
},
"data": 1,
"my_key": 1
}
},
{
$match: {
output: true
}
}
])
If you need original format of data, you can add the below as last stage
{
$project: {
"data": {
"$arrayToObject": "$data"
},
"my_key": 1
}
}

Merge the original array of objects into the "as" field after a $lookup

I have a hero collection where each hero document looks like the following:
{
_id:'the-name-of-the-hero',
name: 'Name of Hero',
(...), //other properties to this hero
relations: [
{
hero: 'the-id-of-another-hero',
relationType: 'trust'
},
{
hero: 'yet-another-id-of-another-hero',
relationType: 'hate'
}
]
}
The relations.hero points to an _id of another hero. I needed to grab some more information of the related heroes, therefore I used aggregate $lookup to match each against the "hero" collection, to grab it's name (and other data, but project simplified for the question). Here the currently working query, docummented:
let aggregate = db.collection('hero').aggregate([
// grabbing an specific hero
{ $match: { _id } },
//populate relations
{
$lookup: {
from: 'hero',
let: { letId: '$relations.hero' }, //create a local variable for the pipeline to use
// localField: "relations.hero", //this would bring entire hero data, which is unnecessary
// foreignField: "_id", //this would bring entire hero data, which is unnecessary
pipeline: [
//match each $relations.hero (as "$$letId") in collection hero's (as "from") $_id
{ $match: { $expr: { $in: ['$_id', '$$letId'] } } },
//grab only the _id and name of the matched heroes
{ $project: { name: 1, _id: 1 } },
//sort by name
{ $sort:{ name: 1 } }
],
//replace the current relations with the new relations
as: 'relations',
},
}
]).toArray(someCallbackHere);
In short, $lookup on hero collection using a pipeline that match each of relations.hero and bring back only the _id and name (which has the real name to be printed on UI) and replace current relations with this new relations, generating the document as:
{
_id:'the-name-of-the-hero',
name: 'Name of Hero',
(...), //other properties to this hero
relations: [
{
_id: 'the-id-of-another-hero',
name: 'The Real Name of Another Hero',
},
{
_id: 'yet-another-id-of-another-hero',
name: 'Yet Another Real Name of Another Hero',
}
]
}
The question:
What can I add on the pipeline to make it merge the matched heroes with the original relations, in order to not only have the projected _id and name, but also the original relationType? That is, have the following result:
{
_id:'the-name-of-the-hero',
name: 'Name of Hero',
(...), //other properties to this hero
relations: [
{
_id: 'the-id-of-another-hero',
name: 'The Real Name of Another Hero',
relationType: 'trust' //<= kept from the original relations
},
{
_id: 'yet-another-id-of-another-hero',
name: 'Yet Another Real Name of Another Hero',
relationType: 'hate' //<= kept from the original relations
}
]
}
I tried exporting as: 'relationsFull' and then tried to $push with $mergeObjects as part of a next step into the aggregation but no luck. I tried to do the same as a pipeline step (instead of a new aggregate step) but always end up relations as empty array..
How would I write a new aggregation step to merge old relations objects with the new looked-up relations?
Note: Consider MongoDB 3.6 or later (that is, $unwind array is not needed, at least for the $lookup). I'm querying using Node.js driver, if that info matters.
You can use below aggregation
db.collection("hero").aggregate([
{ "$match": { _id } },
{ "$unwind": "$relations" },
{ "$lookup": {
"from": "hero",
"let": { "letId": "$relations.hero" },
"pipeline": [
{ "$match": { "$expr": { "$eq": ["$_id", "$$letId"] } } },
{ "$project": { "name": 1 } }
],
"as": "relation"
}},
{ "$unwind": "$relation" },
{ "$addFields": { "relations.name": "$relation.name" }},
{ "$group": {
"_id": "$_id",
"relations": { "$push": "$relations" },
"name": { "$first": "$name" },
"rarity": { "$first": "$rarity" },
"classType": { "$first": "$classType" }
}}
])
Or alternate you can use this as well
db.collection("hero").aggregate([
{ "$match": { _id } },
{ "$lookup": {
"from": "hero",
"let": { "letId": "$relations.hero" },
"pipeline": [
{ "$match": { "$expr": { "$in": ["$_id", "$$letId"] } } },
{ "$project": { "name": 1 } }
],
"as": "lookupRelations"
}},
{ "$addFields": {
"relations": {
"$map": {
"input": "$relations",
"as": "rel",
"in": {
"$mergeObjects": [
"$$rel",
{ "name": { "$arrayElemAt": ["$lookupRelations.name", { "$indexOfArray": ["$lookupRelations._id", "$$rel._id"] }] }}
]
}
}
}
}}
])
Well, I think we should use different name for the as field.From there, we can use the following expression the the $addFields stage.
{
"$addFields": {
"relations": {
"$reduce": {
"input": {
"$reduce": {
"input": {
"$zip": {
"inputs": [
"$relations",
"$relheros"
]
}
},
"initialValue": [
],
"in": {
"$concatArrays": [
"$$value",
"$$this"
]
}
}
},
"initialValue": {
},
"in": {
"$mergeObjects": [
"$$value",
"$$this"
]
}
}
}
}
}
Note that the relheros here is the as field.
We really should not $unwind and $group here, before $unwind is cheap but $group is expensive.

How to create objects inside Mongodb map

I have a document that looks like this, I am using mongo $map to project fields inside the table and rename the key. I can't use $unwind due to some internal complexity.
{
"Table":[
{"Lookup":{
"CreatedBy":{
"id": "User001",
"Name":"UserName"
}
}
}]
}
The output I am expecting looks something like this
{
"Table":[
{"Lookup":{
"CreatedBy":"UserName"
}
}]
}
I am trying to achieve it with mongo $map but it is not supported
db.getCollection('TableDoc').aggregate([
{
"$project": {
"Table": {
"$map": {
"input": "$Table",
"in": {
"Lookup.CreatedAt": "$$this.Lookup.CreatedAt.Name",
}
}
}
}
}
])
Is there any other way to achieve this without using $unwind
This is supported with $map, but just not using "dotted field paths". Instead you use "absolute" object structures:
collection.aggregate([
{ "$addFields": {
"Table": {
"$map": {
"input": "$Table",
"in": {
"Lookup": {
"CreatedBy": "$$this.Lookup.CreatedBy.Name"
}
}
}
}
}}
])
Alternately if you have lots of fields in the objects you can use $mergeObjects where supported:
collection.aggregate([
{ "$addFields": {
"Table": {
"$map": {
"input": "$Table",
"in": {
"$mergeObjects": [
"$$this",
{
"Lookup": {
"CreatedBy": "$$this.Lookup.CreatedBy.Name"
}
}
]
}
}
}
}}
])
That makes more sense when an example shows more fields than your sample in the question does.