How to do a find iterating in a array field - mongodb

Hi i have a expressjs app using mongodb.
At first i find a tv by id on my "tvs" collection, i get it but now i want to find all user info from other collection "users".
This is my JSON for each collection:
tvs
{
"_id" : ObjectId("5203af83396d285ea2ecff8f"),
"brand" : "LG",
"comments" : [{
"user" : ObjectId("521dc636eda03d0f9cab3568"),
"text" : "Sold!"
}, {
"user" : ObjectId("521b2785eda03d0f9cab3566"),
"text" : "Nice TV"
}],
"model" : "47LS5600",
"price" : 499.0
}
users
{
"_id" : ObjectId("521b2785eda03d0f9cab3566"),
"name" : {
"first" : "Ruben",
"last" : "Montes"
}
}
And this is my code
var tvs = db.collection("tvs");
var users = db.collection("users");
exports.findById = function (req, res) {
var id = req.params.id;
tvs.findOne({'_id': new BSON.ObjectID(id)}, function (err, tv) {
users.find( { _id : tv.comments.user_id }).toArray(function (err, items) {
res.send( { tv: tv, users: items } );
});
})
}
I need to know how to iterate the comments array from tvs collection to get the the info user that post a comment
users.find( { _id : tv.comments.user_id })

You can do a bit more logic to efficiently grab the users as a batch using the $in operator.
var mongodb = require('mongodb')
, MongoClient = require('mongodb').MongoClient
, Server = require('mongodb').Server;
MongoClient.connect('mongodb://127.0.0.1:27017/test', function (err, db) {
if (err) throw err;
var tvs = db.collection('tvs');
var users = db.collection('users');
var userNames = {};
var tvId = new mongodb.ObjectID("5203af83396d285ea2ecff8f"); // hard-code
// find a TV
tvs.findOne({ _id : tvId }, function (err, tv) {
var allUserIds = [];
if (tv && tv.comments) {
// build a list of all user IDs used in comments
// this doesn't filter duplicates right now
allUserIds = tv.comments.map(function (comment) {
return comment.user;
});
}
// using the list of UserIds, grab all of them ...,
// and just return the name
users.find({_id: { $in: allUserIds }}, { name: 1 })
.toArray(function (err, users_list) {
// if we got some
if (users_list && users_list.length > 0) {
for(var i= 0, len = users_list.length; i < len ; i++ ) {
userNames[users_list[i]._id] = users_list[i].name;
}
console.log("All comments ========");
// now all the usernames are indexed in userNames by Id
for(var i= 0, len = tv.comments.length; i < len ; i++ ) {
// swap id for name
tv.comments[i].user = userNames[tv.comments[i].user];
console.log(tv.comments[i]);
}
db.close(); // done with everything for this demo
}
});
});
});
I've used find and $in with an array of all userIds found in the comments for a single "tv". By using $in, it significantly reduces the number of calls needed to MongoDB to fetch single User documents. Also, using the second parameter of find, I've reduced the returned fields to just be name.
FYI -- I did simplify your structure to just be 'name' rather than 'first' and 'last'. You certainly can change it to match your exact needs.

Related

Find records with field in a nested document when parent fields are not known

With a collection with documents like below, I need to find the documents where a particular field - eg. lev3_field2 (in document below) is present.
I tried the following, but this doesn't return any results, though the field lev3_field2 is present in some documents.
db.getCollection('some_collection').find({"lev3_field2": { $exists: true, $ne: null } })
{
"_id" : ObjectId("5884de15bebf420cf8bb2857"),
"lev1_field1" : "139521721",
"lev1_field2" : "276183",
"lev1_field3" : {
"lev2_field1" : "4",
"lev2_field2" : {
"lev3_field1" : "1",
"lev3_field2" : {
"lev4_field1" : "1",
"lev4_field2" : "1"
},
"lev3_field3" : "5"
},
"lev2_field3" : {
"lev3_field3" : "0",
"lev3_field4" : "0"
}
}
}
update1: this is an example, however in the real document it is not known what the parent fields are for the field to look for. So instead of lev3_field2 , I would be looking for `levM_fieldN'.
update2: Speed is not a primary concern for me, I can work with relatively a bit slower options as well, as the primary function is to find documents with the criteria discussed and once the document is found and the schema is understood, the query can be re-written for performance by including the parent keys.
To search a key in nested document you need to iterate the documents fields recursively, you can do this in JavaScript by the help of $where method in MongoDB
The below query will search if a key name exists in a documents and its subdocuments.
I have checked this with the example you have given, and it is working perfectly fine.
db.getCollection('test').find({ $where: function () {
var search_key = "lev3_field2";
function check_key(document) {
return Object.keys(document).some(function(key) {
if ( typeof(document[key]) == "object" ) {
if ( key == search_key ) {
return true;
} else {
return check_key(document[key]);
}
} else {
return ( key == search_key );
}
});
}
return check_key(this);
}}
);
There is no built-in function to iterate over document keys in MongoDB, but you can achieve this with MapReduce. The main advantage is that all the code is executed directly in the MongoDB database, and not in the js client, so there is no network overhead, hence it should be faster than client side js
here is the script :
var found;
// save a function in MongoDB to iterate over documents key and check for
// key name. Need to be done only once
db.system.js.save({
_id: 'findObjectByLabel',
value: function(obj, prop) {
Object.keys(obj).forEach(function(key) {
if (key === prop) {
found = true
}
if (!found && typeof obj[key] === 'object') {
findObjectByLabel(obj[key], prop)
}
})
}
})
// run the map reduce fonction
db.ex.mapReduce(
function() {
found = false;
var key = this._id
findObjectByLabel(this, 'lev3_field2')
value = found;
if (found) {
// if the document contains the key we are looking for,
// emit {_id: ..., value: true }
emit(key, value)
}
},
function(key, values) {
return values
}, {
'query': {},
'out': {inline:1}
}
)
this output ( run on 4 sample doc, with only one containing 'lev3_field2' )
{
"results" : [
{
"_id" : ObjectId("5884de15bebf420cf8bb2857"),
"value" : true
}
],
"timeMillis" : 18,
"counts" : {
"input" : 4,
"emit" : 1,
"reduce" : 0,
"output" : 1
},
"ok" : 1
}
to run the script, copy it to a file name "script.js" for example, and then run from your shell
mongo databaseName < script.js
It's because you're trying to see if a nested field exists. This is the query you want:
db.some_collection.find({"lev1_field3.lev2_field2.lev3_field2": { $exists: true, $ne: null } })

how to iterate an object in mongodb documents?

the document in my mongo collection like this:
{
"_id" : ObjectId("568f7e67676b4ddf133999e8"),
"auth_dic" : {
"2406" : [
"44735"
],
"6410" : [
"223423"
]
...
...
},
"user_id" : "fdasd23423"
}
this is one user, there are many id like '2406' '6410' in 'auth_dic'
i want to user map_reduce method to statistic how many users have the '2406', how many users have the '6410', and other types.
my mapper is:
mapper = Code('''
function(){
this.auth_dic.forEach(function(app_id){
emit(app_id, 1);
});
}
''')
my reducer is:
reducer = Code('''
function(key, values){
var total = 0;
for (var i = 0; i < values.length; i++) {
total += values[i];
}
return total;
}
''')
but the mapper can not work, beacause the function 'forEach' can not user to iterate a dic, what can i do to solve this problem?
Run the following MapReduce operation:
mr = db.runCommand({
"mapreduce" : "collectionName",
"map" : function() {
var map = this.auth_dic;
for (var key in map) {
if (map.hasOwnProperty(key)) { emit(key, 1); }
}
},
"reduce" : function(key, val) { return Array.sum(val); },
"out": "collectionName" + "_keys"
})
Then query the resulting collection so as to find all the counts for the dynamic keys:
db[mr.result].find()

Better Alternative to MongoDB db.collection.group()

I'm looking to group a collection of documents, then format them into something I can query dynamically, then query on the resulting collection. I've been able to group the collection and format it successfully using db.collection.group() but there are limitations when it comes to sharding that would prevent me from using it in production.
Here's that code:
var reduceFunction = function(current, result){
var provider = current.provider;
var collection = current.collection || provider;
result[provider] = result[provider] || {};
result[provider][collection] = result[provider][collection] || {};
result[provider][collection] = current.attributes;
}
var result = db.identity.group( {
key: { cookie: 1 },
cond: { organizationId: organization },
reduce: reduceFunction,
initial: {}
} );
Here's an example of the input documents:
{
"_id" : ObjectId("566c92712ae892aeb5278467"),
"provider" : "myProvider",
"collection": "myCollection",
"ip" : 111.111.111.111,
"cookie" : "12381902348asdf",
"updated" : ISODate("2015-12-12T21:32:33.824Z"),
"attributes" : {
company: "My Company"
}
}
Here's what my desired output would be (multiple input documents grouped by cookie)
{
"cookie" : "12381902348asdf",
"myProvider": {
"myCollection": {
"company": "myCompany"
}
},
"myOtherProvider": {
"myOtherCollection": {
"company": "myCompany"
}
}
}
so that way I could query using something like
collection.identity.find({"myProvider.myCollection.company": {$regex: /my/}})
This allows me to have conditions for multiple provider/collection/field groupings without having conflicts in the field names.

Upserting on embedded document

I have the following document strucutre
{
"_id" : "NmBYYasdsa",
"objectId" : "asdsd"
"text" : "test",
....
"publishedAt" : ISODate("2015-05-28T15:31:51Z"),
"updatedAt" : ISODate("2015-05-28T15:31:51Z"),
"data" : {
.....
"likeCount" : 0,
"replyCount" : 0
}
}
That is use to synchronise my database with an external API. To do this, I poll the API once every minute and do a bulk upsert, matching on the object id to keep my database up to date.
Problem is that the data subdocument doesn't get updated when upserting, any ideas as to why?
My bulkwrite method
Mongo.Collection.prototype.upsertBulk = function(matcher, documents, options) {
if (_.isEmpty(documents)) { throw Error('Empty list of documents provided'); }
options = options || {};
var operations = documents.map(function(_document) {
_document._id = Random.id();
var operation = {
updateOne: {
filter: {},
update: { $set: _document },
upsert: true
},
};
operation['updateOne']['filter'][matcher] = _document[matcher];
return operation;
});
this.__mongoCollection(function(collection) {
collection.bulkWrite(operations, options, function(error, result) {
if (error) { throw error; }
return result;
});
});
};

MapReduce trouble with counting

I've got a problem, I have data in mongodb which looks like this:
{"miejscowosci_str":"OneCity", "wojewodztwo":"FirstRegionName", "ZIP-Code" : "...", ...}
{"miejscowosci_str":"TwoCity", "wojewodztwo":"FirstRegionName", "ZIP-Code" : "...", ...}
{"miejscowosci_str":"ThreeCity", "wojewodztwo":"SecondRegionName", "ZIP-Code" : "...", ...}
{"miejscowosci_str":"FourCity", "wojewodztwo":"SecondRegionName", "ZIP-Code" : "...", ...}
and so on
What I want is to list all regions (wojewodztwo) and to count average number of zip codes per region, I know how to count all zip codes in region:
var map = function() {
emit(this.wojewodztwo,1);
};
var reduce = function(key, val) {
var count = 0;
for(i in val) {
count += val[i];
}
return count;
};
db.kodypocztowe.mapReduce(
map,
reduce,
{ out : "result" }
);
But I don't know how to count number of cities (miejscowosci_str) so I could divide number of ZIP-Codes in region through number of cities in the same region.
One city can have multiple number of zip-codes.
Have you got any ideas?
I'm making a couple of assumptions here :
cities can have multiple zip codes
zip codes are unique
you are not trying to get the answer to M101P week 5 questions !
Rather than just counting the cities in one go, why not build up a list of city/zip objects in the map phase and then reduce this to a list of zips and unique cities in the map phase. Then you can use the finalize phase to calculate the averages.
Note : if the data set is large you might want to consider using the aggregation framework instead, this is shown after the map/reduce example
db.kodypocztowe.drop();
db.result.drop();
db.kodypocztowe.insert([
{"miejscowosci_str":"OneCity", "wojewodztwo":"FirstRegionName", "ZIP-Code" : "1"},
{"miejscowosci_str":"TwoCity", "wojewodztwo":"FirstRegionName", "ZIP-Code" : "2"},
{"miejscowosci_str":"ThreeCity", "wojewodztwo":"SecondRegionName", "ZIP-Code" : "3"},
{"miejscowosci_str":"FourCity", "wojewodztwo":"SecondRegionName", "ZIP-Code" : "4"},
{"miejscowosci_str":"FourCity", "wojewodztwo":"SecondRegionName", "ZIP-Code" : "5"},
]);
// map the data to { region : [{citiy : name , zip : code }] }
// Note : a city can be in multiple zips but zips are assumed to be unique
var map = function() {
emit(this.wojewodztwo, {city:this.miejscowosci_str, zip:this['ZIP-Code']});
};
//
// convert the data to :
//
// {region : {cities: [], zips : []}}
//
// note : always add zips
// note : only add cities if they are not already there
//
var reduce = function(key, val) {
var res = {zips:[], cities:[]}
for(i in val) {
var city = val[i].city;
res.zips.push(val[i].zip);
if(res.cities.indexOf(city) == -1) {
res.cities.push(city);
}
}
return res;
};
//
// finalize the data to get the average number of zips / region
var finalize = function(key, res) {
res.average = res.zips.length / res.cities.length;
delete res.cities;
delete res.zips;
return res;
}
print("==============");
print(" map/reduce")
print("==============");
db.kodypocztowe.mapReduce(
map,
reduce,
{ out : "result" , finalize:finalize}
);
db.result.find().pretty()
print("==============");
print(" aggregation")
print("==============");
db.kodypocztowe.aggregate( [
// get the number of zips / [region,city]
{ "$group" :
{
_id : {"region" : "$wojewodztwo", city : "$miejscowosci_str"},
zips:{$sum:1}
}
},
// get the number of cities per region and sum the number of zips
{ "$group" :
{
_id : "$_id.region" ,
cities:{$sum:1},
zips:{$sum:"$zips"},
}
},
// project the data into the same format that map/reduce generated
{ "$project" :
{
"value.average":{$divide: ["$zips","$cities"]}
}
}
]);
I hope that helps.