发新话题
打印

[Rails插件] ferret+ MeCabでTokenize

ferret+ MeCabでTokenize

http://rubyforge.org/projects/actsasferret/

http://rubyforge.org/projects/ferret/



ferretというRubyのテキスト検索エンジンライブラリを見つけました。

http://ferret.davebalmain.com/trac/

主要部分はCで書かれていて高速なようです。またチュートリアルを見る限りでは簡単に使えそうです。ただ、日本語用のTokenizerは用意されていないようなので、MeCabを利用して簡単なものを作成しながら試してみました。

% gem install ferret

チュートリアルに従って一通り試してみたところで、MecabAnalyzer、MecabTokenizerを作りました。

http://ferret.davebalmain.com/api/files/TUTORIAL.html

ソースは以下の通りです。
复制内容到剪贴板
代码:
#!/usr/bin/ruby -Ku

require 'rubygems'
require 'ferret'
require 'MeCab'
include Ferret

class MecabAnalyzer
  def initialize(use_surface=false)
    @use_surface = use_surface
  end
  def token_stream(field, str)
    return MecabTokenizer.new(str, @use_surface)
  end
end

class MecabTokenizer
  def initialize(str, use_surface=false)
    @mecab = MeCab::Tagger.new
    self.text = str
    @use_surface = use_surface
  end
  def text=(str)
    @text = str
    @n = @mecab.parseToNode(text)
    @n = @n.next # skip EOS
    @pos = 0
  end
  attr_reader :text
  def next
    return nil if @n.stat == MeCab::MECAB_EOS_NODE
    features = @n.feature.split(/,/)
    t = @use_surface ? @n.surface : features[6]
    token = Analysis::Token.new(t, @pos, @pos+@n.rlength)
    @pos += @n.rlength
    @n = @n.next
    return token
  end
end
以下、動作確認用のコード。
复制内容到剪贴板
代码:
index = Index::Index.new(:analyzer => MecabAnalyzer.new)

index << {:id=>0, :content=>"寒くて我慢できない。"}
index << {:id=>1, :content=>"今日は寒い一日になりそうです。\nそんな予感。"}
index << {:id=>2, :content=>"今日もしないとね。"}

# check tokenizer
mt = MecabTokenizer.new(index[0].load[:content])
while t = mt.next
  p t
end

query = "寒く"
puts "query: #{query}"
index.search_each(query) do |id,score|
  p [id, score, index[id].load]
  puts index.highlight(query, id,
                       :field => :content,
                       :pre_tag => "[",
                       :post_tag => "]",
                       :excerpt_length => :all)
end
実行結果は以下のようになりました。「寒い」と「寒く」が同一視されていることがわかります。
复制内容到剪贴板
代码:
dara@wasabi:~/ferret$ ruby ferret_test.rb
token["寒い":0:6:1]
token["て":6:9:1]
token["我慢":9:15:1]
token["できる":15:21:1]
token["ない":21:27:1]
token["。":27:30:1]
query: 寒く
[0, 0.0806559845805168, {:content=>"寒くて我慢できない。", :id=>"0"}]
[寒く]て我慢できない。
[1, 0.0537706576287746, {:content=>"今日は寒い一日になりそうです。\nそんな予感。", :id=>"1"}]
今日は[寒い]一日になりそうです。
そんな予感。
次の段階としては、品詞に基づいて抽出する単語を任意に選択できるようにしたいところです。しかし、TokenizerとFilterが分離されているため、形態素解析の結果を利用してフィルタを行うためには、

    * Tokenizerにフィルタを組み込む
    * Tokenを継承したMecabTokenに品詞情報を付加し、MecabToken用のフィルタを用意する

などの工夫が必要になりそうです。とはいえ、ちょっとした応用ならばMecabTokenizer#nextにコチョコチョと書き加えれば済みそうですので、今回はここまでにしておきます。

Cで書いた方がよかろうという話もありますよね。


http://d.hatena.ne.jp/darashi/20061205/1165339208

TOP

原文地址: http://www.railsenvy.com/2007/2/19/acts-as-ferret-tutorial

译者:目前在为公司开发搜人系统,需要用到全文检索。在rails开发中找了一下相关内容,大多文章都会涉及到这篇教程的内容,根据教程的内容,在ubuntu的调试,已经顺利的解决了初步的问题。

这篇文章值得翻译,因为内容涉及很多,虽然找到中文的学习笔记, 不过读来觉得不过瘾。还有更重要的原因,就是翻译的时候更使自己沉静到技术的研究中,收获更多。

同时也感谢http://www.railsenvy.com/ ,提供了其他优秀的教程,我已经做过翻译,这个过程中学到相当多的东西。

Ruby on Rails中的Rake教程(Rake如何把我灌醉!)

Ruby on Rails缓存 第一部分 第二部分

———————————————————————————-

如果你想为你的rails应用添加快速的全文检索,或者你对mysql的全文检索能力不满意,又或者你想使工作更加灵活,这个教程将告诉你如何去做。
什么是Ferret

Ferret,是用ruby开发的基于Apache Lucene的全文检索引擎库, 安装Ferret:

    gem install ferret

在ferret的代码中,只有少量的ruby代码,大部分是c代码。这里有Ferret API,并在其中提供了一份教程Ferret Tutorial。
Acts_As_Ferret

Ferret是ruby库,在rails中如果想使用,就要用到Jens Kramer 的Acts As Ferret了,它提供了简单的接口,我们可以快速的创建复杂的搜索索引。

在你的rails中,以插件的形式安装acts_as_ferret

    ruby script/plugin install svn://projects.jkraemer.net/acts_as_ferret/tags/stable/acts_as_ferret

基本用法

下面我们从一个简单的用法开始。

首先需要在你的model中添加需要被索引 的项目

    class Member < ActiveRecord::Base
    acts_as_ferret :fields => [:first_name, :last_name]
    end

在这里我们是将特定的名字进行索引,搜索的时候将返回这些索引的符合结果。
做一个简单的搜索实例

Acts as Ferret为你的ActiveRecord model增加了搜索方法, 和其他的教程不同,我们使用find_id_by_contents:

当我们调用

    total_results, members = Member.find_id_by_contents(”Gregg”)

那么:

   1. 在我们的rails应用中创建了一个 /index/development/member 文件夹,索引文件将会保留在这里。
   2. 所有Member model的查询都会被保存,并且对first_name 和 last_name进行索引。每当对Member进行 add/update/delete 操作时,索引就会自动更新。如果你需要重新生成一个索引,只需要删除对应的文件夹,重启服务,这样在下次进行表查询时将会重新建立索引。
   3. acts_as_ferret会调用ferret的 Search_Each 方法处理索引
   4. 我们得到一些返回数据

    members = [
    {:model => “Member”, :id => “4″, :score => “1.0″},
    {:model => “Member”, :id => “21″, :score => “0.93211″},
    {:model => “Member”, :id => “27″, :score => “0.32212″}
    ]

我们得到了前10条记录(当然我只显示了3条),包括了每条记录的id和搜索得分(search scores)。

但是,当返回记录有40条的时候,我们仍将得到10条记录。
如何得到多于10条记录呢?

find_id_ by_contents可以传递一些参数进去:

    * offset:默认是0。The offset of the start of the section of the result-set to return(译者:我脑海中的说法和英文的不一样,就是返回结果所需要的偏移量,默认是从0记录开始,返回10条记录,但是如果offset为10,那就是从第10条开始,返回10条记录。用中文这么一句话我说不好。呵呵)。这个用于对返回结果进行分页。
    * limit:默认是10。返回你想要得到的结果数。也是在分页中被使用。

在find_id_by_contents使用代码块

    results = []
    total_results = Member.find_id_by_contents(”Gregg”) {|result|
    results.push result
    }

在这,你可能并不想只得到结果的id,那么可以在这里做一个转换,得到返回的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”)

find_by_contents进行了下面的操作:

   1. 调用find_id_by_contents,得到id集合
   2. 跟踪所有返回的id,得到实际的model
   3. 返回一个貌似ActiveRecord的集合,其实它是一个ActsAsFerret::SearchResults类,下面是它的一些额外特性

注意哦

    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,在数据库中他们并不存在的哦。
进行分页

注:下面代码中使用了Roman Mackovcak’s blog的例子。

在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

在页面中

    <%= 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 %>

上面的代码可以完成大部分工作了,但是acts_as_ferret还有其他的优秀特性的。
其他形式的查询字串

    * “Gregg Pollack”将在所有字段中搜索”Gregg”和”Pollack”
    * “Gregg OR Pollack”将搜索”Gregg”或”Pollack”
    * “Gregg~”模糊搜索,返回搜索包含”Gregg”字样的结果
    * “first_name:Gregg”,搜索first_name是”Gregg”的记录,排除其他索引。
    * “+first_name:Gregg -last_name:Jones”,布尔查询,查询所有first_name是”Gregg”并且last_name不是”Jones”的记录

更多复杂查询,可以参考 Apache Lucene Parser Syntax。
添加非model和非字段(Adding Non-Model or Non-Standard Fields)

现在对我们例子做一个修改,我们有许多书,每本书有一些作者,如果你不仅要索引书的标题,还要索引书的作者,该怎么办呢?

我们需要操作的是两个表,可是我们不能去对两个不同的索引进行查找。这时需要修改我们的model
/model/book.rb

    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方法的返回值进行索引,甚至可以重新格式化你的字段(fields)。

比如你在使用 acts_as_taggable ,对你的model进行tag标注,并且希望在搜索的时候你的tag一并被搜索到。那么:

    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

原文:If you were using the acts_as_taggable plugin you might not even need the extra function, and use “:tag_list” in the ferret field list, as shown on Johnny’s Thoughts. I’m not nearly as cool though, I’m using the acts_as_taggable gem.

译文:如果你在以插件形式使用acts_as_taggable,那么就会出现Johnny’s Thoughts中提到的问题,(具体情况大家去看那个博客吧,这个地方留意一下)。这时你需要在ferret索引字段列表使用”:tag_list”。译者:因为作者也在用插件形式使用ferret。插件冲突的事情由此也需要注意了。
排序

到目前为止,我们得到的记录都是排好序列的搜索结果。那么如果我们想得到按照我们想根据的字段进行排列的结果,比如书的标题,该如何做呢?

原文:The first thing you need to do is make sure the field you are trying to sort by is untokenized. Unfortunately, by making a field untokenized I’m not indexing it to be searchable anymore. This makes for a little funky coding.

作者:”if something is untokenized it will not be searchable ”

译者:首先你需要确定,需要排序的字段没有被索引过。所以我们要对上面的代码做一点修改。

    acts_as_ferret :fields => {
    :title => {},
    :tags_with_spaces => {},
    :title_for_sort => {:index => :untokenized}
    }

    def title_for_sort
    return self.title
    end

记得,如果你想重新建立索引,只需要删除对应的文件夹,并重启服务。
译者:重建索引也可以实用Model.rebuild_index这个方法。

    s = Ferret::Search::SortField.new(:title_for_sort, :reverse => false)
    @total, @members = Book.full_text_search(@query,
    {:page => (params[:page]||1), :sort => s})

这样我就得到了按照书的title的排序。

如果你想按照日期排序,那就需要把日期转换成integer类型,具体的请参考this Slash Dot Dash
字段储存

在进入下一节(相当重要的一节)之前,我们需要研究下如何储存已被索引的数据(data)。

如果你现在看一下你的索引文件(indexes),你会发现那里并没有你的数据。默认情况下,acts_as_ferret在这种可复写的情况下并不储存你的数据(in a recoverable form),仅是索引它。

“那么,如果我的数据很小,我想在我的索引中储存它,该怎么办呢?”

这是个好问题,如果你的数据很小,而且你只是关注一个字段的信息,你可以加速你的索引。

    acts_as_ferret :fields => {
    :title => {:store => :yes},
    :author_name => {:store => :yes}
    }

当我们进行查询时,我们需要对这个特殊的字段进行说明(”lazy load”)

    @books = Book.find_by_contents(”Jason”, :lazy => [:title, :author_name])

这样我们在渲染页面时,并没有调用数据库。

    < % @books.each do |book| %>
    %lt;li> “< %= book.title %>” by
    < %= book.author_name %>%lt;/li%gt;
    < % end %>

高亮显示搜索词

下面是用ferret实现搜索词的高亮显示。
不过这有个前提,就是需要对你的搜索词进行储存(must have your search fields stored),上面已经介绍了。
所以需要对上面的代码做一点点修改了:

    < % @books.each do |book| %>
    <li>
    “< %= book.highlight(”Jason”, :field => :title, :num_excerpts => 1, :pre_tag => ““, :post_tag => ““) %>” by
    < %= book.highlight(”Jason”, :field => :author_name, :num_excerpts => 1, :pre_tag => ““, :post_tag => ““) %>
    </li>
    < % end %>

你的搜索结果会是:
1. “Story of Gregg” by Jason Seifer
2. “Jason’s Book” by Gregg Pollack
3. “Gregg certainly is the Man” by Jason Seifer

高亮显示功能还有其他的方法,比如,如果你搜索的字段内容很长,例如博客文章,那么将会返回一个片断,搜索词被高亮显示。更多参考 Highlight in the API
使用Boost(设定搜索优先级)

最后介绍一下Boost属性。Boost属性可以提升索引的优先顺序。

    acts_as_ferret :fields => {
    :title => {:boost => 2},
    :author => {:boost => 0}
    }

这段代码表明,title搜索结果要优先于author结果的显示。但是这并不是说,所有的title记录在author记录前显示。如果一条author记录完全匹配搜索词,那么它会优先显示。

Perhaps this feature should be called “Nudge” instead of “Boost”. I thought I could use a large boost to get all the title results to appear above the author results. I was mistaken, one can only “Nudge” the scores, but never separate them, as I was hoping.

译者:大家看一下那个连接的文章,这段英文的意思是,使用boost是提升部分优先级,而别指望它能把title和author分开。
产品环境应用

因为很多人是在产品环境下使用,所以一致的想法认为,你需要运行一个DRB Server
结尾

要开发搜人系统,需要对人名进行索引,所以找到了这篇文章,看完后齐发很大。即便后面开发AskNet,也有相当的帮助。因为有很多功能是数据库搜索无法达到的。希望看完文章的朋友在留言中多多交流。

特别感谢:Gregg Pollack,原文作者
Special Thanks To : Gregg Pollack

中文译者:里克

声明:转载请注明出处。谢谢。本文所有:里克的网络自习室,http://railser.cn

TOP

ruby中文分词器 - RMMSeg

http://www.javaeye.com/news/1380

RMMSeg是pluskid(浙江大学的学生)开发的ruby中文分词器,在2008年2月份刚刚发布的。RMMSeg使用了基于词库的最大匹配算法进行中文分词,并且辅以相应的修正算法,而RMMSeg带的词库有大约12万中文词汇,已经比较全面了。根据作者自己博客上面的测试,中文分词的准确率可以达到98%以上。

经过JavaEye网站的试用,RMMSeg当前的0.1.5版本已经可以在小型的ruby应用当中实际使用了,分词的效果也还不错。当然由于 RMMSeg目前是纯ruby代码的实现,因此索引的性能还是有点慢,比目前JavaEye的单字拆分算法慢5倍左右。但考虑到全文检索的索引都是后台 job去运行,所以稍微慢一点也可以接受。pluskid计划在接下来的版本当中使用C来实现一部分功能,提高性能,降低内存耗用。

rmmseg的主要问题是性能和内存消耗。用纯ruby编写的rmmseg分词的速度非常慢,而且内存消耗非常惊人,当使用复杂分词算法的时候,内存呈线性上升的现象,在我作的大量索引测试程序中曾经上升到了900MB以上。

为此rmmseg的作者pluskid再接再厉用C++重写了一遍rmmseg项目,这就是rmmseg-cpp。根据作者自己的简单测试表明:rmmseg-cpp的性能是rmmseg的400倍之多。下面简单介绍rmmseg-cpp的安装和使用:

1、安装
rmmseg-cpp目前只支持Unix操作系统,不支持Windows
Ruby代码 复制代码

   1. gem install pluskid-rmmseg-cpp --source=http://gems.github.com  

2、使用
Ruby代码 复制代码

require 'ferret'
require 'rmmseg'
analyzer = RMMSeg::Ferret::Analyzer.new do |tokenizer|
  Ferret::Analysis::LowerCaseFilter.new(tokenizer)
end


就可以了,然后就可以使用analyzer进行分词索引和查询了。

rmmseg-cpp和libmmseg目前都可以作为比较好的ruby中文分词程序来使用,他们的性能和内存管理都相当不错,经过我们测试和使用的情况来看,都可以放心在生产环境当中使用。不过他们也有一些区别和各自的特点:

1、rmmseg-cpp的分词速度比libmmseg快一倍左右

rmmseg-cpp是专门为ruby的ferret编写的中文分词接口,代码十分简练,和ruby结合的很好;libmmseg主要是用来和 sphinx配合的,并不是为ruby而写,他仅仅提供了一个非常简陋的ruby调用接口,在ruby程序当中使用中文分词的时候,rmmseg-cpp 的性能明显胜出一筹。

2、rmmseg-cpp的内存管理似乎不如libmmseg稳定

Ruby本身是带有GC功能的脚本语言,用C/C++编写Ruby扩展本身是一件很麻烦的事情,因为C/C++自身是需要手工分配内存的,但是如果程序员对于Ruby的GC机制没有足够的了解,会带来很多麻烦,弄不好就可能内存泄漏,或者发生不期望的被使用中的对象被Ruby GC掉的问题。

rmmseg-cpp已经在GC方面下了很大的功夫,因此内存管理相当不错,但是我曾在一次数据量非常大量的全文索引过程当中观察到内存短时间内快速上升了几十MB的现象(但这种现象不常见)。

而libmmseg没有对ruby的依赖,仅仅提供一个简陋的接口,因此在数次测试当中,已经程序长时间运行观察下来,内存管理比rmmseg-cpp显得更加平稳一点,没有出现过一次内存快速上升的现象。

3、rmmseg-cpp目前没有Windows版本,而libmmseg支持windows版本

4、rmmseg-cpp安装配置很简单,不像libmmseg那么麻烦

总得来说,rmmseg-cpp和libmmseg都是用C++编写的优秀的中文分词程序,并且都可以支持在ruby当中实现全文检索的中文分词功能。如果不是特殊的需要,仅仅只是在ruby当中使用中文分词功能,那么推荐使用rmmseg-cpp。

TOP

发新话题