wrong result in MongoDB mapreduce function? - mongodb

I have Collection "cars" from that want to get count of certified cars as trueCount and flaseCount where certified is boolean.
am issuing the following mapreduce query
map:-
function() { for (var idx = 0; idx < this.cars.length; idx++) {
var key = this.cars[idx].carName;
var value = {
count : 1,
certifiedCheck : this.cars[idx].certified
};
emit(key, value);
} }
reduce:-
function(key, values) {
certifiedCount = { trueCount: 0, falseCount: 0 };
values.forEach(function(value) {
if ( value.certifiedCheck )
certifiedCount.trueCount += value.count;
else
certifiedCount.falseCount += value.count;
});
return certifiedCount;
query:
{ "type": "cars" }
getting the following result :
{ "id" : "carName" , "value" : { "true" : 277.0 , "false" : NaN}};
even though I have 457 documents in the collection.
Please someone help me here to fix this issue.
Thanks in advance

You mixed up your map-reduce: to reduce to two keys "true" and "false" you need to emit these as keys. Then, the reducer will run per key.
As pseudo code:
map:
for each car
evaluate whether it should be true or false
key = (true/false)
emit(key, { count : 1 })
reduce:
(input is true/false as key, array of count-documents as value)
for each value-document
sum up the count
return key, sum
This should yields two documents with true / false as key and the respective sum as value.

You should consider using the aggregation framework for running the aggregation since it achieves the same result albeit faster than MapReduce as aggregation runs natively in the server (C++), MapReduce spawns separate javascript thread(s) to run JS code.
Thus said, if you run the following aggregation pipeline which uses the $cond operator to evaluate the counts based on the logic in the field expression, you will get a similar result:
Because you haven't showed your collection schema, I've assumed the following sample documents with a cars field as array having seen in your mapReduce you are doing a for loop on the cars property:
Populate test collection
db.collection.insert([
{ _id: 1, cars: [ { model: "A", certified: true }, { model: "B", certified: true } ] },
{ _id: 2, cars: [ { model: "A", certified: false }, { model: "B", certified: true } ] },
{ _id: 3, cars: [ { model: "A", certified: true }, { model: "B", certified: false } ] },
{ _id: 4, cars: [ { model: "A", certified: true }, { model: "B", certified: false } ] },
{ _id: 5, cars: [ { model: "A", certified: true }, { model: "B", certified: true } ] }
])
Run aggregation operation
db.collection.aggregate([
{ "$unwind": "$cars" },
{
"$group": {
"_id": "$cars.model",
"trueCount": {
"$sum": {
"$cond": [ "$cars.certified", 1, 0 ]
}
},
"falseCount": {
"$sum": {
"$cond": [ "$cars.certified", 0, 1 ]
}
}
}
}
])
Result:
/* 1 */
{
"_id" : "A",
"trueCount" : 4,
"falseCount" : 1
}
/* 2 */
{
"_id" : "B",
"trueCount" : 3,
"falseCount" : 2
}

Related

in mongodb, how to create a unique index for a list of documents?

I have an array of documents like this:
[
{
_id: ObjectId("63845afd1f4ec22ab0d11db9"),
ticker: 'ABCD',
aggregates: [
{ date: '2022-05-20' },
{ date: '2022-05-20' },
{ date: '2022-05-20' }
]
}
]
How may I create an unique index on aggregates.date, so user may not push a duplicate date into array aggregates.
My existing aggregates are as follows:
db.aggregates_1_day.getIndexes()
[
{ v: 2, key: { _id: 1 }, name: '_id_' },
{ v: 2, key: { ticker: 1 }, name: 'ticker_1', unique: true },
{
v: 2,
key: { 'aggregates.date': 1 },
name: 'aggregates.date_1',
unique: true
}
]
Unique index ensure no duplicates across documents , but do not enforce uniqness for objects in array in same collection document.
But you have few other options here:
1. Do not use $push , but use $addToSet instead to add unique objects inside aggregates array of objects:
db.collection.update({},
{
"$addToSet": {
"aggregates": {
date: "2022-05-20"
}
}
})
note: $addToSet
only ensures that there are no duplicate items added to the set and does not affect existing duplicate elements.
Playground
2. You can configure schema validation:
> db.runCommand({collMod:"aggregates_1_day", validator: {$expr:{$eq:[{$size:"$aggregates.date"},{$size:{$setUnion:"$aggregates.date"}}]}}})
> db.aggregates_1_day.insert({aggregates:[{date:1}]}) /* success */
> db.aggregates_1_day.update({},{ '$push' : { 'aggregates':{date:1}}})
WriteResult({
"nMatched" : 0,
"nUpserted" : 0,
"nModified" : 0,
"writeError" : {
"code" : 121,
"errmsg" : "Document failed validation"
}
})
>
more details in the mongoDB ticket
Note: In this approach you will need to clean the duplicates in advance otherways the validation will not allow to $push new objects at all.
In case you dont like it you can remove validation with:
db.runCommand({
collMod: "aggregates_1_day",
validator: {},
validationLevel: "off"
})
3. You can use update/aggregation as follow:
db.collection.update({},
[
{
$set: {
aggregates: {
$cond: [
{
$in: [
"2022-02-02",
"$aggregates.date"
]
},
"$aggregates",
{
$concatArrays: [
"$aggregates",
[
{
date: "2022-02-02"
}
]
]
}
]
}
}
}
])
Explained:
Add the object to the array only if do not exist in the array of objects.
Playground3

Append an object to an array inside a nested object

I have a collection in MongoDB in which one of the documents looks like this:
{
_id: ObjectId("6162883719592ea3350d3c87"),
fullName: 'Random User',
username: 'ruser1',
password: 'asdadasd',
portfolio: [ { equity: [] }, { crypto: [] }, { etf: [] }, { cash: [] } ]
}
I am trying to append a new object of the following format to the equity array inside the portfolio.
Object format:
{
name : "AAPL",
quantity : 1,
price : 100
}
I was trying to use the $push to do this operation, but I'm encountering the following error:
db.users.updateOne(
{_id : ObjectId("6162883719592ea3350d3c87")},
{$push : {"portfolio.equity" : {
name : "AAPL",
quantity : 1,
price : 100
}
}
}
)
MongoServerError: Cannot create field 'equity' in element {portfolio: [ { equity: [] }, { crypto: [] }, { etf: [] }, { cash: [] } ]}
I have also tried to use portfolio.$.equity, but that did not work either.
db.users.updateOne(
{_id : ObjectId("6162883719592ea3350d3c87")} ,
{$push : {"portfolio.$.equity" : {name : "AAPL", price : 100, quantity : 1}}}
)
MongoServerError: The positional operator did not find the match needed from the query.
In short, I am trying to append an object to an array inside an object's object.
How can I resolve this error or what is the appropriate way to do this?
You can use arrayFilters with check portfolio.equity field is existed via $exists.
db.users.updateOne({
_id: ObjectId("6162883719592ea3350d3c87")
},
{
$push: {
"portfolio.$[portfolio].equity": {
name: "AAPL",
price: 100,
quantity: 1
}
}
},
{
arrayFilters: [
{
"portfolio.equity": {
$exists: true
}
}
]
})
Sample Mongo Playground

MongoDB Update array element (document with a key) if exists, else push

I have such a schema:
doc:
{
//Some fields
visits:
[
{
userID: Int32
time: Int64
}
]
}
I want to first check if a specific userID exists, if not, push a document with that userID and system time, else just update time value. I know neither $push nor $addToSet are not able to do that. Also using $ with upsert:true doesn't work, because of official documentation advice which says DB will use $ as field name instead of operator when trying to upsert.
Please guide me about this. Thanks
You can use $addToSet to add an item to the array and $set to update an existing item in this array.
The following will add a new item to the array if the userID is not found in the array :
db.doc.update({
visits: {
"$not": {
"$elemMatch": {
"userID": 4
}
}
}
}, {
$addToSet: {
visits: {
"userID": 4,
"time": 1482607614
}
}
}, { multi: true });
The following will update the subdocument array item if it matches the userId :
db.doc.update({ "visits.userID": 2 }, {
$set: {
"visits.$.time": 1482607614
}
}, { multi: true });
const p = await Transaction.findOneAndUpdate(
{
_id: data.id,
'products.id': { $nin: [product.id] },
},
{
$inc: {
actualCost: product.mrp,
},
$push: {
products: { id: product.id },
},
},
{ new: true }
);
or
db.collection.aggregate([
{
"$match": {
"_id": 1
}
},
{
"$match": {
"sizes.id": {
"$nin": [
7
]
}
}
},
{
"$set": {
"price": 20
}
}
])
https://mongoplayground.net/p/BguFa6E9Tra
I know it's very late. But it may help others. Starting from mongo4.4, we can use $function to use a custom function to implement our own logic. Also, we can use the bulk operation to achieve this output.
Assuming the existing data is as below
{
"_id" : ObjectId("62de4e31daa9b8acd56656ba"),
"entrance" : "Entrance1",
"visits" : [
{
"userId" : 1,
"time" : 1658736074
},
{
"userId" : 2,
"time" : 1658736671
}
]
}
Solution 1: using custom function
db.visitors.updateMany(
{_id: ObjectId('62de4e31daa9b8acd56656ba')},
[
{
$set: {
visits: {
$function: {
lang: "js",
args: ["$visits"],
body: function(visits) {
let v = []
let input = {userId: 3, time: Math.floor(Date.now() / 1000)};
if(Array.isArray(visits)) {
v = visits.filter(x => x.userId != input.userId)
}
v.push(input)
return v;
}
}
}
}
}
]
)
In NodeJS, the function body should be enclosed with ` character
...
lang: 'js',
args: ["$visits"],
body: `function(visits) {
let v = []
let input = {userId: 3, time: Math.floor(Date.now() / 1000)};
if(Array.isArray(visits)) {
v = visits.filter(x => x.userId != input.userId)
}
v.push(input)
return v;
}`
...
Solution 2: Using bulk operation:
Please note that the time here will be in the ISODate
var bulkOp = db.visitors.initializeOrderedBulkOp()
bulkOp.find({ _id: ObjectId('62de4e31daa9b8acd56656ba') }).updateOne({$pull: { visits: { userId: 2 }} });
bulkOp.find({ _id: ObjectId('62de4e31daa9b8acd56656ba') }).updateOne({$push: {visits: {userId: 2, time: new Date()}}})
bulkOp.execute()
Reference link

Mongodb: find documents with array field that contains more than one SAME specified value

There is three documents in collection test:
// document 1
{
"id": 1,
"score": [3,2,5,4,5]
}
// document 2
{
"id": 2,
"score": [5,5]
}
// document 3
{
"id": 3,
"score": [5,3,3]
}
I want to fetch documents that score field contains [5,5].
query:
db.test.find( {"score": {"$all": [5,5]}} )
will return document 1, 2 and 3, but I only want to fetch document 1 and 2.
How can I do this?
After reading your problem I personally think mongodb not supported yet this kind of query. If any one knows about how to find this using mongo query they defiantly post answers here.
But I think this will possible using mongo forEach method, so below code will match your criteria
db.collectionName.find().forEach(function(myDoc) {
var scoreCounts = {};
var arr = myDoc.score;
for (var i = 0; i < arr.length; i++) {
var num = arr[i];
scoreCounts[num] = scoreCounts[num] ? scoreCounts[num] + 1 : 1;
}
if (scoreCounts[5] >= 2) { //scoreCounts[5] this find occurrence of 5
printjsononeline(myDoc);
}
});
Changed in version 2.6.
The $all is equivalent to an $and operation of the specified values; i.e. the following statement:
{ tags: { $all: [ "ssl" , "security" ] } }
is equivalent to:
{ $and: [ { tags: "ssl" }, { tags: "security" } ] }
I think you need to pass in a nested array -
So try
db.test.find( {"score": {"$all": [[5,5]]}} )
Source
Changed in version 2.6.
When passed an array of a nested array (e.g. [ [ "A" ] ] ), $all can now match documents where the field contains the nested array as an element (e.g. field: [ [ "A" ], ... ]), or the field equals the nested array (e.g. field: [ "A" ]).
http://docs.mongodb.org/manual/reference/operator/query/all/
You can do it with an aggregation. The first step can use an index on { "score" : 1 } but the rest is hard work.
db.test.aggregate([
{ "$match" : { "score" : 5 } },
{ "$unwind" : "$score" },
{ "$match" : { "score" : 5 } },
{ "$group" : { "_id" : "$_id", "sz" : { "$sum" : 1 } } }, // use $first here to include other fields in the results
{ "$match" : { "sz" : { "$gte" : 2 } } }
])

How to insert to the certain position in the array in the subdocument by using Mongoose an MongoDB 2.6

A great explanation of how to use new $position operator of Mongodb 2.6 by using Mongoose was given in the answer to my question. The suggested solution works perfect for simple arrays. If array is in subdocument or each element of array is array the suggested solution doesn't work. I mean something like this:
List.collection.update(
{/*....*/},
{ "$push": {
"subdocument.list": {
"$each": [ 1, 2, 3 ],
"$position": 0 }
}
},function(err,NumAffected) {
console.log("done");
});
List.collection.update(
{/*....*/},
{ "$push": {
"list1.$.list2": {
"$each": [ 1, 2, 3 ],
"$position": 0 }
}
},function(err,NumAffected) {
console.log("done");
});
In the current Mongoose version (4.4.2) you can use position like this:
Foo.update(query.id,
{
$push: {
arrayData: {
$each: [{
date: new Date
}], $position: 0
}
}
});
No sure what the issue is here:
db.list.insert({ "list": { "sub": [4] } })
db.list.update(
{},
{ "$push": { "list.sub": { "$each": [1,2,3], "$position": 0 } } }
)
{ "list" : { "sub" : [ 1, 2, 3, 4 ] } }
So that works as expected.
And for the other example:
db.list.insert({
outer: [
{
key: "a",
inner: [4]
},
{
key: "b",
inner: [4]
}
]
})
db.list.update(
{ "outer.key": "b" },
{ "$push": {
"outer.$.inner": {
"$each": [1,2,3], "$position": 0
}
}}
)
Again is as expected:
{
"outer" : [
{
"key" : "a",
"inner" : [
4
]
},
{
"key" : "b",
"inner" : [
1,
2,
3,
4
]
}
]
}
The interaction with specific drivers was already explained so there must be something different in the data, but if so then those statements are not valid.
And so exactly the same using Mongoose:
var mongoose = require('mongoose');
var Schema = mongoose.Schema;
mongoose.connect('mongodb://localhost/nodetest');
var listSchema = new Schema({
});
var List = mongoose.model( "List", listSchema );
var Other = mongoose.model( "Other", listSchema );
List.collection.update(
{},
{ "$push": {
"list.sub": {
"$each": [ 1, 2, 3 ],
"$position": 0 }
}
},function(err,NumAffected) {
console.log("done");
}
);
Other.collection.update(
{ "outer.key": "b" },
{ "$push": {
"outer.$.inner": {
"$each": [ 1, 2, 3 ],
"$position": 0
}
}},function(err,NumAffected) {
console.log("done2")
}
);
I have found the code in the MongooseJS Model.update that omits support for the $position modifier.
In version 3.8.8, line 1928-1941 of query.js:
if ('$each' in val) {
obj[key] = {
$each: this._castUpdateVal(schema, val.$each, op)
}
if (val.$slice) {
obj[key].$slice = val.$slice | 0;
}
if (val.$sort) {
obj[key].$sort = val.$sort;
}
}
Here, obj[key] will be set to val.$each. You can see explicit support for setting the $slice & $sort modifiers, but the $position modifier will never be copied into obj[key].
So, although you may be able to by-pass the Model.update function of MongooseJS to directly access MongoDB's update function, it is clear that MongooseJS's Model.update function does not support the $position modifier.