Yeah, baby!
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.
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
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
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
@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.
@Ryan, well that's nifty! Yes it absolutely is.
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.
Hello!
Why don't you create a simple virtual model (non-activerecord class) with instance attribute parameters?
@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.
Amazing!, Ryan but I emailed you yesterday! lol
Thanks a lot, you don't know how much for this one!!
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...
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?
@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.
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.
Instead of the complicated conditions why not use criteria_query http://agilewebdevelopment.com/plugins/criteria_query
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
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.
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
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!!
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? ;)
@Bizzy, yep! planning to add searching to Railscasts, just haven't gotten to it yet. :)
@Ryan
Why on keyword_conditions do you use the variable keywords twice? Is once not enough?
Thanks
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
Thanks for this screencast!
It's going to be so usefull! I'm going to use it with sphinx search engine!
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.
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.
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
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
@Andrew : thanks RailsWhere looks interesting
I've found the acts_as_ferret plugin very good for plaintext searching.
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.
If you change the condition_parts method to read:
private_methods(false).grep(...
It will work just fine.
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.
This is a valuable resource!
Love it. thanks
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.
Nevermind...I got the search results working! Just forgot my for loop!
Thanks for the cast!
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.
Great job Ryan,
I never thought I will saw such a beautiful code for multiconditional search.
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"?
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
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
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.
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.
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.
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.
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.
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!!
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.
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.
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.
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!
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?
Thanks to Greg's suggestion about including different models. helped me out.
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.
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"..
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


