Mongodb query for 2 level grouping - mongodb

Suppose I have documents that, among others, have these fields:
{
"class" : String,
"type" : String,
"name" : String,
}
For example, many like this:
{
"class": "class A",
"type": "type 1",
"Name": "ObjectA1"
}
{
"class": "class A",
"type": "type 2",
"Name": "ObjectA2_1"
}
{
"class": "class A",
"type": "type 2",
"Name": "ObjectA2_2"
}
{
"class": "class B ",
"type": "type 3",
"Name": "ObjectB3"
}
What I want is a query that returns me the following structure
{
"class A" : {
"type 1" : ["ObjectA1"],
"type 2" : ["ObjectA2_1", "ObjectA2_2"]
},
"class B" : {
"type 3" : ["ObjectB3"]
}
}
I tried using aggregate with $group but could not do this. Any thoughts?
PS: I would like to do this on mongodb shell, not mongoose or something like this.

The problem with using the aggregation framework will be that you cannot specify an arbitrary key name for a property of an object. So reshaping using that would not be possible without being able to specify all of the possible key names.
So to get the result you would need to work something in JavaScript such as mapReduce:
First define a mapper:
var mapper = function () {
var key = this["class"];
delete this._id;
delete this["class"];
emit( key, this );
};
Then a reducer:
var reducer = function (key, values) {
var reducedObj = {};
values.forEach(function(value) {
if ( !reducedObj.hasOwnProperty(value.type) )
reducedObj[value.type] = [];
reducedObj[value.type].push( value.Name );
});
return reducedObj;
};
And because you have ( in your sample at least ) possible items that will be emitted from the mapper with only 1 key value you will also need a finalize function:
var finalize = function (key,value) {
if ( value.hasOwnProperty("name") ) {
value[value.type] = value.name;
delete value.type;
delete value.name;
}
return value;
};
Then you call the mapReduce function as follows:
db.collection.mapReduce(
mapper,
reducer,
{ "out": { "inline": 1 }, "finalize": finalize }
)
And that gives the following output:
"results" : [
{
"_id" : "class A",
"value" : {
"type 1" : [
"ObjectA1"
],
"type 2" : [
"ObjectA2_1",
"ObjectA2_2"
]
}
},
{
"_id" : "class B ",
"value" : {
"type" : "type 3",
"Name" : "ObjectB3"
}
}
],
While the result is formatted in a very mapReduce way, it is definitely much the same as your result.
But if you really did want to take that further, you can always do the following:
Define another mapper:
var mapper2 = function () {
emit( null, this );
};
And another reducer:
var reducer2 = function (key,values) {
reducedObj = {};
values.forEach(function(value) {
reducedObj[value._id] = value.value;
});
return reducedObj;
};
Then run the first mapReduce with the output to a new collection:
db.collection.mapReduce(
mapper,
reducer,
{ "out": { "replace": "newcollection" }, "finalize": finalize }
)
Followed by a second mapReduce on the new collection:
db.newcollection.mapReduce(
mapper2,
reducer2,
{ "out": { "inline": 1 } }
)
And there is your result:
"results" : [
{
"_id" : null,
"value" : {
"class A" : {
"type 1" : [
"ObjectA1"
],
"type 2" : [
"ObjectA2_1",
"ObjectA2_2"
]
},
"class B " : {
"type" : "type 3",
"Name" : "ObjectB3"
}
}
}
],

I found a workaround for what I needed. It's not the same but solves my problem.
db.myDb.aggregate(
{
$group:{
_id: {
class_name : "$class",
type_name : "$name"
},
items: {
$addToSet : "$name"
}
}
},
{
$group:{
_id : "$_id.class_name",
types : {
$addToSet : {
type : "$_id.type_name",
items : "$items"
}
}
}
})
this gave me something like:
{
_id : "class A",
types: [
{
type: "type 1",
items: ["ObjectA1"]
},
{
type: "type 2",
items: ["ObjectA2_1", "ObjectA2_2"]
}
]
},
{
_id : "class B",
types: [
{
type: "type 3",
items: ["ObjectB3"]
}
]
}
Both code and example were written here so there may be typos.
So this is about it. I want to thank #Neil Lunn for his awesome answer and dedication.
Marcel

Related

How to remove an object which is inside [nested] of an object which is in an object array [MongoDB]?

I have a document:
{
"Name": "Downhill 3",
"Apartments": [
{
"Yardage": 55.5,
"Owner": {
"name" : "Timothy",
"surname" : "Notclement",
"phone" : 555666777
}
},
{
"Yardage": 70,
"Owner": {
"name" : "Anya",
"surname" : "Joylor-Tay",
"phone" : 555111000
}
}
]
}
It represents "all" apartments at some street.
I want to delete one entry from Apartments array, let's say the one which is owned by Anya, by specifying it by something like this Apartments.Owner.name:"Anya".
How can I perform such an operation? I tried to $pull and to $unset, but nothing worked. Now I came across findAndModify (docs here) and possibility to use an aggregation pipeline in the update field, but can't really figure out how to form a query.
Mongo commands I tried:
db.ApartCollection.update( { "_id":ObjectId("example123") }, { $unset: { "Apartments.Owner.name" : "Anya" } } )
db.ApartCollection.update( { "_id":ObjectId("example123") }, { $unset: { Apartments: { "Owner.name" : "Anya" } } } )
db.ApartCollection.update( { "_id":ObjectId("example123") }, { $pull: { Apartments: { "Owner.name" : "Anya" } } } )
db.Dokumenty.update( { "_id":ObjectId("61881be8dd6d25184a3b6c3f") }, { $pull: { "Apartments.Owner.name": "Anya" } } )
^This one doesn't even ?compile? It returns error:
Cannot use the part (Owner) of (Apartments.Owner.name) to traverse the element ({Apartments:...blah blah

Using MongoDB $set to update multiple subdocuments

I have such Article-documents:
{
"_id" : "rNiwdR8tFwbTdr2oX",
"createdAt" : ISODate("2018-08-25T12:23:25.797Z"),
"title" : "Happy",
"lines" : [
{
"id" : "5efa6ad451048a0a1807916c",
"text" : "Test 1",
"align" : "left",
"indent" : 0
},
{
"id" : "ae644f39553d46f85c6e1be9",
"text" : "Test 2"
},
{
"id" : "829f874878dfd0b47e9441c2",
"text" : "Test 3"
},
{
"id" : "d0a46ef175351ae1dec70b9a",
"text" : "Test 4"
},
{
"id" : "9bbc8c8d01bc7029220bed3f",
"text" : "Test 5"
},
{
"id" : "6b5c02996a830f807e4d8e35",
"text" : "Test 6",
"indent" : 0
}
]
}
I need to update some Lines.
For example I have array with ids of the line which must be updated.
let lineIds = [
"5efa6ad451048a0a1807916c",
"829f874878dfd0b47e9441c2",
"6b5c02996a830f807e4d8e35"
];
So I try to update attributes "attr" for the "lines" and I do following:
'articles.updateLines': function (articleId, lineIds, attr, value) {
return Articles.update({
'_id': articleId,
'lines.id': { $in: lineIds }
},
{
$set: {
['lines.$.' + attr]: value
}
},
{ multi: true }
);
}
The problem is that just the first line (with id="5efa6ad451048a0a1807916c") is updated.
Any ideas? Thanks! :)
You can use $[]. This will works only MongoDB version 3.6 and above.
Refer link :
https://docs.mongodb.com/manual/reference/operator/update/positional-all/
You can also see this stackoverflow question reference:
How to add new key value or change the key's value inside a nested array in MongoDB?
You can convert below query in your function
db.col.update(
{ '_id':"rNiwdR8tFwbTdr2oX", },
{ $set: { "lines.$[elem].text" : "hello" } },
{ arrayFilters: [ { "elem.id": { $in: lineIds } } ],
multi: true
})
'lines.id': { $in: lineIds }
This won't work, because lines is an array.
What I understand from your question, you can prepare a new array with proper processing and replace the lines array with the new one. Here is an idea how to do this:
'articles.updateLines': function (articleId, lineIds, attr, value) {
let lines = Articles.findOne(articleId).lines;
// prepare a new array with right elements
let newArray = [];
for(let i=0; i<lines.length; i++){
if(lineIds.includes(lines[i].id)){
newArray.push(value)
}
else newArray.push(lines[i])
}
return Articles.update({
'_id': articleId,
},
{
$set: {
lines: newArray
}
}
);
}

How to update a mongodb object using multiple selection criteria

My schema structure looks something like this:
{
"__v":0,
"_id":ObjectId("5708423e897db8255aaaa9dd"),
"podId":169400000005,
"env":[
{
"type":"1",
"id":3852000000035,
"_id":ObjectId("5708423e897db8255aaaa9de")
},
{
"type":"2",
"id":3852000000040,
"_id":ObjectId("5708423e897db8255aaaa9df")
}
],
"name":"Test Build",
"parameters":[
{
"name":"sound.left",
"type":"1",
"paramName":"Left Sound Control",
"paramType":"booleanParameter",
"testValue":null,
"liveValue":null,
"_id":ObjectId("5708423f897db8255aaaaa0d")
},
{
"name":"sound.right",
"type":"1",
"paramName":"Right Sound Control",
"paramType":"booleanParameter",
"testValue":null,
"liveValue":null,
"_id":ObjectId("5708423f897db8255aaaaa0d")
},
...
]
}
I have 3 known variables: podId for podId, codeName for parameters.name, envType for parameters.type. Using them I want to update the object using podId, codeName, and envType, with a new variable paramValue that will contain the testValue value.
What I've tried
db.pods.update({
podId: podId,
parameters: {
$elemMatch: {
name: codeName,
type: envType
}
}
}, {
$set: {
'parameters.$.testValue': paramValue
}
});
I'm trying to update the testValue where podId == podId, parameters.name == codeName, and parameters.type == envType, but the above did not update anything.
I also tried
db.pods.update({
podId: podId,
parameters: {
name: codeName,
type: envType
}
}, {
$push: {
parameters: {
testValue: paramValue
}
}
},
function(err) {
if(err) throw err;
});
basically taking what worked to build the object when I only had to compare the podId, and adding the extra criteria; it didn't work this time.
edit: fixed schema type Number to String
Let these are variables that you have in mongo shell:
> var podId = 169400000005
> var codeName = "sound.right"
> var envType = "1"
> var paramValue = "updatedParamValue"
This query you can try:
db.test.update({"podId":podId, "parameters.name" : codeName, "parameters.type" : envType}, {$set : {"parameters.0.testValue" : paramValue,"parameters.1.testValue" : paramValue}})
Depending upon which testValue you want to update, you can pass index to that.
Update
Following result i get after the update query:
db.test.find().pretty()
{
"_id" : ObjectId("570c850ab9477f18ac5297f9"),
"__v" : 0,
"podId" : 169400000005,
"env" : [
{
"type" : "1",
"id" : 3852000000035,
"_id" : ObjectId("5708423e897db8255aaaa9de")
},
{
"type" : "2",
"id" : 3852000000040,
"_id" : ObjectId("5708423e897db8255aaaa9df")
}
],
"name" : "Test Build",
"parameters" : [
{
"name" : "sound.left",
"type" : "1",
"paramName" : "Left Sound Control",
"paramType" : "booleanParameter",
"testValue" : "updatedParamValue",
"liveValue" : null,
"_id" : ObjectId("5708423f897db8255aaaaa0d")
},
{
"name" : "sound.right",
"type" : "1",
"paramName" : "Right Sound Control",
"paramType" : "booleanParameter",
"testValue" : "updatedParamValue",
"liveValue" : null,
"_id" : ObjectId("5708423f897db8255aaaaa0d")
}
]
}
Thanks.

Search in an array of an embedded object in MongoDB

Given this structure for a school object:
{
"grade_spans" :
{
"0": {
"grade_span_key" : "K_5",
"name": "Elementary School"
},
"1": {
"grade_span_key" : "6_8",
"name": "Junior High-School"
}
}
}
How do I find a school for a given grade_span_key?
db.schools.find({ "grade_span_key": "K_5" })
returns empty.
Update: Sorry, I copied the structure incorrectly. It's actually an Embedded Object not a collection.
Update #2: There was a doctrine2 annotation I was using incorrectly: #MongoDB\EmbedMany(strategy="set"). I change the strategy to pushAll (which is the default)
If this field is just embedded into the main document #sergios answer will work just fine and it is not clear why his query wouldn't work as you don't provide an example of the document structure only of the embedded structure.
Also as #JohnnyHK says, rebuild that object as an array since dynamic keys in this case would be harder.
If you are looking to pick out matching rows from the embedded document and not the full document. This is a little harder but is possible:
db.schools.aggregate({
{$unwind: "$grade_spans"},
{$match: {"grade_spans.grade_span_key": "K_5"}},
{$group: {_id: "$_id", grade_spans: {$push: "$grade_spans"}}}
})
Something like the above should return a document of the structure:
{
_id: {},
grade_spans:[{
"grade_span_key" : "K_5",
"name" : "Elementary School"
}]
}
You should use full path to the property, in dotted notation.
> db.schools.find({"grade_spans.grade_span_key": "K_5"})
{
"_id" : ObjectId("50801cc5ab582e310adc0e41"),
"grade_spans" : [
{
"grade_span_key" : "K_5",
"name" : "Elementary School"
},
{
"grade_span_key" : "6_8",
"name" : "Junior High-School"
}
]
}
Given this structure :
{
"grade_spans" : {
"0": { "grade_span_key" : "K_5",
"name": "Elementary School" },
"1": { "grade_span_key" : "6_8",
"name": "Junior High-School" }
}
}
You can try with map/reduce function :
var mapFunction = function() {
for (var i in this.grade_spans) {
// Add the name of the school in a list
var schools = [];
schools[0] = this.grade_spans[i].name;
// Create out object : { schools : ["Elementary School"] } or { schools : ["Junior High-School"] }
var out = {};
out.schools = schools;
// Create key (K_5 or 6_8)
var key = this.grade_spans[i].grade_span_key;
emit(key, out);
}
};
var reduceFunction = function(key, values) {
var schools = [];
for (var i = 0; i < values.length; i++) {
schools.push.apply(schools, values[i].schools);
}
return {schools:schools};
}
db.schools.mapReduce(
mapFunction,
reduceFunction,
{ out: "map_reduce_grade_spans", sort: {_id:1} }
)
And then :
db.map_reduce_grade_spans.find({_id:"K_5"});

Case Insensitive search with $in

How to search a column in a collection in mongodb with $in which includes an array of elements for search and also caseInsensitive matching of those elements in the column ?
Use $in with the match being case insensitive:
Data example:
{
name : "...Event A",
fieldX : "aAa"
},
{
name : "...Event B",
fieldX : "Bab"
},
{
name : "...Event C",
fieldX : "ccC"
},
{
name : "...Event D",
fieldX : "dDd"
}
And we want documents were "fieldX" is contained in any value of the array (optValues):
var optValues = ['aaa', 'bbb', 'ccc', 'ddd'];
var optRegexp = [];
optValues.forEach(function(opt){
optRegexp.push( new RegExp(opt, "i") );
});
db.collection.find( { fieldX: { $in: optRegexp } } );
This works for $all either.
I hope this helps!
p.s.: This was my solution to search by tags in a web application.
You can use $elemMatch with regular expressions search, e.g. let's search for "blue" color in the following collection:
db.items.save({
name : 'a toy',
colors : ['red', 'BLUE']
})
> ok
db.items.find({
'colors': {
$elemMatch: {
$regex: 'blue',
$options: 'i'
}
}
})
>[
{
"name": "someitem",
"_id": { "$oid": "4fbb7809cc93742e0d073aef"},
"colors": ["red", "BLUE"]
}
]
This works for me perfectly.
From code we can create custom query like this:
{
"first_name":{
"$in":[
{"$regex":"^serina$","$options":"i"},
{"$regex":"^andreW$","$options":"i"}
]
}
}
This will transform to following in mongo after query:
db.mycollection.find({"first_name":{"$in":[/^serina$/i, /^andreW$/i]}})
Same for "$nin".
This is pretty simple
const sampleData = [
RegExp("^" + 'girl' + "$", 'i'),
RegExp("^" + 'boy' + "$", 'i')
];
const filerObj = { gender : {$in : sampleData}};
The way to do it in Java is:
List<String> nameList = Arrays.asList(name.split(PATTERN));
List<Pattern> regexList = new ArrayList<>();
for(String name: nameList) {
regexList.add(Pattern.compile(name , Pattern.CASE_INSENSITIVE));
}
criteria.where("Reference_Path").in(regexList);
Here is my case insensitive search (query) with multiple condition (regex) from data of array, I've used $in but it doesn't support case insensitive search.
Example Data
{
name : "...Event A",
tags : ["tag1", "tag2", "tag3", "tag4]
},
{
name : "...Event B",
tags : ["tag3", "tag2"]
},
{
name : "...Event C",
tags : ["tag1", "tag4"]
},
{
name : "...Event D",
tags : ["tag2", "tag4"]
}
My query
db.event.find(
{ $or: //use $and or $or depends on your needs
[
{ tags : {
$elemMatch : { $regex : '^tag1$', $options : 'i' }
}
},
{ tags : {
$elemMatch : { $regex : '^tag3$', $options : 'i' }
}
}
]
})