How do I recombine unwinded documents? - mongodb

I have the following document:
{
"_id" : ObjectId("5881cfa62189aa40268b458a"),
"description" : "Document A",
"companies" : [
{"code" : "0001"},
{"code" : "0002"},
{"code" : "0003"}
]
}
I want to filter the companies array to remove some objects based on the code field.
I've tried to use unwind and then match to filter out the companies, but I don't know how to recombine the objects. Is there another way of doing this?
Here's what I've tried so far:
db.getCollection('test').aggregate([
{
$unwind: {
'path': '$companies'
}
},
{
$match: {
'companies.code': {$in: ['0001', '0003']}
}
}
// How do I merge them back into a single document?
]);

A better way would be to just use the $filter operator on the array.
db.getCollection('test').aggregate([
{
$project:
{
companies: {
$filter: {
input: '$companies',
as: 'company',
cond: {$in: ['$$company.code', ['0001', '0003']]}
}
}
}
}
])

You can $group and control the document structure like that but its tedious work as you have to specify each and every field you want to preserve.
I recommend instead of unwinding to use $filter to match the companies like so:
db.getCollection('test').aggregate([
{
$addFields: {
companies: {
$filter: {
input: "$companies",
as: "company",
cond: {$in: ["$$company.code", ['0001', '0003']]}
}
}
}
},
{ // we now need this match as documents with no matched companies might exist
$match: {
"companies.0": {$exists: true}
}
}
])

If you want to keep the way you are doing using Aggregation pipeline:
db.getCollection('testcol').aggregate([
{$unwind: {'path': '$companies'}},
{$match: {'companies.code': {$in: ['0001', '0003']}}},
{$group: {_id: "$_id", description: { "$first": "$description" } , "companies": { $push: "$companies" }}} ,
])

Related

MongoDB unwind + filter on two fields of unwinded documents

I'm running a Mongo aggregation which "merges" together 2 different documents.
Everything works fine, matching etc.
Right now I'm trying to match on two fields of a sub-document, but it looks like it results in an OR instead of and AND.
Let me give some contest:
Main Document
{
_id: 1234567890a,
title: "mainTitle",
country: "ITA"
}
First sub-Document
{
_id:1234567890b,
mainDocumentId: 1234567890a,
someField: "123",
property: "car"
}
What I mean to do is to aggregate these 2 documents and filter on someField $eq: 123 AND property $eq: car . If there is a sub-document like:
{
_id:1234567890b,
mainDocumentId: 1234567890a,
someField: "123",
property: "bus"
}
The pipeline shouldn't consider it
My aggregation looks like this:
db.mycollection.aggregate([{$lookup: {
from: 'sub_collection',
localField: '_id',
foreignField: 'mainDocumentId',
as: 'SubCollection'
}},
{ $match : { "SubCollection.someField" : '123' , 'SubCollection.property':'car'} },
{ $unwind : "$_id" },
{ $sort : { "createdAt" : -1}},
{ $skip : 0},
{ $limit : 40}
])
The result I get is every document with a subdocument which matches OR the first OR the second condition.
I tried with $elemMatch but this isn't its purpose and in fact it doesn't work.
I also tried to specify the $and in the $match stage
db.mycollection.aggregate([{$lookup: {
from: 'sub_collection',
localField: '_id',
foreignField: 'mainDocumentId',
as: 'SubCollection'
}},
{ $match : {
$and: [
{
"SubCollection": {
$elemMatch: {
"someField": '123'
}
}
},
{
"SubCollection": {
$elemMatch: {
"property": { $eq : 'car' }
}
}
}
]}
},
{ $unwind : "$_id" },
{ $sort : { "createdAt" : -1}},
{ $skip : 0},
{ $limit : 40}
])
I feel like it's a pretty simple task, but clearly I'm missing something: any tip will be appreciated!
Thank you!
- EDIT -
This is my output based on the $lookup I posted above:
{
_id: 1234567890a,
title: "mainTitle",
country: "ITA",
subDocument: [
{
_id:1234567890b,
mainDocumentId: 1234567890a,
someField: "123",
property: "car"
},
{
_id:1234567890b,
mainDocumentId: 1234567890a,
someField: "123",
property: "bus"
}
]
}
$match is not the correct stage for filtering array elements. You need to use $project or $addFields, along $filter operator, to filter out array elements. Like this:
db.collection.aggregate([
{
"$project": {
subDocument: {
"$filter": {
"input": "$subDocument",
"as": "doc",
"cond": {
"$and": [
{
"$eq": [
"$$doc.someField",
"123"
]
},
{
"$eq": [
"$$doc.property",
"car"
]
}
]
}
}
}
}
}
]);
See it in action here. You can add the above $project stage in your pipeline.

return match item only from array of object mongoose

[
{
item: "journal",
instock: [
{
warehouse: "A",
qty: 5,
items: null
},
{
warehouse: "C",
qty: 15,
items: [
{
name: "alexa",
age: 26
},
{
name: "Shawn",
age: 26
}
]
}
]
}
]
db.collection.find({
"instock.items": {
$elemMatch: {
name: "alexa"
}
}
})
This returns whole items array where as i just want items array with one item {name: 'alexa', age: 26}
Playground Link : https://mongoplayground.net/p/0gB4hNswA6U
You can use the .aggregate(pipeline) function.
Your code will look like:
db.collection.aggregate([{
$unwind: {
path: "$instock"
}
}, {
$unwind: {
path: "$instock.items"
}
}, {
$replaceRoot: {
newRoot: "$instock.items"
}
}, {
$match: {
name: "alexa"
}
}])
Commands used in this pipeline:
$unwind - deconstructs an array of items into multiple documents which all contain the original fields of the original documents except for the unwinded field which now have a value of all the deconstructed objects in the array.
$replaceRoot - takes the inner object referenced on newRoot and puts it as the document.
$match - a way to filter the list of documents you ended up with by some condition. Basically the first argument in the .find() function.
For more information about aggregation visit MongoDB's website:
Aggregation
$unwind
$replaceRoot
$match
EDIT
The wanted result was to get single item arrays as a response, to achieve that you can simply remove the $replaceRoot stage in the pipeline.
Final pipeline:
db.collection.aggregate([{
$unwind: {
path: "$instock"
}
}, {
$unwind: {
path: "$instock.items"
}
}, {
$match: {
"instock.items.name": "alexa"
}
}])
You have to use $elemMatch in projection section:
db.collection.find({
// whatever your query is
}, {
"instock.items": {
$elemMatch: {
name: "alexa"
}
}
});
UPDATE
Here is a good example of this usage. From the official mongodb documentation.
An alternative approach where $filter is the key at the project stage.
db.collection.aggregate([
{
$match: {
"instock.items.name": "alexa"
}
},
{
$unwind: "$instock"
},
{
$project: {
"item": "$item",
qty: "$instock.qty",
warehouse: "$instock.warehouse",
items: {
$filter: {
input: "$instock.items",
as: "item",
cond: {
$eq: [
"$$item.name",
"alexa"
]
}
}
}
}
},
{
$group: {
_id: "$_id",
instock: {
$push: "$$ROOT"
}
}
}
])
The idea is to:
have $match as top level filter
then $unwind instock array items to prepare for the $filter
Use $project for rest of the fields as they are, and use $filter on items array field
Finally $group them back since $unwind was used previously.
Play link

Find({ example: { $elemMatch: { $eq: userId } } }).. in Aggregate - is it possible?

database:
[{to_match: [ userID_1, userId_2 ], data: [{...}] },
{to_match: [ userID_1, userId_2, userId_3 ], data: [{...}] },
{to_match: [ ], data: [{...}] }]
Find by an element in the array 'to-match'.
Current solution:
Replacement.find(
{ applicants: { $elemMatch: { $eq: userId_1 } } },
Aggregate $lookup on the result of 1.
a. Can I Find and Aggregate?
b. Should I first Aggregate and then match ??
if yes, how to match on the element in the array?
I tried Aggregate:
$lookup // OK
{ $match: { applicants: { $in: { userId } } } } // issues
Thank you
Use $lookup and $match in aggregate
Instead of $in use $elemMatch like below:
{ $match: { applicants: { $elemMatch: { $eq: userId_1 } } } }
Doing an $elemMatch on just one field is equivalent to using find(link)
Generally, it is efficient to limit the data that the lookup stage will be working on.
So if I understand correctly, you want to filter the "to_match" array elements and then do a lookup on that result.
Here is what I would suggest:-
aggregate([
{
$project : {
to_match: {
$filter: {
input: "$to_match",
as: "item",
cond: { $eq: [ "$$item", "userId_3" ] }
}
},
data : 1
}
},
{
$match : { "to_match" : {$ne : []}}
},
//// Lookup stage here
])
Based on the field that you want to do a lookup on, you may want to unwind this result.

Mongodb: FInd one article for each author ID

I have a user with an array of authors that he follow, like this:
"authors" : [
ObjectId("5a66d368486631e55a4ed05c"),
ObjectId("5a6765f5486631e55a564ae2")
]
And I have articles with author ID, like this:
"authorId" : ObjectId("5a66d368486631e55a4ed05c"),
I want to get the last article for each author without making multiples calls to the database with a recursivity.
Some ideas?
PD: I'm using the mongodb driver, I don't want to use mongoose for this, thanks
In MongoDB v 3.6 you can use custom pipelines for $lookup operator. In your case you can use $in inside $match stage to get matching articles and then $group those articles by authorId and take last one (using $sort and $last operators). You can add $replaceRoot to get initial shape from articles collection.
db.user.aggregate([
{
$match: { userId: "some user Id" }
},
{
$lookup: {
from: "articles",
let: { authors: "$authors" },
pipeline: [
{
$match: {
$expr: {
$in: [ "$authorId", "$$authors" ]
}
}
},
{
$sort: { createdAt: -1 }
},
{
$group: {
_id: "$authorId",
article: { $first: "$$ROOT" }
}
},
{
$replaceRoot: { newRoot: "$article" }
}
],
as: "articles"
}
}
])

Complex aggregation query with in clause from document array

Below is the sample MongoDB Data Model for a user collection:
{
"_id": ObjectId('58842568c706f50f5c1de662'),
"userId": "123455",
"user_name":"Bob"
"interestedTags": [
"music",
"cricket",
"hiking",
"F1",
"Mobile",
"racing"
],
"listFriends": [
"123456",
"123457",
"123458"
]
}
listFriends is an array of userId for other users
For a particular userId I need to extract the listFriends (userId's) and for those userId's I need to aggregate the interestedTags and their count.
I would be able to achieve this by splitting the query into two parts:
1.) Extract the listFriends for a particular userId,
2.) Use this list in an aggregate() function, something like this
db.user.aggregate([
{ $match: { userId: { $in: [ "123456","123457","123458" ] } } },
{ $unwind: '$interestedTags' },
{ $group: { _id: '$interestedTags', countTags: { $sum : 1 } } }
])
I am trying to solve the question: Is there a way to achieve the above functionality (both steps 1 and 2) in a single aggregate function?
You could use $lookup to look for friend documents. This stage is usually used to join two different collection, but it can also do join upon one single collection, in your case I think it should be fine:
db.user.aggregate([{
$match: {
_id: 'user1',
}
}, {
$unwind: '$listFriends',
}, {
$lookup: {
from: 'user',
localField: 'listFriends',
foreignField: '_id',
as: 'friend',
}
}, {
$project: {
friend: {
$arrayElemAt: ['$friend', 0]
}
}
}, {
$unwind: '$friend.interestedTags'
}, {
$group: {
_id: '$friend.interestedTags',
count: {
$sum: 1
}
}
}]);
Note: I use $lookup and $arrayElemAt which are only available in Mongo 3.2 or newer version, so check your Mongo version before using this pipeline.