Rails Active Record fuzzy-search Array - ruby-on-rails

I have a Postgresql array column in my Rails app. These are, as far as I can tell, stored as long strings.
To search this column (Place#name), I've implemented the following methods in a module that I include like so:
class Place
include ActiveRecord::MetaExt::ArrayAccessor
array_accessor :name, :category
end
and define like so:
module ActiveRecord
module MetaExt
module ArrayAccessor
module ClassMethods
def array_accessor(*symbols)
symbols.each do |singular|
plural = singular.to_s.split("_").join(" ").pluralize.split(" ").join("_")
# These two instance-level methods not super relevant to the question
class_eval do
define_method("add_#{singular}") do |arg|
send( "#{plural}=", (send(plural) + Array(arg)).uniq )
end
define_method(singular) do
send(plural).first
end
end
metaclass.instance_eval do
define_method("without_#{singular}") do
where("? = '{}'", plural)
end
define_method("with_#{singular}") do |arg=nil|
if arg.blank?
where.not("? = '{}'", plural)
else
where("? = ANY (#{plural})", arg.is_a?(Array) ? arg.first : arg)
end
end
end
end
end
end
def self.included(base)
base.extend ClassMethods
end
end
end
end
There are a couple of issues with this.
1) in the with_name (literally, with_#{field}) method, I'm only ever searching for the first name in the array given. I'd love to be able to search for all places containing ANY of those names. Having trouble figuring this out, though. Any ideas?
2) I'd love to be able to do fuzzy matching -- so that a search for "Barbeque Place" would grab a place with the name "The Barbecue Place". Took a look at the Textacular gem, but I can't seem to adapt it to Array columns.
Any ideas how to make this work -- especially in a generalizable way?

Related

Single Table Inheritance or Type Table

I am facing a design decision I cannot solve. In the application a user will have the ability to create a campaign from a set of different campaign types available to them.
Originally, I implemented this by creating a Campaign and CampaignType model where a campaign has a campaign_type_id attribute to know which type of campaign it was.
I seeded the database with the possible CampaignType models. This allows me to fetch all CampaignType's and display them as options to users when creating a Campaign.
I was looking to refactor because in this solution I am stuck using switch or if/else blocks to check what type a campaign is before performing logic (no subclasses).
The alternative is to get rid of CampaignType table and use a simple type attribute on the Campaign model. This allows me to create Subclasses of Campaign and get rid of the switch and if/else blocks.
The problem with this approach is I still need to be able to list all available campaign types to my users. This means I need to iterate Campaign.subclasses to get the classes. This works except it also means I need to add a bunch of attributes to each subclass as methods for displaying in UI.
Original
CampaignType.create! :fa_icon => "fa-line-chart", :avatar=> "spend.png", :name => "Spend Based", :short_description => "Spend X Get Y"
In STI
class SpendBasedCampaign < Campaign
def name
"Spend Based"
end
def fa_icon
"fa-line-chart"
end
def avatar
"spend.png"
end
end
Neither way feels right to me. What is the best approach to this problem?
A not very performant solution using phantom methods. This technique only works with Ruby >= 2.0, because since 2.0, unbound methods from modules can be bound to any object, while in earlier versions, any unbound method can only be bound to the objects kind_of? the class defining that method.
# app/models/campaign.rb
class Campaign < ActiveRecord::Base
enum :campaign_type => [:spend_based, ...]
def method_missing(name, *args, &block)
campaign_type_module.instance_method(name).bind(self).call
rescue NameError
super
end
def respond_to_missing?(name, include_private=false)
super || campaign_type_module.instance_methods(include_private).include?(name)
end
private
def campaign_type_module
Campaigns.const_get(campaign_type.camelize)
end
end
# app/models/campaigns/spend_based.rb
module Campaigns
module SpendBased
def name
"Spend Based"
end
def fa_icon
"fa-line-chart"
end
def avatar
"spend.png"
end
end
end
Update
Use class macros to improve performance, and keep your models as clean as possible by hiding nasty things to concerns and builder.
This is your model class:
# app/models/campaign.rb
class Campaign < ActiveRecord::Base
include CampaignAttributes
enum :campaign_type => [:spend_based, ...]
campaign_attr :name, :fa_icon, :avatar, ...
end
And this is your campaign type definition:
# app/models/campaigns/spend_based.rb
Campaigns.build 'SpendBased' do
name 'Spend Based'
fa_icon 'fa-line-chart'
avatar 'spend.png'
end
A concern providing campaign_attr to your model class:
# app/models/concerns/campaign_attributes.rb
module CampaignAttributes
extend ActiveSupport::Concern
module ClassMethods
private
def campaign_attr(*names)
names.each do |name|
class_eval <<-EOS, __FILE__, __LINE__ + 1
def #{name}
Campaigns.const_get(campaign_type.camelize).instance_method(:#{name}).bind(self).call
end
EOS
end
end
end
end
And finally, the module builder:
# app/models/campaigns/builder.rb
module Campaigns
class Builder < BasicObject
def initialize
#mod = ::Module.new
end
def method_missing(name, *args)
value = args.shift
#mod.send(:define_method, name) { value }
end
def build(&block)
instance_eval &block
#mod
end
end
def self.build(module_name, &block)
const_set module_name, Builder.new.build(&block)
end
end

How do I modify Rails ActiveRecord query results before returning?

I have a table with data that needs to be updated at run-time by additional data from an external service. What I'd like to do is something like this:
MyModel.some_custom_scope.some_other_scope.enhance_with_external_data.each do |object|
puts object.some_attribute_from_external_data_source
end
Even if I can't use this exact syntax, I would like the end result to respect any scopes I may use. I've tried this:
def self.enhance_with_external_data
external_data = get_external_data
Enumerator.new do |yielder|
# mimick some stuff I saw in ActiveRecord and don't quite understand:
relation.to_a.each do |obj|
update_obj_with_external_data(obj)
yielder.yield(obj)
end
end
end
This mostly works, except it doesn't respect any previous scopes that were applied, so if I do this:
MyModel.some_custom_scope.some_other_scope.enhance_with_external_data
It gives back ALL MyModels, not just the ones scoped by some_custom_scope and some_other_scope.
Hopefully what I'm trying to do makes sense. Anyone know how to do it, or am I trying to put a square peg in a round hole?
I figured out a way to do this. Kind of ugly, but seems to work:
def self.merge_with_extra_info
the_scope = scoped
class << the_scope
alias :base_to_a :to_a
def to_a
MyModel.enhance(base_to_a)
end
end
the_scope
end
def self.enhance(items)
items.each do |item|
item = add_extra_item_info(item)
end
items
end
What this does is add a class method to my model - which for reasons unknown to me seems to also make it available to ActiveRecord::Relation instances. It overrides, just for the current scope object, the to_a method that gets called to get the records. That lets me add extra info to each record before returning. So now I get all the chainability and everything like:
MyModel.where(:some_attribute => some_value).merge_with_extra_info.limit(10).all
I'd have liked to be able to get at it as it enumerates versus after it's put into an array like here, but couldn't figure out how to get that deep into AR/Arel.
I achieved something similar to this by extending the relation:
class EnhancedModel < DelegateClass(Model)
def initialize(model, extra_data)
super(model)
#extra_data = extra_data
end
def use_extra_data
#extra_data.whatever
end
end
module EnhanceResults
def to_a
extra_data = get_data_from_external_source(...)
super.to_a.map do |model_obj|
EnhancedModel.new(model_obj, extra_data)
end
end
end
models = Model.where('condition')
models.extend(EnhanceResults)
models.each do |enhanced_model|
enhanced_model.use_extra_data
end

ruby on rails function in helper?

I'm trying to make a function that takes 2 arguments from the user_controller and does a few queries and calculations and return the result in an array. Is this something I should set up in a helper file? And how would I return the results (I'm very new to this).
Thanks for all and any help :)
Unless those calculations are entirely related to your templates (html, js) you should NEVER put that stuff in helpers of any kind. It will make your app hard to test.
Your options:
1) Create a model without ActiveRecord (MyCalculations.rb in ˜/models or ~/lib)
2) Create an extension and include in existing models
Examples (sample code, not realistic)
calculator.rb in ~/models
class Calculator
attr_accessor :amount, :parcs, :interest, :change
#... lots of code
def initialize(amount, parcs, interest)
# do stuff
calculate
end
def self.calculate!(amount, parcs, interest)
Calculator.new(amount, parcs, interest)
end
end
Extensions:
~/lib/models/import/csv_ext.rb
module Models
module Import
module CsvExt
extend ActiveSupport::Concern
included do
end
module ClassMethods
#static
def load_from_csv(csv)
# code comes here
end
end
end
end
end
Then, add it to your models:
include Models::Import::CsvExt

searchlogic with globalize2?

Given there is a model:
class MenuItem < ActiveRecord::Base
translates :title
end
and searchlogic is plugged in, I'd expect the following to work:
>> MenuItem.search(:title_like => 'tea')
Sadly, it doesn't:
Searchlogic::Search::UnknownConditionError: The title_like is not a valid condition. You may only use conditions that map to a named scope
Is there a way to make work?
P.S.
The closest I managed to get workging, was:
>> MenuItem.search(:globalize_translations_title_like => 'tea')
Which doesn't look nice.
I developed searchlogic. By default, it leverages existing named scopes and the database columns. It can't really go beyond that because ultimately it has to create the resulting SQL using valid column names. That said, there really is no way for searchlogic to cleanly understand what your :title attribute means. Even if it did, it would be specific to the logic defined in your translation library. Which is a red flag that this shouldn't be in the library itself, but instead a plugin or code that gets initialized within your app.
Why not override the method_missing method and do the mapping yourself? Searchlogic provides and easy way to alias scoped by doing alias_scope:
alias_scope :title_like, lambda { |value| globalize_translations_title_like(value) }
Here's a quick stab (this is untested):
module TranslationsMapping
def self.included(klass)
klass.class_eval do
extend ClassMethods
end
end
module ClassMethods
protected
def method_missing(name, *args, &block)
translation_attributes = ["title"].join("|")
conditions = (Searchlogic::NamedScopes::Conditions::PRIMARY_CONDITIONS +
Searchlogic::NamedScopes::Conditions::ALIAS_CONDITIONS).join("|"))
if name.to_s =~ /^(#{translation_attributes})_(#{conditions})$/
attribute_name = $1
condition_name = $2
alias_scope "#{attribute_name}_#{condition_name}", lambda { |value| send("globalize_translations_#{attribute_name}_#{condition_name}", value) }
send(name, *args, &block)
else
super
end
end
end
end
ActiveRecord::Base.send(:include, TranslationsMapping)
Hope that helps. Again, I haven't tested the code, but you should get the general idea. But I agree, the implementation of the translations should be behind the scenes, you really should never be typing "globalize_translations" anywhere in your app, that should be take care of transparently on the model level.

How to alias ActiveRecord class methods dynamically in a rails plugin?

I'm having trouble removing some duplication I've introduced in a rails plugin.
The code below modifies the find and calculate methods of ActiveRecord in the same way, but I've been unable to remove the duplication.
The find and calculate methods below make use of the super keyword which is one hurdle as the super keyword can only be used to call a method sharing the same name as the calling method, so I can't move the super keyword to a method shared by find and calculate.
So next I tried aliasing the find and calculate class methods from the superclass ActiveRecord, however, I've not been able to get the syntax right for the aliasing. If someone could show me that, it would be a great help.
If you've got a better way entirely of doing this I'd love for you to post that too.
Below I've trimmed the code down a little to highlight the problem:
module Geocodable #:nodoc:
def self.included(mod)
mod.extend(ClassMethods)
end
module ClassMethods
def acts_as_geocodable(options = {})
extend Geocodable::SingletonMethods
end
end
module SingletonMethods
def find(*args)
some_method_1
super *args.push(options)
some_method_2
end
# TODO: Remove duplication of find above and calculate below.
def calculate(*args)
some_method_1
super *args.push(options)
some_method_2
end
end
end
Your best way to refactor this code is to leave find and calculate unchanged, and add apply the wrapping using a class-level function.
Here's rough sketch, without your module and mixin logic:
class A
def find x
puts 'finding'
end
def calculate x
puts 'calculating'
end
end
class B < A
def self.make_wrapper_method name
define_method name do |*args|
puts "entering"
result = super *args
puts "exiting"
result
end
end
make_wrapper_method :find
make_wrapper_method :calculate
end
Note that this will need to be modified if B has already overridden find or calculate.
To use this code, first make your version work correctly, then modify it to use define_method. (And if you need extremely high performance, you may need to use one of the *_eval functions to create the wrappers instead of define_method.)
This is the option I went for in the end, thanks to emk for guidance to get to this point!
module Geocodable
def self.included(mod)
mod.extend(ClassMethods)
end
module ClassMethods
def acts_as_geocodable(options = {})
geoify_query_methods
end
private
# This is where the duplication has been removed
def geoify_query_methods
class << self
[:calculate, :find].each do |method_name|
define_method method_name do |*args|
some_method_1
super *args.push(options)
some_method_2
end
end
end
end
end
end
To just alias the find method:
module SingletonMethods
def find(*args)
some_method_1
super *args.push(options)
some_method_2
end
alias :calculate :find
end

Resources