Scoped search comes packed with some handy GUI helpers. An ajax auto completer that handles the auto-completer drop-down box, and column sort helpers.
The ajax auto-completer drop-down comes in two flavors one for JQuery and the other for Prototype. The code samples in this post will use the JQuery flavor.
Here is an example of the auto-completer ajax drop-down usage, in the view place the following form tag where you want the search box to show:
<%= form_tag books_path, :method => :get do %>
<%= auto_complete_field_tag_jquery(:search, params[:search], {:placeholder => "Filter ..."}) %>
<button id='submit_search'">Search</button>
<% end -%>
To control the look of the search box use the following css classes: ui-autocomplete-input, ui-autocomplete-loading and ui-autocomplete-error. The auto_complete_field_tag_jquery accepts additional options hash for further controlling it’s behavior such as delay and min_length.
Another useful helper method is the sort helper. The sort helper draws an arrow at the top of the sorted column pointing to the sort direction, it also adds order parameter to the url. Together with the ability of the scoped_search to add an order-by sql clause, it makes a complete sorting solution. The reason for sending a new request to the database instead of sorting in the client is that many applications uses pagination. When using pagination client side sort may cause wrong results. Here is an example of using the sort helper:
<table>
<tr>
<th><%= sort :name %></th>
<th><%= sort :"author.last", :as => "Author" %></th>
</tr>
<% for book in @books %>
<tr>
<td><%= book.name %></td>
<td><%= book.author %></td>
<td><%= link_to "Show", book %></td>
<td><%= link_to "Edit", edit_book_path(book) %></td>
<td><%= link_to "Destroy", book, :confirm => 'Are you sure?', :method => :delete %></td>
</tr>
<% end %>
</table>
Enterprise applications usually have a role base access control (RBAC). The search results as well as the auto-completer suggestions must conform to the access control model. The recommended way to control the search results, using scoped_search, is chaining scopes. The scoped_search is a named scope (scope in rails 3), this means that it can be chained with other scopes in a natural way.
For example:
scope :my_hosts, lambda {
where("owner_id = ?", User.current.id)
}
Host.my_hosts.search_for(query)
In the above code :my_hosts is the RBAC scope (defined in lines 1 to 3). In line 4 the search is chained to the RBAC scope.
For the auto-completer there is a new way of filtering the results in a similar way. In the Model we can define a scope called :compelter_scope , this scope should accept an options hash. The options hash is a way to pass parameters from the controller, such as the current user.
For example:
Model code:
scope :completer_scope, lambda { |options|
where('owner_id = ?', options[:owner_id])
}
Controller code:
def auto_complete_search
@items = Host.complete_for(params[:search], {:owner_id => User.current.id})
render :json => @items
end
To read more about named scopes, here is a link to a detailed post on the subject: Skinny on scopes
The code for my mini_search, a simple RESTful API based search for a none-rails application, is now on github see: mini_search code.
Written by Ohad Levy, the maker of Foreman:
Sometimes when designing a database schema the designer cannot determin the exact content that will be stored in the databse in advanced. The common solution for that is designing a Key-Value pairs schema (Sometimes refered as Attribute-Value schema).
There are a few flavors to the key-value schema I this post we will look at a simple one with one table that repeats the key name for every occurence of the key, and a more complicated one that uses a seperate key table.
Simple schema:
| USER_ATTRIBUTES | ||
| USER_ID | KEY_NAME | VALUE |
| 1 | IS_GOLD_MEMBER | 1 |
| 1 | START_DATE | 2010 |
| 2 | IS_GOLD_MEMBER | 0 |
| 2 | START_DATE | 2010 |
| 3 | IS_GOLD_MEMBER | 1 |
| 3 | START_DATE | 2008 |
Advanced schema
| USER_ATTRIBUTES | ||
| USER_ID | KEY_ID | VALUE |
| 1 | 1 | 1 |
| 1 | 2 | 2010 |
| 2 | 1 | 0 |
| 2 | 2 | 2010 |
| 3 | 1 | 1 |
| 3 | 2 | 2008 |
| ATTRIBUTES_NAME | ||
| KEY_ID | KEY_NAME | |
| 1 | IS_GOLD_MEMBER | |
| 2 | START_DATE | |
In scoped search there is a special declaration for a key-value db schema. The auto-completer complete both keys and values.
The scoped search declaration for the simple schema should look as follows:
scoped_search :in => :user_attributes, :on => :value, :on_key=> :key_name, rename => :attr
For the second schema the search should be defined as follows:
scoped_search :in => :user_attributes, :on => :value, :in_key=> :attributes_name, :on_key=> :name
The auto completer will suggest the same syntax for both schema.
I was asked to add a search to an application that is written in java.
Initially I thought of implementing some of the search in java over hibernate, then I came up with another option.
All I wanted was a service that will expose the scoped search syntax and return the results as json. I tried running it as a Sinatra application, it turned out to be the following short piece of code:
require 'sinatra'
require 'sinatra/activerecord'
require 'scoped_search'
require 'json'
set :database, 'postgresql://candlepin:candlepin@localhost/candlepin'
class Subscription < ActiveRecord::Base
set_table_name 'cp_pool'
belongs_to :organization, :foreign_key => :owner_id
scoped_search :on => :productname, :complete_value => true, :rename => :product
scoped_search :on => :activesubscription, :complete_value => {:true => true, :false => false}, :rename => :active
scoped_search :on => :quantity, :complete_value => true
scoped_search :on => :contractnumber, :complete_value => true, :rename => :contract
scoped_search :on => :enddate, :complete_value => true, :rename => :expire
scoped_search :on => :startdate, :complete_value => true, :rename => :begin
scoped_search :in => :organization, :on => :displayname, :complete_value => true, :rename => :org
end
class Organization < ActiveRecord::Base
set_table_name 'cp_owner'
has_many :subscription
end
get '/subscriptions' do
res = Subscription.search_for("#{params[:search]}")
content_type :json
res.to_json
end
get '/subscriptions/auto_complete_search' do
res = Subscription.complete_for("#{params[:search]}")
content_type :json
res.to_json
end
That’s it. That is all the code I needed, in order to add search and auto-completer capabilities to a non-rails application.
Can it get any simpler then that?
Scoped search can handle many time and date formats, here are some examples of the date and time formats that scoped_search accepts:
“30 minutes ago” , ”1 hour ago” , ”2 hours ago”, Today, Yesterday
“3 weeks ago” , ”1 month ago” , ”6 days ago” , ”July 10,2011″
The date can have different separators, “10-July-2011″ is going to read the same as “10/July/2010″ or “10 July 2011″
Month name could be the full name or a three letter abbreviation, Jan will read the same as January.
Many other formats are also acceptable, however it is not recommended to use ambiguous formats such as “3/4/2011″
The date time operators are ‘=’, ‘<’ and ‘>’ the operators should be read as ‘at’, ‘before’ and ‘after’. This is how the search term interpeted:
The right hand part of a date-time condition is parsed and translated into a specific date-time, “30 minutes ago” is translated to “now – 30 minutes”. “Created > 2011-07-01 12:57:18 EDT” should be read as created after this time. Note that in the same way: Created > “30 minutes ago”, should read “created after 30 minutes ago” not “created more then 30 minutes ago”.
A search query like “published = yesterday” is translated into a period query, it will be translated in run time to the respective dates. For example, if running on July 18, it could be translated into “(published >= July 17,2011 00:00) and (published < July 18,2011 00:00)”.