原创作者: hideto   阅读:2639次   评论:0条   更新时间:2011-05-26    
Ferret是Ruby的文本搜索引擎,它基于Apache Lucene

安装Ferret非常简单:
gem install ferret


Ferret是一堆C代码的Ruby代码封装,Ferret是针对Ruby的而不是RoR的
Acts As Ferret则是针对RoR的

我们有两种方式安装Acts As Ferret:
1,以gem方式安装
gem install acts_as_ferret

然后在environment.rb里添加
require 'acts_as_ferret'

2,以plugin方式安装
ruby script/plugin install svn://projects.jkraemer.net/acts_as_ferret/tags/stable/acts_as_ferret

(虽然推荐以插件方式安装acts_as_ferret,但貌似这样安装不起作用,svn地址改为acts_as_ferret也不行,再议)

先看一个简单的例子,我们先给需要做index的model加上如下代码:
class Member < ActiveRecord::Base
  acts_as_ferret :field => [:first_name, :last_name]
end

然后假如我们做如下search;
members = Member.find_id_by_contents("Gregg")

这将发生如下事情:
1,在Rails程序目录下创建/index/development/member子目录,所有的index文件将在这里创建
2,Members的first/last name将加入到该index,以后每次add/update/delete一个member都会增量更新index
如果你需要重新生成index,只需删除相应的文件目录并重启服务器,下次search时就会重新生成index
3,ActsAsFerret将会在我们的index上调用Ferret的search_each方法
4,我们将得到10条如下形式的结果:
members = [
  {:model => "Member", :id => "4", :score => "1.0"},
  {:model => "Member", :id => "4", :score => "0.93211"},
  {:model => "Member", :id => "4", :score => "0.32212"},
  ...
]

我们得到每个member的id和search score
即使有超过10条的结果,我们默认将只能得到10条返回的结果

Options
1,offset,默认为0
2,limit,默认为10,:all将返回所有结果
3,sort

如果我们需要查询结果的数量
total_results = Member.total_hits("Gregg")

或者这样做
results = []
total_results = Member.find_id_by_contents("Gregg") { |result|
  results.push result
}


如果我们需要查询结果为model
results = []
total_results = Member.find_id_by_contents("Gregg") { |result|
  results.push Member.find(result[:id])
}

我们有更好的办法,find_by_contents
@results = Member.find_by_contents("Gregg")

这将做如下事情:
1,首先使用find_id_by_contents得到ids
2,使用返回的ids来查询Model
3,返回ActsAsFerret::SearchResults对象Array(可以认为是ActiveRecord对象,但是多一些额外的特性)
我们可以做如下的事情
members = Member.find_by_contents("Gregg")

# It gives us total hits!
puts "Total hits = #{members.total_hits}"
for member in members
  puts "#{member.first_name} #{member.last_name}"

  # And the search Score!
  puts "Search Score = #{member.ferret_score}"
end

注意total_hits和ferret_score就是额外的特性

对搜索结果分页
1,对你的model加上如下方法
def self.full_text_search(q, options = {})
  return nil if q.nil? or q==""
  default_options = {:limit => 10, :page => 1}
  options = default_options.merge options

  # get the offset based on what page we're on
  options[:offset] = options[:limit] * (options.delete(:page)to_i-1)

  # now do the query with our options
  results = Member.find_by_contents(q, options)
  return [results.total_hits, results]
end

修改application.rb
def pages_for(size, options = {})
  default_options = {:per_page => 10}
  options = default_options.merge options
  pages = Paginator.new self, size, options[:per_page], (params[:page]||1)
  return pages
end

然后修改controller
def search
  @query = params[:query]
  @total, @members = Member.full_text_search(@query, :page => (params[:page]||1))
  @pages = pages_for(@total)
end

在view中的代码
<%= link_to 'Previous page', { :page => @pages.current.previous, :query => @query } if @pages.current.previous %>
<%= pagination_links(@pages, :params => { :query => @query }) %>
<%= link_to 'Next page', { :page => @pages.current.next, :query => @query } if @pages.current.next %>


查询字符串
1,搜索"Gregg Pollack"将返回在ANY域中以ANY顺序排列的包含"Gregg""Pollack"的结果
2,搜索"Gregg OR Pollack"将返回包含"Gregg""Pollack"的结果
3,搜索"Gregg~"模糊查询,将返回包含"Gregg"的结果
4,搜索"first_name:Gregg"将返回first name为"Gregg"的结果
5,搜索"+first_name:Gregg -last_name:Jones"将返回first name为"Gregg"并且last name不是"Jones"的结果
更复杂的查询条件参考Apache Lucene Parser Syntax

搜索多个表
class Book < ActiveRecord::Base
  acts_as_ferret :fields => [:title, :author_name]

  def author_name
    return "#{self.author.first_name} #{self.author.last_name}"
  end
end

也就是说我们可以搜索model方法返回的任何东西,甚至是tags
class Book < ActiveRecord::Base
  acts_as_taggable
  acts_as_ferret :fields => [:title, :tags_with_spaces]

  def tags_with_spaces
    return self.tag_names.join(" ")
  end
end


排序
假如我们需要对title搜索并对title排序,但是排序的field要求不能被index和search,我们可以这样做
acts_as_ferret :fields => {
  :title => {},
  :tags_with_spaces => {},
  :title_for_sort => {:index => :untokenized}
}

def title_for_sort
  return self.title
end

然后我们可以在controller里按title排序搜索了
s = Ferret::Search::SortField.new(:title_for_sort, :reverse => false)
@total, @members = Member.full_text_search(@query, {:page => (params[:page]||1), :sort => s}


存储数据
默认情况下acts_as_ferret只对数据做index而不存储数据,如果数据很小,我们可以这样做来在index里存储数据来加快search数据
acts_as_ferret :field => {
  :title => {:store => :yes},
  :author_name => {:store => :yes}
}

我们给model添加如下search方法
def self.find_storage_by_contents(query, options = {})
  # Get the index that acts_as_ferret created for us
  index = self.ferret_index
  results = []

  @ search_each is the core search function from Ferret, which Acts_as_ferret hides
  total_hits = index.search_each(query, options) do |doc, score|
    result = {}

    # Sotre each field in a hash which we can reference in our views
    result[:name] = index[doc][:name]
    result[:author_name] = idnex[doc][:author_name]

    # We can even put the score in the hash, nice!
    result[:score] = score

    results.push result
  end
  return block_given? ? total_hits : [ttal_hit, results]
end

这样我们根本就不用接触数据库就可以查询数据,view代码
<% @results.each_with_index do |result, index| %>
  <%= index %>. <%= result[:name] %> by
    <%= result[:author_name] %> <br/>
    Score: <%= result[:score] %>
<% end %>


Highlighting
我们来看看怎样用ferret做出Google搜索结果bold关键字的效果
前提是上面所说存储搜索的fields
我们修改上面的search方法
def find_storage_by_contents(query, options = {})
  index = self.ferret_index # Get the index that acts_as_ferret created for us
  results = []

  # search_each is the core search function from Ferret, which Acts_as_ferret hides
  total_hits = index.search_each(query, options) do |doc, score|
    result = {}

    # Store each field in a hash which we can reference in our views
    result[:name] = index.highlight(query, doc,
                    :field => :name,
                    :pre_tag => "<strong>",
                    :post_tag => "</strong>",
                    :num_excerpts => 1)
    result[:author_name] = index.highlight(query, doc,
                    :field => :author_name,
                    :pre_tag => "<strong>",
                    :post_tag => "</strong>",
                    :num_excerpts => 1)
    result[:score] = score # We can even put the score in the hash, nice!

    results.push result
  end
  return block_given? ? total_hits : [total_hits, results]
end


使用Boost
如果我们希望对title的搜索结果的score要比author的搜索结果的score稍高,我们可以使用boost参数
acts_as_ferret :fields => {
  :title => {:boost => 2}
  :author => {:boost => 0}
}

但是boost参数只能提高score,而不能分开title和author的搜索结果

Production环境下使用
由于性能问题,Production环境下最好以DRb Server的形式运行

注:本文参考Acts_As_Ferret Tutorial
评论 共 0 条 请登录后发表评论

发表评论

您还没有登录,请您登录后再发表评论

文章信息

Global site tag (gtag.js) - Google Analytics