MongoDB Aggregate and Update Exercise - database

I was practicing on mongoDB. Currently, this is my first attempt on NoSql databases. I have a generic movie collection(video_movieDetails). In it, there are "_id" "title" "year" "director" "writers" "countries". I am able to write queries like "Construct a query and return all movies where Ian McKellen or Elijah Woods played in, but not both in movies.
db.video_movieDetails.find({$or:[{$and:[{"actors":"Ian McKellen"},{"actors":{$ne:"Elijah Woods"}}]},{$and:[{"actors":"Elijah Woods"},{"actors":{$ne:"Ian McKellen"}}]}]})
Now I am trying to achieve,
Titles and number of movies of each movie director.(For aggregation)
Updating a director's name from "Tim Burton" to "T. Burton". Eventually, all of the names should be updated on corresponding movie. (Updating)
How may I achieve this two queries? Thank you.

With this setup:
db.foo.insert([
{"_id": 0,title: "Movie A", year: 2010, director: "D1", actors: ["A1","A2"] }
,{"_id": 1,title: "Movie B", year: 2010, director: "D1", actors: ["A1","A5","A6"]}
,{"_id": 2,title: "Movie C", year: 2010, director: "D2", actors: ["A4"]}
,{"_id": 3,title: "Movie D", year: 2012, director: "D3", actors: ["A7"]}
,{"_id": 4,title: "Movie E", year: 2015, director: "D3", actors: ["A7"]}
]);
c = db.foo.aggregate([
{$group: {_id: "$director", movies: {$push: "$title"}, count: {$sum:1}}}
]);
show(c, true);
// For update(), note the option multi:true. Without it, mongodb will update ONLY the
// first doc matching the query criteria, not ALL the docs.
rc = db.foo.update({director: "D3"},{$set: {"director":"T. Burton"}},{multi:true});
printjson(rc);
c = db.foo.aggregate([]);
show(c, true);
you get this:
{ "_id" : "D1", "movies" : [ "Movie A", "Movie B" ], "count" : 2 }
{ "_id" : "D2", "movies" : [ "Movie C" ], "count" : 1 }
{ "_id" : "D3", "movies" : [ "Movie D", "Movie E" ], "count" : 2 }
found 3
{ "nMatched" : 2, "nUpserted" : 0, "nModified" : 2 }
{
"_id" : 0,
"title" : "Movie A",
"year" : 2010,
"director" : "D1",
"actors" : [
"A1",
"A2"
]
}
{
"_id" : 1,
"title" : "Movie B",
"year" : 2010,
"director" : "D1",
"actors" : [
"A1",
"A5",
"A6"
]
}
{
"_id" : 2,
"title" : "Movie C",
"year" : 2010,
"director" : "D2",
"actors" : [
"A4"
]
}
{
"_id" : 3,
"title" : "Movie D",
"year" : 2012,
"director" : "T. Burton",
"actors" : [
"A7"
]
}
{
"_id" : 4,
"title" : "Movie E",
"year" : 2015,
"director" : "T. Burton",
"actors" : [
"A7"
]
}
The $match stage will work on arrays as if they were scalars. For example, to find all movies in which actor A1 or A4 appears:
db.foo.aggregate([
{$match: {"actors": {$in: ["A1","A4"]} }}
]);
A simple $lookup example:
db.foo2.insert([
{_id:0, location: "Spain"},
{_id:1, location: "Italy"},
{_id:2, location: "Italy"},
{_id:3, location: "USA"},
{_id:4, location: "Sweden"},
]);
c = db.foo2.aggregate([
{$match: {"location": "Italy"}}
,{$lookup: {from: "foo", localField: "_id", foreignField: "_id", as: "X"}}
// Lookup can produce a one-to-many relationship; thus, "X" will come back
// as an array. Since we know that the _id-to-_id relationship is 1:1, we
// can just grab the first element of X and reset X to be that. In this
// case, $addFields acts like an assignment expression "x = x + 1" where
// x is overwritten by a new x.
,{$addFields: {X: {$arrayElemAt:["$X",0]} }}
// At this point, if you really want, you can $project just the title and
// location or whatever you want. Sometimes it is more flexible just to
// return the whole shape. Comment out the next line as an experiment:
,{$project: {_id:true, location:true, title: "$X.title"}}
]);
yields:
{ "_id" : 1, "location" : "Italy", "title" : "Movie B" }
{ "_id" : 2, "location" : "Italy", "title" : "Movie C" }

Related

How do I create a MongoDB aggregate to lookup and add fields using ObjectIds in array objects

Using Mongo 4.4
I'm looking to to lookups across collections and add a human readable value from the target collection to the source collection using a aggregate.
This works fine for individual values, but for some lookups the ObjectIds are in objects in arrays, and I can't get that work. I can pull all the values back, but not place the individual values in the array objects.
In this test case, I have a library database with a books collection and a subscribers collection. The subscribers have a checkouts entry with is an array of objects, containing a reference to a book, and the checkout date. I want to add the book title to each object in the array.
Test Database:
books collection:
[
{
"_id" : ObjectId("63208c9f0d97eff0cfbefde6"),
"title" : "There and back again",
"author" : "Bilbo Baggins",
"publisher" : "Middle Earth Books"
},
{
"_id" : ObjectId("63208cd10d97eff0cfbeff02"),
"title" : "Two Towers",
"author" : "JRR Tolkin",
"publisher" : "Dude Books"
},
{
"_id" : ObjectId("63208cf10d97eff0cfbeffa3"),
"title" : "Dune",
"author" : "Frank Herbert",
"publisher" : "Classic Books"
},
{
"_id" : ObjectId("63208d1d0d97eff0cfbf0087"),
"title" : "Old Man's War",
"author" : "John Scalzi",
"publisher" : "Old Man Books"
}
]
subscribers collection:
[
{
"_id" : ObjectId("63208c2e0d97eff0cfbefb46"),
"name" : "Tom",
"checkouts" : [
{
"bookId" : ObjectId("63208cd10d97eff0cfbeff02"),
"checkoutDate" : ISODate("2022-01-01T21:21:20.202Z")
},
{
"bookId" : ObjectId("63208d1d0d97eff0cfbf0087"),
"checkoutDate" : ISODate("2022-01-02T21:22:20.202Z")
}
],
"address" : "123 Somewhere"
},
{
"_id" : ObjectId("63208c4e0d97eff0cfbefc1f"),
"name" : "Bob",
"checkouts" : [],
"address" : "123 Somewhere"
},
{
"_id" : ObjectId("63208c640d97eff0cfbefc9a"),
"name" : "Mary",
"checkouts" : [],
"address" : "123 Somewhere Else"
}
Desired Output for user Tom:
{
"_id" : ObjectId("63208c2e0d97eff0cfbefb46"),
"name" : "Tom",
"checkouts" : [
{
"bookId" : ObjectId("63208cd10d97eff0cfbeff02"),
"checkoutDate" : ISODate("2022-01-01T21:21:20.202Z"),
"title" : "Two Towers"
},
{
"bookId" : ObjectId("63208d1d0d97eff0cfbf0087"),
"checkoutDate" : ISODate("2022-01-02T21:22:20.202Z"),
"title" : "Old Man's War"
}
],
"address" : "123 Somewhere",
}
Using this aggregate:
db.getCollection('subscribers').aggregate([
{$match: {_id: ObjectId("63208c2e0d97eff0cfbefb46") } },
{$lookup: {from: "books", localField: "checkouts.bookId", foreignField: "_id", as: "book_tmp_field" }},
{$addFields: { "checkouts.title": "$book_tmp_field.title"}},
{$project: { book_tmp_field: 0}}
])
This is the closest I can get:
{
"_id" : ObjectId("63208c2e0d97eff0cfbefb46"),
"name" : "Tom",
"checkouts" : [
{
"bookId" : ObjectId("63208cd10d97eff0cfbeff02"),
"checkoutDate" : ISODate("2022-01-01T21:21:20.202Z"),
"title" : [
"Two Towers",
"Old Man's War"
]
},
{
"bookId" : ObjectId("63208d1d0d97eff0cfbf0087"),
"checkoutDate" : ISODate("2022-01-02T21:22:20.202Z"),
"title" : [
"Two Towers",
"Old Man's War"
]
}
],
"address" : "123 Somewhere"
}
Before performing the lookup, you should UNWIND the checkouts array. After all the processing is done, group the documents, to obtain the checkouts in the array. Finally, project your desired output document. Like this:
db.subscribers.aggregate([
{
$match: {
_id: ObjectId("63208c2e0d97eff0cfbefb46")
}
},
{
"$unwind": "$checkouts"
},
{
$lookup: {
from: "books",
localField: "checkouts.bookId",
foreignField: "_id",
as: "book_tmp_field"
}
},
{
$addFields: {
"checkouts.title": "$book_tmp_field.title"
}
},
{
$project: {
book_tmp_field: 0
}
},
{
"$group": {
"_id": {
_id: "$_id",
address: "$address",
name: "$name"
},
"checkouts": {
"$push": "$checkouts"
}
}
},
{
"$replaceRoot": {
"newRoot": {
"$mergeObjects": [
"$_id",
{
checkouts: "$checkouts"
}
]
}
}
}
])
Here's the playground link.

Getting info from two collections in MongoDB

I'm trying to use two collections(reviews and products) to find the title and description of products reviewed by reviewer “A”. I just need to display the title and description. Nothing else.
So far I have:
db.reviews.aggregate([
{$match: {reviewer : 'A'}},
{$lookup: {
from: "products",
localField: "reviewer",
foreignField: "title",
foreignField: "description",
as: "products_docs"}},
{$project: {
_id: 0,
reviewerID: 1,
title: 1,
description: 1
}}
])
There is supposed to be two products which the current output gives the ID so far but not the title or description.
/* 1 */
{
"reviewerID" : "A"
}
/* 2 */
{
"reviewerID" : "A"
}
Am I missing something?
sample docs :
review :
{
"_id" : ObjectId("5d0b70f2d7367de7f5fa1589"),
"reviewerID" : "A",
"asin" : "1",
"reviewerName" : "Bob",
"helpful" : [
0,
0
],
"reviewText" : "It was really good.",
"overall" : 1.0,
"summary" : "Brilliant",
"unixReviewTime" : 1402185600,
"reviewTime" : "06 8, 2014"
}
product :
{
"_id" : ObjectId("5d0b6d1cd7367de7f58b4906"),
"asin" : "1",
"description" : "Perfect for sunny days",
"title" : "Sunglasses",
"imUrl" : "/sunglasses.jpg",
"related" : {
"also_bought" : [
"729300236X"
]
},
"salesRank" : {
"Shoes" : 257607
},
"categories" : [
[
"Clothing, Shoes & Jewellery",
"Women",
"Accessories",
"Sunglasses & Eyewear Accessories",
"Sunglasses"
],
[
"Clothing, Shoes & Jewellery",
"Men",
"Accessories",
"Sunglasses & Eyewear Accessories",
"Sunglasses"
]
]
}
You can do that using $lookup, Try this below query :
db.review.aggregate([
/** filtering out review coll to get required doc */
{
$match: {
"reviewerID": "A"
}
},
{
$lookup: {
from: "product",
let: {
asin: "$asin" // creating local variable from review Coll's field
},
pipeline: [
{
$match: {
$expr: {
$eq: [
"$asin", // foreign field
"$$asin" // local variable created in let
]
}
}
},
/** projecting only required fields from product Coll */
{
$project: {
description: 1,
title: 1,
_id: 0
}
}
],
as: "data"
}
}
])
Test : MongoDB-Playground

Get index of an element mongodb aggregation

Here is my collection
{
"_id" : ObjectId("5c225f9a66d39d55c036fa66"),
"name" : "Sherlock",
"mobile" : "999999",
"adress" : [
{
"street" : "221b baker street",
"city" : "london"
},
{
"street" : "ben street",
"city" : "london"
}
],
"tags" : [
"Detective",
"Magician",
"Avenger"
]
}
Now I want to get the first or second value inside address array.
for that I'm using this command.
> db.agents.findOne({"name" : "Sherlock"},{"adress" : 1})
but instead of giving a single result it is giving the entire array like
{
"_id" : ObjectId("5c225f9a66d39d55c036fa66"),
"adress" : [
{
"street" : "221b baker street",
"city" : "london"
},
{
"street" : "ben street",
"city" : "london"
}
]
}
It can be done by comparing array value like
db.agents.find({"adress.street": "ben street"}, {_id: 0, 'adress.$': 1});
But I don't want to compare just to print the array indexes. How can I get the single result?
Any help is appreciated..
You can use $arrayElemAt to get the specific element from the array
db.collection.aggregate([
{ $addFields: { "$arrayElemAt": ["$adress", 0] }} //index
])
and if you want to get the sliced element then you can use $slice projection
db.collection.find({}, { adress: { $slice: [2, 1] }}) // 2 index and 1 number of element
You can $unwind with includeArrayIndex to get the index of address array
db.t11.aggregate([
{$match : {"adress.street" : "ben street"}},
{$unwind : {path : "$adress", includeArrayIndex : "idx"}},
{$match : {"adress.street" : "ben street"}}
]).pretty()
you can add $project to filter the fields not required
result
> db.t11.aggregate([{$match : {"adress.street" : "ben street"}},{$unwind : {path : "$adress", includeArrayIndex : "idx"}},{$match : {"adress.street" : "ben street"}}]).pretty()
{
"_id" : ObjectId("5c225f9a66d39d55c036fa66"),
"name" : "Sherlock",
"mobile" : "999999",
"adress" : {
"street" : "ben street",
"city" : "london"
},
"tags" : [
"Detective",
"Magician",
"Avenger"
],
"idx" : NumberLong(1)
}
>

How to fetch places based on geoNear 2dsphere index and category array using MongoDB?

This is a sample document from a mongoDB collection:-
{
"place_id" : "57222c6f498e78e3bfe0a575",
"title_AR" : "University Institute Hall",
"title_EN" : "University Institute Hall",
"description_AR" : "",
"description_EN" : "",
"best_photo" : "https://igx.4sqi.net/img/general/250x250/8862268_8oi3vr81Zm3ucytWjoSyvWXXS-7BUpNxKgyJvj0Lusc.jpg",
"city_AR" : "Kolkata",
"city_EN" : "Kolkata",
"state_AR" : "West Bengal",
"state_EN" : "West Bengal",
"country" : "IN",
"postal_code_AR" : "700073",
"postal_code_EN" : "700073",
"location_AR" : "7, Bankim Chatterjee St, Kolkata 700073, West Bengal, India",
"location_EN" : "7, Bankim Chatterjee St, Kolkata 700073, West Bengal, India",
"latitude" : 22.5744745110906,
"longitude" : 88.3630291046031,
"loc" : {
"type" : "Point",
"coordinates" : [
88.3630291046031,
22.5744745110906
]
},
"website" : "",
"email" : "",
"contact_number_AR" : "+913322416214",
"contact_number_EN" : "+913322416214",
"ratings_AR" : "",
"ratings_EN" : "",
"no_of_comments_AR" : 0,
"no_of_comments_EN" : 0,
"categories" : "4d4b7104d754a06370d81259,5032792091d4c4b30a586d5c,4d4b7104d754a06370d81259,5032792091d4c4b30a586d5c",
"category_array" : [
"4d4b7104d754a06370d81259",
"5032792091d4c4b30a586d5c",
"4d4b7104d754a06370d81259",
"5032792091d4c4b30a586d5c"
],
"created_date" : "2018-05-31 16:39:33",
"modified_date" : "2018-05-31 16:39:33",
"photo_saved" : 0,
"tip_saved" : 0,
"updated_at" : ISODate("2018-05-31T11:09:33.000Z"),
"created_at" : ISODate("2018-05-31T11:09:33.000Z")
}
The category array contains all the category ids under which the place falls.
"category_array" : [
"4d4b7104d754a06370d81259",
"5032792091d4c4b30a586d5c",
"4d4b7104d754a06370d81259",
"5032792091d4c4b30a586d5c"
]
I want to fetch the places based on the location and category. Here is my script:-
db.runCommand(
{
geoNear: "foursquare_places",
near: { type: "Point", coordinates: [ 88.363892, 22.572645 ] },
spherical: true,
query: { category_array: { $all: ["5032792091d4c4b30a586d5c"] }},
maxDistance: 500
}
)
Now here is the issue. According to the Mongo document:-
To makes an exact array match, including the order of the elements:-
db.inventory.find( { tags: ["red", "blank"] } )
To find an array that contains both the elements "red" and "blank", without regard to order or other elements in the array:-
db.inventory.find( { tags: { $all: ["red", "blank"] } } )
The following example queries for all documents where tags is an array that contains the string "red" as one of its elements
db.inventory.find( { tags: "red" } )
What will be the query to fetch all the records whose category_array contains either or all of the elements of the following array?
findArray = ['4d4b7104d754a06370d81259', '4d4b7104d754a06370d81259',
'5032792091d4c4b30a586d5c']
"either or all" makes little sense to me. Assuming it is "at least one of", you need to use $in:
query: { category_array: { $in: [
'4d4b7104d754a06370d81259',
'4d4b7104d754a06370d81259',
'5032792091d4c4b30a586d5c'
] } }
Will return all documents where category_array has at least one of 3 strings.

Mongodb - moving a field in an array to a new array in another parent

I have the following data scheme in mongodb database.
Due to a user interaction an entry can be moved from CAT_A to CAT_B, and the angularjs model changes appropriately.
[
{
"_id":"537f4407cb8a077d396bd73e",
"cat":"CAT_A",
"ntype":"category",
"entries":[
{
"title":"111",
"content":"Content One",
"ntype":"entry",
"_id":"537f4407cb8a077d396bd741"
},
{
"title":"222",
"content":"Content Two",
"ntype":"entry",
"_id":"537f4407cb8a077d396bd740"
},
{
"title":"333",
"content":"Content Three",
"ntype":"entry",
"_id":"537f4407cb8a077d396bd73f"
}
]
},
{
"_id":"537f4407cb8a077d396bd742",
"cat":"CAT_B",
"ntype":"category",
"entries":[
{
"title":"444",
"content":"Content Four",
"ntype":"entry",
"_id":"537f4407cb8a077d396bd745"
},
{
"title":"555",
"content":"Content Five",
"ntype":"entry",
"_id":"537f4407cb8a077d396bd744"
},
{
"title":"666",
"content":"Content Six",
"ntype":"entry",
"_id":"537f4407cb8a077d396bd743"
}
]
}
]
How do I save this new model to the mongo database, or really what is the best way to handle this?
Things I've though about doing:
Simply remove all the categories involved(there will be more than 2) from the database, and recreate them from the new model. This seems inefficient, also the content field may contain larger amounts of data, which makes http requests expensive.
Same as 1, but leave the 'content' out of the schema, create a new collection for content only, and somehow link that to the entry ID.
Pull an entry from CAT_A and push to CAT_B, have struggled getting this working and what if I wanted to keep the index position as in the model? ie if I wanted to move entry 6 in CAT_B to between entry 1 and 2 in CAT_A?
cheers
EDIT new schemas:
var CatSchema = new Schema({
name : String,
ntype : String,
incentries: {
ntype : String,
entry_id : { type: Schema.Types.ObjectId, ref: 'Entry' },
entry_title : String
}
});
var EntrySchema = new Schema({
cat : { type: Schema.ObjectId, ref: 'Cat' },
title : String,
content : String,
});
and the code:
exports.editCat = function (req, res) {
Cat.update({_id: req.body.old},
{$pull: {'incentries': {'entry_id': req.body.entry}}},
function (err, data) {
});
Cat.update({_id: req.body.new},
{$addToSet: { incentries : {'entry_id': req.body.entry, 'entry_title': req.body.entryTitle, ntype: 'entry' }}},
function (err, data) {
});
};
Use $pull and $push:
>db.elements.findOne()
{
"_id" : ObjectId("537f6ddcd66d3634fe5963f6"),
"arr" : [
{
"_id" : "537f4407cb8a077d396bd73e",
"cat" : "CAT_A",
"ntype" : "category",
"entries" : [
{
"title" : "111",
"content" : "Content One",
"ntype" : "entry",
"_id" : "537f4407cb8a077d396bd741"
},
{
"title" : "222",
"content" : "Content Two",
"ntype" : "entry",
"_id" : "537f4407cb8a077d396bd740"
},
{
"title" : "333",
"content" : "Content Three",
"ntype" : "entry",
"_id" : "537f4407cb8a077d396bd73f"
}
]
},
{
"_id" : "537f4407cb8a077d396bd742",
"cat" : "CAT_B",
"ntype" : "category",
"entries" : [
{
"title" : "444",
"content" : "Content Four",
"ntype" : "entry",
"_id" : "537f4407cb8a077d396bd745"
},
{
"title" : "555",
"content" : "Content Five",
"ntype" : "entry",
"_id" : "537f4407cb8a077d396bd744"
},
{
"title" : "666",
"content" : "Content Six",
"ntype" : "entry",
"_id" : "537f4407cb8a077d396bd743"
}
]
}
]
}
> db.elements.update({"_id" : ObjectId("537f6ddcd66d3634fe5963f6")},{$pull:{"arr.0.entries":entry},$push:{"arr.1.entries":entry}})
WriteResult({ "nMatched" : 1, "nUpserted" : 0, "nModified" : 1 })
>db.elements.findOne()
{
"_id" : ObjectId("537f6ddcd66d3634fe5963f6"),
"arr" : [
{
"_id" : "537f4407cb8a077d396bd73e",
"cat" : "CAT_A",
"ntype" : "category",
"entries" : [
{
"title" : "222",
"content" : "Content Two",
"ntype" : "entry",
"_id" : "537f4407cb8a077d396bd740"
},
{
"title" : "333",
"content" : "Content Three",
"ntype" : "entry",
"_id" : "537f4407cb8a077d396bd73f"
}
]
},
{
"_id" : "537f4407cb8a077d396bd742",
"cat" : "CAT_B",
"ntype" : "category",
"entries" : [
{
"title" : "444",
"content" : "Content Four",
"ntype" : "entry",
"_id" : "537f4407cb8a077d396bd745"
},
{
"title" : "555",
"content" : "Content Five",
"ntype" : "entry",
"_id" : "537f4407cb8a077d396bd744"
},
{
"title" : "666",
"content" : "Content Six",
"ntype" : "entry",
"_id" : "537f4407cb8a077d396bd743"
},
{
"title" : "111",
"content" : "Content One",
"ntype" : "entry",
"_id" : "537f4407cb8a077d396bd741"
}
]
}
]
}
But you will have to know where CAT_A and CAT_B are within the array (position 0 and position 1)
You could also try a schema like this:
{
"_id" : ObjectId("537f6dddd66d3634fe5963f7"),
"categories" : {
CAT_A: {
"_id" : "537f4407cb8a077d396bd73e",
"cat" : "CAT_A",
"ntype" : "category",
"entries" : [...]
},
CAT_B:{
"_id" : "537f4407cb8a077d396bd742",
"cat" : "CAT_B",
"ntype" : "category",
"entries" : [...]
}
}
}
So you update query would finally be:
> db.elements.update({"_id" : ObjectId("537f6ddcd66d3634fe5963f6")},{$pull:{"categories.CAT_B.entries":entry},$push:{"categories.CAT_A.entries":entry}})
WriteResult({ "nMatched" : 1, "nUpserted" : 0, "nModified" : 1 })
As long as you "cat" field is unique within a single document this other approach seems better to me.

Resources