Sunday, September 9, 2012

Free Full-Text Search with Heroku, Part 1

   If you want to put up a Ruby on Rails app for free, where's the natural place to put it?  Heroku, of course!  :-)

   BUT... what if your app idea involves full-text search?  While Heroku gives you lots of great tools for free, and they do have a number of full-text search tools, they don't give you any level of full-text search tools for free.  (Other than a few things in public beta, or if you're lucky enough to get into it, private beta.)  If you're a cheap-@$$ like me, but want something known to be reliable, this is a problem.  :-(

   BUT... they have PostgreSQL, which has full-text search built-in!  :-)

   BUT... PostgreSQL's full-text search is a royal pain in the proverbial posterior to use (or at least so I'm told).  :-(

   BUT... there's the pg_search gem, which makes it a lot easier.  :-)

   BUT... that has some serious bogons in the scopes it generates.  I've run into two so far.  One is the subject of their very first issue filed on Github.  :-(

   BUT... I've found workarounds, and that's what this post and the next one are about.  :-)

   First, they don't deal well with being handed a blank or nil.  For instance, I am creating a job board, and each job has a title and a description.  I tried putting in the Job model:

  pg_search_scope :has_description, against: :description

  pg_search_scope :has_title, against: :title

   But what happens if your user doesn't care about one (or both) of these?  The HTTP request will most likely not include a string to search on.  Your searching code will thus most likely pass the scope an empty string, or a nil.  In either case, it barfs and hands back a cryptic error message about something not including any lexemes.  That basically means "hey, fool, I need something to search for!"

   You may have seen a very similar situation with normal scopes:

  scope :has_title, lambda { |t|
    where("title LIKE ?", "%#{t}%")
  }

   (Yeah, I know, using LIKE for this is a bad idea for many reasons.  This is just an example, OK?  I wanted to make it a similar purpose.  You can just pretend it's looking for an integer match instead.)

   There is a pretty much standard solution to this, for normal scopes:

  scope :has_title, lambda { |t|
    where("title LIKE ?", "%#{t}%") if t.present?
  }

   That will search the title only if t is "present".  (In Ruby, with the assorted Rails extensions loaded, that means it's not blank.  Being blank means it's nil, or an empty string, or a string of only whitespace.)

   So how do you tell a pg_search_scope to fire only if the argument is present?  Unfortunately, you can't (at least as far as I've seen).

   BUT... you can work around it by wrapping it in a normal scope.  Rename your pg_search_scope to something else, like pg_has_title or _has_title.  Optionally, make it private.  (Yes, I know, Ruby's notion of private isn't really private, but at least marking it as such can keep it out of the way.)  Then, refer to it from a normal scope:

  scope :has_title, lambda { |t|
    _has_title(t) if t.present?
  }

private

  pg_search_scope :_has_title, against: :title

   Now your pg_search_scope won't even be called unless the argument is present.

   In Part Two, we'll see how to work around the bogon that prevents you from chaining two (or more) pg_search_scopes in a single query.