How to write the following MongoDB query - mongodb

Suppose I have the following foobar collection:
{
"foobar": [
{
"_id": "abc",
"history": [
{
"type": "foo",
"timestamp": 123456789.0
},
{
"type": "bar",
"timestamp": 123456789.0
}
]
},
{
"_id": "dfg",
"history": [
{
"type": "baz",
"timestamp": 123456789.0
},
{
"type": "bar",
"timestamp": 123456789.0
}
]
},
{
"_id": "hij",
"history": [
{
"type": "foo",
"timestamp": 123456789.0
},
{
"type": "bar",
"timestamp": 123456789.0
},
{
"type": "foo",
"timestamp": 123456789.0
}
]
}
]
}
How can I query ($gte/$lte) the foobar items based on the timestamp prop inside their history items, but using the timestamp from the item where type: "foo"?
If there is no subdocuments with type equals to foo, the entire document is filtered out, if there is more than one subdocument with type equals to foo then it could match anyone.

You can try below aggregation:
var threshold = 123456788;
db.foobar.aggregate([
{
$addFields: {
foobar: {
$filter: {
input: "$foobar",
as: "doc",
cond: {
$let: {
vars: {
foo: {
$arrayElemAt: [
{
$filter: {
input: "$$doc.history",
as: "history",
cond: {
$eq: [ "$$history.type", "foo" ]
}
}
},
0]
}
},
in: {
$gt: [ "$$foo.timestamp", threshold ]
}
}
}
}
}
}
}
])
$addFields can be used to overwrite existing field (foobar). To filter out all subdocuments not matching your condition you can use $filter (the outer one). For each foobar document you can use $let to define temporary variable foo. You need inner $filter to get all history elements where type is foo and then $arrayElemAt to get first one. Then you just need $gt or $lt to apply your comparison.
For those subdocuments where there's no foo there will be undefined returned which will be then filtered out on $gt stage.

Related

MongoDB: return a single field from the matched item of a sub-document array

Consider I have the following document structure:
{
"_id": ObjectID(),
"foo": "FOO",
"bar": "BAR",
"items": [
{
"foo": "FOO",
"bar": "BAR",
"name": "hello",
"value": "50"
},
{
"foo": "FOO",
"bar": "BAR",
"name": "bye",
"value": "300"
},
{
"foo": "FOO",
"bar": "BAR",
"name": "welcome",
"value": "500"
}
],
}
I would like to find all items that match both the following conditions:
name = "hello"
value != 0
And for each matched item I would like to return only the value field. I don't need all the other fields (foo/bar in this example).
So the ideal result should look like this:
[
{ value: "50" },
{ value: "100" },
{ value: "30" },
…
]
How do I do this with MongoDB?
I've tried this query:
// filter
{
items: {
$elemMatch: {
name: "hello",
value: { $ne: "0" },
},
}
}
// projection
{
"_id": 0,
"items.$": 1
}
It matches the items correctly, but it returns the whole items and I want only a single field from it.
Sadly, I can't use projection like this: "items.$.value": 1.
I've also tried the following aggregation:
{
$unwind: {
path: "$items"
}
}
{
$match: {
"items.name": "hello",
"items.value": { $ne: "0" },
}
}
{
$replaceRoot: {
newRoot: "$items"
}
}
{
$project: {
"value": 1
}
}
It works perfectly and returns the expected result, but I have a feeling that it will have poorer performance.
Is there a way to achieve what I want with optimal performance?
Maybe something like this:
db.collection.aggregate([
{
$match: {
items: {
$elemMatch: {
"name": "hello",
"value": {
$ne: "0"
}
}
}
}
},
{
$project: {
items: {
"$map": {
input: {
"$filter": {
"input": "$items",
"as": "i",
"cond": {
$and: [
{
$ne: [
"$$i.value",
0
]
},
{
$eq: [
"$$i.name",
"hello"
]
}
]
}
}
},
as: "m",
"in": {
"value": "$$m.value"
}
}
}
}
},
{
$unwind: "$items"
},
{
"$replaceRoot": {
"newRoot": "$items"
}
}
])
Explained:
Match only documents having at least 1x items.element with name:"hello" and value!=0 ( good to have index on items.name+items.value , this match stage is expected to reduce the data that you want to pass to the next stages -> less data = better performance )
Filter only the values for matched items in the project stage.
( This will remove unnecessary items array sub-items , again less data = better performance )
Unwind only the already filtered ( keeping unwind in the later stages will save alot of resources if the collection is big ... )
replace the root with the necessary values only ( this is to have the output as in expected format )
Playground
Indeed as identified simple match stage will not provide correct results and $elemMatch must be used in the first $match stage ...

How to count embedded array object elements in mongoDB

{
"orderNo": "123",
"bags": [{
"type": "small",
"products": [{
"id": "1",
"name": "ABC",
"returnable": true
}, {
"id": "2",
"name": "XYZ"
}
]
},{
"type": "big",
"products": [{
"id": "3",
"name": "PQR",
"returnable": true
}, {
"id": "4",
"name": "UVW"
}
]
}
]
}
I have orders collection where documents are in this format. I want to get a total count of products which has the returnable flag. e.g: for the above order the count should be 2. I am very new to MongoDB wanted to know how to write a query to find this out, I have tried few things but did not help:
this is what I tried but not worked:
db.orders.aggregate([
{ "$unwind": "$bags" },
{ "$unwind": "$bags.products" },
{ "$unwind": "$bags.products.returnable" },
{ "$group": {
"_id": "$bags.products.returnable",
"count": { "$sum": 1 }
}}
])
For inner array you can use $filter to check returnable flag and $size to get number of such items. For the outer one you can take advantage of $reduce to sum the values from inner arrays:
db.collection.aggregate([
{
$project: {
totalReturnable: {
$reduce: {
input: "$bags",
initialValue: 0,
in: {
$add: [
"$$value",
{
$size: {
$filter: {
input: "$$this.products",
as: "prod",
cond: {
$eq: [ "$$prod.returnable", true ]
}
}
}
]
}
}
}
}
}
}
])
Mongo Playground

Filter result in mongo sub-sub array

I have some collections such us this:
[{
"_id": ObjectId("604f3ae3194f2135b0ade569"),
"parameters": [
{
"_id": ObjectId("602b7455f4b4bf5b41662ec1"),
"name": "Purpose",
"options": [
{
"id": ObjectId("602b764ff4b4bf5b41662ec2"),
"name": "debug",
"sel": false
},
{
"id": ObjectId("602b767df4b4bf5b41662ec3"),
"name": "performance",
"sel": false
},
{
"id": ObjectId("602b764ff4b4bf5b41662ec4"),
"name": "security",
"sel": false
},
{
"id": ObjectId("602b767df4b4bf5b41662ec5"),
"name": "Not Applicable",
"sel": false
}
],
"type": "multiple"
},
{
"_id": ObjectId("602b79d35d4a1333b8b6e5ba"),
"name": "Organization",
"options": [
{
"id": ObjectId("602b79d353c89933b8238325"),
"name": "SW",
"sel": false
},
{
"id": ObjectId("602b79d353c89933b8238326"),
"name": "HW",
"sel": false
}
],
"type": "multiple"
}
]
}]
The parameters are most 30.
I need to implements in mongo a "filter" collections.
If I filter one or more parameters._id, mongo return:
collection _id that have match options.sel of this parameters._id
collection _id that have all options.sel equal to false of this parameters._id
non return collection _id if parameters._id has set up options.name:"Not Applicable" at value options.sel:true
For example, if I match parameters._id:ObjectId("602b7455f4b4bf5b41662ec1") and this parameters.options.id:ObjectId("602b764ff4b4bf5b41662ec2"), I expect:
not collection _id that has, for parameters._id:ObjectId("602b7455f4b4bf5b41662ec1"), the specific parameters.options.id: ObjectId("602b767df4b4bf5b41662ec5") at value options.sel:true
all collection _id that match with query
all collection _id that has, for parameters._id:ObjectId("602b7455f4b4bf5b41662ec1"), all specific parameters.options.sel:false
Next I need to make this rule for more parameters.
I have think to implements three aggregation for every rule...
Do you have suggestion?
Try this query:
db.testCollection.aggregate([
{ $unwind: "$parameters" },
{
$match: {
"parameters._id": ObjectId("602b7455f4b4bf5b41662ec1"),
"parameters.options.id": ObjectId("602b767df4b4bf5b41662ec5")
}
},
{
$addFields: {
options: {
$filter: {
input: "$parameters.options",
as: "option",
cond: {
$and: [
{ $ne: ["$$option.sel", true] },
{ $ne: ["$$option.name", "Not Applicable"] }
]
}
}
}
}
}
])
With idea of #dheemanth-bath,
I have make this query:
db.testCollection.aggregate([
{ $unwind: "$parameters" },
{
$match: {
"parameters._id": ObjectId("602b7455f4b4bf5b41662ec1"),
}
},
{
$addFields: {
match: {
$filter: {
input: "$parameters.options",
as: "option",
cond: {
$and: [
{ $eq: ["$$option.id", ObjectId('602b767df4b4bf5b41662ec5')] },
{ $eq: ["$$option.sel", true] }
]
}
}
},
notDeclared: {
$filter: {
input: "$parameters.options",
as: "option",
cond: {
$and: [
{ $eq: ["$$option.name", "Not Applicable"]},
{ $eq: ["$$option.sel", true] }
]
}
}
}
}
}
])
The idea is that: after query count number of element of notDeclared. If > 0 waste the collection. Otherwise evaluate number of match, if is >0, there is at least one elements can match.
Good. But how I evaluate if all elements of options.sel are false?
And if I check another parameters, I need to make another aggregation (one for parameters)?

Sort records by array field values in MongoDb

I have a collection which has documents like;
{
"name": "Subject1",
"attributes": [{
"_id": "security_level1",
"level": {
"value": "100",
"valueKey": "ABC"
}
}, {
"_id": "security_score1",
"level": {
"value": "1000",
"valueKey": "CDE"
}
}
]
},
{
"name": "Subject2",
"attributes": [{
"_id": "security_level1",
"level": {
"value": "99",
"valueKey": "XYZ"
}
}, {
"_id": "security_score1",
"level": {
"value": "2000",
"valueKey": "EDF"
}
}
]
},
......
Each document will have so many attributes generated dynamically, can be different in size.
Is it possible to sort records based on level.value of security_level1? (security_level1 is _id field value)
As per above example, the second document ("name": "Subject2") should come first as the value ('level.value') of _id:security_level1 is 99, which is less than of Subject1's security_level1 value (100) - (Ascending order)
Use $filter and $arrayElemAt to get security_level1 item. Then you can use $toInt to convert that value to an integer so that $sort can be applied:
db.collection.aggregate([
{
$addFields: {
level: {
$let: {
vars: {
level_1: { $arrayElemAt: [ { $filter: { input: "$attributes", cond: { $eq: [ "$$this._id", "security_level1" ] } } } ,0] }
},
in: {
$toInt: "$$level_1.level.value"
}
}
}
}
},
{
$sort: {
level: 1
}
}
])
Mongo Playground

Mongo db not in query by having two subset of documents from same collection

I am new to mongodb. Assume the following. There are 3 types of documents in one collection x, y and z.
docs = [{
"item_id": 1
"type": "x"
},
{
"item_id": 2
"type": "x"
},{
"item_id": 3
"type": "y",
"relavent_item_ids": [1, 2]
},
{
"item_id": 3
"type": "y",
"relavent_item_ids": [1, 2, 3]
},{
"item_id": 4
"type": "z",
}]
I want to get the following.
Ignore the documents with type z
Get all the documents of type x where it's item_id is not in relavent_item_ids of type y documents.
The result should have item_id field.
I tried doing match $in but this returns me all the records, I am unable to figure out how to have in condition with subset of documents of type y.
You can use below query
const item_ids = (await db.collection.find({ "type": "y" })).map(({ relavent_item_ids }) => relavent_item_ids)
const result = db.collection.find({
"item_id": { "$exists": true },
"type": { "$ne": "z", "$eq": "x" },
"relavent_item_ids": { "$nin": item_ids }
})
console.log({ result })
Ignore the documents with type z --> Use $ne not equal to query operator to filter out z types.
Get all the documents of type x where it's item_id is not in relavent_item_ids of type y documents --> Use $expr to match the same documents fields.
The result should have item_id field --> Use $exists query operator.
The solution:
db.test.aggregate( [
{
$facet: {
firstQuery: [
{
$match: { type: { $eq: "x", $ne: "z" } }
},
{
$project: {
item_id : 1, _id: 0
}
}
],
secondQuery: [
{
$match: { type: "y" }
},
{
$group: {
_id: null,
relavent: { $push: "$relavent_item_ids" }
}
},
{
$project: {
relavent: {
$reduce: {
input: "$relavent",
initialValue: [ ],
in: { $setUnion: [ "$$value", "$$this" ] }
}
}
}
}
]
}
},
{
$addFields: { secondQuery: { $arrayElemAt: [ "$secondQuery", 0 ] } }
},
{
$project: {
result: {
$filter: {
input: "$firstQuery" ,
as: "e",
cond: { $not: [ { $in: [ "$$e.item_id", "$secondQuery.relavent" ] } ] }
}
}
}
},
] )
Using the input documents in the question post and adding one more following document to the collection:
{
"item_id": 11,
"type": "x",
}
: only this document's item_id (value 11) will show in the output.
The aggregation uses a $facet to make two individual queries with a single pass. The first query gets all the "x" types (and ignores type "z") as an array. The second query gets an array of relavent_item_ids with unique values (from the documents of type "y"). The final, $project stage filters the first query result array with the condition:
Get all the documents of type x where it's item_id is not in
relavent_item_ids of type y documents
I am not sure if its an elegant solution.
db.getCollection('test').aggregate([
{
"$unwind": {
"path": "$relavent_item_ids",
"preserveNullAndEmptyArrays": true
}
},
{
"$group": {
"_id":null,
"relavent_item_ids": {"$addToSet":"$relavent_item_ids"},
"other_ids": {
"$addToSet":{
"$cond":[
{"$eq":["$type", "x"]},
"$item_id",
null
]
}
}
}
},
{
"$project":{
"includeIds": {"$setDifference":["$other_ids", "$relavent_item_ids"]}
}
},
{
"$unwind": "$includeIds"
},
{
"$match": {"includeIds":{"$ne":null}}
},
{
"$lookup":{
"from": "test",
"let": { "includeIds": "$includeIds"},
"pipeline": [
{ "$match":
{ "$expr":
{ "$and":
[
{ "$eq": [ "$item_id", "$$includeIds" ] },
{ "$eq": [ "$type", "x" ] }
]
}
}
}
],
"as": "result"
}
},
{
"$unwind": "$result"
},
])