Search code examples
rubyruby-on-rails-3search-engine

Variable scope definitions and inheritance


I'm working on a extended search feature for my webpage.

I looked at ransack, however it's lacking some functionalities I need, makes the url-query string very long and has some bugs (reported).

Thus I started to implement my own hack.

First I want to present my idea, afterwards I want to ask kindly how to fix my issue and in the end if there are other ways to improve this.

The idea:

A model defines something like this (additionally, the model is inside an engine):

module EngineName
  class Post < ActiveRecord::Base
    search_for :name, :as => :string do |b, q|
      b.where{name =~ "%#{q}%"}
    end
  end
end

:name is to define the query-param to use e.g. this would be ?q[name]=something I know that this is not fully generic like ransack, but well...

:as is to build up the correct form-tag. :string would be for text_field, :integer for number_field and so on. I want to extend it further to implement auto-generating of collections for associations etc.

Now the block is a simple scope to use. I run into several shortcomings with ransack when building up complex queries (like with count() etc.). Now I can specify my own optimized query in squeel.

I extended ActiveRecord::Base to set up the logic (the global one, not inside the engine. I want to use it everywhere). I defined a scope :search so I can use Model.search(param[q]) like in ransack. Also I tried to keep a list of keys which are "searchable" defined by the search_for calls.

class ActiveRecord::Base
@@searchable_attributes = Hash.new({})

def self.search_for(name, *opts, &search_scope)
  return unless search_scope

  @@searchable_attributes[name] = {
    :type => opts[:as],
    :condition => search_scope
  }

  unless @@searchable_attributes.has_key? :nil
    @@searchable_attributes[:nil] = Proc.new { scoped }
  end
end

scope :search, lambda {|q|
  next unless q.kind_of?(Hash)

  base = @@searchable_attributes[:nil].call
  q.each do |key, search|
    next unless base.class.searchable_attributes.has_key?(key)
    base = @@searchable_attributes[key][:condition].call(base, search)
  end
  base
}
end

Now the issues:

It has mostly to do with inheritance of the classes. But even after reading and trying 3, 4 it does not worked.

Please take a look at the second line in the scope :search.

There I'm calling the simple Proc I definied above which only includes "scoped" This is to get arround the issue that self returns "ActiveRecord::Base" and not the model itself like "Post" or "Comment".

It's because the scope is called on the Base class on inheritance, however I did not find anything to fix this.

As search_for is called on the model itself (e.g. Post) the scope-model returned there is "the right one".

Does anyone know how to circumvent this?

The next question would be, how to store the list of "searchable" scopes. I used @@variables. But as they are shared within every subclass, this would be a no-go. However, it needs to be static as the search_for is called without initialize a instance (isn't it?)

Last but not least, it is somekind horrible to always specify the base-model to use on every scope so that I can chain them together.

Is there any other possibilities to improve this?


Solution

  • Ok, it seems I got it finally myself my putting several other answers from other questions together.

    Model:

    module EngineName
      class Post < ActiveRecord::Base
        searchable
    
        search_for :name, :as => :string do |b, q|
         b.where{name =~ "%#{q}%"}
        end
      end
    end
    

    My "Plugin" currently as an initializer:

    class ActiveRecord::Base
      def self.searchable
        include Searchable
      end
    end
    
    module Searchable
      def self.included(base)
        base.class_eval {
    
          @@searchable_attributes = Hash.new({})
    
          def self.search_for(name, opts)
            return unless block_given?
    
            @@searchable_attributes[name] = {
              :type => opts[:as],
              :condition => Proc.new
            }
          end
    
    
          # Named scopes
          scope :search, lambda {|q|
            next unless q.kind_of?(Hash)
    
            base = self.scoped
            q.each do |key, search|
              key = key.to_sym
              next unless @@searchable_attributes.has_key?(key)
              base = @@searchable_attributes[key][:condition].call(base, search)
            end
            base
          }
        }
      end
    end
    

    Hope it'll help some others working on the same problem.