Rails Forum
Rails Work - the best place to post and find great Ruby on Rails jobs.
Username
Password

You are not logged in.

New Posts in this thread
  • Index
  •  » Tutorials
  •  » HOWTO: Adding search to your rails application

#1 2006-09-08 00:24:55

daibatzu
Coach Class
Registered: 2006-06-23
Posts: 63

HOWTO: Adding search to your rails application

Sticky:: Please read the post below. I think you will find the next method even simpler.



This is a short guide on adding search to your Rails models.
For this tutorial, I'm using a model called Product and product has the following attributes: name, description, price. If you're new to rails, what this means is that you have a database table called products, and this table has the columns name, description and price (including id of course).

Now, the first thing to do is to install the ferret gem. Without this, this tutorial is moot. Open a command prompt and type:


Code :   - fold - unfold
  1. gem install ferret
Next, you will need to download the simple_search plugin for rails. The svn repositry is located at:


Code :   - fold - unfold
  1. http://julik.textdriven.com/svn/tools/rails_plugins/simple_search/
So you can get it through Subversion using the command


Code :   - fold - unfold
  1. svn co http://julik.textdriven.com/svn/tools/rails_plugins/simple_search
If you don't have Subversion you can download it from http://subversion.tigris.org/project_packages.html. After you download simple_search, place it in the vendor/plugins directory of your rails application.

Now go to app/models and open product.rb (i.e the file for your Product model or whichever model you are using). Your model should look something like this:


Code :   - fold - unfold
  1. class Product < ActiveRecord::Base
  2.     indexes_columns :title, :description, :into=>'idx'
  3.  
  4.   before_save :make_index
  5.   before_update :make_index
  6.  
  7.   def make_index
  8.     self.idx = self.index_repr
  9.   end 
  10.  
  11. end
Now there is a bit of explaining to do here. In addition to all the other columns Product has, you should also add another one called idx which has the type "TEXT" in your SQL database. This means that the columns for product should be id, name, description, price and idx (which is a TEXT column).  idx contains the searchable terms for your product. You will notice the line at the top which reads "indexes_columns :title, :description, :into => 'idx' ". Well simple_search simply takes the value of these columns and creates a searchable index which it puts into the idx column. I could well have said "indexes_columns :title, :description, :price :into => 'idx' " which will also include the price as part of our index.

I've added some filters to the product model. Before a product is saved or updated, the idx value is first determined using the 'make_index' method. This saves you the trouble of doing this in your controller each time you save or update your product.

And that is pretty much it. You can fire up script/console with "ruby script/console" for your application.
Create a new product with


Code :   - fold - unfold
  1. Product.create(:name => 'ninja_turtle', :description => 'It barks and eats your mother', :price => 20)
Then you can now type Product.find_using_term("ninja") and your recently created product should turn up. Use find_using_term to return any products that match your given term.

Last edited by daibatzu (2006-09-20 09:10:48)

Offline

 

#2 2006-09-20 05:46:30

daibatzu
Coach Class
Registered: 2006-06-23
Posts: 63

Re: HOWTO: Adding search to your rails application

Well I've found an even simpler way to implement search. This is unrelated to the iPod shuffle competition by the way (:-> hehe). Um here goes.

First you will need to download the search library by Duane Johnson and Nate McNamara. You can find it here:


Code :   - fold - unfold
  1. http://wiki.rubyonrails.org/rails/pages/TextSearch
Copy the code listing at the very end of the website above and save it as a file called search.rb. Place the search.rb file in the lib folder of your rails application. Now lets say you have a model called Product, do the following:


Code :   - fold - unfold
  1. #First load the search library in your lib folder
  2. require_dependency "search"
  3.  
  4. class Product < ActiveRecord::Base
  5.    #choose which columns to search
  6.    searches_on :all
  7.    #anything else your model does
  8. end
searches_on defines what columns I want to be made searchable. I can also say


Code :   - fold - unfold
  1. searches_on :title, :description, :list_price
to search only these columns.
Now to search my product, all I have to do is say:


Code :   - fold - unfold
  1. Product.search("ninja turtle")
If I need to search for many different queries I should say Product.search("ninja turtle or transformer") but this didn't really work for me because I don't want my users to type strange commands. So this is the search method I'm using:


Code :   - fold - unfold
  1. Product.search(params[:query].gsub(' or ',' ').split().join(' or ')
This works for me and should work for you too. This is much simpler than the previous method using simple search.

Last edited by daibatzu (2006-10-06 04:43:11)

Offline

 

#3 2006-10-06 01:08:30

tripdragon
Mechanic
Registered: 2006-07-07
Posts: 250

Re: HOWTO: Adding search to your rails application

how did you get it off of the site? I et tons and tons of errors when using copy and paste from just checking that files syntax

Offline

 

#4 2006-10-06 04:54:42

daibatzu
Coach Class
Registered: 2006-06-23
Posts: 63

Re: HOWTO: Adding search to your rails application

Mmm, not sure what happened. It was OK previously. You can use the code below but it could be come outdated. The problem I think is in the quotes and double quotes used. Something like ”. Like they're using HTML character codes in Ruby code, maybe to make it look nicer or something? not sure.


Code :   - fold - unfold
  1. # Adds search method to ActiveRecord::Base.
  2. # The query language supports the operators
  3. # (), not, and, or
  4. # Precedence in that order.
  5. # - is an alias for not.
  6. # If no operator is present, and is assumed.
  7. # Lastly, anything within double quotes is treated as
  8. # a single search term.
  9. #
  10. # For example,
  11. #  ruby rails => records where both ruby and rails appear
  12. #  "ruby on rails" => records where "ruby on rails" appears
  13. #  ruby or rails => records where ruby or rails (or both) appears
  14. #  ruby or chunky bacon => records where ruby appears or both chunky and bacon appear
  15. #  not dead or alive => records where alive appears or dead is absent
  16. #  -(ruby or rails) => records where neither ruby nor rails appears
  17. #  (ruby or rails) -"ruby on rails" => records where ruby or rails appears but not the phrase "ruby on rails"
  18. #
  19. # Query feature by Nate McNamara (nate@mcnamara.net)
  20. # Original TextSearch library by Duane Johnson.
  21. module ActiveRecord
  22.   class Base
  23.     # Allow the user to set the default searchable fields
  24.     def self.searches_on(*args)
  25.       if not args.empty? and args.first != :all
  26.         @searchable_fields = args.collect { |f| f.to_s }
  27.       end
  28.     end
  29.  
  30.     # Return the default set of fields to search on
  31.     def self.searchable_fields(tables = nil, klass = self)
  32.       # If the model has declared what it searches_on, then use that...
  33.       return @searchable_fields unless @searchable_fields.nil?
  34.  
  35.       # ... otherwise, use all text/varchar fields as the default
  36.       fields = []
  37.       tables ||= []
  38.  
  39.       string_columns = klass.columns.select { |c|
  40.         c.type == :text or c.type == :string
  41.       }
  42.       
  43.       fields = string_columns.collect { |c|
  44.         klass.table_name + "." + c.name
  45.       }
  46.  
  47.       if not tables.empty?
  48.         tables.each do |table|
  49.           klass = eval table.to_s.classify
  50.           fields += searchable_fields([], klass)
  51.         end
  52.       end
  53.  
  54.       return fields
  55.     end
  56.  
  57.     # Search the model's text and varchar fields
  58.     #   text = a set of words to search for
  59.     #   :only => an array of fields in which to search for the text;
  60.     #     default is 'all text or string columns'
  61.     #   :except => an array of fields to exclude
  62.     #     from the default searchable columns
  63.     #   :case => :sensitive or :insensitive
  64.     #   :include => an array of tables to include in the joins.  Fields that
  65.     #     have searchable text will automatically be included in the default
  66.     #     set of :search_columns.
  67.     #   :join_include => an array of tables to include in the joins, but only
  68.     #     for joining. (Searchable fields will not automatically be included.)
  69.     #   :conditions => a string of additional conditions (constraints)
  70.     #   :offset => paging offset (integer)
  71.     #   :limit => number of rows to return (integer)
  72.     #   :order => sort order (order_by SQL snippet)
  73.     def self.search(text = nil, options = {})
  74.       fields = options[:only] || searchable_fields(options[:include])
  75.       if options[:except]
  76.         fields -= options[:except]
  77.       end
  78.  
  79.       unless options[:case] == :sensitive
  80.         text.downcase!
  81.         fields.map! { |f| "lower(#{f})" }
  82.       end
  83.  
  84.       condition_list = []
  85.       unless text.nil?
  86.         condition_list << build_text_condition(fields, text)
  87.       end
  88.       if options[:conditions]
  89.         condition_list << "#{options[:conditions]}"
  90.       end
  91.       conditions = condition_list.join " AND "
  92.  
  93.       includes = (options[:include] || []) + (options[:join_include] || [])
  94.  
  95.       find(:all,
  96.            :include => includes.empty? ? nil : includes,
  97.            :conditions => conditions.empty? ? nil : conditions,
  98.            :offset => options[:offset],
  99.            :limit => options[:limit],
  100.            :order => options[:order])
  101.     end
  102.  
  103.  
  104.  
  105.     private
  106.  
  107.     # A chunk is a string of non-whitespace,
  108.     # except that anything inside double quotes
  109.     # is a chunk, including whitespace
  110.     def self.make_chunks(s)
  111.       chunks = []
  112.       while s.length > 0
  113.         next_interesting_index = (s =~ /\s|\"/)
  114.         if next_interesting_index
  115.           if next_interesting_index > 0
  116.             chunks << s[0...next_interesting_index]
  117.             s = s[next_interesting_index..-1]
  118.           else
  119.             if s =~ /^\"/
  120.               s = s[1..-1]
  121.               next_interesting_index = (s =~ /[\"]/)
  122.               if next_interesting_index
  123.                 chunks << s[0...next_interesting_index]
  124.                 s = s[next_interesting_index+1..-1]
  125.               elsif s.length > 0
  126.                 chunks << s
  127.                 s = ''
  128.               end
  129.             else
  130.               next_interesting_index = (s =~ /\S/)
  131.               s = s[next_interesting_index..-1]
  132.             end
  133.           end
  134.         else
  135.           chunks << s
  136.           s = ''
  137.         end
  138.       end
  139.  
  140.       chunks
  141.     end
  142.  
  143.     def self.process_chunk(chunk)
  144.       case chunk
  145.       when /^-/
  146.         if chunk.length == 1
  147.           [:not]
  148.         else
  149.           [:not, *process_chunk(chunk[1..-1])]
  150.         end
  151.       when /^\+/
  152.         if chunk.length == 1
  153.           [:and]
  154.         else
  155.           [:and, *process_chunk(chunk[1..-1])]
  156.         end
  157.       when /^\(.*\)$/
  158.         if chunk.length == 2
  159.           [:left_paren, :right_paren]
  160.        else          
  161. [:left_paren].concat(process_chunk(chunk[1..-2])) << :right_paren
  162.         end
  163.       when /^\(/
  164.         if chunk.length == 1
  165.           [:left_paren]
  166.         else
  167.           [:left_paren].concat(process_chunk(chunk[1..-1]))
  168.         end
  169.       when /\)$/
  170.         if chunk.length == 1
  171.           [:right_paren]
  172.         else
  173.           process_chunk(chunk[0..-2]) << :right_paren
  174.         end
  175.       when 'and'
  176.         [:and]
  177.       when 'or'
  178.         [:or]
  179.       when 'not'
  180.         [:not]
  181.       else
  182.         [chunk]
  183.       end
  184.     end
  185.  
  186.     def self.lex(s)
  187.       tokens = []
  188.  
  189.       make_chunks(s).each { |chunk|
  190.         tokens.concat(process_chunk(chunk))
  191.       }
  192.       
  193.       tokens
  194.     end
  195.  
  196.     def self.parse_paren_expr(tokens)
  197.       expr_tokens = []
  198.       while !tokens.empty? && tokens[0] != :right_paren
  199.         expr_tokens << tokens.shift
  200.       end
  201.  
  202.       if !tokens.empty?
  203.         tokens.shift
  204.       end
  205.       
  206.       parse_expr(expr_tokens)
  207.     end
  208.  
  209.     def self.parse_term(tokens)
  210.       if tokens.empty?
  211.         return ''
  212.       end
  213.  
  214.       token = tokens.shift
  215.       case token
  216.       when :not
  217.           [:not, parse_term(tokens)]
  218.       when :left_paren
  219.         parse_paren_expr(tokens)
  220.       when :right_paren
  221.         '' # skip bogus token
  222.       when :and
  223.           '' # skip bogus token
  224.       when :or
  225.           '' # skip bogus token
  226.       else
  227.         token
  228.       end
  229.     end
  230.  
  231.     def self.parse_and_expr(tokens, operand)
  232.       if (tokens[0] == :and)
  233.         tokens.shift
  234.       end
  235.       # Even if :and is missing, :and is implicit
  236.       [:and, operand, parse_term(tokens)]
  237.     end
  238.  
  239.     def self.parse_or_expr(tokens, operand)
  240.       if (tokens[0] == :or)
  241.         tokens.shift
  242.         [:or, operand, parse_expr(tokens)]
  243.       else
  244.         parse_and_expr(tokens, operand)
  245.       end
  246.     end
  247.  
  248.     def self.parse_expr(tokens)
  249.       if tokens.empty?
  250.         return ''
  251.       end
  252.  
  253.       expr = parse_term(tokens)
  254.       while !tokens.empty?
  255.         expr = parse_or_expr(tokens, expr)
  256.       end
  257.  
  258.       expr
  259.     end
  260.  
  261.     def self.parse_tokens(tokens)
  262.       tree = parse_expr(tokens)
  263.       tree.kind_of?(Array)? tree : [tree]
  264.     end
  265.  
  266.     def self.parse(text)
  267.       parse_tokens(lex(text))
  268.     end
  269.  
  270.     def self.apply_demorgans(tree)
  271.       if tree == []
  272.         return []
  273.       end
  274.       
  275.       token = tree.kind_of?(Array)? tree[0] : tree
  276.       case token
  277.       when :not
  278.           if (tree[1].kind_of?(Array))
  279.             subtree = tree[1]
  280.             if subtree[0] == :and
  281.                 [:or,
  282.                  apply_demorgans([:not, subtree[1]]),
  283.                  apply_demorgans([:not, subtree[2]])]
  284.             elsif tree[1][0] == :or
  285.                 [:and,
  286.                  apply_demorgans([:not, subtree[1]]),
  287.                  apply_demorgans([:not, subtree[2]])]
  288.             else
  289.               # assert tree[1][0] == :not
  290.               apply_demorgans(subtree[1])
  291.             end
  292.           else
  293.             tree
  294.           end
  295.       when :and
  296.           [:and, apply_demorgans(tree[1]), apply_demorgans(tree[2])]
  297.       when :or
  298.           [:or, apply_demorgans(tree[1]), apply_demorgans(tree[2])]
  299.       else
  300.         tree
  301.       end
  302.     end
  303.  
  304.     def self.demorganize(tree)
  305.       result = apply_demorgans(tree)
  306.       result.kind_of?(Array)? result : [result]
  307.     end
  308.  
  309.     def self.sql_escape(s)
  310.       s.gsub('%', '\%').gsub('_', '\_')
  311.     end
  312.  
  313.     def self.compound_tc(fields, tree)
  314.       '(' +
  315.         build_tc_from_tree(fields, tree[1]) +
  316.         ' ' + tree[0].to_s + ' ' +
  317.         build_tc_from_tree(fields, tree[2]) +
  318.         ')'
  319.     end
  320.  
  321.     def self.build_tc_from_tree(fields, tree)
  322.       token = tree.kind_of?(Array)? tree[0] : tree
  323.       case token
  324.       when :and
  325.           compound_tc(fields, tree)
  326.       when :or
  327.           compound_tc(fields, tree)
  328.       when :not
  329.           # assert tree[1].kind_of?(String)
  330.         "(" +
  331.         fields.map { |f|
  332.           "(#{f} is null or #{f} not like #{sanitize('%'+sql_escape(tree[1])+'%')})"
  333.         }.join(" and ") +
  334.           ")"
  335.       else
  336.         "(" +
  337.         fields.map { |f|
  338.           "#{f} like #{sanitize('%'+sql_escape(token)+'%')}"
  339.         }.join(" or ") +
  340.           ")"
  341.       end
  342.     end
  343.  
  344.     def self.build_text_condition(fields, text)
  345.       build_tc_from_tree(fields, demorganize(parse(text)))
  346.     end
  347.   end
  348. end
Arghh.. how do I get rid of line numbers.

Last edited by daibatzu (2006-10-06 04:55:50)

Offline

 

#5 2006-10-24 12:34:55

daibatzu
Coach Class
Registered: 2006-06-23
Posts: 63

Re: HOWTO: Adding search to your rails application

Ok I got an email asking how to build the search form for this. This is really quite simple and there a number of ways you can do it (including with AJAX). Anyway, here goes.

First, you'll need to add a search action to your controller. This could be like this:


Code :   - fold - unfold
  1. def search
  2.   if params[:query]
  3.     @products = Product.search(params[:query])
  4.   else
  5.     @products = []
  6.   end
  7. end
Next, on any of the views you can add this bit of code. This is basically a search box.


Code :   - fold - unfold
  1. <form action='/search'>
  2. <input name="query" type="text" />
  3. <input type='submit' value='Search' />
  4. </form>
Then you create a view called search for your controller. Not the best view but you can use whatever you like and modify it to your liking. So the code below is your search.rhtml.


Code :   - fold - unfold
  1. <% if @products.empty? %>
  2.     <span>There were no results for your query</span><br/><br/>
  3. <% else %>
  4.  
  5. <% for product in @products %>
  6. Result: <% product.title %>
  7. <% end %>
  8.  
  9. <% end %>
So, just a form which accesses the search action in your controller and sends the parameter called query to it. Quite simple.

Last edited by daibatzu (2006-10-24 12:38:10)

Offline

 

#6 2006-12-05 12:17:38

tripdragon
Mechanic
Registered: 2006-07-07
Posts: 250

Re: HOWTO: Adding search to your rails application

off hand would you know how to paginate this??

Offline

 

#7 2006-12-18 01:07:12

tortoise
Mechanic
From: West Virginia
Registered: 2006-11-01
Posts: 286
Website

Re: HOWTO: Adding search to your rails application

paginating is pretty simple. Somewhere on the rails wiki I found someone had written a paginate_collection method. Add it to application.rb


Code :  ruby - fold - unfold
  1.   def paginate_collection(collection, options = {})
  2.     default_options = {:per_page => 10, :page => 1}
  3.     options = default_options.merge options
  4.     
  5.     pages = Paginator.new self, collection.size, options[:per_page], options[:page]
  6.     first = pages.current.offset
  7.     last = [first + options[:per_page], collection.size].min
  8.     slice = collection[first...last]
  9.     return [pages, slice]    
  10.   end 
call it like this in the controller


Code :  ruby - fold - unfold
  1. def search
  2.    if params[:query]
  3.       product_collection = Product.search(params[:query])
  4.    else
  5.        product_collection = []
  6.    end
  7.  
  8.    @product_pages, @products = paginate_collection product_collection, :page => params[:page]
  9. end 
then in the view, paginate like this (I separated this stuff into a partial so I can paginate easily)


Code :  ruby - fold - unfold
  1. <div class="paginate_controls"
  2.   <% if header %>
  3.   Showing <%= @product_pages.current.first_item %>
  4.     to <%= @product_pages.current.last_item %>
  5.     of <%= @product_pages.item_count %><br/>
  6.     <% end %>
  7.        <%= link_to(h('< Previous'), {:page => @product_pages.current.previous})  + " | " if @product_pages.current.previous %>
  8.    <%= pagination_links(@product_pages, :window_size => 4) %>
  9.    <%= " | " + link_to(h('Next >'), {:page => @product_pages.current.next}) if @product_pages.current.next %>
  10. </div>
  11.  
  12. <div class="search_results">
  13. <% @products.each do |product|
  14.    <%= product.name %>
  15. <% end %>
  16. </div>

Offline

 

#8 2006-12-21 22:28:17

tortoise
Mechanic
From: West Virginia
Registered: 2006-11-01
Posts: 286
Website

Re: HOWTO: Adding search to your rails application

I should note that the above paginating does not work for searches, because when you go to page two the search query defaults to nothing and you lose all your search results. Haven't figured out a clean solution to that just yet.

Offline

 

#9 2007-01-11 19:24:03

paron
Ticketholder
Registered: 2006-08-11
Posts: 7

Re: HOWTO: Adding search to your rails application

Ok, I posted a tutorial on paginating search results (with semantically-meaningful labels, yet) at http://wiki.rubyonrails.com/rails/pages … teSearches Let me know what you think.

Last edited by paron (2007-01-24 13:08:57)

Offline

 

#10 2007-01-23 11:45:28

nazrul
Ticketholder
Registered: 2006-11-11
Posts: 8

Re: HOWTO: Adding search to your rails application

Great tutorial! How to search by category ? I mean if I'm developing my school library website, how I want to let the students search by by Category, Year, Author ? Like this forum search options. Thanks!

Offline

 

#11 2007-01-24 12:47:07

paron
Ticketholder
Registered: 2006-08-11
Posts: 7

Re: HOWTO: Adding search to your rails application

In the tutorial, the model provides two methods, "search_name" and "search_all."

For what you're trying to do, I would just provide one search method -- "search_author". I suspect search is overkill for the other two conditions, "category" and "year".

There's probably no need to "search" categories; you already know what they are. It's more of a filter, where you might pass an argument like ":conditions=>["category = 'fiction']. It's the same with year; you really don't need to 'search' it -- you're just going to filter for "1987" or ">1994  AND <2004". You're not going to search in the "year" field for terms like "Einstein" AND "math", so it isn't a search, per se.

Anyway, once your model returns its array of results, you could paginate and semantically label the pages with author data, so that the results are neatly broken into pages labeled "Aaron to Byron", "Camille to Dante", etc.

If it's a multi-year search, you could label them with "Able, 1883 to Anderson, 1994" and "Astarte, 1776 to Ballinger, 1923" or the other way around, like: "1810, Pursley to 1850, Bedinger" and "1851, Sydney to 1876, Lincoln" depending on your ordering of the results.

Is that what you're asking?

Ron

Last edited by paron (2007-01-24 12:47:43)

Offline

 

#12 2007-09-14 19:11:37

seandick
Ticketholder
Registered: 2007-08-15
Posts: 1

Re: HOWTO: Adding search to your rails application

remove line numbers in vim: %s/^.\{,3}\d\{,3}\W//g

Offline

 

#13 2008-03-03 19:52:06

steveroot
Ticketholder
Registered: 2008-03-03
Posts: 1

Re: HOWTO: Adding search to your rails application

RoR newb here,
In suggested search.rhtml I found it didn't display the results. Eventually I found I had to change
Result: <% product.title %>

into

Result: <%= product.title %>

Offline

 

#14 2008-03-19 14:41:35

kingworks
Passenger
From: VA
Registered: 2008-02-19
Posts: 27
Website

Re: HOWTO: Adding search to your rails application

I hope this thread hasn't been dead and buried too long.

I am having trouble with the search.rb (the second version presented) file generating a syntax error.  The problem is identified as being line 245:


Code :   - fold - unfold
  1. 244   def self.parse_and_expr(tokens, operand)
  2. 245     if (tokens[0]  :and)
  3. 246          tokens.shift
  4. 247     end
  5. 248   # Even if :and is missing, :and is implicit
  6. 249    [:and, operand, parse_term(tokens)]
  7. 250    end
But line 253, which is similar, is identified, too:


Code :   - fold - unfold
  1. 252   def self.parse_or_expr(tokens, operand)
  2. 253      if (tokens[0]  :or)
  3. 254           tokens.shift
  4. 255           [:or, operand, parse_expr(tokens)]
  5. 256         else
  6. 257           parse_and_expr(tokens, operand)
  7. 258         end
  8. 259      end
Any help is greatly appreciated.

EDIT: It appears that every instance of ":and" and "yikesr" is generating a syntax error - also appear on lines 293 and 297 of search.rb, respectively.

Last edited by kingworks (2008-03-19 14:46:43)

Offline

 

#15 2008-10-10 08:33:13

jerinantony1
Ticketholder
From: bangalore, india
Registered: 2008-10-07
Posts: 2

Re: HOWTO: Adding search to your rails application

hi all i hv tried out this tutorial but getting an error

-->"undefined method `search' for #<Class:0x467cd08>"

c:/ruby/lib/ruby/gems/1.8/gems/activerecord-2.1.1/lib/active_record/base.rb:1672:in `method_missing'
app/controllers/book_controller.rb:65:in `find'

Offline

 
  • Index
  •  » Tutorials
  •  » HOWTO: Adding search to your rails application

Board footer

Powered by PunBB
© Copyright 2002–2005 Rickard Andersson