DEV Community

Mahmoud Sultan
Mahmoud Sultan

Posted on

Patching Searchkich Gem To Add Custom Queries By Default

Context

So a bit of context, as I'm working on Triplancer.net which is a Multi-Tenant Project developed by Ionite I needed to scope results returning for any Searchkick Query by the Current Tenant Id not to return results from other tenants.

You can do that simply by using a where query something like:

where: { tenant_id: current_tenant_id }

I did not want, however, to have to write this on every query I write, I wanted a way to override search method for models that are tenant-scoped, and add that tiny query by default to whatever Searchkick queries are made on that model.

Luckily, overriding stuff is fairly easy in Ruby, I just needed to look for where exactly should I inject my code.

SearchKick search() method

Looking at the Searchkick codebase for the source code of the method searchkick - that used to initialize searching inside a model - here: https://github.com/ankane/searchkick/blob/master/lib/searchkick/model.rb

You can find that, on line 43, Searchkick inserts a method called searchkick_search to the model eigenclass and conveniently to us alias this method with Searchkick.search_method_name which you can change but is set by default to search here: https://github.com/ankane/searchkick/blob/d10ffd4fd97003b638cb93821dcab3c86a2abba5/lib/searchkick.rb#L42

That makes our job very easy, all we need to do is follow the same convention and define a method, let's call it scope_searchkick, that has the same signature and currys the searchkick_search method with the scoping query.

  def self.scope_searchkick
    class_eval do
      class << self
        def search(term = '*', **options, &block)
          options[:where] = {} unless options.key?(:where)
          options_with_tenant_scope = options[:where].merge(
            { tenant_id: current_tenant_id }
          )

          searchkick_search(term, options_with_tenant_scope, &block)
        end
      end
    end
  end

Now, all we need to do is call this method after the searchkick method in our models.

I've defined this method in the ApplicationRecord class so that it'd be available in every model to call but you can define it anywhere you see suitable.

One last edit: Let's make it defensive and define it in a way that if we change Searchkick.search_method_name our patch would still work.

How to do that?, we can simply use define_method instead of def and pass the Searchkick.search_method_name now our scope_searchkick method is:

  def self.scope_searchkick
    class_eval do
      class << self
        define_method Searchkick.search_method_name do |term= '*', **options, &block|
          options[:where] = {} unless options.key?(:where)
          options_with_tenant_scope = options[:where].merge(
            { tenant_id: current_tenant_id }
          )

          searchkick_search(term, options_with_tenant_scope, &block)
        end
      end
    end
  end

You've got to love the Ruby Metaprogramming.

Top comments (1)

Collapse
 
adirhaz1z profile image
adirhaz1z

Great Work!