DEV Community

Mahmoud Sultan
Mahmoud Sultan

Posted on

3 1

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.

Hostinger image

Get n8n VPS hosting 3x cheaper than a cloud solution

Get fast, easy, secure n8n VPS hosting from $4.99/mo at Hostinger. Automate any workflow using a pre-installed n8n application and no-code customization.

Start now

Top comments (1)

Collapse
 
adirhaz1z profile image
adirhaz1z

Great Work!

Sentry image

See why 4M developers consider Sentry, “not bad.”

Fixing code doesn’t have to be the worst part of your day. Learn how Sentry can help.

Learn more

👋 Kindness is contagious

Please leave a ❤️ or a friendly comment on this post if you found it helpful!

Okay