Ruby Mongo Driver Projection Elemmatch - ruby-on-rails

Following the code in http://www.w3resource.com/mongodb/mongodb-elemmatch-projection-operators.php I have set up a test database using the ruby mongodb driver.
For those following along at home, you first need to install the mongo driver as described at https://docs.mongodb.com/ecosystem/tutorial/ruby-driver-tutorial/#creating-a-client, then run the following commands.
client = Mongo::Client.new([ '127.0.0.1:27017'], :database => 'mydb')
test = client['test']
doc = {
"_id" => 1,
"batch" =>10452,
"tran_details" =>[
{
"qty" =>200,
"prate" =>50,
"mrp" =>70
},
{
"qty" =>250,
"prate" =>50,
"mrp" =>60
},
{
"qty" =>190,
"prate" =>55,
"mrp" =>75
}
]
}
test.insert_one(doc)
Insert all of the different docs created in the w3 tutorial.
If you look at example 2 in the w3 tutorial, the translated ruby find is:
test.find({"batch" => 10452}).projection({"tran_details" => {"$elemMatch" => {"prate" => 50, "mrp" => {"$gte" => 70}}}}).to_a
which returns the same result as in the example.
=> [{"_id"=>1, "tran_details"=>[{"qty"=>200, "prate"=>50, "mrp"=>70}]}, {"_id"=>3}, {"_id"=>4}]
My problem is that I would like to constrain the results with the constraints above (mrp gte 70 etc) while also specifying which fields are returned.
For instance, constraining only the tran_details that have a mrp gte 70, but in the results returned only include the prate field (or any subset of the fields).
I can return only the prate field with the query:
test.find({"batch" => 10452}).projection({"tran_details.prate" => 1}).to_a
I would like to combine the effects of the two different projections, but I haven't seen any documentation about how to do that online. If you string the two projections to each other, only the final projection has an effect.

To anyone out there --
The problem can be solved up to one element by using $elemMatch on projection. However, $elemMatch only returns the first result found. To return only parts of embedded documents multiple layers down that fit certain criteria, you need to use the aggregation framework.
test.find({
'tran_details.prate' => { '$gt' => 56 }
}).projection({
tran_details: {
'$elemMatch' => {
prate: { '$gt' => 56 }
}
},
'tran_details.qty' => 1,
'tran_details.mrp' => 1,
batch: 1,
_id: 0
}).to_a
To return only parts of embedded documents multiple layers down that fit certain criteria, you need to use the aggregation framework.
Here is example code
test.aggregate([{
'$match': {
'$or': [
{'batch': 10452}, {'batch': 73292}]}},
{'$project':
{trans_details:
{'$filter': {
input: '$tran_details',
as: 'item',
cond: {'$and':[
{'$gte' => ['$$item.prate', 51]},
{'gt' => ['$$item.prate', 53]}
]}
}
}
}
}]).to_a
If anyone sees this and knows how to dynamically construct queries in ruby from strings please let me know! Something to do with bson but still trying to find relevant documentation. Thanks -

Related

Searchkick score higher the sooner the word appears in field

Using the Searchkick gem I'm trying to increase the result score in when the word appears in the text so when the search query appears at the start of the field it should get a higher score if when there is a word in front of it, but it should still get a higher score than if the search query appears all the way at the end.
#products = Product.search(
params[:query],
where: {
is_active: true,
is_hidden: false
},
fields: [
{"search_text^20" => :text_start},
{"search_text^5" => :word},
{"name^5" => :word},
{"brand^2" => :word},
{"description^1" => :word}
],
boost_by: {
cached_outgoing_click_count: {
factor: 1
}
}
)
This is the closest I have gotten to what I wanted but it's not entirely what I'm looking for

Laravel Model Factory Error: Trying to get property of non-object

I'm trying to use a model factory to seed my database, but when I run it I get the error:
Trying to get property 'id' of non-object
Here is my code:
// TasksTableSeeder.php
factory(pams\Task::class, '2000', rand(1, 30))->create();
// ModelFactory.php
$factory->defineAs(pams\Task::class, '2000', function (Faker\Generator $faker) {
static $task_number = 01;
return [
'task_number' => $task_number++,
'ata_code' => '52-00-00',
'time_estimate' => $faker->randomFloat($nbMaxDecimals = 2, $min = 0.25, $max = 50),
'work_order_id' => '2000',
'description' => $faker->text($maxNbChars = 75),
'action' => '',
'duplicate' => '0',
'certified_by' => '1',
'certified_date' => '2015-11-08',
'status' => '1',
'created_by' => '1',
'modified_by' => '1',
'created_at' => Carbon\Carbon::now()->format('Y-m-d H:i:s'),
'updated_at' => Carbon\Carbon::now()->format('Y-m-d H:i:s'),
];
});
I've tried removing all variables from the model factory and using constants, but that doesn't fix it. I've tried pulling the data from the ModelFactory.php and put it directly into the TasksTableSeeder.php and it does work, however I was using constants and no variables.
I cannot for the life of me figure out what 'id' it's talking about.
I'm running Laravel v5.1
Your BaseModel is not exactly appropriate as it will force you to create hacks in order to run something like a unit test. It will be better that you have a flag in the BaseModel that you test for true or false before setting created_by and modified_by.
Also there's no guarantee there will be user_id '1' at any point in time, unless you actually create that User in your factory first, before creating the Task.
A way to fix your current setup is have a protected field like $enableAuthUpdates which is set to false or true by default. Then you can override the field in any of your derived models such as your Task model to prevent the creating/updating events from running.
Also it's important to make sure your factory has an actual user it is working with and create it if it doesn't exist.
I found the problem. At one stage I had implemented a BaseModel.php model that automatically inserts the current users ID when creating or updating the model. The seeder fails because there isn't a current user, so I had to add a check to see if there was a user logged in first.
Here is the code:
static::creating(function($model)
{
if(Auth::user())
{
$model->created_by = Auth::user()->id;
$model->modified_by = Auth::user()->id;
}
else
{
$model->created_by = '1';
$model->modified_by = '1';
}
});
static::updating(function($model)
{
if(Auth::user())
{
$model->modified_by = Auth::user()->id;
}
else
{
$model->modified_by = '1';
}
});
It's not super pretty, but it gets the job done :)

Count by group and subgroup

I want to generate some stats regarding some data I have in a model
I want to create stats according to an association and a status column.
i.e.
Model.group(:association_id).group(:status).count
to get an outcome like
[{ association_id1 => { status1 => X1, status2 => y1 } },
{ association_id2 => { status1 => x2, status2 => y2 } }...etc
Not really bothered whether it comes out in as an array or hash, just need the numbers to come out consistently.
Is there a 'rails' way to do this or a handy gem?
Ok. Worked out something a little better, though happy to take advice on how to clean this up.
group_counts = Model.group(["association_id","status"]).count
This returns something like:
=> {[nil, "status1"]=>2,
[ass_id1, "status1"]=>58,
[ass_id2, "status7"]=>1,
[ass_id2, "status3"]=>71 ...etc
Which, while it contains the data, is a pig to work with.
stats = group_counts.group_by{|k,v| k[0]}.map{|k,v| {k => v.map{|x| {x[0][1] => x[1] }}.inject(:merge) }}
Gives me something a little friendlier
=> [{
nil => {
"status1" => 10,
"status2" => 23},
"ass_id1" => {
"status1" => 7,
"status2" => 23
}...etc]
Hope that helps somebody.
This is pretty ugly, inefficient and there must be a better way to do it, but...
Model.pluck(:association_id).uniq.map{ |ass| {
name:(ass.blank? ? nil : Association.find(ass).name), data:
Model.where(association_id:ass).group(:status).count
} }
Gives what I need.
Obviously if you didn't need name the first term would be a little simpler.
i.e.
Model.pluck(:association_id).uniq.map{ |ass| {
id:ass, data:Model.where(association_id:ass).group(:status).count
} }

Optimizing MongoDB driver performance in a rails application

client = Mongo::Client.new(hosts.split(","),
:database => "#{database}",
:replica_set => "#{replica_set}",
:connection_timeout => 1,
:socket_timeout => 1,
:wait_queue_timeout => 1,
:connect => :replica_set,
:read => { :mode => :primary_preferred },
:heartbeat_frequency => 1,
:min_pool_size => 3,
:max_pool_size => 20
)
I use the code above to connect to a MongoDB database configured as a replica set of three different nodes. Queries are made on an indexed field. I noticed that when the :connect option is set to :direct, the performance is better as there are no auto-discoveries. But when set to :replica_set the performance deteriorates to as much as >12,000 ms. There is actually a node that is not available. Is there any way to improve my configuration as written above to achieve a better performance using the :connect => :replica_set option as opposed to :connect => :direct option. Any other performance improvement tips will be appreciated. Additionally , I must add that each document queried is large averaging at 15707.931229566184 as given by the db.stats() command below
uat-replSet:PRIMARY> db.stats()
{
"db" : "fuse_transactions",
"collections" : 5,
"objects" : 1896978,
**"avgObjSize" : 15707.931229566184**,
"dataSize" : 29797599968,
"storageSize" : 44447108352,
"numExtents" : 114,
"indexes" : 9,
"indexSize" : 242091360,
"fileSize" : 46082293760,
"nsSizeMB" : 16,
"extentFreeList" : {
"num" : 0,
"totalSize" : 0
},
"dataFileVersion" : {
"major" : 4,
"minor" : 22
},
"ok" : 1
}

Mongoid or/any_of unexpected behaviour

I'm having an issue with mongoid any_of. I'm trying to find objects that have either one field > 0, or another one > 0. My query is :
Model.any_of(best_friend_method.gt => 0, method.gt => 0).desc(best_friend_method, method)
It is "translated" in :
#<Mongoid::Criteria
selector: {"$or"=>[{:best_friends_lc_sum=>{"$gt"=>0}, :lc_sum=>{"$gt"=>0}}]},
options: {:sort=>[[:best_friends_lc_sum, :desc], [:lc_sum, :desc]]},
class: FbAlbum,
embedded: false>
As I understand it, this is what I want. But it only returns me 6 results. Model.where(:best_friends_lc_sum.gt => 0).count returns me 6 results too, but Model.where(:lc_sum.gt => 0).count returns me ~850 objects.
I expect my query to return the union of those two : is a mongoid/mongodb error, or am I doing something wrong ?
FYI : mongoid 2.4.5, mongodb 2.0.2, rails 3.1.3
Thanks for your time!
It's because you pass only one args and not 2 args. So it's like you have no $or usage.
Try :
Model.any_of({best_friend_method.gt => 0}, {method.gt => 0}).desc(best_friend_method, method)
In this case the Criteria become :
#<Mongoid::Criteria
selector: {"$or"=>[{:best_friends_lc_sum=>{"$gt"=>0}}, {:lc_sum=>{"$gt"=>0}}]},
options: {:sort=>[[:best_friends_lc_sum, :desc], [:lc_sum, :desc]]},
class: FbAlbum,
embedded: false>
Sometime the usage of {} is mandatory to separate different hash.
In case this helps someone...In Mongoid 3, the Origin gem provides the syntax for querying. Here is the list of methods that you can use to write your Mongoid 3 queries. Among those methods is the or method which allows you to perform an $or query:
# Mongoid query:
Model.or(
{ name: "Martin" }, { name: "Dave" }
)
# resulting MongoDB query:
{
"$or" => [
{ "name" => "Martin" }, { "name" => "Dave" }
]
}
Using the OP's original example, it can be rewritten as:
Model.or(
{ best_friend_method.gt => 0 },
{ method.gt => 0 }
).order_by(
best_friend_method,
method
)
At least one of the hashes passed to the or method must match in order for a record to be returned.

Resources