Tuesday, September 25, 2012

Free Heroku Text Search Update

   I volunteered to give a talk on "Full Text Search on Heroku for FREE", subject of two prior blog posts (here and here), at Arlington Ruby Meetup.  So, I've prepared slides, which are available at https://docs.google.com/presentation/d/1NbN0kJMJsSQW2N7ItNMB6VuM_lJok-Xb0epk0anbRIo/edit .  The slides contain code samples (as text, not screenshot graphics) that you can copy.

   UPDATE:  I gave the talk at RubyDCamp, and it was well received.

Saturday, September 22, 2012

Domains Shuffled!

   The domains seem to be all in order.  I don't know if this will automagically get to all the subscribers... this post will also be a test.  :-)

Do the Domain Shuffle!

   Heads up!  I've finally pulled the proverbial trigger on setting up my LLC.  Henceforth, Dave Aronson Software Engineering & Training will be Codosaurus, LCC, and will take over this blog's domain, http://www.codosaur.us.  This blog is about to move to http://blog.codosaur.us.  http://www.davearonson.com will eventually be replaced with stuff more about me personally.

Friday, September 21, 2012

Ruby microhack: kinds of triangles

   Someone recently posted on the RubyOnRails-talk mailing list, about a little exercise he was doing as a beginning Rubyist.  The task was to construct a method that would return the type of a triangle (equilateral, isosceles, or scalene), given the three lengths.  He had the logic down fine, but needed some help with the Ruby syntax, properly ending a series of if-statements.

   A quite different way to solve it, popped into my twisted little mind:


  [:equilateral, :isosceles, :scalene][[a,b,c].uniq.length - 1]


   As I wrote to him, "Do NOT put something that "clever" in anything actually important, as the lack of clarity isn't worth the conciseness.  But it makes a neat
little mind-exercise.  ;-)"


   Alternately, to split it up for a bit better readability:

  types = [:equilateral, :isosceles, :scalene]
  number_of_lengths = [a, b, c].uniq.length - 1
  types[number_of_lengths]

   Just thought y'all might find it amusing....

Sunday, September 9, 2012

Free Full-Text Search with Heroku, Part 2

   Last time, we looked at putting a Ruby on Rails app on Heroku, using the pg_search gem to access PostgreSQL's built-in full-text search by making pg_search_scopes, and how to work around the bogon that you must always feed them something to search on.

   The second bogon, subject of their very first issue filed on Github, is that you can't chain two or more of them in the same statement.  For instance, if my Job model has:

  pg_search_scope :has_description, against: :description
  pg_search_scope :has_title, against: :title

then I can't ask for:

   Job.has_description("Ruby").has_title("developer")

   What happens if I do?  It barfs and gives a cryptic error message about pg_search_rank being ambiguous.  Long story short, that means it's calculating pg_search_rank in two different contexts (one for description, and one for title), and then trying to refer to pg_search_rank, without saying which context to get it from.

   Looking at the generated SQL, we see that it's trying to use it in an ORDER BY clause, specifically:

    ORDER BY pg_search_rank DESC, "jobs"."id" ASC

Luckily, that might not be the order we want.  Sure, sometimes you do want it that way... but in my particular application (a job board), there are other perfectly relevant orders to default to, like posting date, or distance from the center of a search (if the user did a distance-limited search), or various other columns I haven't mentioned before.  So let's try to replace it, and do:

    Job.has_description("Ruby").has_title("developer").
        order(:posted_at)

   Close but no cigar.  Now we have:

    ORDER BY pg_search_rank DESC, "jobs"."id" ASC,
    posted_at DESC

   It seems that order only adds to the ordering, so it can't help us get rid of referring to pg_search_rank.

   BUT... ActiveRelation also provides the lesser-known reorder!  So if we do:

    Job.has_description("Ruby").has_title("developer").
        reorder(:posted_at)

our ORDER BY clause is now simply:

    ORDER BY posted_at DESC

   Long story short, this works.  Later, maybe I'll look into how to actually use the pg_search_rank....

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.