Conditional match on an existence of a field in collection - mongodb

Please help me. I have a collection as below
[
{
"_id":{
"$oid":"62a3673660e2f16c7a7bc088"
},
"merchant":{
"$oid":"62a3640560e2f16c7a7bc078"
},
"title":"24 Test 1",
"filter_conditions":{
"city":[
"AAA",
"BBB",
"CCC",
"DDD"
],
"state":[
],
"pincode":[
"12345"
]
}
},
{
"_id":{
"$oid":"62a3673660e2f16c7a7bc089"
},
"merchant":{
"$oid":"62a3640560e2f16c7a7bc079"
},
"title":"24 Test 2",
"filter_conditions":{
"city":[
"AAA",
"BBB"
]
}
}
]
I want to filter data based on pincode/city/state
if pincode is present match it and ignore city and state
elseif city is present match it and ignore state
else match on state

You can use an aggregation pipeline with a $filter:
If any of the fields does not exist on the doc, create it with an empty array.
Use $filter to grade the docs, so the grade for matching pincode is 100, for matching city is 10 for matching state is 1. Use $max to keep the best grade only.
Return the doc with highest grade.
db.collection.aggregate([
{$set: {
"filter_conditions.pincode": {$ifNull: ["$filter_conditions.pincode", []]},
"filter_conditions.city": {$ifNull: ["$filter_conditions.city", []]},
"filter_conditions.state": {$ifNull: ["$filter_conditions.state", []]}
}
},
{$set: {
grade: {
$max: [
{$multiply: [
{$size: {
$filter: {
input: "$filter_conditions.pincode",
as: "item",
cond: {$eq: ["$$item", "12345"]}
}
}
}, 100]
},
{$multiply: [
{$size: {
$filter: {
input: "$filter_conditions.city",
as: "item",
cond: {$eq: ["$$item", "BBB"]}
}
}
}, 10]
},
{$multiply: [
{$size: {
$filter: {
input: "$filter_conditions.state",
as: "item",
cond: {$eq: ["$$item", "AL"]}
}
}
}, 1]
}
]
}
}
},
{$sort: {grade: -1}},
{$limit: 1}
])
See how it works on the playground example

You can work with nested $cond to perform the filtering.
Concept:
Check filter_conditions.pincode is existed.
1.1. If true, check the value is existed in filter_conditions.pincode array.
1.2. Else, proceed to 2.
Check filter_conditions.city is existed.
2.1. If true, check the value is existed in filter_conditions.city array.
2.2. Else, proceed to 3.
Check if value is existed in filter_conditions.state array (default as empty array if the array is not existed).
db.collection.aggregate([
{
$match: {
$expr: {
$cond: {
if: {
$ne: [
"$filter_conditions.pincode",
undefined
]
},
then: {
$in: [
"", // pincode value
"$filter_conditions.pincode"
]
},
else: {
$cond: {
if: {
$ne: [
"$filter_conditions.city",
undefined
]
},
then: {
$in: [
"", // city value
"$filter_conditions.city"
]
},
else: {
$in: [
"", // state value
{
$ifNull: [
"$filter_conditions.state",
[]
]
}
]
}
}
}
}
}
}
}
])
Sample Mongo Playground

Related

MongoDB - Aggregate get specific objects in an array

How can I get only objects in the sales array matching with 2021-10-14 date ?
My aggregate query currently returns all objects of the sales array if at least one is matching.
Dataset Documents
{
"name": "#0",
"sales": [{
"date": "2021-10-14",
"price": 3.69,
},{
"date": "2021-10-15",
"price": 2.79,
}]
},
{
"name": "#1",
"sales": [{
"date": "2021-10-14",
"price": 1.5,
}]
}
Aggregate
{
$match: {
sales: {
$elemMatch: {
date: '2021-10-14',
},
},
},
},
{
$group: {
_id: 0,
data: {
$push: '$sales',
},
},
},
{
$project: {
data: {
$reduce: {
input: '$data',
initialValue: [],
in: {
$setUnion: ['$$value', '$$this'],
},
},
},
},
}
Result
{"date": "2021-10-14","price": 3.69},
{"date": "2021-10-15","price": 2.79},
{"date": "2021-10-14","price": 1.5}
Result Expected
{"date": "2021-10-14","price": 3.69},
{"date": "2021-10-14","price": 1.5}
You actually need to use a $replaceRoot or $replaceWith pipeline which takes in an expression that gives you the resulting document filtered using $arrayElemAt (or $first) and $filter from the sales array:
[
{ $match: { 'sales.date': '2021-10-14' } },
{ $replaceWith: {
$arrayElemAt: [
{
$filter: {
input: '$sales',
cond: { $eq: ['$$this.date', '2021-10-14'] }
}
},
0
]
} }
]
OR
[
{ $match: { 'sales.date': '2021-10-14' } },
{ $replaceRoot: {
newRoot: {
$arrayElemAt: [
{
$filter: {
input: '$sales',
cond: { $eq: ['$$this.date', '2021-10-14'] }
}
},
0
]
}
} }
]
Mongo Playground
In $project stage, you need $filter operator with input as $reduce operator to filter the documents.
{
$project: {
data: {
$filter: {
input: {
$reduce: {
input: "$data",
initialValue: [],
in: {
$setUnion: [
"$$value",
"$$this"
],
}
}
},
cond: {
$eq: [
"$$this.date",
"2021-10-14"
]
}
}
}
}
}
Sample Mongo Playground
How about using $unwind:
.aggregate([
{$match: { sales: {$elemMatch: {date: '2021-10-14'} } }},
{$unwind: '$sales'},
{$match: {'sales.date': '2021-10-14'}},
{$project: {date: '$sales.date', price: '$sales.price', _id: 0}}
])
This will separate the sales into different documents, each containing only one sale, and allow you to match conditions easily.
See: https://docs.mongodb.com/manual/reference/operator/aggregation/unwind/

Mongodb $exists inside $expr in mongodb

I want to add multiple conditions on join. Join those docs (of the same collection) who met the following conditions:
Have opposite gender
Have age (IF EXISTS) between the primary doc age preference and primary doc have age (IF EXISTS) between the foreign doc preference (i.e two-way check)
My attempt is the following but has two issues:
$exists can't be used inside $expr idk why
Age query is one way right now
$lookup: {
"from": "appusers",
"let": { 'gen': "$gender",'pref': "$preference" },
"pipeline": [{
$match: {$expr: {
$and: [
{ $ne: ["$gender", "$$gen"]},
{ $or: [
{$exists: {"$age": false}},
{$and: [
{ $gte: ["$age", '$$pref.age_from' ] },
{ $lte: [ "$age", '$$pref.age_to' ] }
]}
]}
]
}}}],
"as": "matches"
}
Example:
Input Docs:
{
name: "person1",
age: 36,
gender: "Male",
preference: {
age_from: 25,
age_to: 35
}
}
{
name: "person2",
age: 18,
gender: "Female",
preference: {
age_from: 25,
age_to: 40
}
}
{
name: "person3",
age: 26,
gender: "Female",
preference: {
age_from: 30,
age_to: 35
}
}
{
name: "person4",
age: 26,
gender: "Female",
preference: {
age_from: 30,
age_to: 40
}
}
Output:
For person 1 the matches array will show only person 4 (and similarly person 4 match will show person 1) i.e.:
{
name: person1,
age: 36,
gender: "Male",
preference: {
age_from: 28,
age_to: 35
},
matches: [
{
name: person4,
...
}
]
}
I have viewed this and this but didn't help
$exists can't be used inside $expr idk why
$expr Allows the use of aggregation expressions within the query language, and $exists is not an aggregation operator,
You just need to correct the 2 things:
put $expr condition inside first $and condition
put $expr in last $and condition
db.appusers.aggregate([
{
$lookup: {
from: "appusers",
let: { gen: "$gender", pref: "$preference" },
pipeline: [
{
$match: {
$and: [
{ $expr: { $ne: ["$gender", "$$gen"] } },
{
$or: [
{ age: { $exists: false } },
{
$expr: {
$and: [
{ $gte: ["$age", "$$pref.age_from"] },
{ $lte: ["$age", "$$pref.age_to"] }
]
}
}
]
}
]
}
}
],
as: "matches"
}
}
])
Playground
For the $exists problem, you can wrap age with $ifNull and use $eq to check for the existence.
For the 2-way age matching, I think you just need to repeat your age matching criteria from person1 to person4 for person4 to person1. Although in your current given test case, no match will be found as person4's age is out of person1's preference.
db.appusers.aggregate([
{
"$match": {
name: "person1"
}
},
{
$lookup: {
"from": "appusers",
"let": {
"a": "$age",
"gen": "$gender",
"pref": "$preference"
},
"pipeline": [
{
$match: {
$expr: {
$and: [
{
$ne: [
"$$gen",
"$gender"
]
},
{
$and: [
{
$or: [
{
$eq: [
{
"$ifNull": [
"$age",
"age-not-exists"
]
},
"age-not-exists"
]
},
{
$and: [
{
$gte: [
"$age",
"$$pref.age_from"
]
},
{
$lte: [
"$age",
"$$pref.age_to"
]
}
]
}
]
},
{
$or: [
{
$eq: [
{
"$ifNull": [
"$$a",
"age-not-exists"
]
},
"age-not-exists"
]
},
{
$and: [
{
$gte: [
"$$a",
"$preference.age_from"
]
},
{
$lte: [
"$$a",
"$preference.age_to"
]
}
]
}
]
}
]
}
]
}
}
}
],
"as": "matches"
}
}
])
Here is the Mongo playground for your reference.
You can use $eq undefined for the field age instead of the $exists
{
"from": "appusers",
"let": { 'gen': "$gender",'pref': "$preference" },
"pipeline": [{
$match: {$expr: {
$and: [
{ $ne: ["$gender", "$$gen"]},
{ $or: [
{$eq: ["$age" , undefined]},
{$and: [
{ $gte: ["$age", '$$pref.age_from' ] },
{ $lte: [ "$age", '$$pref.age_to' ] }
]}
]}
]
}}}],
"as": "matches"
}

mongodb query findById() and month and year

I've searched for an answer on how to solve my problem but found nothing, so I am very sorry if I am repeating a question that has been asked before.
i'm trying to find a results for a specific userId by month and year.
the dates has been stored in db in this format : yyyy-mm-dd.
I'm trying the following query with Mongo v3.6.8:
db.collection.find({
$and: [
{
$expr: {
$eq: [
{
$month: {
$dateFromString: {
dateString: "$Ntry.BookgDt",
format: "%Y-%m-%d"
}
}
},
12
]
}
},
{
$expr: {
$eq: [
{
$year: {
$dateFromString: {
dateString: "$Ntry.BookgDt",
format: "%Y-%m-%d"
}
}
},
2020
]
}
},
{
$expr: {
$eq: [
"id",
"5fab9a66c493dc4a3c49a7a3"
]
}
}
]
})
Sample data:
[
{
"userid": "5fab9a66c493dc4a3c49a7a3",
"name": "user name",
"acc": "admin",
"Blas": "00.00",
"Ntry": [
{
"Amt": "11.72",
"BookgDt": "2020-08-16",
},
{
"Amt": "16.72",
"BookgDt": "2020-06-23",
}
]
},
{
"userid": "5fab9a77c493dc4a3c49a7a3",
"name": "user name",
"acc": "user",
"Blas": "00.00",
"Ntry": [
{
"Amt": "11.72",
"BookgDt": "2020-08-23",
},
{
"Amt": "16.72",
"BookgDt": "2020-07-23",
}
]
}
]
so my query is to find all Ntry for UserId 5fab9a66c493dc4a3c49a7a3 in month 8 and year 2020, but I got this erorr:
query failed: (ConversionFailure) $dateFromString requires that 'dateString' be a string, found: array with value ["2020-08-16", "2020-06-23"]
can you please help me to find the suitable query, and thank you in advance.
here is also a mongo play ground link, It's the best for quick editing:
This pipeline should work:
db.foo.aggregate([
// Top level match
{$match: {userid: "5fab9a66c493dc4a3c49a7a3"}}
// Next, only keep entries in the Ntry array with month 8 and year 2020.
// addFields: {"Ntry": {$filter: {input: "$Ntry"}}} means overwrite the
// original Ntry array.
,{$addFields: {"Ntry": {$filter: {
input: "$Ntry",
as: "zz",
cond: { $and: [
{$eq: [{$month:{$dateFromString:{dateString:"$$zz.BookgDt",format: "%Y-%m-%d"}}}\
, 8] },
{$eq: [{$year: {$dateFromString:{dateString:"$$zz.BookgDt",format: "%Y-%m-%d"}}}\
, 2020] }
]}
}}
}}
// It is possible everything got filtered out of the Ntry array, leaving
// an empty (size 0) array. We likely do not want that, so further
// cut down the output material. You can comment this out to see what
// changes, especially if you change the month and year targets above.
,{$match: {$expr: {$ne: [ {$size: "$Ntry"}, 0] } }}
]);
It's probably simpler to call $dateFromString twice but if you are feeling adventurous, then use $let inside the cond to convert the date just once:
db.foo.aggregate([
{$match: {userid: "5fab9a66c493dc4a3c49a7a3"}}
,{$addFields: {"Ntry": {$filter: {
input: "$Ntry",
as: "zz",
cond: {
$let: {
vars: {dd: {$dateFromString:{dateString:"$$zz.BookgDt",format: "%Y-%m-%d"}}},
in: {
$and: [
{$eq: [{$month: "$$dd"}, 8] },
{$eq: [{$year: "$$dd"}, 2020] }
]
}
}
}
}}
}}
,{$match: {$expr: {$ne: [ {$size: "$Ntry"}, 0] } }}
]);

How to properly reference nested field within an array within an aggregate framework (MongoDB)?

I have a collection in the following format:
Collection name: COLL1
{
_id: "a",
list: [
{
_id: "a1",
ranking: 10
},
{
_id: "a2",
ranking: 30
}
...
]
}
{
_id: "b",
list: [
{
_id: "b1",
ranking: 10
},
{
_id: "b2",
ranking: 30
}
...
]
}
When I call: db.getCollection('COLL1').find({"_id": "a","list._id": "a1"}); I can see the results. However, if I call:
db.getCollection('COLL1').aggregate([
{ $match:
{ $expr:
{ $and:
[
{ $eq: ["$_id", "a"] },
{ $eq: ["$list._id", "a1"] }
]
}
}
}
])
Then nothing is returned. Does anyone know why? I think the issue is { $eq: ["$list._id", "a1"] } but I'm not sure what exactly happened here.
I'm trying to get the complete document:
{
_id: "a",
list: [
{
_id: "a1",
ranking: 10
},
{
_id: "a2",
ranking: 30
}
...
]
}
This is part of my aggregate syntax within a $lookup stage, so I have to use aggregate([]) instead of find(). What I'm actually trying to achieve is the following:
...previous stages
{
$lookup:{
from: "COLL1",
let: { local_id: "$_id" }, // this '$_id' is from another collection, not COLL1.
pipeline: [
{ $match:
{ $expr:
{ $and:
[
{ $eq: [ "$_id", "a"] },
{ $eq: [ "$$local_id", "$list._id" ] } // this is where I got in trouble.
]
}
}
}
],
as: "matched_result"
}
},
Thanks a lot!
If you don't need variables from previous pipeline stages, you can just use the same syntax as .find() inside $match
db.getCollection('COLL1').aggregate([
{ $match: { "_id": "a", "list._id": "a1" } }
])
Replacing the second $eq with $in will work just fine. :)
db.getCollection('COLL1').aggregate([
{ $match:
{ $expr:
{ $and:
[
{ $eq: ["$_id", "a"] },
{ $in: ["a1", "$list._id"] }
]
}
}
}
])

MongoDb Aggregate nested documents with $add

I need to get sum value from nested documents.
DB document:
{
"_id": 123,
"products": [
{
"productId": 1,
"charges": [
{
"type": "che",
"amount": 100
}
]
}
]
}
i wanted to get sum value.
sumValue = products.charges.amount+20; where "products.productId" is 1 and "products.charges.type" is "che"
i tried below query but no hope:
db.getCollection('test').aggregate(
[
{"$match":{$and:[{"products.productId": 14117426}, {"products.charges.type":"che"}]},
{ $project: { "_id":0, total: { $add: [ "$products.charges.price", 20 ] } }}
]
)
please help me to solve this.
You have to take a look at $unwind operator which deconstructs an array to output a document for each element of array. Also take a look at add and project operators.
I assume your db query should look like this:
db.test.aggregate([
{$unwind: '$products'}, // Unwind products array
{$match: {'products.productId' : 3}}, // Matching product id
{$unwind: '$products.charges'}, // Unwind charges
{$match: {'products.charges.type' : 'che'}}, // Matching charge type of che
{$project: {'with20': {$add: ["$products.charges.amount", 20]}}}, // project total field which is value + 20
{$group: {_id : null, amount: { $sum: '$with20' }}} // total sum
])
You can run $reduce twice to convert your arrays into scalar value. The outer condition could be applied as $filter, the inner one can be run as $cond:
db.collection.aggregate([
{
"$project": {
_id: 0,
total: {
$reduce: {
input: { $filter: { input: "$products", cond: [ "$$this.productId", 1 ] } },
initialValue: 20,
in: {
$add: [
"$$value",
{
$reduce: {
input: "$$this.charges",
initialValue: 0,
in: {
$cond: [ { $eq: [ "$$this.type", "che" ] }, "$$this.amount", 0 ]
}
}
}
]
}
}
}
}
}
])
Mongo Playground