How can I combine the results of 3 queries in MongoDB? - reactjs

I made the following filter in hopes that I would be combining the results from all 3 $and arrays but it is only matching one of those blocks.
How can I combine the results of what would be returned from each $and array if conditions are met. Hopefully that's clear. I don't know what to call the $and array.
const filter = {
$or: [
{
$and: [
{ category: req.query.category },
{ tags: req.query.subCategory },
{contentType: req.query.contentType},
req.query.searchTerm !== ""
? {
name: {
$regex: "(?i)" + req.query.searchTerm + "(?-i)",
$options: "i",
},
}
: {},
],
$and: [
{ category: req.query.category },
{ tags: req.query.subCategory },
{contentType: req.query.contentType},
req.query.searchTerm !== ""
? {
description: {
$regex: "(?i)" + req.query.searchTerm + "(?-i)",
$options: "i",
},
}
: {},
],
$and: [
{ category: req.query.category },
{ tags: req.query.subCategory },
{contentType: req.query.contentType},
req.query.searchTerm !== ""
? {
tags: {
$regex: "(?i)" + req.query.searchTerm + "(?-i)",
$options: "i",
},
}
: {},
],
},
],
};
await Content.paginate(filter, options, (err, result) => {
if (err) {
res.status(500).send(err);
} else {
res.json(result);
}
});
EDIT: Below is an example of two entries that would be found in the database. The way it should work is it should use category, subCategory, and contentType to filter out the entries in the database so that what I have now are only the entries which have the same category, subCategory, and contentType as specified in req.query, I'll call this the firstFilterResult. From there, I am trying to search within firstFilterResult to see if I have entries that have name, tag, or description matches. So basically catgeory, subCategory and contentType are just used to narrow down the results so that I can find matches for name, tag, and description. My code above doesn't do exactly this but this is the idea behind it and I thought that what I have would do similar, but I guess I'm wrong.
contents: [
{
tags: [
'food',
'drinks',
'card',
'account'
],
_id: '1d13ff7m6db4d5417cd608f4',
name: 'THE NAME FOR THIS PIECE OF CONTENT',
description: 'In here I will begin to talk about...',
content_id: '5dbcb998ad4144390c244093',
contentType: 'quiz',
date: '2019-06-03T04:00:00.000Z',
category: 'food',
image: 'https://IMAGE.PNG',
__v: 0
},
{
tags: [
'computer',
'laptop'
],
_id: '7d1b940b1c9d44000025db8c',
name: 'THE NAME FOR THIS PIECE OF CONTENT',
description: 'This is another description',
content_id: '5f1b963d1c9d44000055db8d',
contentType: 'tool',
date: '2019-06-03T04:00:00.000Z',
category: 'money',
image: 'https://IMAGE.PNG',
__v: 0
}
]

I finally got it to work with this
const catFilter =
req.query.category !== "" ? { category: req.query.category } : {};
const subCatFilter =
req.query.subCategory !== "" ? { tags: req.query.subCategory } : {};
const typeFilter =
req.query.contentType !== ""
? { contentType: req.query.contentType }
: {};
const filter = {
$and: [
{
$or: [
{
name: {
$regex: req.query.searchTerm,
$options: "i",
},
},
{
description: {
$regex: req.query.searchTerm,
$options: "i",
},
},
{
tags: {
$regex: req.query.searchTerm,
$options: "i",
},
},
],
},
catFilter,
subCatFilter,
typeFilter,
],
};

Since each element of the $or contains the same 3 checks with a single one that varies, these can be separated out, and the $or is then only needed if a search term is specified.
Passing options:"i" makes the entire regex match case insensitive, so it is not necessary to surround the search string with (?i) and (?-i)
The following should build the filter that you are attempting, without using empty objects:
// base query that checks the common fields
var filter = {
category: req.query.category,
tags: req.query.subCategory,
contentType: req.query.contentType
};
// if a search term is provided, add in the additional critera
if (req.query.searchTerm !== "") {
var regex = {
$regex: req.query.searchTerm,
options:"i"
};
filter['$or'] = [
{ name: regex },
{ description: regex },
{ tags: regex }
]
}
If this doesn't obtain the results you're after, please edit the question and add in some sample documents so we can see the problem.

Related

MongoDB: nested array count + original document

I have the following document structure which contains an array of votes:
{ _id: ObjectId("6350e2c1a15e0e656f4a7472"),
category: 'business',
votes:
[ { voteType: 'like',
userId: ObjectId("62314007da34df3f32f7cfc0") },
{ voteType: 'like',
userId: ObjectId("6356b5cbe2272ebf628451b") } ] }
What I would like to achieve is to add for each document the sum of votes for which voteType = like, while keeping the original document, such as:
[ [{ _id: ObjectId("6350e2c1a15e0e656f4a7472"),
category: 'business',
votes:
[ { voteType: 'like',
userId: ObjectId("62314007da34df3f32f7cfc0") },
{ voteType: 'like',
userId: ObjectId("6356b5cbe2272ebf628451b") } ] }, {sum: 2, voteType: "like"} ], ...]
At the moment, the only workaround that I found is through an aggregation although I cannot manage to keep the original documents in the results:
db.getCollection('MyDocument') .aggregate([ {
$unwind: "$votes" }, {
$match: {
"votes.voteType": "like",
} }, {
$group: {
_id: {
name: "$_id",
type: "$votes.voteType"
},
count: {
$sum: 1
}
} },
{ $sort : { "count" : -1 } }, {$limit : 5}
])
which gives me:
{ _id: { name: ObjectId("635004f1b96e494947caaa5e"), type: 'like' },
count: 3 }
{ _id: { name: ObjectId("63500456b96e494947cbd448"), type: 'like' },
count: 3 }
{ _id: { name: ObjectId("63500353b6c7eb0a01df268e"), type: 'like' },
count: 2 }
{ _id: { name: ObjectId("634e315bb7d17339f8077c39"), type: 'like' },
count: 1 }
You can do it like this:
$cond with $isArray - to check if the votes property is of the type array.
$filter - to filter votes based on voteType property.
$size - to get the sized of the filtered array.
db.collection.aggregate([
{
"$set": {
"count": {
"$cond": {
"if": {
"$isArray": "$votes"
},
"then": {
"$size": {
"$filter": {
"input": "$votes",
"cond": {
"$eq": [
"$$this.voteType",
"like"
]
}
}
}
},
"else": 0
}
}
}
}
])
Working example

Mongo aggregation framework match a given _id

My model :
const scheduleTaskSchema = new Schema({
activity: { type: Object, required: true },
date: { type: Date, required: true },
crew: Object,
vehicle: Object,
pickups: Array,
details: String,
});
const ScheduleTaskModel = mongoose.model("schedule_task", scheduleTaskSchema),
and this aggregation pipeline :
let aggregation = [
{
$sort: {
"pickups.0.time": 1,
},
},
{
$group: {
_id: "$date",
tasks: { $push: "$$ROOT" },
},
},
{ $sort: { _id: -1 } },
];
if (hasDateQuery) {
aggregation.unshift({
$match: {
date: { $gte: new Date(start_date), $lte: new Date(end_date) },
},
});
} else {
aggregation.push({ $limit: 2 });
}
const scheduledTasksGroups = await ScheduleTaskModel.aggregate(aggregation);
the crew object can have arbitrary number of keys with this structure :
crew : {
drivers: [
{
_id: "656b1e9cf5b894a4f2v643bc",
name: "john"
},
{
_id: "567b1e9cf5b954a4f2c643bhh",
name: "bill"
}
],
officers: [
{
_id: "655b1e9cf5b6632a4f2c643jk",
name: "mark"
},
{
_id: "876b1e9af5b664a4f2c234bb",
name: "jane"
}
],
//...any number of keys that contain an array of objects that all have an _id
}
I'm looking for a way to return all documents (before sorting/grouping) that contain a given _id anywhere within the crew object without knowing which key to search,it can be many different keys that all contain an array of objects that all have an _id
Any ideas ?
You can use $objectToArray for this:
db.collection.aggregate([
{$addFields: {crewFilter: {$objectToArray: "$crew"}}},
{$set: {
crewFilter: {$size: {
$reduce: {
input: "$crewFilter",
initialValue: [],
in: {$concatArrays: [
"$$value",
{$filter: {
input: "$$this.v",
as: "member",
cond: {$eq: ["$$member._id", _id]}
}
}
]
}
}
}}
}},
{$match: {crewFilter: {$gt: 0}}}
])
See how it works on the playground example

how to refactoring $expr, $regexMatch filter for easier reading React/MongoDB?

I would like to explain my problem of the day.
Currently I perform a filter on an input which allows me to search the last name and first name it works really well
I have deleted a lot of things for a simpler reading of the code if there is a need to bring other element do not hesitate to ask
const {
data: packUsersData,
} = useQuery(
[
"pack",
id,
"users",
...(currentOperatorsIds.length ? currentOperatorsIds : []),
value,
],
async () => {
const getExpr = () => ({
$expr: {
$or: [
{
$regexMatch: {
input: {
$concat: ["$firstName", " ", "$lastName"],
},
regex: value,
options: "i",
},
},
{
$regexMatch: {
input: {
$concat: ["$lastName", " ", "$firstName"],
},
regex: value,
options: "i",
},
},
],
},
});
let res = await usersApi.getrs({
pagination: false,
query: {
"roles.name": "operator",
_id: { $nin: currentOperatorsIds },
deletedAt: null,
$or: value
? [
{
entities: [],
...getExpr(),
},
{
entities: { $in: id },
...getExpr(),
},
]
: [
{
entities: [],
},
{
entities: { $in: id },
},
],
},
populate: "entity",
sort: ["lastName", "firstName"],
});
{
refetchOnMount: true,
}
);
and so i find the read a bit too long have any idea how i could shorten all this?
thx for help.
You can reduce entities field $or condition, just concat the empty array and input id,
let res = await usersApi.getrs({
pagination: false,
query: {
"roles.name": "operator",
_id: { $nin: currentOperatorsIds },
deletedAt: null,
entities: { $in: [[], ...id] },
...getExpr()
},
populate: "entity",
sort: ["lastName", "firstName"]
});
If you want to improve the regular expression condition you can try the below approach without using $expr and aggregation operators,
create a function and set input searchKeyword and searchProperties whatever you want to in array of string
function getSearchContiion(searchKeyword, searchProperties) {
let query = {};
if (searchKeyword) {
query = { "$or": [] };
const sk = searchKeyword.trim().split(" ").map(n => new RegExp(n, "i"));
searchProperties.forEach(p => {
query["$or"].push({ [p]: { "$in": [...sk] } });
});
}
return query;
}
// EX:
console.log(getSearchContiion("John Doe", ["firstName", "lastName"]));
Use the above function in query
let res = await usersApi.getrs({
pagination: false,
query: Object.assign(
{
"roles.name": "operator",
_id: { $nin: currentOperatorsIds },
deletedAt: null,
entities: { $in: [[], ...id] }
},
getSearchContiion(value, ["firstName", "lastName"])
},
populate: "entity",
sort: ["lastName", "firstName"]
});

Match $and based on array of filter items in MongoDB

I have a Collection in MongoDB of CatalogItems. Every CatalogItem contains a product that has an array of metafields.
In these metafields there are 2 fields that are Brand_ID and Article_No
Example CatalogItem document:
CatalogItem = {
product: {
metafields: [
{
key: "Brand_ID",
value: "317"
},
{
key: "Article_No",
value: "48630"
}
]
}
}
I have an array of filters that is used to match the CatalogItems documents based on these metafields
filter array
filter = [
{ brandId: '317', articleId: '48630' },
{ brandId: '257', articleId: 'ZSA04036' }
]
I want to return all CatalogItems that match any of the exact combinations in filter.
For example to return the stated CatalogItem I currently use this query
// Checks for { brandId: '317', articleId: '48630' }
query = {
$and: [
{ "product.metafields": { $elemMatch: { key: "Brand_ID", value: filter[0].brandId } } },
{ "product.metafields": { $elemMatch: { key: "Article_No", value: filter[0].articleId } } },
]
}
The issue that I have is that in order for me to look trough all the filter items I have to increment the filter index and rerunning the query.
For example looking for the second filter index I would have to change
filter[0].brandId to filter[1].brandId
Is there a way in Mongo to query using a predefined array of objects instead of rerunning the query multiple times?
I figured out a way to set variables to the query using $or and .forEach()
let query = {
$or: []
};
filter = [
{ brandId: '317', articleId: '48630' },
{ brandId: '257', articleId: 'ZSA04036' }
]
filter.forEach(filterItem => {
query.$or.push(
{
$and: [
{ "product.metafields": { $elemMatch: { key: "Brand_ID", value: filterItem.brandId } } },
{ "product.metafields": { $elemMatch: { key: "Article_No", value: filterItem.articleId } } },
]
}
)
})

mongodb - using join on a local variable

I'm using node.js and mongodb, I have an array of objects which holds the names of an id. Let's say below is my array
let names = [
{ value: 1, text: 'One' },
{ value: 2, text: 'Two' },
{ value: 3, text: 'Three' },
{ value: 4, text: 'Gour' }
]
And this is my query result of a collection using $group which gives me the distinct values.
[
{ _id: { code: '1', number: 5 } },
{ _id: { code: '2', number: 5 } },
{ _id: { code: '3', number: 2 } },
{ _id: { code: '4', number: 22 } },
]
$lookup let's us to join the data from a different collection, but in my case I have an array which holds the text value for each of the codes which I got from the query.
Is there a way we can map the text from the array to the results from mongodb?
Any help will be much appreciated.
EDIT
MongoDB query which I was trying
db.collection.aggregate([
{
$match: {
_Id: id
}
},
{
$lookup: {
localField: "code",
from: names,
foreignField: "value",
as: "renderedNames"
}
},
{
"$group" : {
"_id": {
code: "$code",
number: "$number"
}
}
}
]);
Local variable lives in nodejs app, and mongodb knows nothing about it.
It looks like it belongs to representation layer, where you want to show codes as meaningful names. The mapping should be done there. I believe find is the most suitable here:
names.find(name => name.code === doc._id.code).text
If the names are not truly variable but quite constant, you can move it to own collection, e.g. codeNames:
db.codeNames.insert([
{ _id: "1", text: 'One' },
{ _id: "2", text: 'Two' },
{ _id: "3", text: 'Three' },
{ _id: "4", text: 'Gour' }
]);
and use $lookup as following:
db.collection.aggregate([
{
$match: {
_Id: id
}
},
{
"$group" : {
"_id": {
code: "$code",
number: "$number"
}
}
},
{
$lookup: {
localField: "_id.code",
from: "codeNames",
foreignField: "_id",
as: "renderedNames"
}
}
]);
If none of the above suit your usecase, you can pass the names to the database in each request to map names db-side, but you must be really really sure you cannot use 2 previous options:
db.collection.aggregate([
{
$match: {
_Id: id
}
},
{
"$group" : {
"_id": {
code: "$code",
number: "$number"
}
}
},
{
$project: {
renderedNames: { $filter: {
input: [
{ value: "1", text: 'One' },
{ value: "2", text: 'Two' },
{ value: "3", text: 'Three' },
{ value: "4", text: 'Gour' }
],
as: "name",
cond: { $eq: [ "$$name.value", "$_id.code" ] }
}
}
}
},
]);
As a side note, I find $match: {_Id: id} quite confusing, especially followed by $group. If _Id is _id, it is unique. You can have no more than 1 document after this stage, so there is not too much to group really.

Resources