Picky Active Record 3

ruby / picky / activerecord

This post talks about integrating Picky directly into Rails/ActiveRecord.

(By the way, greetings from Rails Camp X Adelaide – come up and say hi if you are here!)

The last post illustrated a way of writing an active record integration. Still missing is index persistence.

However, in this post I’d like to talk about wrapping the last solution up into a nicer bundle.

Beautifying the last solution

Why? It contains a few advanced Ruby concepts and statements. While I think everybody should know about class << self and define_method, it can get kind of hard to read compared to a more declarative style that Tire (Elastic Search), Thinking Sphinx (Sphinx), or Sunspot (Solr) offer.

However: While I like the declarative style in many cases, some libraries hide away too much important code. Many times even code that is hugely important, or does things to your model which you only find out about after reading the library source. After a crash. In production.

Goals

So what I’d like is

And maybe most important:

A quick reminder what the basic Picky API is:

data = Picky::Index.new :name do
  category :name
end
things = Picky::Search.new data
things.search 'something'

Most other search engine adapters try to elegantify the original API. This is nice.

However, having control over both APIs, I believe that using the original (standard) Picky API creates a pressure on it to stay as elegant as possible and as useable as possible.

If we hide away the Picky API, pressure is only excerted on the ActiveRecord/Picky adapter. This also means that people who only use the Picky ActiveRecord API only come in contact with that one.

Why is this a problem? This is a problem when people want to transcend the AR API to use for example the separate and specific Picky server. If the APIs look and feel fundamentally different, users will not willingly make this jump. In fact, many people then start looking for search engine alternatives. This is a bad thing. Let me put this in bold, because it gets violated so many times:

The jump from the simple API to the harder API should not be noticeable.

The only way to do this is use a subset of the original API for the simpler one. However, since Picky is about giving you the power, we will not constrict you, but instead make the whole API accessible.

A first draft

I am not the biggest fan of the following pattern:

class Model < ActiveRecord::Base
  include Picky::ActiveRecord
  
  some_method_call_from_the included, module
end

I am not sure why since it’s perfectly ok Ruby. I believe it is because it usually consists of two lines, and only one really describes what is going on: “I am using this” and “I am using it like this”.

With this subgoal in mind, I started drafting the API. It turned out like this:

class Model < ActiveRecord::Base
  extend(Picky::ActiveRecord.new(:models) do
    Picky::Index.new :models do
      category :name
      category :surname
    end
  end)
end

Don’t judge me. It gets better.

Why do I use so many round parentheses, having declared them unnecessary not so long ago?

Turns out, extend gobbles up my block. Try running the following code:

module A; end
class B
  extend A do
  	# ...
  end
end

I am unsure what happens here. Looking at the CRuby code didn’t help. Ideas?

I guess we can all agree that this API is neither good looking nor elegant. Let’s try again.

A better draft

So, teeth grinding, we return back to the standard solution of having a separate include and declarations. However, I’d like to be able to use the Picky API.

This is what I’ve come up with:

class Model < ActiveRecord::Base
  include Picky::ActiveRecord
  
  index = Picky::Index.new :models do
    category :name
    category :surname
  end
  
  search = Picky::Search.new index
  
  updates_picky index
  searches_picky search
end

Let’s look at the design in detail.

In detail

First of all, note that no saving of indexes in instance variables is done. You can do it, should you need it, but Picky is not saving anything like @__picky_index for you. Instead, the index and the search are both passed into the a method in which they are captured in a closure.

Let’s look at the API code.

The line

include Picky::ActiveRecord

does two things: First, it includes two other modules, Picky::ActiveRecord::Indexing and Picky::ActiveRecord::Searching, that are concerned with indexing and searching, respectively. It is well imaginable that one doesn’t want realtime indexing, just searching, or vice versa.

index = Picky::Index.new :models do
  category :name
  category :surname
end

search = Picky::Search.new index

This is the standard Picky API. You create an index (definition) and pass it into the search.

The line

updates_picky index

tells this class to automatically update the given index as soon as the after_commit method is called.

This method can also be called as follows:

updates_picky :models

or

updates_picky

The first one uses the index called :models and the second one uses model_class.name.tableize to find the model name.

Finally, the line

searches_picky search

installs a Model.search method using the given search.

Also of note

This API does not really care where anything is set up. This is well possible:

class Model < ActiveRecord::Base
  include Picky::ActiveRecord
end

# In e.g. initializers/picky.rb
#
index = Picky::Index.new :models do
  category :name
  category :surname
end
  
search = Picky::Search.new index
  
Model.updates_picky index
Model.searches_picky search

for the case where you’d like your search code outside the model.

Also, you can call updates_picky multiple times:

Model.updates_picky index
Model.updates_picky index2
Model.updates_picky index3

Any updates to the model will update each index.

Implementation

If you’re interested in the implementation, see the Picky::ActiveRecord module (code at the time of this writing).

Finally, you

Hope you like the API design series. The API is certainly turning out to be simple. Too simple? Who knows.

Opinions, ideas?

We still haven’t looked at index persistence. We save this for another blog post.

Next CocoaPods Search Design

Share


Previous

Comments?