select custom array element by index from document field - mongodb

Ho can I select specific array element by index defined in document?
For example I have following document:
{ "_id" : 1, "idx" : 1, "vals" : [ 1, 2 ] }
And I want to select vals element defined by idx index.
I have managed to select specific array element defined by literal:
> db.test.find({_id:1}, {vals:{$slice:[1, 1]}})
{ "_id" : 1, "idx" : 1, "vals" : [ 2 ] }
But how can I use idx field in $slice operator?

The optimal way to do this is in MongoDB 3.2 using the $arrayElemAt operator:
db.test.aggregate([
{ "$project": { "vals": { "$arrayElemAt": [ "$vals", "$idx" ] } } }
])
You can also use findOne if you use _id in your query criteria and to get idx value.
var idx = db.test.findOne({ "_id": 1 }).idx
db.test.find({ "_id": 1 }, { "vals": { "$slice": [ idx, 1 ]}})
With find you need to use cursor.map
db.test.find().map(function(doc) {
doc.vals = doc.vals.slice(doc.idx, 2);
return doc;
})
Result:
[ { "_id" : 1, "asd" : 1, "vals" : [ 2 ] } ]

Related

Mongodb - find query based on an element of an array

I have the following type of document where I have to find instock elements based on a given value. i.e Return all instocks where ele = 5.
{"item":"journal",
"instock":[
{ "warehouse":"A", "ele":[2,4,5] },
{ "warehouse":"C", "ele":[8,5,2] },
{ "warehouse":"F", "ele":[3] },
{ "warehouse":"K", "ele":[2,8,4] }
]
}
I tried to use $elemMatch but it just produces the first element.
db.inventory.find({"item": "journal"}, {"_id": 0, "item": 0, "instock":{$elemMatch: {"ele": {$in: [5]}}} })
But it just gives:
{ "instock" : [
{ "warehouse" : "A", "ele" : [ 2, 4, 5 ] }
]}
And the expectation is
{ "instock" : [
{ "warehouse" : "A", "ele" : [ 2, 4, 5 ] },
{ "warehouse" : "C", "ele" : [ 8, 5, 2 ] }
]}
How should I get the expected result?
The $elemMatch or instock.$ will return first match document in find()'s projection,
You can use aggregation expression in projection from MongoDB 4.4, for your example use array $filter operator in projection,
db.collection.find({
"item": "journal"
},
{
instock: {
$filter: {
input: "$instock",
cond: { $in: [5, "$$this.ele"] }
}
}
})
Playground
For the old version you can use aggregate() using above same operator.
I suggest you to unwind the instock array, then filter the data, and finally if you want you can group the result by item to obtain the desired result.
Here is an exemple: solution

How to get unique array from sub-document filed of object array in mongodb

Let's say you have documents in your collection and each document has an array of objects and you want to get in your projection a distinct array of one of your fields in these objects, how do you do that?
for example, your collection includes 2 documents:
db.products.insertMany([
{name:"A",subarray:[{id:1,value:1},{id:1,value:2},{id:2,value:3}]},
{name:"B",subarray:[{id:1,value:1},{id:2,value:2},{id:3,value:3}]},
{name:"C",subarray:[{id:2,value:1},{id:2,value:2},{id:3,value:3}]}])
and you want to get this (unique_id array of the id's of each document ):
{ "name" : "a", "unique_id" : [ 1, 2 ] }
{ "name" : "b", "unique_id" : [ 1, 2, 3 ] }
{ "name" : "b", "unique_id" : [ 2, 3 ] }
how do you do that?
you can do it with $reduce and $setUnion like this:
db.products.aggregate([{
$project:{
_id:0,
name:1,
unique_id:{
$reduce:{
input: "$subarray.id",
initialValue: [],
in: {
$setUnion: ["$$value", ["$$this"]]
}
}
}
}
}])

how to query records which field A before field B in the doc

Does MongoDB support query like this?
for example I have data like this
> db.foo.find()
{ "_id" : 1, "x" : 1, "y" : 2, "z" : 3 }
{ "_id" : 2, "y" : 2, "x" : 1, "z" : 3 }
{ "_id" : 3, "z" : 3, "y" : 2, "x" : 1 }
now I want to query the records which field y before field x, that is the last two records.
Does MongoDB support it?
You can use following aggregation:
db.foo.aggregate([
{
$addFields: {
keys: {
$map: {
input: { $objectToArray: "$$ROOT" },
as: "item",
in: "$$item.k"
}
}
}
},
{
$match: {
$expr: { $lt: [ { $indexOfArray: [ "$keys", "y" ] } , { $indexOfArray: [ "$keys", "x" ] } ] }
}
},
{
$project: {
keys: 0
}
}
])
$objectToArray can transform your root document to an array of key-value pairs. Then you can use $indexOfArray to get the position of x and y keys and compare them using $expr.
Two things you need to be aware of (based on this page):
Updates that include renaming of field names may result in the reordering of fields in the document.
Starting in version 2.6, MongoDB actively attempts to preserve the field order in a document. Before version 2.6, MongoDB did not actively preserve the order of the fields in a document.

Mongodb retrieving subarray, aggregation performance [duplicate]

This question already has answers here:
Retrieve only the queried element in an object array in MongoDB collection
(18 answers)
Closed 5 years ago.
The community reviewed whether to reopen this question 5 months ago and left it closed:
Original close reason(s) were not resolved
I have array in subdocument like this
{
"_id" : ObjectId("512e28984815cbfcb21646a7"),
"list" : [
{
"a" : 1
},
{
"a" : 2
},
{
"a" : 3
},
{
"a" : 4
},
{
"a" : 5
}
]
}
Can I filter subdocument for a > 3
My expect result below
{
"_id" : ObjectId("512e28984815cbfcb21646a7"),
"list" : [
{
"a" : 4
},
{
"a" : 5
}
]
}
I try to use $elemMatch but returns the first matching element in the array
My query:
db.test.find( { _id" : ObjectId("512e28984815cbfcb21646a7") }, {
list: {
$elemMatch:
{ a: { $gt:3 }
}
}
} )
The result return one element in array
{ "_id" : ObjectId("512e28984815cbfcb21646a7"), "list" : [ { "a" : 4 } ] }
and I try to use aggregate with $match but not work
db.test.aggregate({$match:{_id:ObjectId("512e28984815cbfcb21646a7"), 'list.a':{$gte:5} }})
It's return all element in array
{
"_id" : ObjectId("512e28984815cbfcb21646a7"),
"list" : [
{
"a" : 1
},
{
"a" : 2
},
{
"a" : 3
},
{
"a" : 4
},
{
"a" : 5
}
]
}
Can I filter element in array to get result as expect result?
Using aggregate is the right approach, but you need to $unwind the list array before applying the $match so that you can filter individual elements and then use $group to put it back together:
db.test.aggregate([
{ $match: {_id: ObjectId("512e28984815cbfcb21646a7")}},
{ $unwind: '$list'},
{ $match: {'list.a': {$gt: 3}}},
{ $group: {_id: '$_id', list: {$push: '$list.a'}}}
])
outputs:
{
"result": [
{
"_id": ObjectId("512e28984815cbfcb21646a7"),
"list": [
4,
5
]
}
],
"ok": 1
}
MongoDB 3.2 Update
Starting with the 3.2 release, you can use the new $filter aggregation operator to do this more efficiently by only including the list elements you want during a $project:
db.test.aggregate([
{ $match: {_id: ObjectId("512e28984815cbfcb21646a7")}},
{ $project: {
list: {$filter: {
input: '$list',
as: 'item',
cond: {$gt: ['$$item.a', 3]}
}}
}}
])
$and:
get data between 0-5:
cond: {
$and: [
{ $gt: [ "$$item.a", 0 ] },
{ $lt: [ "$$item.a", 5) ] }
]}
Above solution works best if multiple matching sub documents are required.
$elemMatch also comes in very use if single matching sub document is required as output
db.test.find({list: {$elemMatch: {a: 1}}}, {'list.$': 1})
Result:
{
"_id": ObjectId("..."),
"list": [{a: 1}]
}
Use $filter aggregation
Selects a subset of the array to return based on the specified
condition. Returns an array with only those elements that match the
condition. The returned elements are in the original order.
db.test.aggregate([
{$match: {"list.a": {$gt:3}}}, // <-- match only the document which have a matching element
{$project: {
list: {$filter: {
input: "$list",
as: "list",
cond: {$gt: ["$$list.a", 3]} //<-- filter sub-array based on condition
}}
}}
]);

sort array in query and project all fields

I would like to sort a nested array at query time while also projecting all fields in the document.
Example document:
{ "_id" : 0, "unknown_field" : "foo", "array_to_sort" : [ { "a" : 3, "b" : 4 }, { "a" : 3, "b" : 3 }, { "a" : 1, "b" : 0 } ] }
I can perform the sorting with an aggregation but I cannot preserve all the fields I need. The application does not know at query time what other fields may appear in each document, so I am not able to explicitly project them. If I had a wildcard to project all fields then this would work:
db.c.aggregate([
{$unwind: "$array_to_sort"},
{$sort: {"array_to_sort.b":1, "array_to_sort:a": 1}},
{$group: {_id:"$_id", array_to_sort: {$push:"$array_to_sort"}}}
]);
...but unfortunately, it produces a result that does not contain the "unknown_field":
{
"_id" : 0,
"array_to_sort" : [
{
"a" : 1,
"b" : 0
},
{
"a" : 3,
"b" : 3
},
{
"a" : 3,
"b" : 4
}
]
}
Here is the insert command incase you would like to experiment:
db.c.insert({"unknown_field": "foo", "array_to_sort": [{"a": 3, "b": 4}, {"a": 3, "b":3}, {"a": 1, "b":0}]})
I cannot pre-sort the array because the sort criteria is dynamic. I may be sorting by any combination of a and/or b ascending/descending at query time. I realize I may need to do this in my client application, but it would be sweet if I could do it in mongo because then I could also $slice/skip/limit the results for paging instead of retrieving the entire array every time.
Since you are grouping on the document _id you can simply place the fields you wish to keep within the grouping _id. Then you can re-form using $project
db.c.aggregate([
{ "$unwind": "$array_to_sort"},
{ "$sort": {"array_to_sort.b":1, "array_to_sort:a": 1}},
{ "$group": {
"_id": {
"_id": "$_id",
"unknown_field": "$unknown_field"
},
"Oarray_to_sort": { "$push":"$array_to_sort"}
}},
{ "$project": {
"_id": "$_id._id",
"unknown_field": "$_id.unknown_field",
"array_to_sort": "$Oarray_to_sort"
}}
]);
The other "trick" in there is using a temporary name for the array in the grouping stage. This is so when you $project and change the name, you get the fields in the order specified in the projection statement. If you did not, then the "array_to_sort" field would not be the last field in the order, as it is copied from the prior stage.
That is an intended optimization in $project, but if you want the order then you can do it as above.
For completely unknown structures there is the mapReduce way of doing things:
db.c.mapReduce(
function () {
this["array_to_sort"].sort(function(a,b) {
return a.a - b.a || a.b - b.b;
});
emit( this._id, this );
},
function(){},
{ "out": { "inline": 1 } }
)
Of course that has an output format that is specific to mapReduce and therefore not exactly the document you had, but all the fields are contained under "values":
{
"results" : [
{
"_id" : 0,
"value" : {
"_id" : 0,
"some_field" : "a",
"array_to_sort" : [
{
"a" : 1,
"b" : 0
},
{
"a" : 3,
"b" : 3
},
{
"a" : 3,
"b" : 4
}
]
}
}
],
}
Future releases ( as of writing ) allow you to use a $$ROOT variable in aggregate to represent the document:
db.c.aggregate([
{ "$project": {
"_id": "$$ROOT",
"array_to_sort": "$array_to_sort"
}},
{ "$unwind": "$array_to_sort"},
{ "$sort": {"array_to_sort.b":1, "array_to_sort:a": 1}},
{ "$group": {
"_id": "$_id",
"array_to_sort": { "$push":"$array_to_sort"}
}}
]);
So there is no point there using the final "project" stage as you do not actually know the other fields in the document. But they will all be contained (including the original array and order ) within the _id field of the result document.