Searchlogic 1.5.7 is by far my favorite release because it takes Searchlogic to a whole new level. It solves a problem I thought it would never solve. Before I explain the new features, let me give you a quick run down on my perspective of Searchlogic:
A lot of people think Searchlogic is a “console” searching utility. Meaning you can pop into your console and execute some simple searches quickly and easily, and it is, but by accident. My goal with Searchlogic has always been freeing your application of searching clutter. If you’ve ever done an app with searching you know there is a lot of “cruft” that goes along with it: nasty controller actions, excessive named_scopes, etc. Searchlogic rids you of this by representing an entire search’s criteria with a hash: conditions, ordering, pagination, the whole package. Why is this nice? Because GET and POST parameters are a hash. What’s nice about that? Because an HTML form’s sole purpose is to send GET and POST parameters to a URI. This means you can build a form that represents your entire search. Adding a condition to your search is as easy as adding a field to your form. This ultimately makes your controller dead simple, frees it of any search clutter, and rids your models of excessive named_scopes. Here is what your controller action should look like with Searchlogic:
@search = User.new_search(params[:search]) @users = @search.all
This is great for simple searching, but how do you represent complex searches this way? Let’s take this query:
select * from users where id > 5 AND (email LIKE 'ben%' or email LIKE '%binarylogic.com') AND first_name LIKE '%ben%'
Before, Searchlogic couldn’t handle this. It did not support grouping (parenthesis) and it did not allow you to mix and match “AND” and “OR”, you had to pick one or the other. Why? Because I never intended for Searchlogic to replace complex searching. If you need to perform a complex search there is nothing wrong with using Searchlogic in conjunction with some named_scopes. SQL is not bad, but what is bad is that all of that searching cruft starts seeping back into your project. All of a sudden your controller is not clean, your form has to stray away from using the form builder, and your model gets cluttered with named scopes that you are only using for one section in your application. Searchlogic fails to meet its goal of keeping your application free of “search clutter” and a tear runs down your cheek.
Solving the problem of mixing “AND” and “OR” was easier than I thought. The only major change I had to make was making the conditions order relevant. The order you set your conditions is the same order they will appear in your SQL statement. Before, the conditions were stored in a hash, where order was not preserved. Now you can slap an “and_” or “or_” prefix in front of your conditions:
search = User.new_search search.conditions.name_like = "Ben" search.conditions.or_email_like = "a@a.com" search.conditions.and_id_gt = 5 # => name LIKE '%Ben%' OR email like '%a@a.com%' AND id > 5
The last condition “and_id_gt” could also be written as “id_gt”, because, by default, we are joining with “AND”. Not specifying a prefix uses whatever you are joining your conditions with by default, specifying a prefix will always use that join. If you want your conditions to be joined by “OR” by default just do:
search.any = true
What about grouping conditions with parenthesis? No problem. Take the above query:
search = User.new_search search.conditions.id_gt = 5 search.conditions.group do |group| group.email_begins_with = "ben" group.or_email_ends_with = "binarylogic.com" end search.conditions.first_name_contains = "ben" search.all
What about a hash?
search = User.new_search(:conditions => [
{:id_gt => 5},
{:group => [
{:email_begins_with => "ben"},
{:or_email_ends_with => "binarylogic.com"}
]},
{:first_name_contains => "ben"}
])
What about a form?
- form_for @search do |search|
- search.fields_for "conditions" do |conditions_array|
- conditions_array.fields_for "[]", search.object.conditions do |conditions|
= conditions.text_field :id_gt
- conditions.fields_for "group" do |group_array|
- group_array.fields_for "[]" do |group|
= group.text_field :email_begins_with
= group.text_field :or_email_ends_with
= conditions.text_field :first_name_contains
You will notice the hash and the form are implementing arrays, thats because hashes do not preserve the order. In our case above, order is important to us. What does preserve the order? An array. So if order is relevant to you, then you need to structure your conditions into an array. There is no other choice with this, because forms can either submit a hash or an array of parameters.
I know what you are thinking: “the above looks a little confusing and messy”. I agree with you, to a point. The word “messy” is a relative term. In our case, we have no other choice, this is the 100% correct way to do this. That being said, this is clean, because there is no other cleaner option. Sorry to be a debbie downer here, but this is how forms work, there is nothing I can do about that. Regardless, the above is still cleaner than having a nasty controller action or adding a bunch of named_scopes into you model. Your search logic is nice and DRY, in one spot: the view.
I think this is pretty nifty, now you have the tools to construct any type of search in your form. I’ve been getting a lot of emails asking me about this, and I had to be a debbie downer and tell them to use named scopes. Now you can ditch the named scopes and keep your search logic nice and DRY in your form, where it probably belongs.
The last thing I want to say is that there is a time and a place for a named scope, by no means am I discouraging them, because they are by far one of the nicest features in ActiveRecord. But something about creating a complex named scope or a bunch of small named scopes for a single action in one of my controllers felt dirty. It felt like clutter. I knew I would never use those named scopes anywhere else, and they were solely for that search form. After a while all of these named scopes start overlapping and start sharing search logic. If you’re extremely picky like me, your don’t feel like your code is DRY. So how far do you go to break down a named scope? Breaking them down too much defeats their purpose, but not breaking them down enough seems to create redundant search logic. Searchlogic solved my problem with this, now all of my view specific search logic is where it should be, in my view.
Ben, i gotta say that this is an awesome plugin. I am using it since the beginning and it solved all my problems. It’s a pretty straight solution if you dont need the full text search capabilities of Ferret or Sphinx. Its an stable part of my prefered plugin list.
Thanks and keep up the good work.
Lucas
Very nice !
But how would you build such a complex form for defining groups ?
Jerome, take a look at the last code example, it is a from.
Looking good, Ben. Nice choice of "group" since it is a SQL keyword and highly unlikely to conflict with a column name.
In Ruby 1.9 hashes will preserve the order, so this code will be cleaner. thanks ben, this plugin looks very promising
I’ve been toying with full text searches using AR and it works, but damn it is messy. Let’s say you have Companies and Employees and you would like to throw up a very comprehensive, full text search site-wide. That means you’ll be doing a few MATCH/OR statements to cover most of the string/text fields (name, phone, address, etc) in *both* tables, plus a JOIN. If you construct your condition statement with just AND and it searches fields across two tables, MySQL will not use any existing indices. Bummer, big time!
The solution is to use the MATCH and OR with IN BOOLEAN MODE. That fixes the problem, but I’ll admit that the MATCH statement is not for the uninitiated. More at: http://dev.mysql.com/doc/refman/5.0/en/fulltext-search.html
What you end up with is one big, hairy, tangled mess of a named scope.
Ben, do you see any relief that could be provided by Searchlogic? I can send you a named scope example if you like. Supporting MATCH/OR would nicely position Searchlogic as a beautiful alternative to Ferret/Sphinx/EtAl.
Karl, send away, just email me or create a ticket in lighthouse, I would be very interested in that.
I was wrong above, MATCH/OR will also cause MySQL to no use indices. You need a UNION DISTINCT between SELETCs. Sorry, I copied that from one of my early test.
Can’t possible see how this query could be constructed in any way using AR as it exists today. Not even sure how Searchlogic would fit in.
Ben, I’ll just pop you an email and see what you think.
Is there a way to set one text field to search multiple attributes of a model? Say I have a product model with a title string field and a description text field and I want to run a search that would match either or of those attributes. Also, thank you for another amazing plugin, it’s amazing how easy this makes it to search AR records.
Hi,
I’m getting the following. Any ideas? Are there any requirements on the model object?
=================
ActionView::TemplateError (undefined method `account_items_path’ for #<ActionView::Base:0×2551a84>) on line #3 of app/views/account_item/custom_list.html.erb:
1: <h1>Custom AccountItem List</h1>
2:
3: <% form_for @search do |f| %>
4: <fieldset>
5: <legend>Search Users</legend>
6: </fieldset>
/opt/local/lib/ruby/gems/1.8/gems/actionpack-2.2.2/lib/action_controller/polymorphic_routes.rb:112:in `__send__’
/opt/local/lib/ruby/gems/1.8/gems/actionpack-2.2.2/lib/action_controller/polymorphic_routes.rb:112:in `polymorphic_url’
/opt/local/lib/ruby/gems/1.8/gems/actionpack-2.2.2/lib/action_controller/polymorphic_routes.rb:119:in `polymorphic_path’
/opt/local/lib/ruby/gems/1.8/gems/searchlogic-1.6.3/lib/searchlogic/helpers/form.rb:111:in `searchlogic_args’
/opt/local/lib/ruby/gems/1.8/gems/searchlogic-1.6.3/lib/searchlogic/helpers/form.rb:158:in `form_for’
app/views/account_item/custom_list.html.erb:3
=================
Thanks
@ben, this plugin rocks. I have a client that requires an iTunes-like smart-playlist adv search form where a user can add new criteria by adding a row. This seems to be the best way to pull that off, but I’m having a tough time figuring out how to build the search form without having to transpose the form to something searchlogic can understand. Am I missing something?