#111
May 26, 2008

Advanced Search Form

If you need to create an advanced search with a lot of fields, it may not be ideal to use a GET request as I showed in episode 37. In this episode I will show you how to handle this by creating a Search resource.
Download (33.4 MB, 7:38)
alternative download for iPod & Apple TV (10.2 MB, 7:38)

Resources

<!-- views/searches/new.html.erb -->
<% form_for @search do |f| %>
  <p>
    <%= f.label :keywords %><br />
    <%= f.text_field :keywords %>
  </p>
  <p>
    <%= f.label :category_id %><br />
    <%= f.collection_select :category_id, Category.all, :id, :name, :include_blank => true %>
  </p>
  <p>
    Price Range<br />
    <%= f.text_field :minimum_price, :size => 7 %> -
    <%= f.text_field :maximum_price, :size => 7 %>
  </p>
  <p><%= f.submit "Submit" %></p>
<% end %>
# models/search.rb
def products
  @products ||= find_products
end

private

def find_products
  Product.find(:all, :conditions => conditions)
end

def keyword_conditions
  ["products.name LIKE ?", "%#{keywords}%"] unless keywords.blank?
end

def minimum_price_conditions
  ["products.price >= ?", minimum_price] unless minimum_price.blank?
end

def maximum_price_conditions
  ["products.price <= ?", maximum_price] unless maximum_price.blank?
end

def category_conditions
  ["products.category_id = ?", category_id] unless category_id.blank?
end

def conditions
  [conditions_clauses.join(' AND '), *conditions_options]
end

def conditions_clauses
  conditions_parts.map { |condition| condition.first }
end

def conditions_options
  conditions_parts.map { |condition| condition[1..-1] }.flatten
end

def conditions_parts
  private_methods(false).grep(/_conditions$/).map { |m| send(m) }.compact
end

RSS Feed for Episode Comments 67 comments

1. DHH May 26, 2008 at 00:57

Yeah, baby!


2. taelor May 26, 2008 at 01:37

Dude, I would never thought, nor prolly would have ever thought about making a search a resource. Was this an original idea or something that someone else hinted at for you?

Once again Ryan, Bravo.


3. Aditya Sanghi May 26, 2008 at 02:07

Hi Ryan,

The scaffolded controller only had new and show, right? Wouldnt the search form invoke the non-existent create action on the server? Have you modified the controller code for the above to make it work?

Cheers,
Aditya


4. Rafael Schär May 26, 2008 at 02:50

Uhh... like it... ...that simple.

I've done something similar to store it for customized RSS Feeds.
But I have it to simplify with a search model.

Thank you Ryan
Rafael


5. Maciej May 26, 2008 at 04:15

Great screencast.

But please, please never ever use floats in real applications when it comes to money. I don't want to have rounding errors on my bank account in the future.

Cheers,
Maciej


6. Ryan Bates May 26, 2008 at 08:15

@taelor, it's not an original idea. I heard about it elsewhere but don't remember where.

@Aditya, the nifty_scaffold generator will generate the create action automatically if there's a new action.

@Maciej, good point! I normally use the decimal type for prices, but thought a float would be okay in this case since this isn't really a price. It's just a number that's used to search prices. I wouldn't be performing calculations on this value.


7. Aditya Sanghi May 26, 2008 at 08:57

@Ryan, well that's nifty! Yes it absolutely is.


8. Dandre Gregory May 26, 2008 at 09:39

How would you implement pagination?
I've done something similar, the only difference is the search class generates a conditions block passed to another model. This makes it easy for me to tack on pagination.


9. Avalon May 26, 2008 at 09:56

Hello!

Why don't you create a simple virtual model (non-activerecord class) with instance attribute parameters?


10. Sebastian May 26, 2008 at 11:08

Nice!


11. Ryan Bates May 26, 2008 at 13:24

@Dandre, good question. What I would probably do is pass the page number into the Search model and call paginate inside the search model. This opens up the ability of storing pagination parameters in the searches table (such as how many results are displayed on a page).

Since we have a model dedicated to searching, to me it seems best for it to handle as much of the searching as it can.

@Avalon, that's a possibility, but the problem is then we have no way to fetch the results again. There's no way to bookmark the URL, or add pagination if you need to. Storing the search in the database allows you to do this. However it really depends on the requirements for your app.


12. Jose Espinal May 26, 2008 at 17:06

Amazing!, Ryan but I emailed you yesterday! lol

Thanks a lot, you don't know how much for this one!!


13. Marcos Neves May 26, 2008 at 19:41

Episode 112 could show us how to make this search polymorphic, cause I would like a search for products, one for comments, other for posts...


14. Jose Espinal May 26, 2008 at 19:48

In the show page I'm getting:

Couldn't find template file for restaurants/_restaurant

Where line of error is:

<%= render :partial => @search.restaurants %>

What could be wrong, anything else needs to be added?


15. Ryan Bates May 26, 2008 at 21:02

@Marcos, I'm not sure how a polymorphic association would help in that case. I'd probably just make a separate search model for each one (ProductSearch, CommentSearch, etc.). Unless you need the same attributes for each one but they will likely need to be different. Then if you have some duplication you can move that out into a generic Search module.

@Jose, yep, this will look for a partial under "restaurants/_restaurants.html.erb", so you should make one there which has your restaurant display. Alternatively you can use a "for" loop to loop around the restaurants array instead of using a partial.


16. Karl May 26, 2008 at 23:01

For those who may not know, if you don't want to save your searches in the database, but would like all benefits of using an AR model (like validations), you can use virtual attributes:

class Search < ActiveRecord::Base
  attr_accessor :keywords, :category_id, : minimum_price, :maximum_price
  validates_presence_of :keywords
  # blah...
end

Everything works the same, just doesn't save anything. And no need for a migration or 'searches' table in db.


17. Alex May 27, 2008 at 12:31

Instead of the complicated conditions why not use criteria_query http://agilewebdevelopment.com/plugins/criteria_query


18. Sam Granieri May 27, 2008 at 12:31

Ryan, this screencast looks like it's going to be a HUGE help in making some of my resources more restful.

Thanks for the great work


19. Uzytkownik May 28, 2008 at 01:32

I've also thought about the off-database model.
In current solution:
- You can easly run out of the free id (it is not so uncommon as it may appear to be, hard to solve and detect) - the deleted ids are not reused
- Also easy DOS attac (run out of ids).

Run out of id can be solved but it requires in fact stop of service.


20. remco May 28, 2008 at 04:46

He Ryan,

Great article..i am going to used it for sure in my new rails project.

You talked about storing the search results of the user in the database. I looked arround the net, but with no result. Do you have some links where i can find information about this kind functionality?

Grtz..remco


21. Bizzy Bee May 28, 2008 at 06:46

I find it ironic how easily you can implement a search as complex as this, but there is no search implemented on this site!

D'oh!!


22. Sig May 28, 2008 at 16:53

Thanks Ryan, Today I started working on a search engine for my application and this railscast is exactly what I need.

PS Does someone have a paginated version of the form to let me see? ;)


23. Ryan Bates May 29, 2008 at 13:16

@Bizzy, yep! planning to add searching to Railscasts, just haven't gotten to it yet. :)


24. Sig May 29, 2008 at 15:31

@Ryan
Why on keyword_conditions do you use the variable keywords twice? Is once not enough?

Thanks


25. Sig May 29, 2008 at 16:03

It's still me
I have noticed that in the code you post here as a resource you have made all the methods (unless find_products") private. However, in the railscast all are public. With private methods it looks like the search doesn't work. Am I missing something on my app or they must be public?

Thanks again


26. Pedro Pimentel May 30, 2008 at 05:20

Thanks for this screencast!
It's going to be so usefull! I'm going to use it with sphinx search engine!


27. Beony May 30, 2008 at 07:17

I think the _conditions methods need to be public because the the 'methods' method doesn't return private methods. Which means you can't run grep on them.


28. Andrew Selder May 30, 2008 at 08:47

Another great plugin to make writing complex conditions easy:

RailsWhere (http://code.google.com/p/railswhere/)

I think that using this plugin makes the code a lot more readable that writing seperate functions for each condition and then using metaprogramming fu to join them all together.


29. Andrew Selder May 30, 2008 at 09:09

Example rewrite of the Search class using RailsWhere.

class Search < ActiveRecord::Base

  def products
    @products ||= find_products
  end

  def find_products
    Product.find(:all, :conditions => to_conditions)
  end

  private

  def to_conditions
    w = Where.new
    w.and "products.name LIKE ?", "%#{keywords}%" unless keywords.blank?
    w.and "products.price >= ?", minimum_price unless minimum_price.blank?
    w.and "products.price <= ?", maximum_price unless maximum_price.blank?
    w.and "products.category_id = ?", category_id unless category_id.blank?
    w.to_s
  end

end


30. Devin May 30, 2008 at 10:58

Ryan, I love your screecasts. You do a wonderful job, but I have one gripe. The intro makes me want to commit suicide. The clickety click of a keyboard, the big BLAHMMMMM noise, etc. Consider changing it? Please?

Love,
d


31. SIg May 30, 2008 at 15:42

@Andrew : thanks RailsWhere looks interesting


32. Andrew Chalkley May 31, 2008 at 00:56

Spot on Ryan!


33. Bryce May 31, 2008 at 01:41

I've found the acts_as_ferret plugin very good for plaintext searching.


34. Tomek Jun 01, 2008 at 01:54

I've found one pitfall with your approach though: if you want to exchange searches between users ? If somebody does a search in Google they can easily pass that url to me and I can enjoy the same results. That's why I think that having complete query strings in the url is not that bad idea.


35. Russ Smith Jun 01, 2008 at 08:58

If you change the condition_parts method to read:

private_methods(false).grep(...

It will work just fine.


36. Ryan Bates Jun 01, 2008 at 10:25

Apologies for the incorrect code. It should be fixed now.

@Devin, planning to make a new intro soon. Sorry about that. ;)

@Tomek, since the search id is in the URL, it's still possible to share the searches with anyone.


37. Ed Shuler Jun 01, 2008 at 22:10

This is a valuable resource!
Love it. thanks


38. Johnny Jun 05, 2008 at 09:36

Rails newbie here. Thanks so much! Your railscasts are amazing. What should my partial page look like? I can't seem to get the partial to display my search results correctly.


39. Johnny Jun 05, 2008 at 10:35

Nevermind...I got the search results working! Just forgot my for loop!

Thanks for the cast!


40. Faktura Jun 05, 2008 at 14:08

Good cast! Thank you!


41. Finanzamt Jun 05, 2008 at 14:10

Very good cast for beginners!


42. Simone Carletti Jun 09, 2008 at 07:59

That's an amazing solution! :)

I've adopted a similar approach but using a MultiCondition object (http://github.com/weppos/activerecord-multiconditions/).

The main advantage is that I can queue illimitate conditions and I can decide which conditions should be queued for which methods.
Using a solution based on method names might cause some trouble when I would use, for instance, all conditions in action A and all conditions - 1 in action B.


43. Marcin Truszel Jun 09, 2008 at 08:37

Great job Ryan,
I never thought I will saw such a beautiful code for multiconditional search.


44. Greg Jun 09, 2008 at 14:27

If your Product model "has_many :colors" how would you also add the ability to to return only products that come in a certain color - assuming you have a text field in your search form named "color" and the Color model has a field named "name"?


45. Greg Jun 10, 2008 at 14:06

I figured it out. You need to add in an ":include" to the "find" like so:

def find_products
  Product.find(:all, :conditions => conditions, :include => :colors)
end

and then you use:

def color_conditions
    ["colors.name = LIKE", "#{color}"] unless color.blank?
end


46. Dennis Krupenik Jun 16, 2008 at 19:28

Marcos, Ryan and Greg, please, check out a chunk of code i wrote tonight:

http://wild-tatar.livejournal.com/41283.html#cutid1

i use it to search anything:

Search.new(:source => 'members', :assoc => {:profile => {:height => 100..190}}).result
or
Search.new(:source => 'members', :assoc => {:favourites => true}).result
or
Search.new(:source => 'members', :root => {:first_name => 'LIKE Dennis%'}).result
or combination of anything of the above


47. Dennis Krupenik Jun 16, 2008 at 19:57

Follow-up: i realize it still needs a lot of improvement.

In possibly-not-so-distant future I plan like to move it inside AR::Base.find to accept search clauses from :conditions, to make associations' table_name discovery more generic, etc. I would be very grateful for any kind of constructive feedback.


48. Tomek Jun 17, 2008 at 01:36

Ryan: Surely, the id is a perfect way to reference the search but if the user changes the search the others can see the change too which might be undesireable.


49. KUscroft Jun 19, 2008 at 18:19

Love the screencast. Just to say I did something similar, the difference is that mine returned a sql string and a 'human string'.

  def self.create_search(search, admin = false)
    @search = search
    @admin = admin
    @human_string = 'all properties'
    dept_condition self.methods(false).grep(/_conditions$/).map { |m| logger.debug "Searching #{m} "
      self.send(m) }
    return @condition.join(' AND '), @human_string
  end

with one of the private methods

  def self.price_range_conditions
    if @search[:min].to_i > 0 and @search[:max].to_i > @search[:min].to_i
      @criteria << replace_bind_variables("#{table_name}.`price` BETWEEN ? AND ?", [@search[:min].to_i, @search[:max].to_i])
      @human_string += ", costing between " + number_to_currency(@search[:min]) + ' and ' + number_to_currency(@search[:max])
    end
  end

with both the sql and human string I then do the find and let the user save the search into the database for later use if neaded.

PS keep up the good work.


50. Danny Jul 18, 2008 at 07:07

Lately I have been using a search controller, which I use the new action from to start of a complex search. I have been working on a search engine at work, which needed the most complex search code I have ever had to write. Finally I came up with this: http://www.styleless.nl/search.rb

It's still really slow, takes about a minute before the search is complete (any hints anyone? ;)). I'm still refining it at the moment, but I'm going to take this to a search model after watching this :)

Thanks (again) for a great screencast.


51. Jason Boxman Aug 16, 2008 at 14:47

I have found using squirrel with some modifications to resource_search allows for much of the above, with little addititional work. With squirrel, there is little reason to ever build your own SQL WHERE clauses manually again.


52. Gwendydd Aug 25, 2008 at 11:18

I can't tell you how grateful I am for this: I spent weeks beating my head against various search options, and none of them seemed to make any sense for my needs until I finally found this, and suddenly I have everything I need!

I do have a question, though. How would I write the conditions for a boolean field? I have several boolean (true/false) fields in my tables, and I want users to be able to check a box in a search field when a field should be true. How should I do this?

Many thanks!!


53. Bennie Aug 26, 2008 at 03:48

Thanks for this screencast.

I use this searchmethod and the result is presented in combination with wicegrid (http://redmine.wice.eu/api/wice_grid/index.html) and it works great.
The method for making a SQL condition is awesome.


54. Jeremy Aug 29, 2008 at 11:49

How do you make this kind of thing drop the :conditions => conditions thing in the case that the user doesn't fill in any of the form. Actually, what I'm really doing is adding GeoKit in here, so I want to do a search which uses the :origin => and :within => stuff. It works fine as long as somebody searches using the conditions, but if you search JUST with origin and within, it throws a syntax error because of an AND with nothing in front of it.


55. simonsayz Aug 29, 2008 at 13:18

I hope someone can help me.
I don't want to use the nifty scaffolding approach. What does it generate in the new and show methods?
Thanks.


56. i'm_down Sep 10, 2008 at 09:12

Hello!
Thanks for all this great tuts.
Keep up!
I have question about your data base in this tutorial. How she luck like in the moment of creating nifty_scaffold. Please tell me!
Best regards and thanks again!


57. Franz Sep 24, 2008 at 11:52

What if my search model uses a HABTM relation.

Where I select multiply options from a checkbox list and want to use them in my search.

Do my model have a string field containing the id's like (11,54,32) or do I create a separate link table?

suppose that my checkbox list is a existing model.

Any ideas?


58. Aegis Oct 04, 2008 at 17:27

Thanks to Greg's suggestion about including different models. helped me out.


59. Alfie Oct 12, 2008 at 15:50

Any hints on what that partial page looks like? I'm getting some newbie associations mixed up and I can't show my results. Thanks for all the screencasts.


60. kris connolly Nov 03, 2008 at 15:52

Correct me if i'm wrong here but this code appears to have a little bug. conditions_parts is called twice and hence the methods function is invoked twice. I noted however, due to my application breaking, that methods does not always return an array of methods in the same order btween calls (ie its return value is not have consitent ordering). This breaks the coonection between filter_options and filter_clauses, meaning incorrect values get passed to the final query "sometimes"..


61. kris connolly Nov 03, 2008 at 16:28

further to my above cooment. I was getting incorrect search results until I fixed it. herse the code change i made which means methods is only called once..

def filter_parts
  @parts ||= methods.grep.....
  return @parts
end


62. holic online gold Nov 20, 2008 at 18:27

Thanks Ryan,I think this is one of the most wonderful sites. I have great admiration for you.


63. kal geons Nov 24, 2008 at 22:46

I have great admiration for you.


64. lily Nov 27, 2008 at 19:31

Thank you Ryan, your screencast is good. Please look at our URL, if necessary we can learn from each other.


65. evden eve nakliyat Dec 01, 2008 at 01:09

evden eve nakliyat


66. evden eve nakliyat Dec 01, 2008 at 01:09

evden eve nakliyatf


67. wow patch 3.0.4 Dec 03, 2008 at 18:45

good job, thank you very much

Add your comment:

(SKIP THIS ONE)

(required)

(not shown)


(use pastie or gist for code)

sponsored by:
if you want to help:
required:
Get Quicktime Player