How do we 'Search' in a RESTful world?

Dear RubyReportist's I've been beating up the Rails web forums for the last week trying to understand how 'mature' Rails web developers' elect to allow their users to 'mince' the data. We all know that a normal INDEX action in Rails usually does a Table.find(:all) and there we show the user a list (page by page) of the 'whole show'. Now, what happen's when the user wants to just see a part of it, or sort by a field, or select a result set a group of separate fields? I've seen code like this: def self.search(town, beds, min_price, max_price)   conditions =   conditions << ['town = ?', town] if town   conditions << ['beds = ?', beds] if beds   conditions << ['price >= ?', min_price] if min_price   conditions << ['price <= ?', max_price] if max_price   find(:all, :conditions => [conditions.transpose.first.join(' and '), *conditions.transpose.last]) But not found anyone that can tie it all together. Are there any people in this group that would be willing to consult to show me a simple example of how this could be done? David Kennedy DauntlessDavid@gmail.com 775-885-9125

Dear RubyReportist's I've been beating up the Rails web forums for the last week trying to understand how 'mature' Rails web developers' elect to allow their users to 'mince' the data. We all know that a normal INDEX action in Rails usually does a Table.find(:all) and there we show the user a list (page by page) of the 'whole show'. Now, what happen's when the user wants to just see a part of it, or sort by a field, or select a result set a group of separate fields? I've seen code like this: def self.search(town, beds, min_price, max_price)   conditions =   conditions << ['town = ?', town] if town   conditions << ['beds = ?', beds] if beds   conditions << ['price >= ?', min_price] if min_price   conditions << ['price <= ?', max_price] if max_price   find(:all, :conditions => [conditions.transpose.first.join(' and '), *conditions.transpose.last]) But not found anyone that can tie it all together. Are there any people in this group that would be willing to consult to show me a simple example of how this could be done?

David,

First off, I like your approach here, I may implement it myself. But to answer your question as in how I've done it. I needed to be able to search within associated tables of has_one, has_many and HABTM all within the same search. I also needed to pass additional filters, that changed the semantics of the search. I have a metaprogramming based approach to this. First, I've created a master search class that defines some class level methods and some instance methods like to_conditions, to_includes, to_querystring etc. Each form's controller derives a subclass from that, assigning fields matching those on the form.

E.g. In my controller:

class HotelController    class HotelSearcher < MasterSearcher       attr_accessor :town, :beds, :price # these are accessable to the form    end

   def index      hotel_searcher = HotelSearcher.new    end    def search      hotel_searcher = HotelSearcher.new(params[:hotel_searcher])      Hotels.find, :all, :conditions => hotel_seacher.to_conditions # here be majik    end

In the view, just do a form_for(@hotel_searcher) or use a FormBuilder, etc.

To get fancy:    class HotelSearcher < MasterSearcher       attr_accessor :town, :beds, :price       fk_accessor :state, :id, :alias => :state_id       ignored :filter, :default => 'all'    end

The fk_accessor uses the model and the field to search on. An optional alias can be used for things like id's in select lists where the options are from a lookup table. (They are all called 'id' and you can only have one of the same name.) There is an additional #to_includes that gathers the correct include => array.

The ignored works for form fields that should not participate in the conditions clause, but modify the filter in some way (like order or something else.) You can customize the to_conditions by    def to_conditions      super do |conditions|          # your code goes here      end    end

HTH, Ed

Ed, I feel like I've got a tiger by tail here, and am struggling to keep up! I've read this post many times and wonder if you'd help me sort it out?

First, I've created a master search class that defines some class level methods and some instance methods like to_conditions, to_includes, to_querystring etc. Each form's controller

derives a subclass from that, assigning fields matching those on the form.

What does this 'search class' inherit from? Is it like a 'login' table that has no AR backbone? Did you put it in the app/models folder with a name like mother_of_all_query_parameters.rb? What blows me away is that you use the 'form_for' helper and (from what I've read) this supports ONE AR model table?

So, if you'll let me try to tell the story here...it goes like this: Your 'MasterSearcher' class is designed to hold stuff like query :conditions :filters :sort_by, etc. and is uniquely configured for each 'usage' by adding the correct attr_accessor fields that are appropriate for the model or combination of models being searched. When the user clicks on SEARCH the search.controller.index action fires and presents the index.rhtml where the user fills in search criteria using the form_for helper. When the user hits the SUBMIT button it fires the SEARCH action that then delivers the search.rhtml to show the user the 'goodies' they wanted. I can't see where the @hotel is that you pass to 'feed' this view?

Could you combine your SEARCH action logic into your INDEX logic something like this; def index   if the params[:hotel_searcher]) are nil     MasterTable.find(:all)   else     do that voodoo that you do so well   end end I'm so new and impressionable to Rails, that I'm striving to be a 'good boy' and only use the 7 CRUD actions. Thank you, David

Ed, I feel like I've got a tiger by tail here, and am struggling to keep up! I've read this post many times and wonder if you'd help me sort it out? >First, I've created a master search class that defines some class level methods and some instance methods like to_conditions, to_includes, to_querystring etc. Each form's controller derives a subclass from that, assigning fields matching those on the form.

What does this 'search class' inherit from? Is it like a 'login' table that has no AR backbone? Did you put it in the app/models folder with a name like mother_of_all_query_parameters.rb? What blows me away is that you use the 'form_for' helper and (from what I've read) this supports ONE AR model table?

I actually put it in a module in app/helpers called SearchHelper and a class called SearchForm. So in reality my code in the controller looks more like this:   class HotelSearcher < SearchHelper::SearchForm. I did it this way to be able to run RSpec tests (which has a convenient spec:helpers Rake task. Also, it is visible to the controller and the view.

The form_for doesn't care about AR. The object just has to be initialized in the controller and respond to method calls for the fields. That's why I used att_accessor to create them in the class def in the controller.

So, if you'll let me try to tell the story here...it goes like this: Your 'MasterSearcher' class is designed to hold stuff like query :conditions :filters :sort_by, etc. and is uniquely configured for each 'usage' by adding the correct attr_accessor fields that are appropriate for the model or combination of models being searched.

It actually doesn't hold the sort by but you could do that. The reason I didn't was because I am using the OpenRico DB grid and it handles (column) sorting via JavaScript and Ajax call backs. I just get the conditions from the derived SearchForm and combine it with the stuff from OpenRico.

When the user clicks on SEARCH the search.controller.index action fires and presents the index.rhtml where the user fills in search criteria using the form_for helper. When the user hits the SUBMIT button it fires the SEARCH action that then delivers the search.rhtml to show the user the 'goodies' they wanted. I can't see where the @hotel is that you pass to 'feed' this view?

You are right, my example code was missing that. Should have been: def search     hotel_searcher = HotelSearcher.new(params[:hotel_searcher])     @hotels = Hotels.find, :all, :conditions => hotel_seacher.to_conditions # end

Could you combine your SEARCH action logic into your INDEX logic something like this; def index   if the params[:hotel_searcher]) are nil     MasterTable.find(:all)   else     do that voodoo that you do so well   end end

Yes, that is possible, but not very Railish or RESTful. You should let the router make decisions about which actions to call. Almost all the Rails code I've seen has had a form in the index that POSTs to another action. Especially if you do Ajax forms.

I'm so new and impressionable to Rails, that I'm striving to be a 'good boy' and only use the 7 CRUD actions.

My approach is really REST agnostic. If by 7 you mean (index, new, create, show, edit, update, destroy) only index displays a list of records. Search isn't usually one of these CRUD things, but something nearly every app I've written has needed.

In REST you have these two actions:    GET /articles      and    GET /article/123 The first responds with a list, the second with a view of one particular element of that list. Note the the index action has no filter. It is not very modern UI (you'd get back all of the articles of which there could be thousands or more.) How do you filter? How do you page the results?

I think Searching and REST are out of each other's scope. Or it seems that way to me.

Alternatively, you could use PragDave's RADAR approach to architect the app.

http://pragdave.pragprog.com/pragdave/2007/03/the_radar_archi.html

While not a solution to the search issue, I've heard of using this to implement a search in the presentation layer that talks to the back end via REST. Can't find the URL for it.

Thank you, David

Sure.

Hth, Ed

I’m not sure that this answers your question at all, but there is a nice plugin that helps with searching AR records without the fulltext searching. EZWhere by Ezra, You can start looking at it on his blog at http://brainspl.at/articles/2006/10/03/nested-joins-and-ez-where-update

HTH Daniel

I'm not sure that this answers your question at all, but there is a nice plugin that helps with searching AR records without the fulltext searching. EZWhere by Ezra, You can start looking at it on his blog at Ruby on Rails Blog / What is Ruby on Rails for?

HTH Daniel

This is a nice conditions generator and it handles the nested joins that many I've seen lacked. I like the embedding of order by in the syntax.

I was trying to solve the first half of the problem (for which this plugin solves the second 1/2 nicely.) IOW, how do you go from a search form to a conditions array? Esp. when you have multiple join tables that participate in the search.

In many cases, if you have a single model to search, you can create it in the search index action   @model = MyModel.new Then use form_for on @model

and then again in the SUBMIT action    @model = MyModel.new(params[:model]    ... somehow create the conditions array from the fields in the model.    ... perhaps via EXWhere above.

But how do you make this DRY? It seems that each form is going to have to be handled independently, and this make sense, but I wanted to convert from any form into any conditions array and do it OAOO.

My approach to form processing could use an EZWhere plugin internally, because I am using metaprogramming to generate the fields.

Ed