How to use custom script inside aggregation in elasticsearch with nodejs - javascript

Here is the my code:
const convertCurrency = async (currrencyType) => {
try{
const res = await got("https://api.exchangerate-api.com/v4/latest/USD");
return (JSON.parse(res.body)['rates'][currrencyType]);
}
catch(err){
console.log(err);
}
}
async function search(){
try{
client.search({
index : '03776182',
body: {
aggs : {
price_ranges : {
range : {
field : "price",
script : {source : "convertCurrency(/EUR/) * _value"},
ranges : [
{ "from" : 10, "to" : 20 },
]
}
}
}
}
},(err,resp,status)=>{
err ? console.log(err): console.log(JSON.stringify(resp));
});
}
catch(err){console.log(err);}
}
I am trying to insert the custom script inside the source field. But it gives below error.
response: `{"error":{"root_cause":[{"type":"script_exception","reason":"compile error","script_stack":["convertCurrency(/EUR/) * _value"," ^---- HERE"],"script":"convertCurrency(/EUR/) * _value","lang":"painless"}],"type":"search_phase_execution_exception","reason":"all shards failed","phase":"query","grouped":true,"failed_shards":[{"shard":0,"index":"03776182","node":"C4p_ha96QhK2uB47jV9pxg","reason":{"type":"script_exception","reason":"compile error","script_stack":["convertCurrency(/EUR/) * _value"," ^---- HERE"],"script":"convertCurrency(/EUR/) * _value","lang":"painless","caused_by":{"type":"illegal_state_exception","reason":"Regexes are disabled. Set [script.painless.regex.enabled] to [true] in elasticsearch.yaml to allow them. Be careful though, regexes break out of Painless's protection against deep recursion and long loops."}}}]},"status":500}`,
toString: [Function],
At the same time normal expression are working well.

You cannot invoke a JS callback from an ES script, it doesn't make sense as they don't execute in the same context.
What you should do is to first retrieve the EUR exchange rate in your Node.js code and then send your query with that exchange rate as a parameter to your script.
In pseudo code, it would look like this:
// 1. retrieve EUR/USD exchange rate
const EUR_USD = convertCurrency('EUR');
// 2. send your query
client.search({
index : '03776182',
body: {
aggs : {
price_ranges : {
range : {
field : "price",
script : {
source : "params.rate * _value"
params: {
rate: EUR_USD
}
},
ranges : [
{ "from" : 10, "to" : 20 },
]
}
}
}
}

Related

Sorting MongoDB result by another MongoDB result is returning [null, null, null]

I'm performing two MongoDB queries, and then I want to synchronize the resulting arrays, to make sure they are in the same order.
The first array is a set of (20) questions ids (this is the correct order):
q_id_arr: [
"5f86da2d37e3d200040ba523",
"5f86b6ce37e3d200040ba4c6",
"5ffc4abea04f3c0004e46cf3",
"5f86b66537e3d200040ba4c5",
"5f87f368554f370004ed17b4",
"5f86e48c37e3d200040ba53c",
"5ffc4dc4a04f3c0004e46d0b",
"5f86e19037e3d200040ba534",
"5f86aaa237e3d200040ba49b",
"5ffc479ba04f3c0004e46ce0",
"5f86b9dc37e3d200040ba4d2",
"5f85828e0e1bd30004361430",
"5f8700c937e3d200040ba548",
"5f86d81737e3d200040ba51c",
"5f8708d237e3d200040ba568",
"5f87060d37e3d200040ba55c",
"5f857dac0e1bd3000436141c",
"5f85703e0e1bd300043613ec",
"5f87e9d4554f370004ed178e",
"5f8073c04ad88e00041f015f"
]
The second array is a set of (20) results associated with the question ids:
team_trends: [
{
"_id":"5f87e9d4554f370004ed178e",
"positive":0.93,
"engaged":0.558
},
{
"_id":"5f86e19037e3d200040ba534",
"positive":0.585,
"engaged":0.567
},
{
"_id":"5f85828e0e1bd30004361430",
"positive":0.7,
"engaged":0.666
},
{
"_id":"5f8073c04ad88e00041f015f",
"positive":0.31,
"engaged":0.30999999999999994
},
{
"_id":"5f87f368554f370004ed17b4",
"positive":0.5449999999999999,
"engaged":0.57
},
{
"_id":"5f86b6ce37e3d200040ba4c6",
"positive":0.855,
"engaged":0.46599999999999997
},
{
"_id":"5f857dac0e1bd3000436141c",
"positive":0.92,
"engaged":0.524
},
{
"_id":"5f85703e0e1bd300043613ec",
"positive":0.15,
"engaged":0.39
},
{
"_id":"5f86aaa237e3d200040ba49b",
"positive":0.15000000000000002,
"engaged":0.584
},
{
"_id":"5f86b66537e3d200040ba4c5",
"positive":0.37,
"engaged":0.386
},
{
"_id":"5f86e48c37e3d200040ba53c",
"positive":0.615,
"engaged":0.548
},
{
"_id":"5ffc479ba04f3c0004e46ce0",
"positive":0.42000000000000004,
"engaged":0.583
},
{
"_id":"5f86b9dc37e3d200040ba4d2",
"positive":0.68,
"engaged":0.662
},
{
"_id":"5f86d81737e3d200040ba51c",
"positive":0.03,
"engaged":0.516
},
{
"_id":"5f87060d37e3d200040ba55c",
"positive":0.14,
"engaged":0.454
},
{
"_id":"5f86da2d37e3d200040ba523",
"positive":0.47,
"engaged":0.41500000000000004
},
{
"_id":"5f8708d237e3d200040ba568",
"positive":0.17,
"engaged":0.76
},
{
"_id":"5ffc4dc4a04f3c0004e46d0b",
"positive":0.395,
"engaged":0.53
},
{
"_id":"5ffc4abea04f3c0004e46cf3",
"positive":0.365,
"engaged":0.679
},
{
"_id":"5f8700c937e3d200040ba548",
"positive":0.93,
"engaged":0.6980000000000001
}
]
I want to reorganize team_trends into the same order as q_id_arr
Here is the code I'm using (following this SO Answer):
let c = [];
q_id_arr.forEach((q_oid => c.push(team_trends.find((obj => obj._id == q_oid)))));
However when I print console.log("the result of c"+ c) I get this result:
the result of c: [null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null]
Is this the right approach? Any suggestions are appreciated!
More Details:
Before this step I acquired the q_id_arr through mapping over an aggregate result, like this:
let q_id_arr = await user_trends.map(({ question_oid }) => question_oid)
When I tested console.log(typeof q_id_arr) it returned object.
How can I sort through the object??
Final code that fixed the issue.
user_trends.forEach((user => c.push(team_trends.find((obj => obj._id.toString() === user.question_oid.toString())))));
A couple of points to note here:
If you are using mongoose then it already returns an array from the aggregate function.
Mongoose uses the MongoDB NodeJs native driver at its core. In core driver ObjectId has a function .equals(otherId). It is always best to use this function for id comparisons.
References:
Comparing mongoose _id and strings
.aggregate(...).toArray is not a function
https://mongodb.github.io/node-mongodb-native/api-bson-generated/objectid.html#equals
You can try this using the arrays map method:
let sorted_team_trends = q_id_arr.map( q => team_trends.find(t => t._id === q) );
Assuming the two arrays are defined as fields like this:
let q_id_arr = [
"5f86da2d37e3d200040ba523",
"5f86b6ce37e3d200040ba4c6",
"5ffc4abea04f3c0004e46cf3",
...
];
let team_trends = [
{
"_id" : "5f87e9d4554f370004ed178e",
"positive" : 0.93,
"engaged" : 0.558
},
{
"_id" : "5f86e19037e3d200040ba534",
"positive" : 0.585,
"engaged" : 0.567
},
...
]

How to give default values and increase a specific one in mongoDB object

Im trying to increase one value in an object , while giving the others the default value if the document doesnt exist in the collection yet.
// the default should be 100 if the document is new :
_id : someObjectId,
gameId : someObjectId,
votes : {
a: 100,
b: 100,
c: 100
}
// my code :
function addVote(userVote) {
userVote.gameId = new ObjectId(userVote.gameId)
return mongoService.connect()
.then(db => {
const collection = db.collection('totalVotes');
return collection.findOneAndUpdate(
{ gameId: userVote.gameId },
{ $set: { gameId: userVote.gameId }, $inc: {["votes." + userVote.vote] : 1 } },
{ upsert:true , returnOriginal :false}
)
})
}
So my code right now creates the new document with the vote chosen + 1 .
any idea on how can i also give the default values on the same query ?

Update Data from Within a Loop

I am trying to get my nodejs controller to update the rate in the currency table.
Everything works fine in S3T / RoboMongo, but for some reason it just wont fire the update inside the nodejs controller.
Here is my currency table
{
"_id" : "USD",
"index" : NumberInt(6),
"name" : "Dollar",
"currency" : "USD",
"symbol" : "$",
"active" : true,
"default" : false,
"rate" : 0
}
{
"_id" : "EUR",
"index" : NumberInt(2),
"name" : "Euro",
"currency" : "EUR",
"symbol" : "€",
"active" : true,
"default" : false,
"rate" : 0
}
I tried both of these, works fine in S3T but not inside nodejs:
db.currency.update (
{ _id : "EUR" },
{ $set: { rate : 123 }},
{ upsert: true }
)
db.currency.updateOne (
{ _id : "EUR" },
{ $set: { rate : 123 }},
{ upsert: true }
)
Here is my nodejs code:
var mongoose = require('mongoose');
var currencyModel = require('../models/currencyModel');
var currencyTable = mongoose.model('currencyModel');
var updateRates = () => {
return new Promise((resolve, reject) => {
for (var key in data.quotes) {
var currencyID = key.substring(3);
var newRate = (data.quotes[key] * THBUSD).toFixed(5);
console.log("currencyID: " + currencyID)
console.log("newRate: " + newRate)
currencyTable.update (
{ _id: currencyID },
{ $set: { rate : newRate }},
{ upsert: true }
),function (err, data) {
if (err) {
reject(new Error('updateRates: ' + err));
};
};
};
resolve();
})};
And here is my currencyModel (which is where I think the problem is?!?)
// Currency Model
// This model is the structure containing data from the Currency table
//
var mongoose = require('mongoose');
var Schema = mongoose.Schema;
var currencySchema = new Schema({
_id: String, // Unique Currency code
index: Number, // Indes for sorting
name: String, // Currency name
symbol: String, // Currency symbol
active: Boolean, // Active True False
rate: Number // Exchange rate (multiply with THB price)
});
module.exports = mongoose.model('currencyModel', currencySchema, 'currency');
I cannot see why it wont fire the currencyTable.update from inside nodejs.
I turned debug on in mongoose, and I see all other mongodb operations in the console like Mongoose: price.findOne({ _id: 'ATL-D406' }, { fields: {} }) etc.. but I do not see this currency.update in the console, which is why I dont think its fired off to mongodb - and I cannot see the reason.
You have a "loop" that completes execution before the inner callbacks fire. Instead just use Promises all the way through and call Promise.all() to collect all the iterated Promises and resolve them:
var updaterates = () => {
return Promise.all(
Object.keys(data.quotes).map(k => {
return currencyTable.update(
{ _id: k.substring(0,3) },
{ $set: { rate : (data.quotes[k] * THBUSD).toFixed(5) }},
{ upsert: true }
).exec()
});
)
};
The returned response of Promise.all() is an array of the response objects from the updates. Also note that this is a "fast fail" operation and calls will be made in parallel.
Object.keys() returns an "array of the key names" in the specified object. .map() iterates those keys and returns an "array" of the return value for the iterator.
We use the k as the "key name" to access the wanted key from data.quotes and use the values to perform each .update() with .exec() to return a "real" Promise. The iterator returns an "array" Promise which becomes the argument to Promise.all().

how do i replace some properties returned from database in sails

So i'm trying to replace fields on the data queried from the database in sails.
async.waterfall( [
function getscores(callback) {
Score.find({course : courseId}).paginate({page : 1 , limit: 10}).populate('course')
.exec(function(err,data) {
callback(null,data);
});
}
, function addUserInfo(result,callback) {
for(var i=0; i < result.length; i++){
result[i].user = User.findOne({id : result[i].user}).exec(function(err,data) {
var temp = {
"name" : data.name,
"id" : data.id,
"user_id" : data.user_id
}
return temp;
});
}
res.json(messageGenerator(200, 'Sucecss', result));
}],function(err) {
console.log(err);
}
);
the first function 'getScores' returns the scores array but each score property only has a user id. Now in addUserInfo function, i want to be able to add the user's name to the score property.
But the above code fails to return the users inside. the user property of score is empty. i believe the response is already sent before the program gets to add the user property ( due to asyncronousness of the database adapter).
Following a brief comment chat, the following replacement for the addUserInfo function should help you achieve what you desire:
function addUserInfo(results,callback) {
async.map(results, function(result, callback) {
User.findOne({id : result.user}).exec(function(err, data) {
callback(err, Object.assign(result, {
"user": {
"name" : data.name || null,
"id" : data.id || null,
"user_id" : data.user_id || null
}
}));
});
}, function(err, output) {
return res.json(messageGenerator(200, "Success", output))
});
}
Async map allows us to asynchronously iterate over results, allowing us to amend the result by supplying the callback with the new result as the second parameter. The Final function is our final callback, that gets provided with any err's that have occured along the way and our new Array as output.

Dynamically building MongoDB queries in NodeJS

I receive a POST argument that looks like this:
sort:
[
{ field: 'name', dir: 'asc', compare: '' },
{ field: 'org', dir: 'asc', compare: '' }
]
}
and I need to create a MongoDB query based on that, so it should look like:
db.collection("my_collection").find( ... ).sort({'name': 'asc', 'org': 'asc'}).toArray(...);
Anyways, keep in mind that more fields could be passed. Also, it could happen that none of those fields is passed, meaning that the query won't have .sort().
My question: How can I create dynamically a query with Node's MongoDB driver? Is there a query builder or something similar?
I've found that most cases are unique regarding passed data, so building query objects varies from project to project.
So first ideas was to create middleware for express (in my case), that would parse query arguments into objects that are valid for query.
mongo-native can use as chained options to cursor, as well as in object:
Chained:
items.find({ type: 'location' }).sort({ title: 1 }).limit(42).toArray(function(err, data) {
// ...
});
Non-chained:
items.find({ type: 'location' }, { sort: { title: 1 }, limit: 42 }).toArray(function(err, data) {
// ...
});
As you can see Non-Chained can accept everything as object, while chained returns cursor after every method and can be reused. So generally you have two options:
For Chained:
var cursor = items.find({ type: 'location' });
if (sort) {
cursor.sort(sort);
}
cursor.toArray(function(err, data) {
// ...
});
For Non-Chained:
var options = { };
if (sort) {
options.sort = sort;
}
items.find({ type: 'location' }, options).toArray(function(err, data) {
// ...
});
It is important to remember that any data from query have to be validated and parsed properly. As well if you are developing API (for example), and will decide to change the way sorting arguments are passed or will want to add new way, then making middleware (in express.js) for parsing this data - is the way to go.
Example for pagination:
function pagination(options) {
return function(req, res, next) {
var limit = options.limit ? options.limit : 0;
var skip = 0;
if (req.query.limit) {
var tmp = parseInt(req.query.limit);
if (tmp != NaN) {
limit = tmp;
}
}
if (req.query.skip) {
var tmp = parseInt(req.query.skip);
if (tmp != NaN && tmp > 0) {
skip = tmp;
}
}
if (options.max) {
limit = Math.min(limit, options.max);
}
if (options.min) {
limit = Math.max(limit, options.min);
}
req.pagination = {
limit: limit,
skip: skip
};
next();
}
}
Usage:
app.get('/items', pagination({
limit: 8, // by default will return up to 8 items
min: 1, // minimum 1
max: 64 // maximum 64
}), function(req, res, next) {
var options = {
limit: req.pagination.limit,
skip: req.pagination.limit
};
items.find({ }, options).toArray(function(err, data) {
if (!err) {
res.json(data);
} else {
next(err);
}
});
});
And url examples:
http://example.com/items
http://example.com/items?skip=64
http://example.com/items?skip=256&limit=32
So it is the way to develop well flexible framework, which does not creates any rules of how things have to be coded as well as solving your challenge.

Categories

Resources