Merge arrays by matching similar values in mongodb - mongodb

This is an extension of the below question.
Filter arrays in mongodb
I have a collection where each document contains 2 arrays as below.
{
users:[
{
id:1,
name:"A"
},
{
id:2,
name:"B"
},
{
id:3,
name:"C"
}
]
priv_users:[
{
name:"X12/A",
priv:"foobar"
},
{
name:"Y34.B",
priv:"foo"
}
]
}
From the linked question, I learnt to use $map to merge 2 document arrays. But I can't figure out to match users.name to priv_users.name to get below output.
{
users:[
{
id:1,
name:"A",
priv:"foobar"
},
{
id:2,
name:"B",
priv:"foo"
},
{
id:3,
name:"C"
}
]
}
users.name and priv_users.name don't have a consistent pattern, but users.name exists within priv_users.name.
MongoDB version is 4.0

This may not be as generic but will push you in the right direction. Consider using the operators $mergeObjects to merge the filtered document from the priv_users array with the document in users.
Filtering takes the $substr of the priv_users name field and compares it with the users name field. The resulting pipeline will be as follows
db.collection.aggregate([
{ '$addFields': {
'users': {
'$map': {
'input': '$users',
'in': {
'$mergeObjects': [
{
'$arrayElemAt': [
{
'$filter': {
'input': '$priv_users',
'as': 'usr',
'cond': {
'$eq': [
'$$this.name',
{ '$substr': [
'$$usr.name', 4, -1
] }
]
}
}
},
0
]
},
'$$this'
]
}
}
}
} }
])
If using MongoDB 4.2 and newer versions, consider using $regexMatch operator for matching the priv_users name field with the users name field as the regex pattern. Your $cond operator now becomes:
'cond': {
'$regexMatch': {
'input': '$$usr.name',
'regex': '$$this.name',
'options': "i"
}
}

Related

How to compare fields from different collections in mongodb

Here, I have multiple fields from multiple tables those values needs to compared and need to display desired result.
SQL QUERY:
select pd.service_id,ps.service_id from player pd, service ps where pd.subject_id=ps.subject_id and pd.service_id = ps.service_id
Mongo query:
db.player.aggregate([
{
"$lookup":{
"from":"service",
"localField":"player.subject_id",
"foreignField":"subject_id",
"as":"ps"
}
},
{
"$unwind":"$ps"
},
{
"$match":{
"service_id":{
"$eq": "ps.service_id"
}
}
}
];
sample input records:
player:
[{subject_id:23,service_id:1},{subject_id:76,service_id:9}]
service:
[{subject_id:76,service_id:9},{subject_id:99,service_id:10}]
The match is not working. I have to match service_id's of both collections. Need to get matched records. But not able to see any result. Can anyone please help me to find out the mistake...
In your query, if you want to compare 2 values from the document itself, you need to use $expr operator
{
"$match":{
"$expr":{
"$eq": ["$service_id", "$ps.service_id"]
}
}
}
MongoPlayground
Alternative solution: You need to use Uncorrelated sub-query to "* join" with 2 o more conditions
db.player.aggregate([
{
"$lookup": {
"from": "service",
"let": {
subject_id: "$subject_id",
service_id: "$service_id"
},
"pipeline": [
{
$match: {
$expr: {
$and: [
{
$eq: [
"$$subject_id",
"$subject_id"
]
},
{
$eq: [
"$$service_id",
"$service_id"
]
}
]
}
}
}
],
"as": "ps"
}
},
// Remove non matched results
{
$match: {
"ps.0": {
$exists: true
}
}
},
// Remove temporal "ps" field
{
$addFields: {
"ps": "$$REMOVE"
}
}
])
MongoPlayground

mongodb aggregation framework $match - $lookup - $project (failing to use $project for structuring the result)

Sample Data:
users collection:
[
{
"_id":ObjectId("614804bb4236d9b0896f4e7c"),
"firstName":"Fabian",
"lastName":"Barlow"
},
{
"_id":ObjectId("614804bb4236d9b0896f4e7d"),
"firstName":"Chloe",
"lastName":"Mendez"
},
{
"_id":ObjectId("614804bb4236d9b0896f4e7e"),
"firstName":"Valentine",
"lastName":"Sharp"
}
]
bookings collection:
[
{
"_id":ObjectId("614805534236d9b0896f4e86"),
"resourceName":"res-1",
"duration":"10 days",
"status":"active",
"bookingCreatorId":ObjectId("614804bb4236d9b0896f4e7c")
},
{
"_id":ObjectId("614805534236d9b0896f4e87"),
"resourceName":"res-2",
"duration":"15 days",
"status":"active",
"bookingCreatorId":ObjectId("614804bb4236d9b0896f4e7d")
},
{
"_id":ObjectId("614805534236d9b0896f4e88"),
"resourceName":"res-3",
"duration":"15 days",
"status":"completed",
"bookingCreatorId":ObjectId("614804bb4236d9b0896f4e7d")
}
]
I am trying to perform an aggregation query on the above collection "bookings", to get the result in a particular structure,
While using project i am trying to add a new field "bookingCreatorDetails" which needs to hold data from "users" collections, but instead of getting the correct data for users collections, i am getting "Array" type for the fields inside "bookingCreatorDetails"
Attempt on creating the query ("bookings"):
[
{
'$match': {
'status': 'active'
}
}, {
'$lookup': {
'from': 'users',
'localField': 'bookingCreatorId',
'foreignField': '_id',
'as': 'bookingCreator'
}
}, {
'$project': {
'resourceName': 1,
'duration': 1,
'bookingCreatorId': 1,
'bookingCreatorDetails.firstName': '$bookingCreator.firstName',
'bookingCreatorDetails.lastName': '$bookingCreator.lastName'
}
}
]
Current incorrect result:
[
{
"_id":ObjectId("614805534236d9b0896f4e86"),
"resourceName":"res-1",
"duration":"10 days",
"status":"active",
"bookingCreatorId":ObjectId("614804bb4236d9b0896f4e7c"),
"bookingCreatorDetails": {
"firstName":Array,
"lastName":Array
}
},
{
"_id":ObjectId("614805534236d9b0896f4e87"),
"resourceName":"res-2",
"duration":"15 days",
"status":"active",
"bookingCreatorId":ObjectId("614804bb4236d9b0896f4e7d"),
"bookingCreatorDetails": {
"firstName":Array,
"lastName":Array
}
}
]
Expected Result:
[
{
"_id":ObjectId("614805534236d9b0896f4e86"),
"resourceName":"res-1",
"duration":"10 days",
"status":"active",
"bookingCreatorId":ObjectId("614804bb4236d9b0896f4e7c"),
"bookingCreatorDetails": {
"firstName":"Fabian",
"lastName":"Barlow"
}
},
{
"_id":ObjectId("614805534236d9b0896f4e87"),
"resourceName":"res-2",
"duration":"15 days",
"status":"active",
"bookingCreatorId":ObjectId("614804bb4236d9b0896f4e7d"),
"bookingCreatorDetails": {
"firstName":"Chloe",
"lastName":"Mendez"
}
}
]
The $lookup will always return in array of objects format, You can use $arrayElemAt operator to select first element from the array or $first operator from 4.4,
using $arrayElemAt operator
{
"$project": {
"resourceName": 1,
"duration": 1,
"bookingCreatorId": 1,
"bookingCreatorDetails": {
$arrayElemAt: ["$bookingCreator", 0]
}
}
}
using $first operator
{
"$project": {
"resourceName": 1,
"duration": 1,
"bookingCreatorId": 1,
"bookingCreatorDetails": {
$first: "$bookingCreator"
}
}
}
Out of the question if you want to remove _id from lookup result you can use $unset stage after your $project stage,
{ $unset: "bookingCreatorDetails._id" }

mongoose $elemMatch returns all results instead only the first

As I understand $elemMatch should find and return only the first elem` it encounters
but instead it returns all the array, this is the DB data
{
_id: "123123123",
"user_id": "0",
"list": [
{
"employee_code": 1111,
"list": []
},
{
"employee_code": 2222,
"list": []
}
]
}
I want to return only the first elem'
this is my query:
Reports.find(
{user_id:"123123123"},
{"list":{$elemMatch:{"employee_code": 1111}}
})
I expect it to return only the first obj' list:[{employee_code:1111,...}]
but instead it returns all the array
better ex:
you should use $filter in aggregration
db.collection.aggregrate([
[
{
'$project': {
'list': {
'$filter': {
'input': '$list',
'as': 'list1',
'cond': {
'$eq': [
'$$list1.employee_code', 1111
]
}
}
},
'user_id': 1
}
}
]
])
$elemMatch will return document that contain an array field with at least one element that matches all the specified query criteria. You should use positional operator $ instead:
db.collection.find({
"_id": "123123123",
"list.employee_code": 1111
},
{
"list.$": 1
});
Here is the working example: https://mongoplayground.net/p/gQvKniEVgfT

how to use $elemMatch on array specifying an upper field as part of the query

I'd like to retrieve for a specific user, his chats with unread messages.
Lets say I have a simplified chat model like that :
{
lastMessageAt: Date,
participants: [
{
user: String(id),
lastReadAt: Date
}
]
}
How can I achieve my query ?
I have tried several thing like with $elemMatch, but lastMessageAt is unknown at this level...
ChatDB.find({
'participants': {
$elemMatch: { user: '12345', lastReadAt: { $lt: '$lastMessageAt' } }
}
}
Thanks in advance for your help ! :)
$elemMatch operator will find those documents in ChatDB collection that have at least 1 element in participants that matches your criteria. Also my research ended with the conslusion that it is not yet possible to access other document field in $elemMatch operator. Anyway, if this is your goal, then you can use this query:
ChatDB.aggregate([
{
$match: {
"participants.user": "12345",
$expr: {
$lt: [
"$participants.lastReadAt",
"$lastMessageAt"
]
}
}
}
])
Mongo playground
If you also want to filter participants that really matched the criteria, then you need to add a projection stage:
ChatDB.aggregate([
{
$match: {
"participants.user": "12345",
$expr: {
$lt: [
"$participants.lastReadAt",
"$lastMessageAt"
]
}
}
},
{
$project: {
participants: {
$filter: {
input: "$participants",
as: "participant",
cond: {
$and: [
{
$eq: [
"$$participant.user",
"12345"
]
},
{
$lt: [
"$$participant.lastReadAt",
"$lastMessageAt"
]
}
]
}
}
}
}
}
])
Mongo playground
I have found the solution witch is to use the aggregator with the $unwind operator.
await ChatDB.aggregate([
{
$unwind: '$participants'
},
{
$match: {
'participants.user': '12345',
$expr: {
$lt: [
'$participants.lastReadAt',
'$lastMessageAt'
]
}
}
}]);
Hope this will be usefull

How do I access a field inside array inside object inside array in the aggregation framework?

Suppose I had this data structure as my document:
{
id: 1,
customers: [
{
id: 2,
addresses: [
{
phone: "555-342",
zipCode: "1946873193",
addressLine: "whatever"
}
]
}
]
}
How do I access the phone property in the aggregation framework?
$ROOT.customers.0.addresses.0.phone doesn't work as a field path expression.
You can use $let with $arrayElemAt to handle two nesting levels but then you can add $project to get phone property.
db.collection.aggregate([
{
$addFields: {
selectedElement: {
$let: {
vars: {
fstCustomer: { $arrayElemAt: [ '$customers', 0 ] }
},
in: { $arrayElemAt: [ '$$fstCustomer.addresses', 0 ] }
}
}
}
},
{
$project: {
phone: "$selectedElement.phone"
}
}
])
You can chain the $arrayElemAt in following way.
Outer $let expression to get the first customer and inner $let expression to get the first address element of first customer followed by projection of phone property.
db.col.aggregate(
{"$project":{
"phone":{
"$let":{
"vars":{
"customer":{"$arrayElemAt":["$customers",0]}
},
"in":{
"$let":{
"vars":{
"address":{"$arrayElemAt":["$$customer.addresses",0]}
},
"in":"$$address.phone"
}
}
}
}
}})