Search in an array of an embedded object in MongoDB - 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"});

Related

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
}
}
);
}

Mongo remove from nested object by value

I have a Mongo collection the consists of a document and a nested object describing what collections the document is in and when it was added. I would like to remove key-value pairs from a nested object based on a condition, e.g. is the value (a date) before 1-1-2016.
Example:
{
"_id" : ObjectId("581214940911ad3de98002db"),
"collections" : {
"c01" : ISODate("2016-10-27T15:52:04.512Z"),
"c02" : ISODate("2015-11-21T16:06:06.546Z")
}
}
needs to become
{
"_id" : ObjectId("581214940911ad3de98002db"),
"collections" : {
"c01" : ISODate("2016-10-27T15:52:04.512Z"),
}
}
One alternative would be to change the schema to something like this:
{
"_id" : ObjectId("581214940911ad3de98002db"),
"collections" : [
{
"id": "c01",
"date": ISODate("2016-10-27T15:52:04.512Z")
},
{
"id": "c02",
"date" : ISODate("2015-11-21T16:06:06.546Z")
}
]
}
in which case removing a document from a would be easy. I am a bit reluctant to do that because it would complicate some of the other queries I would like to support. Thanks!
I prefer the second structure for your schema
{
"_id" : ObjectId("581214940911ad3de98002db"),
"collections" : [
{
"id": "c01",
"date": ISODate("2016-10-27T15:52:04.512Z")
},
{
"id": "c02",
"date" : ISODate("2015-11-21T16:06:06.546Z")
}
]
}
then able to remove from collections like this
db.collectionName.update(
{ },// if you want can add query for specific Id {"_id" : requestId},
{ $pull: { collections: { date: {$lt: yourDate} } } }, // if need can convert iso date string like: new Date(yourDate).toISOString()
{ multi: true }
)

How do I add an array of elements in MongoDB to an array in an existing document?

In MongoDB, I'm trying to write a query to add elements from an array to an existing document, but instead of adding the elements as objects:
property: ObjectID(xxx)
the elements are getting added as just
ObjectID(xxx)
Forgive me if I get the terminology wrong. I'm completely new to MongoDB; I normally only work with relational databases. How do I properly add these new elements?
I have a collection called auctions which has two fields: ID and properties. Properties is an array of objects named property. Here's an example with two auction documents:
** I changed the object IDs to make them easier to reference in our discussion
Collection db.auctions
{
"_id" : ObjectId("abc"),
"properties" : [
{
"property" : ObjectId("prop1")
},
{
"property" : ObjectId("prop2")
},
{
"property" : ObjectId("prop3")
}]
}
{
"_id" : ObjectId("def"),
"properties" : [
{
"property" : ObjectId("prop97")
},
{
"property" : ObjectId("prop98")
}]
}
I want to add 3 new properties to auction "abc". How do I do this?
Here's is what I tried:
I have an array of properties that looks like this:
Array PropsToAdd
[
ObjectId("prop4"),
ObjectId("prop5"),
ObjectId("prop6")
]
I wrote an update query to push these properties into the properties array in auctions:
db.auctions.update(
{"_id": "abc"}
,
{ $push: { properties: { $each: PropsToAdd } } }
);
This query gave the result below. Notice that instead of adding elements named property with a value from my array, it's just added my values from my array. I obviously need to add that "property" part, but how do I do that?
Collection db.auctions (_id "abc" only)
{
"_id" : ObjectId("abc"),
"properties" : [
{
"property" : ObjectId("prop1")
},
{
"property" : ObjectId("prop2")
},
{
"property" : ObjectId("prop3")
},
ObjectId("prop4"),
ObjectId("prop5"),
ObjectId("prop6"),
ObjectId("prop7")]
}
The result I'm looking for is this:
Collection db.auctions (_id "abc" only)
{
"_id" : ObjectId("abc"),
"properties" : [
{
"property" : ObjectId("prop1")
},
{
"property" : ObjectId("prop2")
},
{
"property" : ObjectId("prop3")
},
{
"property" : ObjectId("prop4")
},
{
"property" : ObjectId("prop5")
},
{
"property" : ObjectId("prop6")
}
}
Here is some further information on that array of properties I'm adding. I get it from running these queries. Perhaps one of them needs changed?
This query gets an array of current properties:
var oldActiveProperties = db.properties.distinct( "saleNumber", { "active": true, "auction": ObjectId("abc") } );
Then those results are used to find properties in the new file that weren't in the old file:
var PropsToAdd = db.newProperties.distinct(
"_id"
, { "saleNumber": { "$nin": oldActiveProperties }, "active": true}
);
The resulting array is what I need to add to the auctions collection.
Use the JavaScript's native map() method to map the array into an array of documents. The following shows this:
var PropsToAdd = db.newProperties.distinct("_id",
{ "saleNumber": { "$nin": oldActiveProperties }, "active": true}
).map(function (p) { return { property: p }; });
db.auctions.update(
{"_id": "abc"},
{ $push: { "properties": { "$each": PropsToAdd } } }
);

how to query for exact mach in unknown number of subfields in mongodb

I have a collection where documents can have an unknown number of sub documents:
"agent_id": {
"0":"1234",
"1":"2234",...etc
How do I search for an exact match in all the agent_id sub-fields?
You need to dynamically create an object with properties that are a concatenation of the embedded document name agent_id with the dot (.) and the field name, enclosed in quotes, something like this:
var query = {
"agent_id.0": "78343",
"agent_id.1": "78343",
"agent_id.2": "78343",
"agent_id.3": "78343",
...
"agent_id.n": "78343"
}
One way to create the object is generate the sub-documents keys with mapReduce. The following demonstrates this approach. In the Map-Reduce operation, an array of keys in the agent_id subdocument is generated to an output collection "collection_keys" and then used to produce the find() query expression:
Suppose you populate a sample collection
db.collection.insert([
{
"agent_id": {
"0":"1234",
"1":"2234",
"56":"8451",
"74":"1475",
"10":"1234"
}
},
{
"agent_id": {
"5":"5874",
"18":"2351"
}
}
])
Running the following mapReduce operation
var mr = db.runCommand({
"mapreduce" : "collection",
"map" : function() {
for (var key in this.agent_id) { emit(key, null); }
},
"reduce" : function(key, stuff) {
return null
},
"out": "collection" + "_keys"
});
var query = { "$or": [] },
value = "1234";
db[mr.result].distinct("_id").forEach(function (key){
var obj = {};
obj["agent_id." + key] = value;
query["$or"].push(obj)
});
printjson(query);
will produce:
{
"$or" : [
{
"agent_id.0" : "1234"
},
{
"agent_id.1" : "1234"
},
{
"agent_id.10" : "1234"
},
{
"agent_id.18" : "1234"
},
{
"agent_id.5" : "1234"
},
{
"agent_id.56" : "1234"
},
{
"agent_id.74" : "1234"
}
]
})
You can then use the query document in your find() query:
db.collection.find(query)
which will produce the result:
/* 0 */
{
"_id" : ObjectId("561d5312cd05efc95a1ea1f4"),
"agent_id" : {
"0" : "1234",
"1" : "2234",
"56" : "8451",
"74" : "1475",
"10" : "1234"
}
}

Is it possible to retrieve a 'time span' from a MongoDB query, using the timestamp within an ObjectId?

We have a basic enquiry management tool that we're using to track some website enquiries in our administration suite, and we're using the ObjectId of each document in our enquiries collection to sort the enquiries by the date they were added.
{
"_id" : ObjectId("53a007db144ff47be1000003"),
"comments" : "This is a test enquiry. Please ignore. We'll delete it shortly.",
"customer" : {
"name" : "Test Enquiry",
"email" : "test#test.com",
"telephone" : "07890123456",
"mobile" : "07890123456",
"quote" : false,
"valuation" : false
},
"site" : [],
"test" : true,
"updates" : [
{
"_id" : ObjectId("53a007db144ff47be1000001"),
"status" : "New",
"status_id" : ObjectId("537de7c3a5e6e668ffc2335c"),
"status_index" : 100,
"substatus" : "New Web Enquiry",
"substatus_id" : ObjectId("5396bb9fa5e6e668ffc23388"),
"notes" : "New enquiry received from website.",
},
{
"_id" : ObjectId("53a80c977d299cfe91bacf81"),
"status" : "New",
"status_id" : ObjectId("537de7c3a5e6e668ffc2335c"),
"status_index" : 100,
"substatus" : "Attempted Contact",
"substatus_id" : ObjectId("53a80e06a5e6e668ffc2339e"),
"notes" : "In this test, we pretend that we've not managed to get hold of the customer on the first attempt.",
},
{
"_id" : ObjectId("53a80e539b966b8da5c40c36"),
"status" : "Approved",
"status_id" : ObjectId("52e77a49d85e95f00ebf6c72"),
"status_index" : 200,
"substatus" : "Enquiry Confirmed",
"substatus_id" : ObjectId("53901f1ba5e6e668ffc23372"),
"notes" : "In this test, we pretend that we've got hold of the customer after failing to contact them on the first attempt.",
}
]
}
Within each enquiry is an updates array of objects which also have an ObjectId as their main identity field. We're using an $unwind and $group aggregation to pull the first and latest updates, as well as the count of updates, making sure we only take enquiries where there have been more than one update (as one is automatically inserted when the enquiry is made):
db.enquiries.aggregate([
{
$match: {
"test": true
}
},
{
$unwind: "$updates"
},
{
$group: {
"_id": "$_id",
"latest_update_id": {
$last: "$updates._id"
},
"first_update_id": {
$first: "$updates._id"
},
"update_count": {
$sum: 1
}
}
},
{
$match: {
"update_count": {
$gt: 1
}
}
}
])
This results in the following output:
{
"result" : [
{
"_id" : ObjectId("53a295ad122ea80200000005"),
"latest_update_id" : ObjectId("53a80bdc7d299cfe91bacf7e"),
"first_update_id" : ObjectId("53a295ad122ea80200000003"),
"update_count" : 2
},
{
"_id" : ObjectId("53a007db144ff47be1000003"),
"latest_update_id" : ObjectId("53a80e539b966b8da5c40c36"),
"first_update_id" : ObjectId("53a007db144ff47be1000001"),
"update_count" : 3
}
],
"ok" : 1
}
This is then passed through to our code (node.js, in this case) where we perform a few operations on it and then present some information on our dashboard.
Ideally, I'd like to add another $group pipeline aggregation to the query which would subtract the timestamp of first_update_id from the timestamp of latest_update_id to give us a timespan, which we could then use $avg on.
Can anyone tell me if this is possible? (Thank you!)
As Neil already pointed out, you can't get to the timestamp from the ObjectId in the aggregation framework.
You said that speed is not important, so using MapReduce you can get what you want:
var map = function() {
if (this.updates.length > 1) {
var first = this.updates[0];
var last = this.updates[this.updates.length - 1];
var diff = last._id.getTimestamp() - first._id.getTimestamp();
var val = {
latest_update_id : last._id,
first_update_id : first._id,
update_count : this.updates.length,
diff: diff
}
emit(this._id, val);
}
};
var reduce = function() { };
db.runCommand(
{
mapReduce: "enquiries",
map: map,
reduce: reduce,
out: "mrresults",
query: { test : true}
}
);
This are the results:
{
"_id" : ObjectId("53a007db144ff47be1000003"),
"value" : {
"latest_update_id" : ObjectId("53a80e539b966b8da5c40c36"),
"first_update_id" : ObjectId("53a007db144ff47be1000001"),
"update_count" : 3,
"diff" : 525944000
}
}
Edit:
If you want to get the average diff for all documents you can do it like this:
var map = function() {
if (this.updates.length > 1) {
var first = this.updates[0];
var last = this.updates[this.updates.length - 1];
var diff = last._id.getTimestamp() - first._id.getTimestamp();
emit("1", {diff : diff});
}
};
var reduce = function(key, values) {
var reducedVal = { count: 0, sum: 0 };
for (var idx = 0; idx < values.length; idx++) {
reducedVal.count += 1;
reducedVal.sum += values[idx].diff;
}
return reducedVal;
};
var finalize = function (key, reducedVal) {
reducedVal.avg = reducedVal.sum/reducedVal.count;
return reducedVal;
};
db.runCommand(
{
mapReduce: "y",
map: map,
reduce: reduce,
finalize : finalize,
out: "mrtest",
query: { test : true}
}
);
And the example output:
> db.mrtest.find().pretty()
{
"_id" : "1",
"value" : {
"count" : 2,
"sum" : 1051888000,
"avg" : 525944000
}
}