How do we 'Search' in a RESTful world?


#1

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 K.
removed_email_address@domain.invalid
775-885-9125


#2

On 5/11/07, BraveDave removed_email_address@domain.invalid wrote:

I’ve seen code like this:
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

David K.
removed_email_address@domain.invalid
775-885-9125


Ed Howland


#3

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


#4

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


#5

On 5/12/07, BraveDave removed_email_address@domain.invalid wrote:

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’ve seen code like this:
simple example of how this could be done?
to_conditions, to_includes, to_querystring etc. Each form’s controller
def index
To get fancy:
that gathers the correct include => [] array.
HTH,


Ed Howland


#6

On 5/14/07, Daniel N removed_email_address@domain.invalid wrote:

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

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

out?
that you use the ‘form_for’ helper and (from what I’ve read) this

to show the user the ‘goodies’ they wanted. I can’t see where the
something like this;
Rails code I’ve seen has had a form in the index that POSTs to another
In REST you have these two actions:
that way to me.

Dear RubyReportist’s
def self.search(town, beds, min_price, max_price)
simple example of how this could be done?
to_conditions, to_includes, to_querystring etc. Each form’s controller

etc.

options are from a lookup table. (They are all called ‘id’ and you can
end
quoted text -
http://greenprogrammer.blogspot.com


Ed Howland