How to retrieve the document exactly matching a given value in mongodb - mongodb

from the mongodb official documentation :
The following examples query against the inventory collection with the following documents:
{ _id: 1, item: { name: "ab", code: "123" }, qty: 15, tags: [ "A", "B", "C" ] }
{ _id: 2, item: { name: "cd", code: "123" }, qty: 20, tags: [ "B" ] }
{ _id: 3, item: { name: "ij", code: "456" }, qty: 25, tags: [ "A", "B" ] }
{ _id: 4, item: { name: "xy", code: "456" }, qty: 30, tags: [ "B", "A" ] }
{ _id: 5, item: { name: "mn", code: "000" }, qty: 20, tags: [ [ "A", "B" ], "C" ] }
The following example queries the inventory collection to select all documents where the tags array equals exactly the specified array or the tags array contains an element that equals the array [ "A", "B" ].
db.inventory.find( { tags: { $eq: [ "A", "B" ] } } )
The query is equivalent to:
db.inventory.find( { tags: [ "A", "B" ] } )
Both queries match the following documents:
{ _id: 3, item: { name: "ij", code: "456" }, qty: 25, tags: [ "A", "B" ] }
{ _id: 5, item: { name: "mn", code: "000" }, qty: 20, tags: [ [ "A", "B" ], "C" ] }
Now i wish to know how i can query in order to get the document(s) having its tags field exactly equal to [ "A", "B" ] and not containing it alone or among other elements ? i want the result for the example above will be only the first document returned :
{ _id: 3, item: { name: "ij", code: "456" }, qty: 25, tags: [ "A", "B" ] }

If you want to only extract the documents that accurately match the array that you provide, you can add a $size operand in your query:
db.inventory.find({
$and: [
{ tags: "A" },
{ tags: "B" },
{ tags: { $size: 2 }}
]
});
The above query will only match documents that have the tags field equal to the specified array, with its elements in that exact order.
The solution provided by chridam in the comments is a more elegant solution:
db.inventory.find({ "tags": { "$all": [ "A", "B" ], "$size": 2 } })
UPDATE:
I inserted the documents you provided in a local MongoDB instance to test my and chridam's queries, and they both return the same result set from the documents that you provided:
{ "_id" : ObjectId("580146168ff3eea72fd1edc7"), "item" : { "name" : "ij", "code" : "456" }, "qty" : 25, "tags" : [ "A", "B" ] }
{ "_id" : ObjectId("580146168ff3eea72fd1edc8"), "item" : { "name" : "xy", "code" : "456" }, "qty" : 30, "tags" : [ "B", "A" ] }
As you can see, it matches the elements of the array and the size, but it does not account for the order in which they appear in the array.
Therefore, I explored different approaches in order to provide a working solution for the outcome you specified, which is to match both the exact contents of the array, as well as their order.
I managed to write the following query using the $where operator, which complies with your request:
db.items.find({ $where: function() {
var arr = ["A", "B"],
tags = this.tags;
if(tags.length !== arr.length) {
return false;
}
for(var i = 0; i < tags.length; i++) {
if(tags[i] !== arr[i]) {
return false;
}
}
return true;
}});
/*
* RESULT SET
*/
{ "_id" : ObjectId("580146168ff3eea72fd1edc7"), "item" : { "name" : "ij", "code" : "456" }, "qty" : 25, "tags" : [ "A", "B" ] }

Related

Java MongoDB Projection

I am referring mongodb official page for projection where I came across following example where elements of array in subdocument is filtered:
https://docs.mongodb.com/manual/reference/operator/aggregation/filter/#exp._S_filter
db.sales.aggregate([
{
$project: {
items: {
$filter: {
input: "$items",
as: "item",
cond: { $gte: [ "$$item.price", 100 ] }
}
}
}
}
])
I am trying to implement this in Java but I am not doing it correctly and elements in subdocument array are not filtered.
Input Collection:
{
_id: 0,
items: [
{ item_id: 43, quantity: 2, price: 10 },
{ item_id: 2, quantity: 1, price: 240 }
]
}
{
_id: 1,
items: [
{ item_id: 23, quantity: 3, price: 110 },
{ item_id: 103, quantity: 4, price: 5 },
{ item_id: 38, quantity: 1, price: 300 }
]
}
{
_id: 2,
items: [
{ item_id: 4, quantity: 1, price: 23 }
]
}
Expected Output Collection:
{
"_id" : 0,
"items" : [
{ "item_id" : 2, "quantity" : 1, "price" : 240 }
]
}
{
"_id" : 1,
"items" : [
{ "item_id" : 23, "quantity" : 3, "price" : 110 },
{ "item_id" : 38, "quantity" : 1, "price" : 300 }
]
}
{ "_id" : 2, "items" : [ ] }
In Java(mongo Driver 3.9.1), this is what I am doing:
Bson priceFilter = Filters.gte("items.price", 100);
mongoCollection.aggregate(
Aggregates.project(Projections.fields(priceFilter))
);
How do I project with aggregate function for the subdocument arrays where I need to filter out elements from subdocument array based on some condition?
In MongoDB Java Driver 3.9.1, collection.aggregate() takes a java.util.List as parameter. So you need to replace your Java code with the below.
mongoCollection.aggregate(
Arrays.asList(
Aggregates.project(Projections.computed("items",
new Document().append("$filter",
new Document().append("input", "$items").append("as", "item").append("cond",
new Document().append("$gte", Arrays.asList("$$item.price",100))))))
)
);

MongoDB compass - Get the field name (key) with the max value, from 3 fields and their values in a document

I have this sample mongodb document -
{
_id: 5db85ee97d9fb13ead4fc54c
applId: 5d48f34f7d9fb10ce171f905
fileId: "dd386cf7-4139-45c2-9853-cbb126621b51"
job: Object
country: "US"
fullName: "abcd xyz"
htmlWordCount: 2766
textWordCount: 1867
rchilliTextWordCount: 2840
deleted: 0
dateEntered: 2019-10-29 15:46:49.237
dateModified: 2019-10-29 15:46:49.237
}
I want to build a query in compass so that I have following fields in the output -
{
_id: 5db85ee97d9fb13ead4fc54c
country: "US"
fullName: "abcd xyz"
htmlWordCount: 2766
textWordCount: 1867
rchilliTextWordCount: 2840
winner: "rchilliTextWordCount"
}
Please note that it has a new field called "winner" which always returns the column with maximum wordcount (out of 3 "htmlWordCount", "textWordCount", "rchilliTextWordCount" columns). This new column "winner" is to be produced on runtime on query. Also this query is filtered on country = "US".
How do I do this in MongoDB Compass or what should the aggregation pipeline look like?
You may use $switch or $cond
db.collection.aggregate([
{
$match: {
country: "US"
}
},
{
$project: {
country: 1,
fullName: 1,
htmlWordCount: 1,
textWordCount: 1,
rchilliTextWordCount: 1,
winner: {
$switch: {
branches: [
{
case: {
$and: [
{
$gt: [
"$htmlWordCount",
"$textWordCount"
]
},
{
$gt: [
"$htmlWordCount",
"$rchilliTextWordCount"
]
}
]
},
then: "htmlWordCount"
},
{
case: {
$and: [
{
$gt: [
"$textWordCount",
"$htmlWordCount"
]
},
{
$gt: [
"$textWordCount",
"$rchilliTextWordCount"
]
}
]
},
then: "textWordCount"
},
{
case: {
$and: [
{
$gt: [
"$rchilliTextWordCount",
"$htmlWordCount"
]
},
{
$gt: [
"$rchilliTextWordCount",
"$textWordCount"
]
}
]
},
then: "rchilliTextWordCount"
}
],
default: "No winners"
}
}
}
}
])
MongoPlayground
This is another approach of getting the result:
Get the document's field names and their values
Find the maximum value for the fields with names in [ "htmlWordCount", "textWordCount", "rchilliTextWordCount" ].
In general, finding a maximum value from an array is a kind of reduction; so I used the $reduce in this case. Note the code is simpler. In case you want add another field for calculating the maximum, just add it to the array.
db.winner.aggregate([
{ $match: { country: "US"} },
{ $addFields: { fieldNameValues: { "$objectToArray": "$$ROOT" } } },
{ $project: { _id: 1, country: 1, fullName: 1, htmlWordCount: 1, textWordCount: 1, rchilliTextWordCount: 1,
winner: {
$reduce: {
input: "$fieldNameValues",
initialValue: { },
in: {
$cond: [
{ $and: [
{ $in: [ "$$this.k", [ "htmlWordCount", "textWordCount", "rchilliTextWordCount" ] ] },
{ $gt: [ "$$this.v", "$$value.v"] } ]
},
"$$this",
"$$value"
]
}
}
}
} },
{ $addFields: { winner: "$winner.k" } }
] )
[ EDIT ADD ]
Sample Data and Result:
{
"_id" : 1,
"fileId" : "dd386cf7-4139-45c2-9853-cbb126621b51",
"job" : { },
"country" : "US",
"fullName" : "abcd xyz",
"htmlWordCount" : 2766,
"textWordCount" : 1867,
"rchilliTextWordCount" : 2840
}
{
"_id" : 2,
"fileId" : "dd386cf7-4139-45c2-9853-cbb126621b51",
"job" : { },
"country" : "US",
"fullName" : "lmn opqrs",
"htmlWordCount" : 5,
"textWordCount" : 9,
"rchilliTextWordCount" : 2
}
Output:
{
"_id" : 1,
"country" : "US",
"fullName" : "abcd xyz",
"htmlWordCount" : 2766,
"textWordCount" : 1867,
"rchilliTextWordCount" : 2840,
"winner" : "rchilliTextWordCount"
}
{
"_id" : 2,
"country" : "US",
"fullName" : "lmn opqrs",
"htmlWordCount" : 5,
"textWordCount" : 9,
"rchilliTextWordCount" : 2,
"winner" : "textWordCount"
}

Atomically move object by ID from one array to another in same document [duplicate]

This question already has an answer here:
Move an element from one array to another within same document MongoDB
(1 answer)
Closed 3 years ago.
I have data that looks like this:
{
"_id": ObjectId("4d525ab2924f0000000022ad"),
"arrayField": [
{ id: 1, other: 23 },
{ id: 2, other: 21 },
{ id: 0, other: 235 },
{ id: 3, other: 765 }
],
"someOtherArrayField": []
}
Given a nested object's ID (0), I'd like to $pull the element from one array (arrayField) and $push it to another array (someOtherArrayField) within the same document. The result should look like this:
{
"_id": ObjectId("id"),
"arrayField": [
{ id: 1, other: 23 },
{ id: 2, other: 21 },
{ id: 3, other: 765 }
],
"someOtherArrayField": [
{ id: 0, other: 235 }
]
}
I realize that I can accomplish this with a find followed by an update, i.e.
db.foo.findOne({"_id": param._id})
.then((doc)=>{
db.foo.update(
{
"_id": param._id
},
{
"$pull": {"arrayField": {id: 0}},
"$push": {"someOtherArrayField": {doc.array[2]} }
}
)
})
But I'm looking for an atomic operation like, in pseudocode, this:
db.foo.update({"_id": param._id}, {"$move": [{"arrayField": {id: 0}}, {"someOtherArrayField": 1}]}
Is there an atomic way to do this, perhaps using MongoDB 4.2's ability to specify a pipeline to an update command? How would that look?
I found this post that generously provided the data I used, but the provided solution isn't an atomic operation. Has an atomic solution become possible with MongoDB 4.2?
Here's an example:
> db.baz.find()
> db.baz.insert({
... "_id": ObjectId("4d525ab2924f0000000022ad"),
... "arrayField": [
... { id: 1, other: 23 },
... { id: 2, other: 21 },
... { id: 0, other: 235 },
... { id: 3, other: 765 }
... ],
... "someOtherArrayField": []
... })
WriteResult({ "nInserted" : 1 })
function extractIdZero(arrayFieldName) {
return {$arrayElemAt: [
{$filter: {input: arrayFieldName, cond: {$eq: ["$$this.id", 0]}}},
0
]};
}
extractIdZero("$arrayField")
{
"$arrayElemAt" : [
{
"$filter" : {
"input" : "$arrayField",
"cond" : {
"$eq" : [
"$$this.id",
0
]
}
}
},
0
]
}
db.baz.updateOne(
{_id: ObjectId("4d525ab2924f0000000022ad")},
[{$set: {
arrayField: {$filter: {
input: "$arrayField",
cond: {$ne: ["$$this.id", 0]}
}},
someOtherArrayField: {$concatArrays: [
"$someOtherArrayField",
[extractIdZero("$arrayField")]
]}
}}
])
{ "acknowledged" : true, "matchedCount" : 1, "modifiedCount" : 1 }
> db.baz.findOne()
{
"_id" : ObjectId("4d525ab2924f0000000022ad"),
"arrayField" : [
{
"id" : 1,
"other" : 23
},
{
"id" : 2,
"other" : 21
},
{
"id" : 3,
"other" : 765
}
],
"someOtherArrayField" : [
{
"id" : 0,
"other" : 235
}
]
}

MongoDB: Find and then modify the result depending on if certain value in field array

Is there an easy solution in MongoDB to find some objects that match a query and then to modify the result without modifying the persistent data depending on if a certain value is contained in an array?
Let explain me using an example:
students = [
{
name: "Alice",
age: 25,
courses: [ { name: "Databases", credits: 6 },{ name: "Java", credits: 4 }]
},
{
name: "Bob",
age: 22,
courses: [ { name: "Java", credits: 4 } ]
},
{
name: "Carol",
age: 19,
courses: [ { name: "Databases", credits: 6 } ]
},
{
name: "Dave", age: 18
}
]
Now, I want to query all students. The result should return all their data except 'courses'. Instead, I want to output a flag 'participant' indicating whether that person participates in the Databases course:
result = [
{ name: "Alice", age: 25, participant: 1 },
{ name: "Bob", age: 22, participant: 0 },
{ name: "Carol", age: 19, participant: 1 },
{ name: "Dave", age: 18, participant: 0}
]
without changing anything in the database.
I've already found a solution using aggregate. But it's very complicated and unhandy and so, I would like to know if there is a more handy solution for this problem.
My current solution looks like the following:
db.students.aggregate([
{$project: {"courses": {$ifNull: ["$courses", [{name: 0}]]}, name: 1, _id: 1, age: 1}},
{$unwind: "$courses"},
{$project: {name: 1, age: 1, participant: {$cond: [{$eq: ["$courses.name", "DB"]}, 1, 0]}}},
{$group: {_id: {_id: "$_id", age: 1, name: "$name"}, participant: {$sum: "$participant"}}},
{$project: {_id: 0, _id: "$_id._id", age: "$_id.age", name: "$_id.name", participant: 1}}
]);
One point I don't like in this solution is that I have to specify the output fields exactly three times. Also, this pipe is quite long.
Run the following aggregation pipeline to get the desired result:
db.students.aggregate([
{
"$project": {
"name": 1,
"age": 1,
"participant": {
"$size": {
"$ifNull" : [
{
"$setIntersection" : [
{
"$map": {
"input": "$courses",
"as": "el",
"in": {
"$eq": [ "$$el.name", "Databases" ]
}
}
},
[true]
]
},
[]
]
}
}
}
}
])
Output:
{
"result" : [
{
"_id" : ObjectId("564f1bb67d3c273d063cd216"),
"name" : "Alice",
"age" : 25,
"participant" : 1
},
{
"_id" : ObjectId("564f1bb67d3c273d063cd217"),
"name" : "Bob",
"age" : 22,
"participant" : 0
},
{
"_id" : ObjectId("564f1bb67d3c273d063cd218"),
"name" : "Carol",
"age" : 19,
"participant" : 1
},
{
"_id" : ObjectId("564f1bb67d3c273d063cd219"),
"name" : "Dave",
"age" : 18,
"participant" : 0
}
],
"ok" : 1
}
The above pipeline uses only one step, $project in which the new field participant is created via a series of nested operators.
Crucial to the operations is the deeply nested $map operator which in essence creates a new array field that holds values as a result of the evaluated logic in a subexpression to each element of an array. Let's demonstrate this operation only by executing the pipeline with just the $map part:
db.students.aggregate([
{
"$project": {
"name": 1,
"age": 1,
"participant": {
"$map": {
"input": "$courses",
"as": "el",
"in": {
"$eq": [ "$$el.name", "Databases" ]
}
}
}
}
}
])
Output
{
"result" : [
{
"_id" : ObjectId("564f1bb67d3c273d063cd216"),
"name" : "Alice",
"age" : 25,
"participant" : [
true,
false
]
},
{
"_id" : ObjectId("564f1bb67d3c273d063cd217"),
"name" : "Bob",
"age" : 22,
"participant" : [
false
]
},
{
"_id" : ObjectId("564f1bb67d3c273d063cd218"),
"name" : "Carol",
"age" : 19,
"participant" : [
true
]
},
{
"_id" : ObjectId("564f1bb67d3c273d063cd219"),
"name" : "Dave",
"age" : 18,
"participant" : null
}
],
"ok" : 1
}
Probe the array further by introducing the $setIntersection operator which returns a set with elements that appear in all of the input sets. Thus in the above you would need to get a resulting array that has true to denote that document user participated in a Database course, else it will return an empty or null array. Let's see how adding that operator affects the previous result:
db.students.aggregate([
{
"$project": {
"name": 1,
"age": 1,
"participant": {
"$setIntersection" : [
{
"$map": {
"input": "$courses",
"as": "el",
"in": {
"$eq": [ "$$el.name", "Databases" ]
}
}
},
[true]
]
}
}
}
])
Output:
{
"result" : [
{
"_id" : ObjectId("564f1bb67d3c273d063cd216"),
"name" : "Alice",
"age" : 25,
"participant" : [
true
]
},
{
"_id" : ObjectId("564f1bb67d3c273d063cd217"),
"name" : "Bob",
"age" : 22,
"participant" : []
},
{
"_id" : ObjectId("564f1bb67d3c273d063cd218"),
"name" : "Carol",
"age" : 19,
"participant" : [
true
]
},
{
"_id" : ObjectId("564f1bb67d3c273d063cd219"),
"name" : "Dave",
"age" : 18,
"participant" : null
}
],
"ok" : 1
}
To handle nulls, apply the $ifNull operator, equivalent to the coalesce command in SQL to substitute null values with an empty array:
db.students.aggregate([
{
"$project": {
"name": 1,
"age": 1,
"participant": {
"$ifNull" : [
{
"$setIntersection" : [
{
"$map": {
"input": "$courses",
"as": "el",
"in": {
"$eq": [ "$$el.name", "Databases" ]
}
}
},
[true]
]
},
[]
]
}
}
}
])
After this you can then wrap the $ifNull operator with the $size operator to return the number of elements in the participants array, and that yields the final output as above.
Based on what you said about the small number of objects, how about simply pulling out the database name and using JavaScript map to transform it? You're not saving much in terms of transfer and the code will be way more readable than the pipeline.

find documents having a specific count of matches array

I've searched high and low but not been able to find what i'm looking for so apologies if this has already been asked.
Consider the following documents
{
_id: 1,
items: [
{
category: "A"
},
{
category: "A"
},
{
category: "B"
},
{
category: "C"
}]
},
{
_id: 2,
items: [
{
category: "A"
},
{
category: "B"
}]
},
{
_id: 3,
items: [
{
category: "A"
},
{
category: "A"
},
{
category: "A"
}]
}
I'd like to be able to find those documents which have more than 1 category "A" item in the items array. So this should find documents 1 and 3.
Is this possible?
Using aggregation
> db.spam.aggregate([
{$unwind: "$items"},
{$match: {"items.category" :"A"}},
{$group: {
_id: "$_id",
item: {$push: "$items.category"}, count: {$sum: 1}}
},
{$match: {count: {$gt: 1}}}
])
Output
{ "_id" : 3, "item" : [ "A", "A", "A" ], "count" : 3 }
{ "_id" : 1, "item" : [ "A", "A" ], "count" : 2 }