Facet search with dynamic ransackers - ruby-on-rails

I am trying to create a generic product catalog application with Rails and in order to have products of varying types with varying attributes I have abstracted product properties into their own table with a link table in between the product and the property that stores the value.
------------- --------------------
|products | |product_properties| ------------
|-----------| |------------------| |properties|
|name |---|value |---|----------|
|description| |product_id | |name |
|etc... | |property_id | ------------
------------- --------------------
For example a product could have a width property (which will be stored in the property table so it can be reused) whilst the value for the width will be stored in the product_properties table with a record that links the property to the product.
This works fine but I need to implement facet-able search within the products model and have chosen to use ransack. So to find all products that have a width greater than 30 I must do
Product.ransack(product_properties_property_name_eq: 'width', product_properties_value_gt: 30).result
This again works fine but I would prefer to 'ransack' using the property name
Product.ransack(width_gt: 30).result
Are there any ways to dynamically create ransackers (or alternatives) that will allow me to do this? I have tried using method_missing but this confused me to no end. I was thinking of creating scopes on the model using all the name values in the properties table but thought I would ask for some advice first.
UPDATE
I have attempted implementing a series of custom ransackers on the product model
class Product < ActiveRecord::Base
Property.pluck(:name, :id).each do |name, id|
ransacker name, formatter: -> (value) { value.to_s.downcase } do
product_properties = Arel::Table.new(:product_properties)
product_properties[:value]
end
end
end
This is getting me ever closer to the answer I can feel it. What else shoudl I be doing here?

This does the job perfectly. The gotcha here is the Arel::Nodes.build_quoted. I had originally left this out and I would get no errors/warning back but I would equally get no results either which left me rather stumped. This apparently is only necessary when usingRails 4.2+ (Arel 6.0+).
Property.pluck(:id, :name).each do |id, name|
product_properties = Arel::Table.new(:product_properties)
ransacker name.to_sym, formatter: -> (value) { value.to_s.downcase } do
Arel::Nodes::InfixOperation.new('AND',
Arel::Nodes::InfixOperation.new('=',
product_properties[:property_id], Arel::Nodes.build_quoted(id)
),
product_properties[:value]
)
end
end
To actually use this I then need to explicitly join the product_properties table to the query
Product.joins(:product_properties).ransack(width_gt: 30)
As the ransack documentation states the difficulty some people encounter with using ransackers stems not from Ransack, but from not understanding Arel. This was definitely the case here.

Related

How to use select but allow all fields from model

I do the following so I am able to group all LineItem's together by count and display the LineItem by count along with the vendor_name
line_items = LineItem.all
vendor_line_items = line_items.group(:vendor_name).select('COUNT(*) as count', 'vendor_name').order('count desc')
My issue is that I am only able to receive the following params: id: nil, vendor_name: "name_here"
Is there a way to accomplish the same thing but allow all params from the model to be passed?
You can't select the rest of the columns since you have different values for each coulmn inside the group (like... if you have 2 LineItem in the same group, which ID do you expect to have?)
You could apply aggregate functions (like COUNT, MAX, MIN, etc) to other columns on the SELECT to tell the database which columns you want for each column I guess.
Personally, I would first get the groups ordered by count and then do more queries when needed to fetch the actual record for the groups.
counts = LineItem.group(:vendor_name).count
# counts should be something like: {vendor_1: X, vendor_2, Y, vendor_3: Z}
# order the vendors using the count for each vendor
ordered_vendors = counts.keys.sort_by { |ven| counts[ven] }
ordered_vendors.each do |vendor|
# do something with each vendor, fetch LineItems, etc
end
The reason why you only see the count and the vendor name is because that is all you are grouping by. Suppose in the database, you have 5 different Vendor A shown below.
vendor_name | product_name
-----------------------------
Vendor A | test
Vendor A | test2
Vendor A | test3
Vendor A | test4
Vendor A | test5
...
When you run your query, SQL will not know what to display for product_name as the group_by will only show 1 row instead of 5. Have a read about it here.
To achieve this you will need to either to group by the other columns too or use a min/max select to pick a value to display. Here is an example:
vendor_line_items = LineItem.select('COUNT(*) AS count', 'vendor_name', 'MAX(product_name)').group(:vendor_name).order('count DESC')
Now each of those results, you can call the attributes method.
Which will give you the following hash:
vendor_line_items.each do |x|
result = x.attributes
# Here result will be a hash.
# {"count" => 5, "vendor_name" => "Vendor A", "product_name" => "test5"}
end
(Not accepted answer unless a better way is received)
I did:
vendor_line_items = Vendor.joins(:line_items).group(:id).order('COUNT(line_items.id) DESC')
This gives me what I want by ordering the results by vendor.line_items.count and allowing me to get all of the associations to display any param I want.
I assume this way is much slower than what I was previously doing as it fetches all records and then on the front end goes through associations to get more records.
In the original way I was doing this. It is what I want minus an extra parameter that I would want the SUM of. The parameter is a decimal attribute. In the same way I count the LineItem that have the same vendor_name, I want to sum of the LineItem.attribute that share the same vendor_name.
Better Answer:
LineItem.select(:vendor_name, 'sum(line_item_revenue) as line_item_revenue', 'COUNT(*) as count').group(:vendor_name)
This seems to get me what I want with less queries (i believe) --- correct me if I am wrong on the queries.
I am quite confused about your code and your expectation. You are selecting the COUNT but the expected result is id instead of count?
If you want to group by vendor_name and show the count of group_by you can try
line_items.group(:vendor_name).count

customising GORM basic collections

As well as associations between different domain classes, GORM also supports mapping of basic collection types. . For example, the following class creates a nicknames association that is a Set of String instances
class Person {
static hasMany = [nicknames:String]
}
This will store the nicknames in a separate table person_nicknames:
---------------------------------------------
| person_id | nickname |
---------------------------------------------
| 1 | Fred |
---------------------------------------------
By default both columns are nullable and there are no indices present. I would like to make the following changes
make both columns not null
put a composite unique index on (person_id, nickname)
Obviously I could just run an SQL script to make these changes, but is it possible for me to express this in the domain model, so that GORM does it when creating and updating the schema?
No, this presently isn't possible. You can fake it by making Nickname an explicit domain class (Andre Steingress gives an example in the question comments), but otherwise you have to write a migration.
You can use joinTable and basic collection types.
http://grails.org/doc/2.4.3/ref/Database%20Mapping/joinTable.html
You can specify SQL behavior with column mapping:
http://grails.org/doc/2.4.3/ref/Database%20Mapping/column.html

How to select table column names in a view and pass to controller in rails?

So I am new to Rails, and OO programming in general. I have some grasp of the MVC architecture. My goal is to make a (nearly) completely dynamic plug-and-play plotting web server. I am fairly confused with params, forms, and select helpers.
What I want to do is use Rails drop downs to basically pass parameters as strings to my controller, which will use the params to select certain column data from my database and plot it dynamically. I have the latter part of the task working, but I can't seem to pass values from my view to controller.
For simplicity's sake, say my database schema looks like this:
--------------Plot---------------
|____x____|____y1____|____y2____|
| 1 | 1 | 1 |
| 2 | 2 | 4 |
| 3 | 3 | 9 |
| 4 | 4 | 16 |
| 5 | 5 | 25 |
...
and in my Model, I have dynamic selector scopes that will let me select just certain columns of data:
in Plot.rb
class Plot < ActiveRecord::Base
scope :select_var, lambda {|varname| select(varname)}
scope :between_x, lambda {|x1,x2| where("x BETWEEN ? and ?","#{x1}","#{x2}")}
So this way, I can call:
irb>>#p1 = Plot.select_var(['x','y1']).between_x(1,3)
and get in return a class where #p1.x and #p1.y1 are my only attributes, only for values between x=1 to x=4, which I dynamically plot.
I want to start off in a view (plot/index), where I can dynamically select which variable names (table column names), and which rows from the database to fetch and plot. The problem is, most select helpers don't seem to work with columns in the database, only rows.
So to select columns, I first get an array of column names that exist in my database with a function I wrote.
Plots Controller
def index
d=Plot.first
#tags = d.list_vars
end
So #tags = ['x','y1','y2']
Then in my plot/index.html.erb I try to use a drop down to select wich variables I send back to the controller.
index.html.erb
<%= select_tag( :variable, options_for_select(#plots.first.list_vars,:name,:multiple=>:true) )%>
<%= button_to 'Plot now!', :controller =>"plots/plot_vars", :variable => params[:variable]%>
Finally, in the controller again
Plots controller
...
def plot_vars
#plot_data=Plot.select_vars([params[:variable]])
end
The problem is everytime I try this (or one of a hundred variations thereof), the params[:variable] is nill.
How can I use a drop down to pass a parameter with string variable names to the controller?
Sorry its so long, I have been struggling with this for about a month now. :-( I think my biggest problem is that this setup doesn't really match the Rails architecture. I don't have "users" and "articles" as individual entities. I really have a data structure, not a data object. Trying to work with the structure in terms of data object speak is not necessarily the easiest thing to do I think.
For background: My actual database has about 250 columns and a couple million rows, and they get changed and modified from time to time. I know I can make the database smarter, but its not worth it on my end. I work at a scientific institute where there are a ton of projects with databases just like this. Each one has a web developer that spends months setting up a web interface and their own janky plotting setups. I want to make this completely dynamic, as a plug-and-play solution so all you have to do is specify your database connection, and this rails setup will automatically show and plot which data you want in it. I am more of a sequential programmer and number cruncher, as are many people here. I think this project could be very helpful in the end, but its difficult to figure out for me right now.

Rails custom meta model?

I'd like to be able to add "meta" information to a model, basically user-defined fields. So, for instance, let's imagine a User model:
I define fields for first name, last name, age, gender.
I would like users to be able to define some "meta information", basically to go in their profile page and share other information. So one user might want to add "hobbies", "occupation", and "hometown", and another might want to add "hobbies", and "education".
So, I'd like to be able to have a standard view for this kind of stuff, so for instance in the view I might do something like (in HAML):
- for item in #meta
%li
%strong= item.key + ":"
= item.value
This way I can ensure that the information is consistently displayed, rather than just providing a user with a markdown textbox that they may format all different ways.
I'd also love to be able to click on meta and see other users who have given the same thing, so in the example above both users defined "hobbies", it would be nice to be able to say I want to see users who have shared hobbies -- or even better I want to see users whose hobbies are ___.
So, since I don't know what fields users will want to define in advance, what kind of options are there for providing that kind of functionality?
Is there a gem that handles custom meta information on a model like this, or at least sort of similarly? Has anyone had experience with this kind of problem? If so, how did you solve it?
Thanks!
The dynamic field implementation depends upon following factors:
Ability to dynamically add attributes
Ability to support new data types
Ability to retrieve the dynamic attributes without additional query
Ability to access dynamic attributes like regular attributes
Ability query the objects based on dynamic attributes. (eg: find the users with
skiing hobbies)
Typically, a solution doesn't address all the requirements. Mike's solution addresses 1, and 5 elegantly. You should use his solution if 1 & 5 are important for you.
Here is a long solution that addresses 1,2,3, 4 and 5
Update the users table
Add a text field called meta to the users table.
Update your User model
class User < ActiveRecord::Base
serialize :meta, Hash
def after_initialize
self.meta ||= {} if new_record?
end
end
Adding a new meta field
u = User.first
u.meta[:hobbies] = "skiing"
u.save
Accessing a meta field
puts "hobbies=#{u.meta[:hobbies]}"
Iterating the meta fields
u.meta.each do |k, v|
puts "#{k}=#{v}"
end
To address the 5th requirement you need to use Solr Or Sphinx full text search engines. They are efficient than relying on DB for LIKE queries.
Here is one approach if you use Solr through Sunspot gem.
class User
searchable do
integer(:user_id, :using => :id)
meta.each do |key, value|
t = solr_type(value)
send(t, key.to_sym) {value} if t
end
end
def solr_type(value)
return nil if value.nil?
return :integer if value.is_a?(Fixnum)
return :float if value.is_a?(Float)
return :string if value.is_a?(String)
return :date if value.is_a?(Date)
return :time if value.is_a?(Time)
end
def similar_users(*args)
keys = args.empty? ? meta.keys : [args].flatten.compact
User.search do
without(:user_id, id)
any_of do
keys.each do |key|
value = meta[key]
with(key, value) if value
end
and
end
end
end
Looking up similar users
u = User.first
u.similar_users # matching any one of the meta fields
u.similar_users :hobbies # with matching hobbies
u.similar_users :hobbies, :city # with matching hobbies or the same city
The performance gain here is significant.
If each user is allowed to define their own attributes, one option might be to have a table with three columns: user_id, attribute_name, attribute_value. It might look like:
| user_id | attribute_name | attribute_value |
| 2 | hobbies | skiing |
| 2 | hobbies | running |
| 2 | pets | dog |
| 3 | hobbies | skiing |
| 3 | colours | green |
This table would be used for finding other users who have the same hobbies/pets/etc.
For performance reasons (this table is going to get large) you may want to maintain multiple places that the info is stored -- different sources of info for different purposes. I don't think it's bad to store the same info in multiple tables if absolutely necessary for performance.
It all depends on what functionality you need. Maybe it will end up making sense that each user has their key/value pairs serialized into a string column on the users table (Rails provides nice support for this type of serialization), so when you display info for a particular user you don't even need to touch the huge table. Or maybe you will end up having another table that looks like this:
| user_id | keys | values |
| 2 | hobbies, pets | skiing, running, dog |
| 3 | hobbies, colours | skiing, green |
This table would be useful if you need to find all users that have hobbies (run LIKE sql against the keys column), or all users that have anything to do with a dog (run LIKE sql against the values column).
That's the best answer I can give with the requirements you gave. Maybe there is a third-party solution available, but I'm skeptical. It's not really a "pop in a gem" type of problem.
In this case, I would at least consider a documentdb like mongo or couch, which can deal with this type of scenario much easier then an rdms.
If that isn't the case, I would probably end up doing something along the lines of what Mike A. described.

Counting boolean values under rails

I have a standard master-detail relationship between two models in a RoR application. The detail records contain four boolean fields indicating presence / absence of something.
When I display the detail records I want to add a summary indicating the number of records which have their boolean value set to True for each of the four boolean fields.
For example:
Date | Boolean Field 1 | Boolean Field 2 | etc
2009/08/29 | T | T |
2009/08/30 | T | F |
2009/08/31 | F | T |
2009/09/01 | F | T |
Total: 4 2 3
I tried using something like #entries.count(["Boolean Field 1", true])
The way I see it, there are two ways to calculate these values: one at the model by executing an SQL query (ugly) or at the view level by using a counter (ugly again.) Is there some other way to achieve what I want?
Thank you for your time,
Angelos Arampatzis
May be
#entries.select {|r| r.bool_field1}.size
You can either do:
#entries.count(:conditions => { :boolean_field_1 => true })
You can pretty this up by doing a named scope:
named_scope :booleans, :conditions => { :boolean_field_1 => true })
and then
#entries.booleans.count
Or if you already have ALL the items in an array (rather than a select few) and do not want to hit the database…
Rails provides a ? method for all columns. So while you have:
#entry.boolean_field
You also have:
#entry.boolean_field?
So you can do this:
#entries.collect(&:boolean_field?).length
sql isn't as ugly as rails makes it out to be and it is rather efficient, just make it a named_scope and your controller/view will still look pretty
Because you have all your entries as Rails objects, you can use the shortest form:
#entries.count(&:boolean_field1?)
It's using Enumerable#count.
Keep in mind though, that it counts using Ruby (as opposed to SQL). If you'll ever want to count without reading all records from DB, you will need to use something different for efficiency.

Resources