I’ve spent the last few months on a Ruby on Rails project for a client. I’m integrating a lot of different applications into it, creating quite the “mashup”. One part of the project requires the ability to search the system for results that fit within a radius of a given postal code. So to do this, I need some sort of searching algorithm or application and a geocoding application for zip code relationships. The search results need to be able to be sorted by the different attributes on the results.
So this meant I needed several pieces. One was a searching module. I found ferret and the acts_as_ferret Rails plugin to do full text searching. From what I could gather online, this was one of the best solutions out there. I want to be able to display distances between zip codes, which I can do using GeoKit. So I find myself off and running.
I was able to do everything successfully from getting the right results back to calculating distances (Side note. If you need a start with acts_as_ferret, there’s a good article here at Rails Envy). However, because the distances between zip codes are calculated and not part of the ferret index, I can’t sort by results. Uh oh…
The solution was, in my opinion, a hack, but it works. What I did was let acts_as_ferret handle sorting for everything except distances (it couldn’t do it anyway, so fine). After I get my results back, I decided, well, I guess I can sort them again, right? So, let’s do this:
@total, search_results = MyModel.full_text_search(@search_term, {:sort => s}, {:include => [:zip_code], :conditions => conditions})
This gets me my search results. What about distances? Well, this can be done, even though its an issue performance wise:
for sr in search_results sr.destination_distance = round_to(sr.zip_code.distance_to(@search_zip_code), 2) end
So now each result knows what its distance is from the searched upon zip code. Now what about sorting?
if params[:sort] == "distance" search_results = search_results.sort @total = search_results.length elsif params[:sort] == "distance_reverse" search_results = search_results.sort search_results = search_results.reverse @total = search_results.length end
So now you’re thinking, ok, but how do you know how to sort MyModel? Easy, I decided I’d override <=> for the MyModel class so that a MyModel was less than, greater than, or equal to another MyModel based on distance. So I did this:
def <=>(item) if self.destination_distance < item.destination_distance return -1 elsif self.destination_distance > item.destination_distance return 1 else return 0 end end
So you can see with the example above, I can sort by just calling sort. To reverse the sort, just call reverse after sorting.
So there you go, sorting by distance values. There are definitely drawbacks with this method. First, you have to iterate over all of the search results to set the distance on them. Second, what if I need to sort by some other calculated value? Since I overroad <=> for distance, I can’t really do it for another value. But for now, this works. Maybe I, or someone else, can come up with a better solution.