Mongodb lookup like search: local field as array of objects - mongodb

I have two collections userProfile and skills,
Eg:userProfile
{
"_id": "5f72c6d4e23732390c96b031",
"name":"name"
"other_skills": [
"1","2"
],
"primary_skills": [
{
"_id": "607ffd1549e13876fef7f2c5",
"years": 4.5,
"skill_id": "1"
},
{
"_id": "607ffd1549e13876fef7f2c6",
"years": 2,
"skill_id": "2"
},
{
"_id": "607ffd1549e13876fef7f2c7",
"years": 1,
"skill_id": "3"
}
]
}
Eg:Skills
{
"_id":1,
"name": "Ruby on Rails",
}
{
"_id":2,
"name": "PHP",
}
{
"_id":3,
"name": "php",
}
I want to retrieve the userprofile based on the skills
eg: input of skill php i want to retrieve the userprofiles that matches either in primary_skills or other_skills
But I got confused about the implementation, I think it can do with pipeline in lookup and the elemMatch. This is the query I tried so far
const skills = ['php','PHP']
userProfile.aggrigate([{
$lookup:{
from:'skills',
let:{'primary_skills':'$primary_skills'},
pipeline:[
{
$match:{
primary_skills:{
$elemMatch:{
name:'' //not sure how to write match
}
}
}
}
]
}
}])
Can somebody help me with this, Thanks in advance

I'll first show you how to correct your pipeline to work, however this approach is very inefficient as you will have to $lookup on every single user in your db which is obviously a lot of overhead.
Here is how to properly match your condition:
const skills = ['php','PHP']
db.userProfile.aggregate([
{
$lookup: {
from: "skills",
let: {
"primary_skills": {
$map: {
input: "$primary_skills",
as: "skill",
in: "$$skill.skill_id"
}
},
"other_skills": "$other_skills"
},
pipeline: [
{
$match: {
$expr: {
"$in": [
"$_id",
{
"$concatArrays": [
"$$other_skills",
"$$primary_skills"
]
}
]
}
}
}
],
as: "skills"
}
},
{
$match: {
'skills.name': {$in: skills}
}
}
])
Mongo Playground
As I've said I recommend you do not do this. what I suggest you do is split it into 2 calls, first fetch the relevant skill ids. and then query on users.
By doing this you can also utilize indexes for much faster queries, like so:
const skills = ['php', 'PHP'];
const matchedSkillIds = await skills.distinct('_id', {name: {$in: skills}});
const users = await userProfile.find({
$or: [
{
'primary_skills.skill_id': {$in: matchedSkillIds}
},
{
'other_skills': {$in: matchedSkillIds}
}
]
})
Finally if you do insist on doing it in one query at the very least start the pipeline from the skill collection.

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

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

Issues with lookup and match multipe collections

Having issues with aggregate and lookup in multiple stages. The issue is that I cannot match by userId In the last lookup. If I omit the { $eq: ['$userId', '$$userId'] } it works and match by the other criteria. But not by the userid.
I've tried added pools as a let and use it as { $eq: ['$userId', '$$pools.userId'] } in the last stage but that doesn't work either. I get an empty coupon array.
I get this with the below query. I think I need to use $unwind in some way? But haven't got that to work yet. Any pointers?
There is three collections total to be joined. First the userModel, it should contain pools and then the pools should contain a users coupons.
{
"userId": "5df344a1372f345308dac12a", // Match this usedId with below userId coming from the coupon
"pools": [
{
"_id": "5e1ebbc6cffd4b042fc081ab",
"eventId": "id999",
"eventStartTime": "some date",
"trackName": "tracky",
"type": "foo bar",
"coupon": []
}
]
},
I need the coupon array to be filled with the correct data (below) which has a matching userId in it.
"coupon": [
{
"eventId": "id999",
"userId": "5df344a1372f345308dac12a", // This userId need to match the above one
"checked": true,
"pool": "a pool",
}
poolProject:
const poolProject = {
eventId: 1,
eventStartTime: 1,
trackName: 1,
type: 1,
};
Userproject:
const userProjection = {
_id: {
$toString: '$_id',
},
paper: 1,
correctBetsLastWeek: 1,
correctBetsTotal: 1,
totalScore: 1,
role: 1,
};
The aggregate query
const result = await userModel.aggregate([
{ $project: userProjection },
{
$match: {
$or: [{ role: 'User' },
{ role: 'SuperUser' }],
},
},
{ $addFields: { userId: { $toString: '$_id' } } },
{
$lookup: {
from: 'pools',
as: 'pools',
let: { eventId: '$eventId' },
pipeline: [
{ $project: poolProject },
{
$match: {
$expr: {
$in: ['$eventId', eventIds],
},
},
},
{
$lookup: {
from: 'coupons',
as: 'coupon',
let: { innerUserId: '$$userId' },
pipeline: [
{
$match: {
$expr: {
$eq: ['$userId', '$$innerUserId'],
},
},
},
],
},
},
],
},
},
]);
Thanks for any input!
Edit:
If i move the second lookup (coupon) so they are in the same "level" it works but i would like to have it inside of the pool. If I add as: 'pools.coupon', in the last lookup it overwrites the lookedup pool data.
When you access fields with the $$ prefix it means they are defined as "special" system variables by Mongo.
We don't know exactly how Mongo the magic happens but you're naming two variables with the same name, which causes a conflict as it seems.
So either remove userId: '$userId' from the first lookup as you're not even using it.
Or rename or second userId: '$userId' a different name like innerUserId: '$userId' to avoid conflicts when you access it.
Just dont forget to change { $eq: ['$userId', '$$userId'] } to { $eq: ['$userId', '$$innerUserId'] } after.
EDIT:
Now that its clear theres no field userId in pools collection just change the variable in the second lookup collection from:
let: { innerUserId: '$userId' } //userId does not exist in pools.
To:
let: { innerUserId: '$$userId' }

If condition in MongoDB for Nested JSON to retrieve a particular value

I've nested JSON like this. I want to retrieve the value of "_value" in second level. i,e. "Living Organisms" This is my JSON document.
{
"name": "Biology Book",
"data": {
"toc": {
"_version": "1",
"ge": [
{
"_name": "The Fundamental Unit of Life",
"_id": "5a",
"ge": [
{
"_value": "Living Organisms",
"_id": "5b"
}
]
}
]
}
}
}
This is what I've tried, using the "_id", I want to retrieve it's "_value"
db.products.aggregate([{"$match":{ "data.toc.ge.ge._id": "5b"}}])
This is the closest I could get to the output you mentioned in the comment above. Hope it helps.
db.collection.aggregate([
{
$match: {
"data.toc.ge.ge._id": "5b"
}
},
{
$unwind: "$data.toc.ge"
},
{
$unwind: "$data.toc.ge.ge"
},
{
$group: {
_id: null,
book: {
$push: "$data.toc.ge.ge._value"
}
}
},
{
$project: {
_id: 0,
first: {
$arrayElemAt: [
"$book",
0
]
},
}
}
])
Output:
[
{
"first": "Living Organisms"
}
]
You can check what I tried here
If you are using Mongoid:
(1..6).inject(Model.where('data.toc.ge.ge._id' => '5b').pluck('data.toc.ge.ge._value').first) { |v| v.values.first rescue v.first rescue v }
# => "Living Organisms"
6 is the number of containers to trim from the output (4 hashes and 2 arrays).
If I understand your question correctly, you only care about _value, so it sounds like you might want to use a projection:
db.products.aggregate([{"$match":{ "data.toc.ge.ge._id": "5b"}}, { "$project": {"data.toc.ge.ge._value": 1}}])

Mongodb aggregate match query with priority on full match

I am attempting to do a mongodb regex query on a field. I'd like the query to prioritize a full match if it finds one and then partials afterwards.
For instance if I have a database full of the following entries.
{
"username": "patrick"
},
{
"username": "robert"
},
{
"username": "patrice"
},
{
"username": "pat"
},
{
"username": "patter"
},
{
"username": "john_patrick"
}
And I query for the username 'pat' I'd like to get back the results with the direct match first, followed by the partials. So the results would be ordered ['pat', 'patrick', 'patrice', 'patter', 'john_patrick'].
Is it possible to do this with a mongo query alone? If so could someone point me towards a resource detailing how to accomplish it?
Here is the query that I am attempting to use to perform this.
db.accounts.aggregate({ $match :
{
$or : [
{ "usernameLowercase" : "pat" },
{ "usernameLowercase" : { $regex : "pat" } }
]
} })
Given your precise example, this could be accomplished in the following way - if your real world scenario is a little bit more complex you may hit problems, though:
db.accounts.aggregate([{
$match: {
"username": /pat/i // find all documents that somehow match "pat" in a case-insensitive fashion
}
}, {
$addFields: {
"exact": {
$eq: [ "$username", "pat" ] // add a field that indicates if a document matches exactly
},
"startswith": {
$eq: [ { $substr: [ "$username", 0, 3 ] }, "pat" ] // add a field that indicates if a document matches at the start
}
}
}, {
$sort: {
"exact": -1, // sort by our primary temporary field
"startswith": -1 // sort by our seconday temporary
}
}, {
$project: {
"exact": 0, // get rid of the "exact" field,
"startswith": 0 // same for "startswith"
}
}])
Another way would be using $facet which may prove a bit more powerful by enabling more complex scenarios but slower (several people here will hate me, though, for this proposal):
db.accounts.aggregate([{
$facet: { // run two pipelines against all documents
"exact": [{ // this one will capture all exact matches
$match: {
"username": "pat"
}
}],
"others": [{ // this one will capture all others
$match: {
"username": { $ne: "pat", $regex: /pat/i }
}
}]
}
}, {
$project: {
"result": { // merge the two arrays
$concatArrays: [ "$exact", "$others" ]
}
}
}, {
$unwind: "$result" // flatten the resulting array into separate documents
}, {
$replaceRoot: { // restore the original document structure
"newRoot": "$result"
}
}])