Elasticsearch Query on multiple object fields of an Array of Objects - node.js

I have a product document that looks like this:
{
"_index": "productss",
"_type": "_doc",
"_id": "2fb60b1880f0251af4340af009",
"_score": 5.0262785,
"_source": {
"prepTime": {
"durationType": "Min",
"value": 8,
"imageUrl": ""
},
"shopId": "CCXow8ALRDrALRSKFC",
"productTimings": [
{
"startHour": 8,
"endHour": 9,
"startMin": 30,
"endMin": 45,
"dayOfWeek": [
"Mon",
"Tue",
"Wed",
"Thu",
"Fri"
]
},
{
"startHour": 16,
"endHour": 18,
"startMin": 30,
"endMin": 45,
"dayOfWeek": [
"Sat",
"Sun"
]
}
]
}
}
My model looks like this.
const mongoosastic = require('mongoosastic');
const { Schema } = mongoose;
const Timing = new Schema({
startHour: { type: Number, es_indexed: true },
endHour: { type: Number, es_indexed: true },
startMin: { type: Number, es_indexed: true },
endMin: { type: Number, es_indexed: true },
dayOfWeek: [{ type: String, es_indexed: true }],
});
const productsSchema = mongoose.Schema({
_id: { type: String },
prepTime : {
durationType : { type: String, es_indexed: true },
value : { type: Number, es_indexed: true },
imageUrl : { type: String, es_indexed: true }
},
shopId: { type: String, es_indexed: true },
productTimings: {
type: [Timing],
es_indexed: true,
es_type: 'nested',
es_include_in_parent: true,
}
});
productsSchema.plugin(mongoosastic);
module.exports = mongoose.model('Products', productsSchema, 'Products');
I need to fetch all products whose startHours:Startminute is less than the current time and endHour:endMinute is less than the current time. It should also match the dayOfWeek from current date.
Note that, there could be a morning slot and a evening slot in the productTimings.
I have tried the following and got this far, but cannot proceed further:
prodDetails = await client.search({
index: 'productss',
body:
{
query:
{
bool: {
must: [
{ match: { shopId } },
{
"nested": {
"path": "productTimings",
"query": {
"range": {
"productTimings.startHour": {
"lte": 12,
}
},
"range": {
"productTimings.endHour":{
"gte": 11,
}
}
}
}
}
]
}
}
},
},
from: pageno * size,
size,
});```

You were almost there but the query was heavily malformed.
Try this — I've adapted it to match the sample document from above:
const body = {
size,
from: pageno * size,
"query": {
"bool": {
"must": [
{
"match": { shopId }
},
{
"nested": {
"path": "productTimings",
"query": {
"bool": {
"must": [
{
"range": {
"productTimings.startHour": {
"gte": 8
}
}
},
{
"range": {
"productTimings.endHour": {
"lte": 9
}
}
},
{
"match": {
"productTimings.dayOfWeek": "Fri"
}
}
]
}
}
}
}
]
}
}
}
prodDetails = await client.search({
index: 'productss',
body: body
})
💡 Tip: I'd recommend using a term query instead of match. Even before that, I'd convert the shopId and dayOfWeek to keywords. When they're mapped as String (= text), they'll be lowercased by the standard analyzer — and you don't want that. You likely want case-sensitive, exact matches that the keyword mapping guarantees.

Related

How query for elements in array of subdocuments

This is mongoose schema of route model
const routeSchema = new mongoose.Schema({
route: {
type: [{
stationCode: {
type: String,
required: true,
uppercase: true,
validate: {
validator: async function(val) {
const doc = await Station.findOne({
code: val,
});
if (!doc) return false;
return true;
},
message: `A Station with code {VALUE} not found`,
},
},
distanceFromOrigin: {
type: Number,
required: [
true,
'A station must have distance from origin, 0 for origin',
],
},
}, ],
validate: {
validator: function(val) {
return val.length >= 2;
},
message: 'A Route must have at least two stops',
},
},
}, {
toJSON: {
virtuals: true
},
toObject: {
virtuals: true
},
});
This schema has a field route as array of documents,
each document has a stationCode,
I want to query for all the documents which has "KMME" and "ASN" stationCode in the specified order.
Below is example of a document created with this schema
{
"_id": {
"$oid": "636957ce994af955df472ebc"
},
"route": [{
"stationCode": "DHN",
"distanceFromOrigin": 0,
"_id": {
"$oid": "636957ce994af955df472ebd"
}
},
{
"stationCode": "KMME",
"distanceFromOrigin": 38,
"_id": {
"$oid": "636957ce994af955df472ebe"
}
},
{
"stationCode": "ASN",
"distanceFromOrigin": 54,
"_id": {
"$oid": "636957ce994af955df472ebf"
}
}
],
"__v": 0
}
Please suggest a query for this problem or another schema definition for this problem
One simple option is:
db.collection.aggregate([
{$match: {$expr: {$setIsSubset: [["ASN", "KMME"], "$route.stationCode"]}}},
{$set: {
wanted: {$first:{
$filter: {
input: "$route",
cond: {$in: ["$$this.stationCode", ["ASN", "KMME"]]}
}
}}
}},
{$match: {"wanted.stationCode": "KMME"}},
{$unset: "wanted"}
])
See how it works on the playground example

Querying nested objects using find not working in mongoose (MongoDB)

I'm trying to get an object which has isDraft value true, but I'm also getting objects which have isDraft value false. I need only objects having isDraft value true. I have tried all possible ways but am not able to find a solution for this. Can anyone help me with this?
Below are the schema, query and response.
Schema
const mongoose = require('mongoose');
const Schema = mongoose.Schema;
const Contract = new Schema({
name: {
type: String,
unqiue: true,
required: true
},
version: [
{
no: {
type: Number,
required: true
},
sections: [
{
sectionName: {
type: String,
required: true
},
clause: [{
description: {
type: String,
required: true
},
}]
}
],
approvedBy: [
{
user: {
type: Schema.Types.ObjectId,
ref: 'user'
},
}
],
acceptedBy: [
{
name: {
type: String,
},
eamil: {
type: String,
},
}
],
isDraft: {
type: Boolean,
required: true
},
date: {
type: Date,
default: Date.now
}
}
],
createdBy: {
type: Schema.Types.ObjectId,
ref: 'user',
required: true
},
});
module.exports = mongoose.model('contract', Contract);
Query
query = {
$and: [
{ createdBy: clientAdminDetails._id },
{ "version.isDraft": true }
],
};
await Contract
.find(query)
.skip(req.body.noOfItems * (req.body.pageNumber - 1))
.limit(req.body.noOfItems)
.exec((err, contract) => {
if (err) {
return res.json(err);
}
Contract.countDocuments(query).exec((count_error, count) => {
if (err) {
return res.json(count_error);
}
return res.json({
total: count,
page: req.body.pageNumber,
pageSize: contract.length,
contracts: contract
});
});
});
Response
{
"total": 1,
"page": 1,
"pageSize": 1,
"contracts": [
{
"_id": "61449469775..",
"name": "Octavia Blankenship",
"version": [
{
"_id": "614496593cc..",
"sections": [
{
"_id": "61449469775..",
"sectionName": "Est dolore dolorem n Updated `1323",
"clause": [
{
"_id": "614494697..",
"description": "Numquam nostrud et a"
}
]
}
],
"isDraft": false,
"no": 1,
"approvedBy": [],
"acceptedBy": [],
"date": "2021-09-17T13:21:29.509Z"
},
{
"_id": "614496122904ee4e046fbee8",
"sections": [
{
"_id": "6144955a8c0061025499606f",
"sectionName": "Praesentium suscipit",
"clause": [
{
"_id": "6144955a8c00610254996070",
"description": "Velit aperiam ut vel"
}
]
}
],
"isDraft": true,
"no": 2,
"approvedBy": [],
"acceptedBy": [],
"date": "2021-09-17T13:20:18.128Z"
}
],
"createdBy": "614367e980b29e6c...",
"__v": 0
}
]
}
This is why using your query you are telling mongo "Give me a document where createdBy is desired id and version.isdraft is true" So, as the DOCUMENT contains both values, is returned, even existing false into the array.
To solve this you have many ways.
First one is using $elemMatch into projection (docs here). But using this way only the first element is returned, so I think you prefer other ways.
So you can use an aggregation query using $filter like this:
First $match by values you want (as in your query).
Then override version array filtering by values where isDraft = true.
db.collection.aggregate([
{
"$match": {
"createdBy": "",
"version.isDraft": true
}
},
{
"$set": {
"version": {
"$filter": {
"input": "$version",
"as": "v",
"cond": {
"$eq": [
"$$v.isDraft",
true
]
}
}
}
}
}
])
Example here

Update object with value of array

For a project where we have actions and donations. We store the donations in an array in the related action. For the connection we use Mongoose.
The schema for an action is as follows, for readability I've removed some fields which are not related to this problem:
const donationSchema = new Schema(
{
id: {
type: String,
unique: true,
required: true,
index: true,
},
amount: { type: Number },
status: {
type: String,
enum: ['pending', 'collected', 'failed'],
default: 'pending',
},
},
{ timestamps: true, versionKey: false, _id: false },
);
const schema = new Schema(
{
donations: { type: [donationSchema], default: [] },
target: { type: Number, default: 0 },
collected: { type: Number, default: 0 },
},
{
timestamps: true,
versionKey: false,
},
);
const Action = model<IAction>('Action', schema);
Let say I have an Action with three donations, one in every state:
{
"_id": "6098fb22101f22cfcbd31e3b"
"target": 10000,
"collected": 25,
"donations": [
{
"uuid": "dd90f6f1-56d7-4d8b-a51f-f9e5382d3cd9",
"amount": 25,
"status": "collected"
},
{
"uuid": "eea0ac5e-1e52-4eba-aa1f-c1f4d072a37a",
"amount": 10,
"status": "failed"
},
{
"uuid": "215237bd-bfe6-4d5a-934f-90e3ec9d2aa1",
"amount": 50,
"status": "pending"
}
]
}
Now I want to update the pending donation to collected.
This would be
Action.findOneAndUpdate(
{
_id: '6098fb22101f22cfcbd31e3b',
'donations.id': '215237bd-bfe6-4d5a-934f-90e3ec9d2aa1',
},
{
$set: {
'donations.$.status': 'collected',
},
},
{
upsert: false,
returnOriginal: false,
}
).then((action) => console.log(action);
I want to update the status to collected, but also update the collected so that it is the same as all the donations with status equal to collected. I thought of using the $inc operator, but this keeps saying that donations.$.amount is not a number and therefore not able to increment collected.
Is there a way to do this in the same update call? The reason why I cannot get the object and just count collected amount is that maybe two donation callbacks occur at the same time, so we don't want the to overwrite the previous given amount.
This aggregation can help you I believe:
db.collection.aggregate([
{
"$match": {
_id: "6098fb22101f22cfcbd31e3b"
}
},
{
"$set": {
"donations.status": {
"$reduce": {
"input": "$donations",
"initialValue": {
uuid: "215237bd-bfe6-4d5a-934f-90e3ec9d2aa1"
},
"in": {
$cond: [
{
$eq: [
"$$this.uuid",
"$$value.uuid"
]
},
"collected",
"$$this.status"
]
}
}
}
}
},
{
"$set": {
"collected": {
"$reduce": {
"input": "$donations",
"initialValue": "$collected",
"in": {
$cond: [
{
$eq: [
"$$this.status",
"collected"
]
},
{
$sum: [
"$$value",
"$$this.amount"
]
},
"$$value"
]
}
}
}
}
}
])
Edit: Above aggregation wasn't properly update status field to "collected" dunno why..
But update query below should work. I couldn't test it too. So, please let me know if something goes wrong.
db.collection.update({
"_id": "6098fb22101f22cfcbd31e3b"
},
{
"$set": {
"donations.$[element].status": "collected",
"$inc": {
"donations.$[element].amount": {
"$cond": [
{
"$eq": [
"donations.$[element].status",
"collected"
]
},
"donations.$[element].amount",
"collected"
]
}
}
}
},
{
"arrayFilters": [
{
"element.uuid": "215237bd-bfe6-4d5a-934f-90e3ec9d2aa1"
}
]
})

mongodb insert into array of objects

I need to find the element by id, then go into the array of objects, find the desired by id and add the field guessId into it and insert the id key into it and assign the array of id. How can I do that ?
https://jsoneditoronline.org/?id=442f1dae0b2d4997ac69d44614e55aa6
In general, I need to create a GuessId field with such a structure
in fact, I am doing the voting, that is, the key in guessId will be go to vote, and the array to it, this is users who vote
{
"_id": "f58482b1-ae3a-4d8a-b53b-ede80fe1e225",
"bio": {
"firstname": "Лена",
"lastname": "фыв",
"middlename": "",
"company": "вв"
},
"files": [
{
"_id": "2e4e40c7-4df6-4974-8d16-bb24cd8134d6",
"destination": "./uploads/f58482b1-ae3a-4d8a-b53b-ede80fe1e225",
"filename": "2e4e40c7-4df6-4974-8d16-bb24cd8134d6.mp3",
"path": "uploads\\f58482b1-ae3a-4d8a-b53b-ede80fe1e225\\2e4e40c7-4df6-4974-8d16-bb24cd8134d6.mp3",
"folder": "f58482b1-ae3a-4d8a-b53b-ede80fe1e225",
"info": {
"size": 20805727,
"mimetype": "audio/mp3",
"encoding": "7bit",
"originalname": "Ахуевший Ленусик (Банк русский стандарт). Выпуск #5..mp3",
"fieldname": "selectedFile"
},
"userId": "5e05da745b21e61ccc84a892",
"date": "2019-12-27T10:19:12.213Z"
},
{
"_id": "81b94dea-ece6-421c-b68a-0aa59332cd0d",
"destination": "./uploads/f58482b1-ae3a-4d8a-b53b-ede80fe1e225",
"filename": "81b94dea-ece6-421c-b68a-0aa59332cd0d.mp3",
"path": "uploads\\f58482b1-ae3a-4d8a-b53b-ede80fe1e225\\81b94dea-ece6-421c-b68a-0aa59332cd0d.mp3",
"folder": "f58482b1-ae3a-4d8a-b53b-ede80fe1e225",
"info": {
"size": 13515683,
"mimetype": "audio/mp3",
"encoding": "7bit",
"originalname": "Выпуск #75 Попрошайка НСВ..mp3",
"fieldname": "selectedFile"
},
"userId": "5e05da745b21e61ccc84a892",
"date": "2019-12-27T10:25:37.710Z"
}
],
"date": "2019-12-27T10:19:12.213Z",
"__v": 1
}
Schema
const mongoose = require('mongoose');
const Schema = mongoose.Schema;
const HabalkaSchema = new Schema({
_id: {
type: String
},
bio: {
firstname: String,
lastname: String,
middlename: String,
company: String
},
files: [
{
_id: {
type: String
},
destination: {
type: String
},
filename: {
type: String
},
path: {
type: String
},
folder: {
type: String
},
info: {
size: {
type: Number
},
mimetype: {
type: String
},
encoding: {
type: String
},
originalname: {
type: String
},
fieldname: {
type: String
},
},
date: {
type: Date,
default: Date.now
},
bio: {
type: Object
},
userId: String,
guessId: {},
}
],
date: {
type: Date,
default: Date.now
}
});
module.exports = Habalka = mongoose.model('habalka', HabalkaSchema);
You can use the positional $ operator like this:
router.put("/habalka/:id/:fileId/:guessId", async (req, res) => {
const result = await Habalka.findOneAndUpdate(
{
_id: req.params.id,
"files._id": req.params.fileId
},
{
"files.$.guessId": {
[req.params.guessId]: ["a", "b"] //todo: get this array from req.body
}
},
{
new: true
}
);
res.send(result);
});
Route url: http://..../habalka/f58482b1-ae3a-4d8a-b53b-ede80fe1e225/2e4e40c7-4df6-4974-8d16-bb24cd8134d6/asda2
This will give you a result like this in the given file:
"files": [
{
"info": {
"size": 20805727,
"mimetype": "audio/mp3",
"encoding": "7bit",
"originalname": "Ахуевший Ленусик (Банк русский стандарт). Выпуск #5..mp3",
"fieldname": "selectedFile"
},
"date": "2019-12-27T10:19:12.213Z",
"_id": "2e4e40c7-4df6-4974-8d16-bb24cd8134d6",
"destination": "./uploads/f58482b1-ae3a-4d8a-b53b-ede80fe1e225",
"filename": "2e4e40c7-4df6-4974-8d16-bb24cd8134d6.mp3",
"path": "uploads\\f58482b1-ae3a-4d8a-b53b-ede80fe1e225\\2e4e40c7-4df6-4974-8d16-bb24cd8134d6.mp3",
"folder": "f58482b1-ae3a-4d8a-b53b-ede80fe1e225",
"userId": "5e05da745b21e61ccc84a892",
"guessId": {
"asda2": [
"a",
"b"
]
}
},
And later if you want to add an item inside one of the guessId arrays, you can use the following code:
router.put("/habalka/:id/:fileId/:guessId", async (req, res) => {
const result = await Habalka.findOneAndUpdate(
{
_id: req.params.id,
"files._id": req.params.fileId
},
{ $push: { [`files.$.guessId.${req.params.guessId}`]: "c" } },
{
new: true
}
);
res.send(result);
});

Mongoose upsert without deleting existing fields

The goal of my code is to upsert an object into my database. I created the Schema which is a bit complex
const mongoose = require("mongoose");
const Schema = mongoose.Schema;
const objectSchema = new Schema(
{ message: String },
{ discriminatorKey: "type", _id: false }
);
const cameraSchema = new Schema(
{
number: { type: Number, required: true },
name: { type: String, required: true } //Do not update if exists
},
{ _id: false }
);
const mainSchema = new Schema(
{
name: { type: String, required: true, trim: true },
lastUpdateTime: { type: Date, default: Date.now },
objects: [objectSchema]
},
{ collection: "mainCollection" }
);
mainSchema.path("objects").discriminator(
"TypeA",
new Schema(
{
cameras: [cameraSchema],
enabled: { type: Boolean } //Do not update if exists
},
{ _id: false }
)
);
mainSchema.path("objects").discriminator(
"TypeB",
new Schema(
{
available: { type: Boolean, required: true },
enabled: { type: Boolean } //Do not update if exists
},
{ _id: false }
)
);
module.exports = mongoose.model("mainModel", mainSchema);
I am receiving the following object from an external source
{
"name": "040C7",
"objects": [
{
"type": "TypeA",
"cameras": [
{
"number": 1
},
{
"number": 2
},
{
"number": 3
}
]
},
{
"type": "TypeB",
"available": false
}
]
}
If the name exists in my database, I should update every field that I received without removing/updating the fields :
enabled in TypeA and TypeB
name in the camera objects
Otherwise, I should insert it in the database with all the fields and default values for enabled (true) and name (Camera)
I already tried the UpdateOne() function but it is deleting my fields enabled and name when they already are in my database
const obj = new myModel(message);
let upsertObj = obj.toObject();
delete upsertRcm._id;
MyModel.updateOne(
{ name: obj.name },
upsertObj,
{ upsert: true },
err => {
console.log(err)
}
);
How can I say to mongoose to keep fields that are already in my database but not in my received object ?
Document in database
{
"lastUpdateTime": "2018-11-07T09:12:30.750Z",
"name": "040C7",
"objects": [
{
"type": "TypeA",
"enabled": true,
"cameras": [
{
"number": 1,
"name": "Camera",
}
]
},
{
"type": "TypeB",
"enabled": true,
"available": false
}
]
}
After the update with the received object
{
"lastUpdateTime": "2018-11-07T09:14:30.600Z",
"name": "040C7",
"objects": [
{
"type": "TypeA",
"cameras": [
{
"number": 1,
},
{
"number": 2,
},
{
"number": 3,
}
]
},
{
"type": "TypeB",
"available": false
}
]
}
But it should be
{
"lastUpdateTime": "2018-11-07T09:14:30.600Z",
"name": "040C7",
"objects": [
{
"type": "TypeA",
"enabled": true
"cameras": [
{
"number": 1,
"name": "Camera"
},
{
"number": 2,
},
{
"number": 3,
}
]
},
{
"type": "TypeB",
"enabled": true,
"available": false
}
]
}
MongoDB version: 4.0.2
The name field is my unique identifier. Only one type "TypeA", "TypeB", ... will be present in the array. I will never receive 2 "TypeA" in my objects
The list of fields that have to be updated :
objects["TypeA"].cameras.number (value from new object)
objects["TypeB"].available (value from new object)
lastUpdateTime (Date now)
The list of fields that are in the database and should kept :
objects["TypeA"].enabled
objects["TypeA"].cameras.name
objects["TypeB"].enabled

Resources