MongoDB - group documents by field value - mongodb

I need to group following documents by featured and trending.
[
{
"_id" : ObjectId("5f22546cd49ffe0d6f087a2e"),
"title" : "....",
"image" : "...",
"featured" : true,
"trending" : false,
"creationDate" : ISODate("2020-07-30T05:02:36.592Z")
},
{
"_id" : ObjectId("5f22546cd49ffe0d6f087a2f"),
"title" : "Cras non dolor",
"image" : "...",
"featured" : false,
"trending" : true,
"creationDate" : ISODate("2020-07-30T05:02:36.592Z")
}
]
So, after grouping they should be in following format -
[
{
_id: null,
featured: [
{
"_id" : ObjectId("5f22546cd49ffe0d6f087a2e"),
"title" : "....",
"image" : "...",
"featured" : true,
"trending" : false,
"creationDate" : ISODate("2020-07-30T05:02:36.592Z")
}
],
trending: [
{
"_id" : ObjectId("5f22546cd49ffe0d6f087a2f"),
"title" : "Cras non dolor",
"image" : "...",
"featured" : false,
"trending" : true,
"creationDate" : ISODate("2020-07-30T05:02:36.592Z")
}
]
}
]
How can I get this result through aggregation or any other way?
I have been trying with aggregate $group. But I can't figure out how can I $group via featured/trending value with equal true.
So, I don't need corresponding false value when grouping them. Also, there might have other fields like highlighted etc along with featured trending.

This is quite easy to achieve, the easiest way to do it is using $facet
db.collection.aggregate([
{
$facet: {
featured: [
{
$match: {
"featured": true
}
}
],
trending: [
{
$match: {
"trending": true
}
}
]
}
}
])
MongoPlayground

Related

MongoDB get subdocuments array from multiple documents into own array

I have the following document structure.
{
"_id" : "0026",
"description" : "test",
"name" : "test",
"options" : [
{
"_id" : "002",
"color" : true,
"visible" : true,
"label" : "sample",
},
{
"_id" : "003",
"color" : true,
"visible" : true,
"label" : "sample",
}
],
},
{
"_id" : "0027",
"description" : "test",
"name" : "test",
"options" : [
{
"_id" : "001",
"color" : true,
"visible" : true,
"label" : "sample",
},
{
"_id" : "002",
"color" : true,
"visible" : true,
"label" : "sample",
}
],
},
I am trying to return one array of only the 'options' subdocuments that match an options label regex search.
I think I'm pretty much there but I seem to getting back a separate array of options for each document, how do I return just one array of 'options'?
My current code is:
{
$match: {
options: {
$elemMatch: {
label: { $regex: '^sample', $options: 'i' },
},
},
},
},
{ "$unwind": '$options' },
{
$project: {
_id: 0,
options: 1,
},
},
The structure I would like back is just an array of options:
[
{
"_id" : "001",
"color" : true,
"visible" : true,
"label" : "sample",
},
{
"_id" : "002",
"color" : true,
"visible" : true,
"label" : "sample",
},
{
"_id" : "002",
"color" : true,
"visible" : true,
"label" : "sample",
},
{
"_id" : "003",
"color" : true,
"visible" : true,
"label" : "sample",
}
]
Ta.
Always the way, you post a question then finally get the answer yourself!
Just needed to add:
{ $replaceRoot: { newRoot: '$options' } }
as the last step.
Full answer:
{
$match: {
options: {
$elemMatch: {
label: { $regex: '^sample', $options: 'i' },
},
},
},
},
{ "$unwind": '$options' },
{
$project: {
_id: 0,
options: 1,
},
},
{ $replaceRoot: { newRoot: '$options' } },

Whats the alternative to $replaceRoot on mongoDB? $replaceRoot is incompatible with documentDB

The problem: I'm trying to make a query on MongoDB, but I'm using the DocumentDb from amazon, where some operations are no supported. I wanted to find an alternative to get the same result, if possible. Basically I want to change the root of the result, instead of being the first entity, I need it to be some merging of some values in different levels of the document.
So, I have the following structure in my collection:
{
"_id" : ObjectId("5e598bf4d98f7c70f9aa3b58"),
"status" : "active",
"invoices" : [
{
"_id" : ObjectId("5e598bf13b24713f50600375"),
"value" : 1157.52,
"receivables" : [
{
"situation" : {
"status" : "active",
"reason" : []
},
"rec_code" : "001",
"_id" : ObjectId("5e598bf13b24713f50600374"),
"expiration_date" : ISODate("2020-03-25T00:00:00.000Z"),
"value" : 1157.52
}
],
"invoice_code" : 9773,
"buyer" : {
"legal_name" : "test name",
"buyer_code" : "223132165498797"
}
},
],
"seller" : {
"code" : "321654897986",
"name" : "test name 2"
}
}
What I want to achieve is to list all "receivables" like this, where the _id is the _id of the receivable:
[{
"_id" : ObjectId("5e598bf13b24713f50600374"),
"situation" : {
"status" : "active",
"reason" : []
},
"rec_code" : "001",
"expiration_date" : ISODate("2020-03-25T00:00:00.000Z"),
"value" : 1157.52,
"status" : "active",
"seller" : {
"cnpj" : "321654897986",
"name" : "test name 2"
},
"invoice_code" : 9773.0,
"buyer" : {
"legal_name" : "test name",
"cnpj" : "223132165498797"
}
}]
This I can do with $replaceRoot in with the query below on MongoDB, but using documentDB I can't use $replaceRoot or $mergeObjects. Do you know how can I get the same result with other operators?:
db.testCollection.aggregate([
{ $unwind: "$invoices" },
{ $replaceRoot: {
newRoot: {
$mergeObjects: ["$$ROOT","$invoices"]}
}
},
{$project: {"_id": 0, "value": 0, "created_at": 0, "situation": 0}},
{ $unwind: "$receivables" },
{ $replaceRoot: {
newRoot: {
$mergeObjects: ["$receivables", "$$ROOT"]
}
}
},
{$project:{"created_at": 0, "receivables": 0, "invoices": 0}}
])
After going through mongodb operations, I could get a similar result fro what I wanted with the following query without $replaceRoot. It turns out it was a better query, I think:
db.testCollection.aggregate([
{$unwind: "$invoices"},
{$project : {
created_at: 1,
seller: "$seller",
buyer: "$invoices.buyer",
nnf: "$invoices.nnf",
receivable: '$invoices.receivables'
}
},
{$unwind: "$receivable"},
{$project : {
_id: '$receivable._id',
seller: 1,
buyer: 1,
invoice_code: 1,
receivable: 1,
created_at: 1,
}
},
{$sort: {"created_at": -1}},
])
This query resulted in the following structure list:
[{
"created_at" : ISODate("2020-03-06T09:47:26.161Z"),
"seller" : {
"name" : "Test name",
"cnpj" : "21231232131232"
},
"buyer" : {
"cnpj" : "21322132164654",
"legal_name" : "Test name 2"
},
"invoice_code" : 66119,
"receivable" : {
"rec_code" : "001",
"_id" : ObjectId("5e601bb5efff82b92935bad4"),
"expiration_date" : ISODate("2020-03-17T00:00:00.000Z"),
"value" : 6540.7,
"situation" : {
"status" : "active",
"reason" : []
}
},
"_id" : ObjectId("5e601bb5efff82b92935bad4")
}]
Support for $replaceRoot was added to Amazon DocumentDB in January 2021.

Embed root field in a subdocument within an aggregation pipeline

Maybe someone can help me with Mongo's Aggregation Pipeline. I am trying to put an object in another object but I'm new to Mongo and ist very difficult:
{
"_id" : ObjectId("5888a74f137ed66828367585"),
"name" : "Unis",
"tags" : [...],
"editable" : true,
"token" : "YfFzaoNvWPbvyUmSulXfMPq4a9QgGxN1ElIzAUmSJRX4cN7zCl",
"columns" : [...],
"description" : "...",
"sites" : {
"_id" : ObjectId("5888ae2f137ed668fb95a03d"),
"url" : "www.....de",
"column_values" : [
"University XXX",
"XXX",
"false"
],
"list_id" : ObjectId("5888a74f137ed66828367585")
},
"scan" : [
{
"_id" : ObjectId("5888b1074e2123c22ae7f4d3"),
"site_id" : ObjectId("5888ae2f137ed668fb95a03d"),
"scan_group_id" : ObjectId("5888a970a7f75fbd49052ed6"),
"date" : ISODate("2017-01-18T16:00:00Z"),
"score" : "B",
"https" : false,
"cookies" : 12
}
]
}
I want to put every object in the "scan"-array into "sites". So that it looks like this:
{
"_id" : ObjectId("5888a74f137ed66828367585"),
"name" : "Unis",
"tags" : [...],
"editable" : true,
"token" : "YfFzaoNvWPbvyUmSulXfMPq4a9QgGxN1ElIzAUmSJRX4cN7zCl",
"columns" : [...],
"description" : "...",
"sites" : {
"_id" : ObjectId("5888ae2f137ed668fb95a03d"),
"url" : "www.....de",
"column_values" : [
"University XXX",
"XXX",
"false"
],
"list_id" : ObjectId("5888a74f137ed66828367585"),
"scan" : [
{
"_id" : ObjectId("5888b1074e2123c22ae7f4d3"),
"site_id" : ObjectId("5888ae2f137ed668fb95a03d"),
"scan_group_id" : ObjectId("5888a970a7f75fbd49052ed6"),
"date" : ISODate("2017-01-18T16:00:00Z"),
"score" : "B",
"https" : false,
"cookies" : 12
}
]
}
}
Is there a step in the aggregation pipeline to perform this task?
With a single pipeline I don't see any other way but specifying each field individually as:
db.collection.aggregate([
{
"$project": {
"name": 1, "tags": 1,
"editable": 1,
"token": 1, "columns": 1,
"description": 1,
"sites._id": "$sites._id",
"sites.url": "$sites.url" ,
"sites.column_values": "$sites.column_values" ,
"sites.list_id": "$sites.list_id",
"sites.scan": "$scan"
}
}
])
With MongoDB 3.4 and newer, you can use the $addFields pipeline step instead of specifying all fields using $project. The advantage is that it adds new fields to documents and outputs documents that contain all existing fields from the input documents and the newly added fields:
db.collection.aggregate([
{
"$addFields": {
"sites._id": "$sites._id",
"sites.url": "$sites.url" ,
"sites.column_values": "$sites.column_values" ,
"sites.list_id": "$sites.list_id",
"sites.scan": "$scan"
}
}, { "$project": { "scan": 0 } }
])

Mongodb : get whether a document is the latest with a field value and filter on the result

I am trying to port an existing SQL schema into Mongo.
We have document tables, with sometimes several times the same document, with a different revision but the same reference. I want to get only the latest revisions of the documents.
A sample input data:
{
"Uid" : "xxx",
"status" : "ACCEPTED",
"reference" : "DOC305",
"code" : "305-D",
"title" : "Document 305",
"creationdate" : ISODate("2011-11-24T15:13:28.887Z"),
"creator" : "X"
},
{
"Uid" : "xxx",
"status" : "COMMENTED",
"reference" : "DOC306",
"code" : "306-A",
"title" : "Document 306",
"creationdate" : ISODate("2011-11-28T07:23:18.807Z"),
"creator" : "X"
},
{
"Uid" : "xxx",
"status" : "COMMENTED",
"reference" : "DOC306",
"code" : "306-B",
"title" : "Document 306",
"creationdate" : ISODate("2011-11-28T07:26:49.447Z"),
"creator" : "X"
},
{
"Uid" : "xxx",
"status" : "ACCEPTED",
"reference" : "DOC501",
"code" : "501-A",
"title" : "Document 501",
"creationdate" : ISODate("2011-11-19T06:30:35.757Z"),
"creator" : "X"
},
{
"Uid" : "xxx",
"status" : "ACCEPTED",
"reference" : "DOC501",
"code" : "501-B",
"title" : "Document 501",
"creationdate" : ISODate("2011-11-19T06:40:32.957Z"),
"creator" : "X"
}
Given this data, I want this result set (sometimes I want only the last revision, sometimes I want all revisions with an attribute telling me whether it's the latest):
{
"Uid" : "xxx",
"status" : "ACCEPTED",
"reference" : "DOC305",
"code" : "305-D",
"title" : "Document 305",
"creationdate" : ISODate("2011-11-24T15:13:28.887Z"),
"creator" : "X",
"lastrev" : true
},
{
"Uid" : "xxx",
"status" : "COMMENTED",
"reference" : "DOC306",
"code" : "306-B",
"title" : "Document 306",
"creationdate" : ISODate("2011-11-28T07:26:49.447Z"),
"creator" : "X",
"lastrev" : true
},
{
"Uid" : "xxx",
"status" : "ACCEPTED",
"reference" : "DOC501",
"code" : "501-B",
"title" : "Document 501",
"creationdate" : ISODate("2011-11-19T06:40:32.957Z"),
"creator" : "X",
"lastrev" : true
}
I already have a bunch of filters, sorting, and skip/limit (for pagination of data), so the final result set should be mindful of these constraints.
The current "find" query (built with the .Net driver), which filters fine but gives me all revisions of each document:
coll.find(
{ "$and" : [
{ "$or" : [
{ "deletedid" : { "$exists" : false } },
{ "deletedid" : null }
] },
{ "$or" : [
{ "taskid" : { "$exists" : false } },
{ "taskid" : null }
] },
{ "objecttypeuid" : { "$in" : ["xxxxx"] } }
] },
{ "_id" : 0, "Uid" : 1, "lastrev" : 1, "title" : 1, "code" : 1, "creator" : 1, "owner" : 1, "modificator" : 1, "status" : 1, "reference": 1, "creationdate": 1 }
).sort({ "creationdate" : 1 }).skip(0).limit(10);
Using another question, I have been able to build this aggregation, which gives me the latest revision of each document, but with not enough attributes in the result:
coll.aggregate([
{ $sort: { "creationdate": 1 } },
{
$group: {
"_id": "$reference",
result: { $last: "$creationdate" },
creationdate: { $last: "$creationdate" }
}
}
]);
I would like to integrating the aggregate with the find query.
I have found the way to mix aggregation and filtering:
coll.aggregate(
[
{ $match: {
"$and" : [
{ "$or" : [
{ "deletedid" : { "$exists" : false } },
{ "deletedid" : null }
] },
{ "$or" : [
{ "taskid" : { "$exists" : false } },
{ "taskid" : null }
] },
{ "objecttypeuid" : { "$in" : ["xxx"] } }
]
}
},
{ $sort: { "creationdate": 1 } },
{ $group: {
"_id": "$reference",
"doc": { "$last": "$$ROOT" }
}
},
{ $sort: { "doc.creationdate": 1 } },
{ $skip: skip },
{ $limit: limit }
],
{ allowDiskUse: true }
);
For each result node, this gives me a "doc" node with the document data. It has too much data still (it's missing projections), but it's a start.
Translated in .Net:
FilterDefinitionBuilder<BsonDocument> filterBuilder = Builders<BsonDocument>.Filter;
FilterDefinition<BsonDocument> filters = filterBuilder.Empty;
filters = filters & (filterBuilder.Not(filterBuilder.Exists("deletedid")) | filterBuilder.Eq("deletedid", BsonNull.Value));
filters = filters & (filterBuilder.Not(filterBuilder.Exists("taskid")) | filterBuilder.Eq("taskid", BsonNull.Value));
foreach (var f in fieldFilters) {
filters = filters & filterBuilder.In(f.Key, f.Value);
}
var sort = Builders<BsonDocument>.Sort.Ascending(orderby);
var group = new BsonDocument {
{ "_id", "$reference" },
{ "doc", new BsonDocument("$last", "$$ROOT") }
};
var aggregate = coll.Aggregate(new AggregateOptions { AllowDiskUse = true })
.Match(filters)
.Sort(sort)
.Group(group)
.Sort(sort)
.Skip(skip)
.Limit(rows);
return aggregate.ToList();
I'm pretty sure there are better ways to do this, though.
You answer is pretty close. Instead of $last, $max is better.
About $last operator:
Returns the value that results from applying an expression to the last document in a group of documents that share the same group by a field. Only meaningful when documents are in a defined order.
Get the last revision in each group, see code below in mongo shell:
db.collection.aggregate([
{
$group: {
_id: '$reference',
doc: {
$max: {
"creationdate" : "$creationdate",
"code" : "$code",
"Uid" : "$Uid",
"status" : "$status",
"title" : "$title",
"creator" : "$creator"
}
}
}
},
{
$project: {
_id: 0,
Uid: "$doc.Uid",
status: "$doc.status",
reference: "$_id",
code: "$doc.code",
title: "$doc.title",
creationdate: "$doc.creationdate",
creator: "$doc.creator"
}
}
]).pretty()
The output as your expect:
{
"Uid" : "xxx",
"status" : "ACCEPTED",
"reference" : "DOC501",
"code" : "501-B",
"title" : "Document 501",
"creationdate" : ISODate("2011-11-19T06:40:32.957Z"),
"creator" : "X"
}
{
"Uid" : "xxx",
"status" : "COMMENTED",
"reference" : "DOC306",
"code" : "306-B",
"title" : "Document 306",
"creationdate" : ISODate("2011-11-28T07:26:49.447Z"),
"creator" : "X"
}
{
"Uid" : "xxx",
"status" : "ACCEPTED",
"reference" : "DOC305",
"code" : "305-D",
"title" : "Document 305",
"creationdate" : ISODate("2011-11-24T15:13:28.887Z"),
"creator" : "X"
}

How to publish posts depend on privacy?

I need to publish a queue from collection Posts depending of privacy. But I don't have any idea how to get this. I've draw mindmap of main concept what I try to realize:
Input:
var currentUser = "Andrey";
Events.find();
Output:
[
{
//it's going to output, becouse I've created this post
"_id" : "1",
"createdBy" : "Andrey",
"private" : true,
"title" : "",
"text": "",
"members" : [
"Sheldon", "Mike"
]
},
{
//it's going to output, becouse this post not private
"_id" : "2",
"createdBy" : "Sheldon",
"private" : false,
"title" : "",
"members" : []
},
{
//it's going to output, becouse I'm one of members'
"_id" : "3",
"createdBy" : "Mike",
"private" : true,
"title" : "",
"text": "",
"members" : [
"Andrey"
]
},
{
//it's NOT going to output, becouse it's private post, I'm not the member or author
"_id" : "4",
"createdBy" : "Ana",
"private" : true,
"title" : "",
"text": "",
"members" : [
"Sheldon"
]
},
]
Expected result:
[
{
"_id" : "1",
"createdBy" : "Andrey",
"private" : true,
"title" : "",
"text": "",
"members" : [
"Sheldon", "Mike"
]
},
{
"_id" : "2",
"createdBy" : "Sheldon",
"private" : false,
"title" : "",
"text": "",
"members" : []
},
{
"_id" : "3",
"createdBy" : "Mike",
"private" : true,
"title" : "",
"text": "",
"members" : [
"Andrey"
]
}
]
But this idea can be wrong at all, maybe you have another way?
I am not sure if this is the right approach but probably I'd try something like that if I face this issue. (By the way I'd use userId instead of names. If you used names in order to explain your aim, please ignore this comment)
As long as I remember, you can return multiple query results like that
return [
Result1,
Result2,
]
For the public posts, no need to worry I guess. Just returning the ones with private: false is sufficient.
For the private ones, I'd use userId as parameter and try this composite publication:
Meteor.publish('posts', function postPublication(userId) {
return [
Posts.find({
$and: [{
$or: [
{_id: userId},
{memberId: {$in: userId}}
]},
{private: {$eq: true}}
]}), // => should return private posts created or commented by member
Posts.find({private: {$eq: false}}) // => should return public ones
];
}
Then do the subscription with userId
Meteor.subscribe('posts', userId);
This code how I solved this task, with $or $and. But not sure too, if it's a best solution. But I get expected result.
Meteor.publish('posts', function(currentUser) {
selector = {
$or : [
{ $and : [ {'private': true, 'members': currentUser} ] },
{ $and : [ {'private': true, 'author': currentUser} ] },
{ $and : [ {'private': false, 'author': currentUser} ] },
{ $and : [ {'private': undefined} ] },
{ $and : [ {'private': false} ] },
]
}
return Posts.find(selector);
});