How I can replace the object key with root, in MongoDB - arrays

"data": {
"abc": {
"Id": "100",
"print": "number",
"uploadAt" : "2021-22-01",
"servicesAt" : "2020-01-12"
},
"xyz": {
"Id": "123",
"print": "number",
"uploadAt" : "2021-22-01",
"servicesAt" : "2020-01-12"
}
}
Explanation
I want to remove the root data, rest of the in side the data objects abc, xyz will remain as it is, in the 2nd step I want to replace the object value number with id key , I want to to do this dynamically,
Expected Output
{
"abc": {
"number": "100",
"print": "number",
"uploadAt" : "2021-22-01",
"servicesAt" : "2020-01-12"
},
"xyz": {
"number": "123",
"print": "number",
"uploadAt" : "2021-22-01",
"servicesAt" : "2020-01-12"
}
}

UPDATE based on comments
Solution #3:
db.testCollection.aggregate([
{
$addFields: {
"array": { $objectToArray: "$data" }
}
},
{ $unwind: "$array" },
{
$addFields: { "array.v.number": "$array.v.Id" }
},
{
$project: { "array.v.Id": 0 } // Optional
},
{
$group: {
_id: "$_id",
array: { $push: "$array" }
}
},
{
$replaceRoot: {
newRoot: { $arrayToObject: "$array" }
}
}
]);
Solution #2: If you do not wat to rename filed Id to number then the solution is simple:
db.testCollection.aggregate([
{
$replaceRoot: {
newRoot: {
$arrayToObject: { $objectToArray: "$data" }
}
}
}
]);
Output:
{
"abc" : {
"Id" : "100",
"print" : "number",
"uploadAt" : "2021-22-01",
"servicesAt" : "2020-01-12"
},
"xyz" : {
"Id" : "123",
"print" : "number",
"uploadAt" : "2021-22-01",
"servicesAt" : "2020-01-12"
}
}
OLD SOLUTION
Try this
db.testCollection.aggregate([
{
$replaceRoot: {
newRoot: {
$arrayToObject: {
$map: {
input: { $objectToArray: "$data" },
as: "item",
in: {
k: "$$item.k",
v: {
number: "$$item.v.Id",
print: "$$item.v.print",
uploadAt: "$$item.v.uploadAt",
servicesAt: "$$item.v.servicesAt"
}
}
}
}
}
}
}
]);
Output
{
"abc" : {
"number" : "100",
"print" : "number",
"uploadAt" : "2021-22-01",
"servicesAt" : "2020-01-12"
},
"xyz" : {
"number" : "123",
"print" : "number",
"uploadAt" : "2021-22-01",
"servicesAt" : "2020-01-12"
}
}

Related

MongoDB: Create an aggregation pipeline

In the MongoDB aggregation framework, I was hoping to use the $unwind operator on a map. Looks like it's not possible.
case class MatchStatus(
totalRows: Int,
fullMatch: Int,
noMatch: Int,
partialMatch: Int
)
This is the sample JSON where I have matchStatus => Map<String,MatchStatus>
{
"_id" : ObjectId("61e8c7bbd0597207179faa89"),
"clientId" : "DEMO",
"matchStatus" : {
"summary" : {
"totalRows" : "10",
"fullMatch" : "5",
"noMatch" : "1",
"partialMatch" : "4"
},
"income" : {
"totalRows" : "10",
"fullMatch" : "1",
"noMatch" : "0",
"partialMatch" : "1"
}
},
"date" : "18-01-2022"
},
{
"_id" : ObjectId("61e8c7bbd0597207179faa89"),
"clientId" : "DEMO-1",
"sizes" : [
"1",
"2"
],
"matchStatus" : {
"summary" : {
"totalRows" : "10",
"fullMatch" : "5",
"noMatch" : "1",
"partialMatch" : "4"
},
"income" : {
"totalRows" : "10",
"fullMatch" : "1",
"noMatch" : "0",
"partialMatch" : "1"
},
"slip" : {
"totalRows" : "10",
"fullMatch" : "1",
"noMatch" : "0",
"partialMatch" : "1"
},
},
"date" : "18-01-2022"
}
So the output I want is =>
{
"summary":{
"totalRows" : "20",
"fullMatch" : "10",
"noMatch" : "2",
"partialMatch" : "8"
},
"income":{
"totalRows" : "20",
"fullMatch" : "10",
"noMatch" : "0",
"partialMatch" : "2"
},
"slip":{
"totalRows" : "10",
"fullMatch" : "1",
"noMatch" : "0",
"partialMatch" : "1"
}
}
Or something similar to this where I fetch the key (summary, income, slip) and total the values.
Tried $unwind but did not work on the map structure.
Maybe there is a smaller aggregate
db.collection.aggregate([
{
"$match": {
"_id": {
"$in": [
ObjectId("61e8c7bbd0597207179faa89"),
ObjectId("61e8c7bbd0597207179faa90")
]
}
}
},
{
"$set": {
"matchStatus": {
$objectToArray: "$matchStatus"
}
}
},
{
"$unwind": "$matchStatus"
},
{
"$replaceRoot": {
"newRoot": "$matchStatus"
}
},
{
"$set": {
"v": {
$objectToArray: "$v"
}
}
},
{
"$unwind": "$v"
},
{
"$group": {
"_id": {
k1: "$k",
k2: "$v.k"
},
"sum": {
"$sum": {
"$toInt": "$v.v"
}
}
}
},
{
"$group": {
"_id": "$_id.k1",
"field": {
"$push": {
k: "$$ROOT._id.k2",
v: "$$ROOT.sum"
}
}
}
},
{
"$project": {
"v": {
$arrayToObject: "$field"
}
}
},
{
"$group": {
"_id": null,
"field": {
"$push": {
k: "$$ROOT._id",
v: "$$ROOT.v"
}
}
}
},
{
"$replaceRoot": {
"newRoot": {
$arrayToObject: "$field"
}
}
}
])
mongoplayground

Remove object in nested array mongodb

I have somwthing like the next json in mongo.
My objective is to delete all nested objects with "id_s": "1"
{
"_id": "5150a1199fac0e6910000002",
"name": "some name",
"p_a": [
{
"sub_name": "subname"
},
{
"sub_name": "subname2",
"p_p": [
{
"last_level": "toDelete",
"id_s": "1"
},
{
"last_level": "toKeep",
"id_s": "2"
}
]
},
{
"sub_name": "subname3",
"p_p": [
{
"last_level": "toDelete",
"id_s": "1"
},
{
"last_level": "toKeep",
"id_s": "2"
}
]
}
]
}
Expected JSON:
{
"_id": "5150a1199fac0e6910000002",
"name": "some name",
"p_a": [
{
"sub_name": "subname"
},
{
"sub_name": "subname2",
"p_p": [
{
"last_level": "toKeep",
"id_s": "2"
}
]
},
{
"sub_name": "subname3",
"p_p": [
{
"last_level": "toKeep",
"id_s": "2"
}
]
}
]
}
This will give you documents where id_s: 2 and ignore rest of them.
db.getCollection("test").aggregate(
[
{
"$match" : {
"_id" : ObjectId("5150a1199fac0e6910000002")
}
},
{
"$unwind" : {
"path" : "$p_a"
}
},
{
"$unwind" : {
"path" : "$p_a.p_p"
}
},
{
"$match" : {
"p_a.p_p.id_s" : "2"
}
}
],
{
"allowDiskUse" : false
}
);
Output:
/* 1 */
{
"_id" : ObjectId("606d83fe44c9fc09f60d0756"),
"name" : "some name",
"p_a" : {
"sub_name" : "subname2",
"p_p" : {
"last_level" : "toKeep",
"id_s" : "2"
}
}
}
/* 2 */
{
"_id" : ObjectId("606d83fe44c9fc09f60d0756"),
"name" : "some name",
"p_a" : {
"sub_name" : "subname3",
"p_p" : {
"last_level" : "toKeep",
"id_s" : "2"
}
}
}

take out some fields key and values from the objects and transform into column structure, in MongoDB

{
"abc": {
"value": "100",
"print": "number1",
"uploadAt" : "2021-22-01",
"name" : "delta1",
"service" : "31-12-2021",
"type" : "typeA"
},
"xyz": {
"value": "123",
"print": "number2",
"uploadAt" : "2021-22-01",
"name" : "delta2",
"service" : "31-12-2021",
"type" : "typeB"
}
}
Explanation: "abc", "xyz" these are my parameter, I am passing dynamically, it could be more than 2. but inside the objects the fields are static, I want to convert my data in the respective format that contains the only name and value fields as output. please check the Expected Output
Expected Output.
[{
"_id": null,
"output": {
"name": "delta1",
"value": "100"
}
}, {
"_id": null,
"output": {
"name": "delta2",
"value": "123"
}
}]
|---------------------|------------------|
| name | value |
|---------------------|------------------|
| delta1 | 100 |
|---------------------|------------------|
| delta2 | 123 |
|---------------------|------------------|
Try this:
db.testCollection.aggregate([
{
$lookup: {
from: "testCollection",
localField: "_id",
foreignField: "_id",
as: "parameters"
}
},
{
$project: {
parameters: {
$map: {
input: { $objectToArray: { $arrayElemAt: ["$parameters", 0] } },
as: "parameter",
in: {
k: "output",
v: {
$cond: {
if: { $ifNull: ["$$parameter.v.name", false] },
then: {
"name": "$$parameter.v.name",
"value": "$$parameter.v.value",
},
else: false
}
}
}
}
}
}
},
{ $unwind: "$parameters" },
{
$replaceRoot: {
newRoot: { $arrayToObject: [["$parameters"]] }
}
},
{
$match: {
"output": { $ne: false }
}
}
]);

MongoDB aggregation match criteria to find if a field exists with value in array

{
"attributes" : [
{
"dept" : "accounts",
"location" : "onshore"
},
{
"dept" : "HR",
"location" : "offshore"
},
{
"dept" : "technology"
"location": "NL"
}
]
},
{
"attributes" : [
{
"dept" : "accounts",
"location" : "onshore"
},
{
"dept" : "technology"
"location": "London"
}
]
},
{
"attributes" : [
{
"dept" : "accounts",
"location" : "onshore"
},
{
"dept" : "HR"
"location": "London"
}
]
}
I want to get those documents where attributes array has dept :technology with location NOTequal to london or the attributes array does not have field dept :technology. So the final output will be like below:
{
"attributes" : [
{
"dept" : "accounts",
"location" : "onshore"
},
{
"dept" : "HR",
"location" : "offshore"
},
{
"dept" : "technology"
"location": "NL"
}
]
},
{
"attributes" : [
{
"dept" : "accounts",
"location" : "onshore"
},
{
"dept" : "HR"
"location": "London"
}
]
}
I have tried this solution but it gives me all the documents:
{
"attributes": {
"$elemMatch": {
"$or": [
{
"dept": {
"$nin": ["technology"]
}
},
{
"$and": [
{
"dept": "technology"
},
{
"location": {"$ne" : "London"}
}
]
}
]
}
}
}
Since $elemMatch evaluates each item of the array, we cannot use $nin or $ne operators if the condition is only to negate the boolean expression.
Instead, we should use the $not to performs a logical NOT operation on the specified < operator-expression >.
Try this one:
db.collection.find({
"$or": [
{
"attributes": {
$not: {
"$elemMatch": {
dept: "technology"
}
}
}
},
{
"attributes": {
"$elemMatch": {
dept: "technology",
location: {
$ne: "London"
}
}
}
}
]
})
MongoPlayground

how to limit the count of every single array elements in find using $in function in mongodb? [duplicate]

For example, I have these documents:
{
"addr": "address1",
"book": "book1"
},
{
"addr": "address2",
"book": "book1"
},
{
"addr": "address1",
"book": "book5"
},
{
"addr": "address3",
"book": "book9"
},
{
"addr": "address2",
"book": "book5"
},
{
"addr": "address2",
"book": "book1"
},
{
"addr": "address1",
"book": "book1"
},
{
"addr": "address15",
"book": "book1"
},
{
"addr": "address9",
"book": "book99"
},
{
"addr": "address90",
"book": "book33"
},
{
"addr": "address4",
"book": "book3"
},
{
"addr": "address5",
"book": "book1"
},
{
"addr": "address77",
"book": "book11"
},
{
"addr": "address1",
"book": "book1"
}
and so on.How can I make a request, which will describe the top N addresses and the top M books per address?Example of expected result: address1 | book_1: 5 | book_2: 10 | book_3: 50 | total: 65 ______________________ address2 | book_1: 10 | book_2: 10 |... | book_M: 10 | total: M*10... ______________________ addressN | book_1: 20 | book_2: 20 |... | book_M: 20 | total: M*20
TLDR Summary
In modern MongoDB releases you can brute force this with $slice just off the basic aggregation result. For "large" results, run parallel queries instead for each grouping ( a demonstration listing is at the end of the answer ), or wait for SERVER-9377 to resolve, which would allow a "limit" to the number of items to $push to an array.
db.books.aggregate([
{ "$group": {
"_id": {
"addr": "$addr",
"book": "$book"
},
"bookCount": { "$sum": 1 }
}},
{ "$group": {
"_id": "$_id.addr",
"books": {
"$push": {
"book": "$_id.book",
"count": "$bookCount"
},
},
"count": { "$sum": "$bookCount" }
}},
{ "$sort": { "count": -1 } },
{ "$limit": 2 },
{ "$project": {
"books": { "$slice": [ "$books", 2 ] },
"count": 1
}}
])
MongoDB 3.6 Preview
Still not resolving SERVER-9377, but in this release $lookup allows a new "non-correlated" option which takes an "pipeline" expression as an argument instead of the "localFields" and "foreignFields" options. This then allows a "self-join" with another pipeline expression, in which we can apply $limit in order to return the "top-n" results.
db.books.aggregate([
{ "$group": {
"_id": "$addr",
"count": { "$sum": 1 }
}},
{ "$sort": { "count": -1 } },
{ "$limit": 2 },
{ "$lookup": {
"from": "books",
"let": {
"addr": "$_id"
},
"pipeline": [
{ "$match": {
"$expr": { "$eq": [ "$addr", "$$addr"] }
}},
{ "$group": {
"_id": "$book",
"count": { "$sum": 1 }
}},
{ "$sort": { "count": -1 } },
{ "$limit": 2 }
],
"as": "books"
}}
])
The other addition here is of course the ability to interpolate the variable through $expr using $match to select the matching items in the "join", but the general premise is a "pipeline within a pipeline" where the inner content can be filtered by matches from the parent. Since they are both "pipelines" themselves we can $limit each result separately.
This would be the next best option to running parallel queries, and actually would be better if the $match were allowed and able to use an index in the "sub-pipeline" processing. So which is does not use the "limit to $push" as the referenced issue asks, it actually delivers something that should work better.
Original Content
You seem have stumbled upon the top "N" problem. In a way your problem is fairly easy to solve though not with the exact limiting that you ask for:
db.books.aggregate([
{ "$group": {
"_id": {
"addr": "$addr",
"book": "$book"
},
"bookCount": { "$sum": 1 }
}},
{ "$group": {
"_id": "$_id.addr",
"books": {
"$push": {
"book": "$_id.book",
"count": "$bookCount"
},
},
"count": { "$sum": "$bookCount" }
}},
{ "$sort": { "count": -1 } },
{ "$limit": 2 }
])
Now that will give you a result like this:
{
"result" : [
{
"_id" : "address1",
"books" : [
{
"book" : "book4",
"count" : 1
},
{
"book" : "book5",
"count" : 1
},
{
"book" : "book1",
"count" : 3
}
],
"count" : 5
},
{
"_id" : "address2",
"books" : [
{
"book" : "book5",
"count" : 1
},
{
"book" : "book1",
"count" : 2
}
],
"count" : 3
}
],
"ok" : 1
}
So this differs from what you are asking in that, while we do get the top results for the address values the underlying "books" selection is not limited to only a required amount of results.
This turns out to be very difficult to do, but it can be done though the complexity just increases with the number of items you need to match. To keep it simple we can keep this at 2 matches at most:
db.books.aggregate([
{ "$group": {
"_id": {
"addr": "$addr",
"book": "$book"
},
"bookCount": { "$sum": 1 }
}},
{ "$group": {
"_id": "$_id.addr",
"books": {
"$push": {
"book": "$_id.book",
"count": "$bookCount"
},
},
"count": { "$sum": "$bookCount" }
}},
{ "$sort": { "count": -1 } },
{ "$limit": 2 },
{ "$unwind": "$books" },
{ "$sort": { "count": 1, "books.count": -1 } },
{ "$group": {
"_id": "$_id",
"books": { "$push": "$books" },
"count": { "$first": "$count" }
}},
{ "$project": {
"_id": {
"_id": "$_id",
"books": "$books",
"count": "$count"
},
"newBooks": "$books"
}},
{ "$unwind": "$newBooks" },
{ "$group": {
"_id": "$_id",
"num1": { "$first": "$newBooks" }
}},
{ "$project": {
"_id": "$_id",
"newBooks": "$_id.books",
"num1": 1
}},
{ "$unwind": "$newBooks" },
{ "$project": {
"_id": "$_id",
"num1": 1,
"newBooks": 1,
"seen": { "$eq": [
"$num1",
"$newBooks"
]}
}},
{ "$match": { "seen": false } },
{ "$group":{
"_id": "$_id._id",
"num1": { "$first": "$num1" },
"num2": { "$first": "$newBooks" },
"count": { "$first": "$_id.count" }
}},
{ "$project": {
"num1": 1,
"num2": 1,
"count": 1,
"type": { "$cond": [ 1, [true,false],0 ] }
}},
{ "$unwind": "$type" },
{ "$project": {
"books": { "$cond": [
"$type",
"$num1",
"$num2"
]},
"count": 1
}},
{ "$group": {
"_id": "$_id",
"count": { "$first": "$count" },
"books": { "$push": "$books" }
}},
{ "$sort": { "count": -1 } }
])
So that will actually give you the top 2 "books" from the top two "address" entries.
But for my money, stay with the first form and then simply "slice" the elements of the array that are returned to take the first "N" elements.
Demonstration Code
The demonstration code is appropriate for usage with current LTS versions of NodeJS from v8.x and v10.x releases. That's mostly for the async/await syntax, but there is nothing really within the general flow that has any such restriction, and adapts with little alteration to plain promises or even back to plain callback implementation.
index.js
const { MongoClient } = require('mongodb');
const fs = require('mz/fs');
const uri = 'mongodb://localhost:27017';
const log = data => console.log(JSON.stringify(data, undefined, 2));
(async function() {
try {
const client = await MongoClient.connect(uri);
const db = client.db('bookDemo');
const books = db.collection('books');
let { version } = await db.command({ buildInfo: 1 });
version = parseFloat(version.match(new RegExp(/(?:(?!-).)*/))[0]);
// Clear and load books
await books.deleteMany({});
await books.insertMany(
(await fs.readFile('books.json'))
.toString()
.replace(/\n$/,"")
.split("\n")
.map(JSON.parse)
);
if ( version >= 3.6 ) {
// Non-correlated pipeline with limits
let result = await books.aggregate([
{ "$group": {
"_id": "$addr",
"count": { "$sum": 1 }
}},
{ "$sort": { "count": -1 } },
{ "$limit": 2 },
{ "$lookup": {
"from": "books",
"as": "books",
"let": { "addr": "$_id" },
"pipeline": [
{ "$match": {
"$expr": { "$eq": [ "$addr", "$$addr" ] }
}},
{ "$group": {
"_id": "$book",
"count": { "$sum": 1 },
}},
{ "$sort": { "count": -1 } },
{ "$limit": 2 }
]
}}
]).toArray();
log({ result });
}
// Serial result procesing with parallel fetch
// First get top addr items
let topaddr = await books.aggregate([
{ "$group": {
"_id": "$addr",
"count": { "$sum": 1 }
}},
{ "$sort": { "count": -1 } },
{ "$limit": 2 }
]).toArray();
// Run parallel top books for each addr
let topbooks = await Promise.all(
topaddr.map(({ _id: addr }) =>
books.aggregate([
{ "$match": { addr } },
{ "$group": {
"_id": "$book",
"count": { "$sum": 1 }
}},
{ "$sort": { "count": -1 } },
{ "$limit": 2 }
]).toArray()
)
);
// Merge output
topaddr = topaddr.map((d,i) => ({ ...d, books: topbooks[i] }));
log({ topaddr });
client.close();
} catch(e) {
console.error(e)
} finally {
process.exit()
}
})()
books.json
{ "addr": "address1", "book": "book1" }
{ "addr": "address2", "book": "book1" }
{ "addr": "address1", "book": "book5" }
{ "addr": "address3", "book": "book9" }
{ "addr": "address2", "book": "book5" }
{ "addr": "address2", "book": "book1" }
{ "addr": "address1", "book": "book1" }
{ "addr": "address15", "book": "book1" }
{ "addr": "address9", "book": "book99" }
{ "addr": "address90", "book": "book33" }
{ "addr": "address4", "book": "book3" }
{ "addr": "address5", "book": "book1" }
{ "addr": "address77", "book": "book11" }
{ "addr": "address1", "book": "book1" }
Using aggregate function like below :
[
{$group: {_id : {book : '$book',address:'$addr'}, total:{$sum :1}}},
{$project : {book : '$_id.book', address : '$_id.address', total : '$total', _id : 0}}
]
it will give you result like following :
{
"total" : 1,
"book" : "book33",
"address" : "address90"
},
{
"total" : 1,
"book" : "book5",
"address" : "address1"
},
{
"total" : 1,
"book" : "book99",
"address" : "address9"
},
{
"total" : 1,
"book" : "book1",
"address" : "address5"
},
{
"total" : 1,
"book" : "book5",
"address" : "address2"
},
{
"total" : 1,
"book" : "book3",
"address" : "address4"
},
{
"total" : 1,
"book" : "book11",
"address" : "address77"
},
{
"total" : 1,
"book" : "book9",
"address" : "address3"
},
{
"total" : 1,
"book" : "book1",
"address" : "address15"
},
{
"total" : 2,
"book" : "book1",
"address" : "address2"
},
{
"total" : 3,
"book" : "book1",
"address" : "address1"
}
I didn't quite get your expected result format, so feel free to modify this to one you need.
Below query will provide exactly the same result as given in the desired response:
db.books.aggregate([
{
$group: {
_id: { addresses: "$addr", books: "$book" },
num: { $sum :1 }
}
},
{
$group: {
_id: "$_id.addresses",
bookCounts: { $push: { bookName: "$_id.books",count: "$num" } }
}
},
{
$project: {
_id: 1,
bookCounts:1,
"totalBookAtAddress": {
"$sum": "$bookCounts.count"
}
}
}
])
The response will be looking like below:
/* 1 */
{
"_id" : "address4",
"bookCounts" : [
{
"bookName" : "book3",
"count" : 1
}
],
"totalBookAtAddress" : 1
},
/* 2 */
{
"_id" : "address90",
"bookCounts" : [
{
"bookName" : "book33",
"count" : 1
}
],
"totalBookAtAddress" : 1
},
/* 3 */
{
"_id" : "address15",
"bookCounts" : [
{
"bookName" : "book1",
"count" : 1
}
],
"totalBookAtAddress" : 1
},
/* 4 */
{
"_id" : "address3",
"bookCounts" : [
{
"bookName" : "book9",
"count" : 1
}
],
"totalBookAtAddress" : 1
},
/* 5 */
{
"_id" : "address5",
"bookCounts" : [
{
"bookName" : "book1",
"count" : 1
}
],
"totalBookAtAddress" : 1
},
/* 6 */
{
"_id" : "address1",
"bookCounts" : [
{
"bookName" : "book1",
"count" : 3
},
{
"bookName" : "book5",
"count" : 1
}
],
"totalBookAtAddress" : 4
},
/* 7 */
{
"_id" : "address2",
"bookCounts" : [
{
"bookName" : "book1",
"count" : 2
},
{
"bookName" : "book5",
"count" : 1
}
],
"totalBookAtAddress" : 3
},
/* 8 */
{
"_id" : "address77",
"bookCounts" : [
{
"bookName" : "book11",
"count" : 1
}
],
"totalBookAtAddress" : 1
},
/* 9 */
{
"_id" : "address9",
"bookCounts" : [
{
"bookName" : "book99",
"count" : 1
}
],
"totalBookAtAddress" : 1
}
Since mongoDB version 3.6 this is easy to do, using $group, $slice, $limit, and $sort:
$group the books to count them
$sort so they will be later pushed according to count
$group by address, $push relevant books, and $sum the total per address.
$sort by address total
$limit the address results to topN
Limit the books in the array to topM using $slice
db.collection.aggregate([
{$group: {_id: {book: "$book", addr: "$addr"}, count: {$sum: 1}}},
{$sort: {"_id.addr": 1, count: -1}},
{$group: {
_id: "$_id.addr", totalCount: {$sum: "$count"},
books: {$push: {book: "$_id.book", count: "$count"}}
}
},
{$sort: {totalCount: -1}},
{$limit: topN},
{$set: {addr: "$_id", _id: "$$REMOVE", books: {$slice: ["$books", 0, topM]}}}
])
See how it works on the playground example-v3.4
On mongoDB version 5.2 there is a topN accumulator that can simplify even more:
db.collection.aggregate([
{$group: {_id: {book: "$book", addr: "$addr"}, count: {$sum: 1}}},
{$group: {
_id: "$_id.addr",
totalCount: {$sum: "$count"},
books: {$topN: {output: {book: "$_id.book", count: "$count"},
sortBy: {count: -1},
n: topM
}}
}},
{$sort: {totalCount: -1}},
{$limit: topN},
{$project: {addr: "$_id", _id: 0, books: 1, totalCount: 1}}
])
See how it works on the playground example-v5.2

Resources