Search code examples
ruby-on-railsransack

Facet search with dynamic ransackers


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?


Solution

  • 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.