views:

285

answers:

4

I have built a blog application w/ ruby on rails and I am trying to implement a search feature. The blog application allows for users to tag posts. The tags are created in their own table and belong_to :post. When a tag is created, so is a record in the tag table where the name of the tag is tag_name and associated by post_id. Tags are strings.

I am trying to allow a user to search for any word tag_name in any order. Here is what I mean. Lets say a particular post has a tag that is 'ruby code controller'. In my current search feature, that tag will be found if the user searches for 'ruby', 'ruby code', or 'ruby code controller'. It will not be found if the user types in 'ruby controller'.

Essentially what I am saying is that I would like each word entered in the search to be searched for, not necessarily the 'string' that is entered into the search.

I have been experimenting with providing multiple textfields to allow the user to type in multiple words, and also have been playing around with the code below, but can't seem to accomplish the above. I am new to ruby and rails so sorry if this is an obvious question and prior to installing a gem or plugin I thought I would check to see if there was a simple fix. Here is my code:

View: /views/tags/index.html.erb

<% form_tag tags_path, :method => 'get' do %>
    <p>
      <%= text_field_tag :search, params[:search], :class => "textfield-search" %>
      <%= submit_tag "Search", :name => nil, :class => "search-button" %>
    </p>
  <% end %>

TagsController

 def index
    @tags = Tag.search(params[:search]).paginate :page => params[:page], :per_page => 5
    @tagsearch = Tag.search(params[:search])
    @tag_counts = Tag.count(:group => :tag_name, 
       :order => 'count_all DESC', :limit => 100)

    respond_to do |format|
      format.html # index.html.erb
      format.xml  { render :xml => @tags }
    end
  end

Tag Model

class Tag < ActiveRecord::Base
  belongs_to :post
  validates_length_of :tag_name, :maximum=>42
  validates_presence_of :tag_name

  def self.search(search)
    if search
      find(:all, :order => "created_at DESC", :conditions => ['tag_name LIKE ?', "%#{search}%"])
    else
      find(:all, :order => "created_at DESC")
    end
  end

end
A: 

You can try to set up ferret, or if you are really bend on just using rails, try this:

# Break the search string into words
words = params[:search].blank? ? [] : params[:search].split(' ')

conditions = [[]] # Why this way? You'll know soon
words.each do |word|
  conditions[0] << ["tag_name LIKE ?"]
  conditions << "%#{word}%"
end
conditions[0] = conditions.first.join(" OR ") # Converts condition string to include " OR " easily ;-)

# Proceed to find using `:conditions => conditions` in your find

hope this helps =)

Staelen
+2  A: 

If I read your problem correctly, you want to return a row if the tag names for the row matches one of the words passed in the query string.

You can rewrite your search method as follows:

def self.search(search)
  all :conditions =>  (search ? { :tag_name => search.split} : [])
end

If you need partial matching then do the following:

def self.search(str)
  return [] if str.blank?
  cond_text   = str.split.map{|w| "tag_name LIKE ? "}.join(" OR ")
  cond_values = str.split.map{|w| "%#{w}%"}
  all(:conditions =>  (str ? [cond_text, *cond_values] : []))
end

Edit 1 If you want pass multiple search strings then:

def self.search(*args)
  return [] if args.blank?
  cond_text, cond_values = [], []
  args.each do |str|
    next if str.blank?  
    cond_text << "( %s )" % str.split.map{|w| "tag_name LIKE ? "}.join(" OR ")
    cond_values.concat(str.split.map{|w| "%#{w}%"})
  end
  all :conditions =>  [cond_text.join(" AND "), *cond_values]
end

Now you can make calls such as:

Tag.search("Ruby On Rails")

Tag.search("Ruby On Rails", "Houston")

Tag.search("Ruby On Rails", "Houston", "TX")

Tag.search("Ruby On Rails", "Houston", "TX", "Blah")

Tag.search("Ruby On Rails", "Houston", "TX", "Blah", ....) # n parameters

Caveat:

The wild card LIKE searches are not very efficient(as they don't use the index). You should consider using Sphinx (via ThinkingSphinx) OR Solr(via SunSpot) if you have lot of data.

KandadaBoggu
I just tried your second code block and got the following error: SQLite3::SQLException: no such column: controller: SELECT * FROM "tags" WHERE (ruby OR controller)
bgadoci
for search: ruby controller
bgadoci
There was bug in my code. Updated the code take a look.
KandadaBoggu
perfect. That worked great. Next question (while your thinking about this) is there a way to use a second textfield to essentially say "search for that 'first tag' but also make sure it has 'this tag'." This is likely a more complicated question but thought I would throw it out here before making a new question.
bgadoci
Updated the answer.
KandadaBoggu
Ok, I see what you are saying and that is what I want to do. The Edit 1 code you provided doesn't seem to work though. I don't get an error message, it just doesn't seem to register the submit.
bgadoci
In the code I had method and variable names `search`. That is the reason why it didn't work. I have fixed the code. Take a look.
KandadaBoggu
Thanks. Seems like this is still returning any post that has that particular tag, not necessarily only the posts that have both tags. No problem though. Close enough. I will look into searchlogic if I want to go any further. Sounds like that is a easier way to go. Just trying to learn as much as I can before going to plugins/gems. Thanks again.
bgadoci
A: 

Sounds like you need a full text search. The best search integration right now is with Sphinx and the Thinking_Sphinx plugin. I have used it on several projects and it's super easy to setup.

You do need to install sphinx on your host so if you are using a shared host that could present some issues.

You could also use full text search in a MyISAM MySQL database, but performance on that is pretty poor.

Once you have your sphinx installed you just put what you want to index in your model and call model.search. The results will be a list of model objects. It supports will_paginate as well.

Bill Leeper
A: 

I'd suggest looking at Searchlogic if you don't want to use a separate fulltext search engine (Ferret, Sphinx, etc). It makes simple searches extremely easy, although you may not want to use it in a public facing area without lots of testing.

Also check out the Railscast on it: http://railscasts.com/episodes/176-searchlogic

dunedain289