I have the following structure of document:
{
"input": {
"fields": [
{
"name": "last_name_hebrew",
"text": "test1",
},
],
},
"output": {
"fields": [
{
"name": "last_name_hebrew",
"text": "test1"
},
],
},
},
I want to get all documents, where fields has object that has name of value last_name_hebrew as with text value of the output.fields.
For example in the given structure it would return this documents because input.fields.name is last_name_hebrew and text is equal to the text in output.
Note I cannot guarantee that fields array in either input or output will have name: last_name_hebrew in the array.
How can I do so?
This is my try to first force the arrays to have document with name of last_name_hebrew:
db.collection.find({
"input.fields": {
$elemMatch: {
"name": "last_name_hebrew"
}
},
"output.fields": {
$elemMatch: {
"name": "last_name_hebrew"
}
},
})
But now I need to compare the text values.
Your first 2 condition with $elemMatch is correct
add expression match, first find the matching element that having last_name_hebrew name from input using $filter and get first element from that filtered result using $arrayElemAt, same process for output field and then match both object using $eq
db.collection.find({
"input.fields": { $elemMatch: { "name": "last_name_hebrew" } },
"output.fields": { $elemMatch: { "name": "last_name_hebrew" } },
$expr: {
$eq: [
{
$arrayElemAt: [
{
$filter: {
input: "$input.fields",
cond: { $eq: ["$$this.name", "last_name_hebrew"] }
}
},
0
]
},
{
$arrayElemAt: [
{
$filter: {
input: "$output.fields",
cond: { $eq: ["$$this.name", "last_name_hebrew"] }
}
},
0
]
}
]
}
});
Playground
Second option: if you want to go with more specific to match exact 2 fields name and text both just need to add $let operator to return fields from filter,
db.collection.find({
"input.fields": { $elemMatch: { "name": "last_name_hebrew" } },
"output.fields": { $elemMatch: { "name": "last_name_hebrew" } },
$expr: {
$eq: [
{
$let: {
vars: {
input: {
$arrayElemAt: [
{
$filter: {
input: "$input.fields",
cond: { $eq: ["$$this.name", "last_name_hebrew"] }
}
},
0
]
}
},
in: { name: "$$input.name", text: "$$input.text" }
}
},
{
$let: {
vars: {
output: {
$arrayElemAt: [
{
$filter: {
input: "$output.fields",
cond: { $eq: ["$$this.name", "last_name_hebrew"] }
}
},
0
]
}
},
in: { name: "$$output.name", text: "$$output.text" }
}
}
]
}
})
Playground
Third option: for more specific to check both fields in loop,
first filter the matching elements by name in input field using $filter
pass above filter result in another filter
filter to match name and text field in output field, if its not [] empty then return filter result
$ne to check return result is not [] empty
db.collection.find({
"input.fields": { $elemMatch: { "name": "last_name_hebrew" } },
"output.fields": { $elemMatch: { "name": "last_name_hebrew" } },
$expr: {
$ne: [
{
$filter: {
input: {
$filter: {
input: "$input.fields",
cond: { $eq: ["$$this.name", "last_name_hebrew"] }
}
},
as: "i",
cond: {
$ne: [
{
$filter: {
input: "$output.fields",
cond: {
$and: [
{ $eq: ["$$this.name", "$$i.name"] },
{ $eq: ["$$this.text", "$$i.text"] }
]
}
}
},
[]
]
}
}
},
[]
]
}
})
Playground
You will have to use an aggregation pipeline to achieve this, there are several ways to do so, here is one example:
db.collection.aggregate([
{
$match: {
$expr: {
$gt: [
{
$size: {
$filter: {
input: "$input.fields",
as: "inputField",
cond: {
$and: [
{
$eq: [
"$$inputField.name",
"last_name_hebrew"
]
},
{
"$setIsSubset": [
[
"$$inputField.text"
],
"$output.fields.text"
]
}
]
}
}
}
},
0
]
}
}
}
])
Mongo Playground
One thing to note is that with this query there are no restrictions on the output.fields.name (as it was not required), if you do require the names to match then you can drop the .text field in the $setIsSubset operator.
Related
Collection in the database:
[{
"value": {
"shipmentId": 1079,
"customer_orders": [
{
"customer_order_id": 1124,
"active": false
},
{
"customer_order_id": 1277,
"active": true,
"items": [
{
"item_id": 281,
"active": false,
"qty": 1,
"name": "apples",
"attributes": null
},
{
"item_id": 282,
"active": true,
"qty": 2,
"name": "bananas"
}
]
}
],
"carrier_orders": [
{
"carrier_order_id": 744,
"active": true
}
]
}
}]
Query I am trying:
db.getCollection('shipments').aggregate([
{
"$match": {
"value.shipmentId": {
"$in": [
1079
]
}
}
},
{
"$project": {
"value.shipmentId": 1,
"value.customer_orders": 1,
"value.carrier_orders": 1,
}
},
{
"$addFields":{
"value.customer_orders":{
$filter:{
input: "$value.customer_orders",
as: "customer_order",
cond: {
$eq: ["$$customer_order.active", true]
}
}
},
"value.customer_orders.items":{
$filter:{
input: "$value.customer_orders.items",
as: "item",
cond: {
$eq: ["$$item.active", true]
}
}
},
"value.carrier_orders": {
$filter:{
input: "$value.carrier_orders",
as: "carrier_order",
cond: {
$eq: ["$$carrier_order.active", true]
}
}
}
}
}
]
);
Desired output:
[{
"value": {
"shipmentId": 1079,
"customer_orders": [
{
"customer_order_id": 1277,
"active": true,
"items": [
{
"item_id": 282,
"active": true,
"qty": 2,
"name": "bananas"
}
]
}
],
"carrier_orders": [
{
"carrier_order_id": 744,
"active": true
}
]
}
}]
I am trying to apply filters at two different levels:
“value.customer_orders”
“value.customer_orders.items”
What I want is to filter out inactive customer orders, and within active customer orders, filter out inactive items. While doing this, if there are any attributes at the customer order level, we want to retain them too in the output.
How can I achieve this multi-level nesting of conditions and retain attributes using the aggregate pipeline?
Playground: https://mongoplayground.net/p/hcNzTeAkiks
Solution 1
$map - Iterate each document in the value.customer_orders array and return a new array.
1.1. $mergeObjects - Merge the current iterate object with the document with items array.
1.1.1. $filter - Filter the document with active: true in the items array.
db.collection.aggregate([
{
"$match": {
"value.shipmentId": {
"$in": [
1079
]
}
}
},
{
"$project": {
"value.shipmentId": 1,
"value.customer_orders": 1,
"value.carrier_orders": 1,
}
},
{
"$addFields": {
"value.customer_orders": {
$filter: {
input: "$value.customer_orders",
as: "customer_order",
cond: {
$eq: [
"$$customer_order.active",
true
]
}
}
},
"value.carrier_orders": {
$filter: {
input: "$value.carrier_orders",
as: "carrier_order",
cond: {
$eq: [
"$$carrier_order.active",
true
]
}
}
}
}
},
{
$set: {
"value.customer_orders": {
$map: {
input: "$value.customer_orders",
in: {
$mergeObjects: [
"$$this",
{
items: {
$filter: {
input: "$$this.items",
as: "item",
cond: {
$eq: [
"$$item.active",
true
]
}
}
}
}
]
}
}
}
}
}
])
Demo Solution 1 # Mongo Playground
Solution 2
Can combine the above filter within a single operator.
$reduce - Iterate the document in the value.customer_orders array and transform into a new array.
1.1. $concatArrays - Combine arrays into a single array.
1.1.1. $cond - If the current document's active: true, then combine with the array result from 1.1.1.1. Else combine with an empty array. This aims to filter the document with active: true.
1.1.1.1. $mergeObjects - Merge current document with filtered items array with nested item with active: true.
db.collection.aggregate([
{
"$match": {
"value.shipmentId": {
"$in": [
1079
]
}
}
},
{
"$project": {
"value.shipmentId": 1,
"value.customer_orders": 1,
"value.carrier_orders": 1,
}
},
{
"$addFields": {
"value.customer_orders": {
$reduce: {
input: "$value.customer_orders",
initialValue: [],
in: {
$concatArrays: [
"$$value",
{
$cond: {
if: {
$eq: [
"$$this.active",
true
]
},
then: [
{
$mergeObjects: [
"$$this",
{
items: {
$filter: {
input: "$$this.items",
as: "item",
cond: {
$eq: [
"$$item.active",
true
]
}
}
}
}
]
}
],
else: []
}
}
]
}
}
},
"value.carrier_orders": {
$filter: {
input: "$value.carrier_orders",
as: "carrier_order",
cond: {
$eq: [
"$$carrier_order.active",
true
]
}
}
}
}
}
])
Demo Solution 2 # Mongo Playground
So far, after i tried, i came up with solution where i am able to remove the whole object inside of the array if that object has field with empty value. That does not work in my case. I only need to remove the field and keep rest of the object. In this case, "Comment" field is the one having empty values occasionally. Thanks in advance!
Structure:
someArray: [
{
field1:"value",
field2:"value",
Comment:"",
Answer:"",
},
{
field1:"value",
field2:"value",
Comment:"",
Answer:"",
}]
Code:
$project: {
someArray: {
$filter: {
input: "$someArray", as: "array",
cond: { $ne: [ "$$array.Comment", ""]}}}}
Use $map to loop over the array elements.For each array element where comment is not an empty string, return whole element, otherwise return the document excluding comment field. Like this:
db.collection.aggregate([
{
"$project": {
someArray: {
$map: {
input: "$someArray",
as: "element",
in: {
$cond: {
if: {
$eq: [
"",
"$$element.Comment"
]
},
then: {
field1: "$$element.field1",
field2: "$$element.field2"
},
else: "$$element"
}
}
}
}
}
},
])
Here, is the working link.
Here is a solution where an array's nested object can have multiple fields and these need not be referred in the aggregation. Removes the nested object's field with value as an empty string (""):
db.collection.aggregate([
{
$set: {
someArray: {
$map: {
input: '$someArray',
as: 'e',
in: {
$let: {
vars: {
temp_var: {
$filter: {
input: { $objectToArray: '$$e' },
cond: { $ne: [ '', '$$this.v' ] },
}
}
},
in: {
$arrayToObject: '$$temp_var'
}
}
}
}
}
}
},
])
Solution from Charchit Kapoor works only if your array has exactly
{
field1: ...
field2: ...
Comment:""
}
But it does not work for arbitrary fields. I was looking for more generic solution, my first idea was this:
db.collection.aggregate([
{
"$project": {
someArray: {
$map: {
input: "$someArray",
in: {
$cond: {
if: { $eq: ["$$this.Comment", ""] },
then: { $mergeObjects: ["$$this", { Comment: "$$REMOVE" }] },
else: "$$this"
}
}
}
}
}
}
])
but it does not work.
I ended on this one:
db.collection.aggregate([
{
"$project": {
someArray: {
$map: {
input: "$someArray",
in: {
$cond: {
if: { $eq: ["", "$$this.Comment"] },
then: {
$arrayToObject: {
$filter: {
input: {
$map: {
input: { $objectToArray: "$$this" },
as: "element",
in: { $cond: [{ $eq: ["$$element.k", "Comment"] }, null, "$$element"] }
}
},
as: "filter",
cond: "$$filter" // removes null's from array
}
}
},
else: "$$this"
}
}
}
}
}
}
])
Mongo Playground
I have managed to create an aggregate which returns two array in a document like so:
"b": [
{
"_id": "6258bdfe983a2d31e1cc6a4b",
"booking_room_id": "619395ba18984a0016caae6e",
"checkIn_date_time": "2022-04-16",
"checkOut_date_time": "2022-05-17"
}
]
"r": [
{
"_id": "619395ba18984a0016caae6e",
}
]
I want to remove the item from r if _id in r matches booking_room_id in b.
Also, since these array exist inside a parent document. I want to remove the parent document from the query if r is empty after performing the filter.
Use $expr and $filter
db.collection.aggregate([
{
$match: {
$expr: {
$ne: [
{
$filter: {
input: "$r",
as: "r",
cond: {
$not: { $in: [ "$$r._id", "$b.booking_room_id" ] }
}
}
},
[]
]
}
}
},
{
$set: {
r: {
$filter: {
input: "$r",
as: "r",
cond: {
$not: { $in: [ "$$r._id", "$b.booking_room_id" ] }
}
}
}
}
}
])
mongoplayground
I have a class model which has field ref.
I'm trying to fetch only records that match the condition in lookup.
so what i did:
{
$lookup: {
from: 'fields',
localField: "field",
foreignField: "_id",
as: 'FieldCollege',
},
},
{
$addFields: {
"FieldCollege": {
$arrayElemAt: [
{
$filter: {
input: "$FieldCollege",
as: "field",
cond: {
$eq: ["$$field.level", req.query.level]
}
}
}, 0
]
}
}
},
The above code works fine and returning the FieldCollege if the cond is matched.
but the thing is, i wanted to return the class records only if the FieldCollege is not empty.
I'm totally new to mongodb. so i tried something like this:
{
$match: {
'FieldCollege': { $exists: true, $ne: [] }
}
},
Obv this didn't work.
does mongodb support something like this or am i complicating things?
EDIT:
the result from the above code:
"Classes": [
{
"_id": "613245664c6ea614e001fcef",
"name": "test",
"language": "en",
"year_cost": "3232323",
"FieldCollege":[] // with $unwind
}
],
expected Result:
"Classes": [
// FieldCollege is empty
],
I think the good option is to use lookup with pipeline, and see the final version of your query,
$lookup with fields collection and match your both conditions
$limit to result one document
$match FieldCollege is not empty []
$addElemAt to get first element from result FieldCollege
[
{
$lookup: {
from: "fields",
let: { field: "$field" },
pipeline: [
{
$match: {
$and: [
{ $expr: { $eq: ["$$field", "$_id"] } },
{ level: req.query.level }
]
}
},
{ $limit: 1 }
],
as: "FieldCollege"
}
},
{ $match: { FieldCollege: { $ne: [] } } },
{
$addFields: {
FieldCollege: { $arrayElemAt: ["$FieldCollege", 0] }
}
}
]
I have field x which can be [[], [], ...] or ["", "", ....] I want to filter them out and keeps the document at least have 1 non-empty list or 1 non-empty string. for example [[], [1,2], [], ...]
This is an aggregation query which filters out collection documents with the array field x, having elements with all empty strings or all empty arrays.
db.collection.aggregate([
{
$addFields: {
filtered: {
$filter: {
input: "$x",
as: "e",
cond: {
$or: [
{ $and: [
{ $eq: [ { "$type": "$$e" }, "array" ] },
{ $gt: [ { $size: "$$e" }, 0 ] }
] },
{ $and: [
{ $eq: [ { "$type": "$$e" }, "string" ] },
{ $gt: [ { $strLenCP: "$$e" }, 0 ] }
] }
]
}
}
}
}
},
{
$match: {
$expr: { $gt: [ { $size: "$filtered" }, 0 ] }
}
},
{
$project: { filtered: 0 }
}
])
Reference: Various aggregation operators ($size, $type, $strLenCP, etc.) used.