注意:改库已被重命名,并在2013年不在更新。
看一看在http://github.com/elasticsearch/elasticsearch-ruby的gem包,其中那个包含类似的功能集。
Tire是一个Elasticsearch的基于Ruby1.8/1.9的客户端。
Elasticsearch是一个可扩展的分布式、云就绪,高可用性,全文搜索引擎和数据库具有强大的聚合功能。通过JSON REST风格的HTTP通信,基于Lucene, 用Java编写。
本自述提供了Tire功能的简要概述。更详细的文档是http://karmi.github.com/retire/.
这两个文件中包含了大量的信息。请留出一些时间来仔细阅读。
首先,你需要一个运行的Elasticsearch服务。向下面这样做。
$ curl -k -L -o elasticsearch-0.20.6.tar.gz http://download.elasticsearch.org/elasticsearch/elasticsearch/elasticsearch-0.20.6.tar.gz $ tar -zxvf elasticsearch-0.20.6.tar.gz $ ./elasticsearch-0.20.6/bin/elasticsearch -f
看,非常简单,如果你使用的是Mac,你同样可以通过Homebrew安装。
$ brew install elasticsearch
现在,我们通过Rubygems安装这个gem.
$ gem install tire
同样,你也可以从源码安装:
$ git clone git://github.com/karmi/tire.git $ cd tire $ rake install
Tire通过特定的DSL来与Elasticsearch交互。
它很容易与您加载的ActiveRecord/,Rails中的ActiveRecord集成。
我们需要gem来驱动Elasticsearch.
require 'rubygems' require 'tire'
请注意,你可以从examples/tire-dsl.rb中复制这些片段。
同样的,我们在这里做一些繁重的JSON升降。Tire使用multi_json gem作为通用的json封装,它允许你使用自己的json库。我们在这里使用yajl-ruby gem使用json全速模式。
require 'yajl/json_gem'
让我们创建一个articles的索引,并存储/索引一些文档。
Tire.index 'articles' do delete create store :title => 'One', :tags => ['ruby'] store :title => 'Two', :tags => ['ruby', 'python'] store :title => 'Three', :tags => ['java'] store :title => 'Four', :tags => ['ruby', 'php'] refresh end
我们还可以为特定文档类型创建自定义映射的索引:
Tire.index 'articles' do delete create :mappings => { :article => { :properties => { :id => { :type => 'string', :index => 'not_analyzed', :include_in_all => false }, :title => { :type => 'string', :boost => 2.0, :analyzer => 'snowball' }, :tags => { :type => 'string', :analyzer => 'keyword' }, :content => { :type => 'string', :analyzer => 'snowball' } } } } end
当然,我们也可以有大量数据,将它们一个一个添加到索引中,或许是不切实际的。我们可以使用Elasticsearch三列存储。注意,如果你为索引设置任何特定的映射,项目集合必须有一个id属性或方法,和应该提供一个类型属性。
articles = [ { :id => '1', :type => 'article', :title => 'one', :tags => ['ruby'] }, { :id => '2', :type => 'article', :title => 'two', :tags => ['ruby', 'python'] }, { :id => '3', :type => 'article', :title => 'three', :tags => ['java'] }, { :id => '4', :type => 'article', :title => 'four', :tags => ['ruby', 'php'] } ] Tire.index 'articles' do import articles end
我们通过传递一个block, 在文档存储在索引之前操纵文档。
Tire.index 'articles' do import articles do |documents| documents.each { |document| document[:title].capitalize! } end refresh end
如果这个声明符号(猜测是refresh方法)不适合你的情况,你可以直接使用Tire类,以更加命令的方式:
index = Tire::Index.new('oldskool') index.delete index.create index.store :title => "Let's do it the old way!" index.refresh
好了,现在,我们去搜索所有数据。
我们将寻找开头字母为"T"的标题,按标题降序排列,并过滤标记为"ruby"的项,也从数据库检索一些facets。
s = Tire.search 'articles' do query do string 'title:T*' end filter :terms, :tags => ['ruby'] sort { by :title, 'desc' } facet 'global-tags', :global => true do terms :tags end facet 'current-tags' do terms :tags end end
(同样,我们也可以对结果分页,使用from和size查询选项,指定查询的字段或突出显示匹配内容)
让我们来显示结果:
s.results.each do |document| puts "* #{ document.title } [tags: #{document.tags.join(', ')}]" end # * Two [tags: ruby, python]
让我们来显示全局facets(通过分布在数据库中的标记):
s.results.facets['global-tags']['terms'].each do |f| puts "#{f['term'].ljust(10)} #{f['count']}" end # ruby 3 # python 1 # php 1 # java 1
现在,让我们根据目前的显示facets查询(注意有标签'java'包含在内,即使它不是由我们的查询返回的。有标签'php'的文章排除在外,因为它们不匹配当前查询。)。
s.results.facets['current-tags']['terms'].each do |f| puts "#{f['term'].ljust(10)} #{f['count']}" end # ruby 1 # python 1 # java 1
注意,只有从封闭的范围内的变量是可访问的。如果想从外部范围访问的变量或方法,通过传递的search和query对象。
@query = 'title:T*' Tire.search 'articles' do |search| search.query do |query| query.string @query end end
很多时候,我们需要布尔逻辑查询,如 ruby:tags or java:tags and not tags:python. 我们可以使用bool查询,在Tire中,我们这么构建。
Tire.search 'articles' do query do boolean do should { string 'tags:ruby' } should { string 'tags:java' } must_not { string 'tags:python' } end end end
对于布尔查询,最好的事情是,我们可以很容易的保存这些部分查询为 ruby的块,在之后混入重用它们。所以,我们可以定义标签属性的查询。
tags_query = lambda do |boolean| boolean.should { string 'tags:ruby' } boolean.should { string 'tags:java' } end
对于公布财产的查询:
published_on_query = lambda do |boolean| boolean.must { string 'published_on:[2011-01-01 TO 2011-01-02]' } end
现在,我们可以将这个查询用于不同的搜索语句中:
Tire.search 'articles' do query do boolean &tags_query boolean &published_on_query end end
请注意,您可以通过将哈希作为最后一个参数传递给方法调用的选项来配置查询、面向方面等。
Tire.search 'articles' do query do string 'ruby python', :default_operator => 'AND', :use_dis_max => true end end
你不必在一个整体的Ruby块中定义搜索条件-你可以一步一步地简历搜索,知道你调用results方法。
s = Tire.search('articles') { query { string 'title:T*' } } s.filter :terms, :tags => ['ruby'] p s.results
如果配置搜索载荷与块对你来说感觉太弱,可以通过普通的Ruby哈希(或json字符串)与查询方法声明:
Tire.search 'articles', :query => { :prefix => { :title => 'fou' } }
如果这听起来是一个不错的想法,你可以只使用curl, sed和awk.
不过要注意,你不绑在声明块式DSL提供给你,如果你的背景更有意义,可以直接使用该API。
search = Tire::Search::Search.new('articles') search.query { string('title:T*') } search.filter :terms, :tags => ['ruby'] search.sort { by :title, 'desc' } search.facet('global-tags') { terms :tags, :global => true } # ... p search.results
要调试,我们可以调用它to_json方法:
puts s.to_json # {"facets":{"current-tags":{"terms":{"field":"tags"}},"global-tags":{"global":true,"terms":{"field":"tags"}}},"query":{"query_string":{"query":"title:T*"}},"filter":{"terms":{"tags":["ruby"]}},"sort":[{"title":"desc"}]}
或者,更好的是,我们可以调用to_curl方法,在终端中调试:
puts s.to_curl # curl -X POST "http://localhost:9200/articles/_search?pretty=true" -d '{"facets":{"current-tags":{"terms":{"field":"tags"}},"global-tags":{"global":true,"terms":{"field":"tags"}}},"query":{"query_string":{"query":"title:T*"}},"filter":{"terms":{"tags":["ruby"]}},"sort":[{"title":"desc"}]}'
然而,我们可以记录每次搜索查询(和其他请求)在curl-友好形式:
Tire.configure { logger 'elasticsearch.log' }
当你设置log级别为debug:
Tire.configure { logger 'elasticsearch.log', :level => 'debug' }
JSON响应记录,在生产环境中不是一个好主意,但如果你想要粘贴一个复杂事物到邮件、IRC频道中,它是一个不错的主意。
Tire DSL致力于提供一个强有力的Ruby风格的API.
默认情况下,Tire将结果集放在一个可枚举的Results::Collection类对象中,结果集项是Result::Item,它很像一个Hash或Openstruct的子类,平稳遍历和显示结果。
你可以设置Tire.configuration.wrapper属性为你自己的类。你的类必须初始化哈希属性。
如果这是一个好主意,你可能已经有了这样的类。
人们可能希望这是一个ActiveRecord或类加载ActiveModel包含Rails程序模型。
幸运的是,Tire是的混合Elasticsearch特性整合到你的模型很容易。
如果你没有时间看这些冗长的介绍,您可以生成一个完全工作示例的Rails程序,一个ActiveRecord模型和一个搜索表单,去玩吧(它甚至下载Elasticsearch本身,生成应用程序骨架,留给你一个Git仓库探索步骤和代码):
$ rails new searchapp -m https://raw.github.com/karmi/tire/master/examples/rails-application-template.rb
对我们其余的人来说,我们假设你的Rails程序中有一个Article类。
在Tire中搜索,现在包含它:
class Article < ActiveRecord::Base include Tire::Model::Search include Tire::Model::Callbacks end
当你保存一条记录:
Article.create :title => "I Love Elasticsearch", :content => "...", :author => "Captain Nemo", :published_on => Time.now
它会自动添加索引到'articles'中,因为你已经包含进了Tire的callbacks回调。
当你调用#to_json方法,文章属性和索引一样。
现在你可以搜索记录:
Article.search 'love'
好的,搜索游戏结束了,经常这样做,不在这里:
首先,你可以使用完整的查询DSL,如上所述,过滤、排序、聚合、高级的面向方面等。
Article.search do query { string 'love' } facet('timeline') { date :published_on, :interval => 'month' } sort { by :published_on, 'desc' } end
其次,对于原型设计,动态映射是天赐良机。对于重度使用,你需要对映射自定义:
class Article < ActiveRecord::Base include Tire::Model::Search include Tire::Model::Callbacks mapping do indexes :id, :index => :not_analyzed indexes :title, :analyzer => 'snowball', :boost => 100 indexes :content, :analyzer => 'snowball' indexes :content_size, :as => 'content.size' indexes :author, :analyzer => 'keyword' indexes :published_on, :type => 'date', :include_in_all => false end end
在这种情况下,只有限定模型属性索引。当类被装载,活着当输入功能的使用,并且只有它不存在映射声明创建的索引。
你可以定义不同的分析器,增加不同的性能水平,或任何其他的配置。
模型属性的映射与序列化文档之间并不局限于1:1的映射,使用as:选项,你可以传递一个字符串或一个Proc对象的实例在上下文中计算(见content_size属性)。
很有可能,你还想生命自定义设置索引,如碎片的数量,副本,或创建复杂的分析器,如超人的选择: ngrams, 在这种情况下,只是包装的映射方法设置一个,把它设置为一个散列:
class URL < ActiveRecord::Base include Tire::Model::Search include Tire::Model::Callbacks settings :number_of_shards => 1, :number_of_replicas => 1, :analysis => { :filter => { :url_ngram => { "type" => "nGram", "max_gram" => 5, "min_gram" => 3 } }, :analyzer => { :url_analyzer => { "tokenizer" => "lowercase", "filter" => ["stop", "url_ngram"], "type" => "custom" } } } do mapping { indexes :url, :type => 'string', :analyzer => "url_analyzer" } end end
注意,只有索引不存在时,才会创建它。要重新创建与正确的配置指标,先要删除Url.index.delete,然后创建它:URL.createelasticsearchindex
这可能是合理的包装与Tire.index(url)索引创建逻辑。在模型中建立类方法,创建模块方法等等,引导的应用程序对索引创建更好的控制Rake任务或建立测试套件的时候。Tire不会要写你的。
你可能停下来想:如果我有自己的设置定义类方法?或者如果其他Ruby定义设置,或其他Tire的方法,比如update_index?它会停止吗?不,他们不会。
事实上,这么长时间,你一直只使用代理Tire的方法,生活在Tire类和实例方法模型。只有当不践踏别人的脚-- 在大多数情况下,Tire将其方法放在类的命名空间。
因此,不用Articel.search,而是用Article.tire.search, 用@article.tire.updateindex替换@article.updateindex,更安全可靠。让我们看一个例子与映射方法:
class Article < ActiveRecord::Base include Tire::Model::Search include Tire::Model::Callbacks tire.mapping do indexes :id, :type => 'string', :index => :not_analyzed # ... end end
当然,你也可以用块形式:
class Article < ActiveRecord::Base include Tire::Model::Search include Tire::Model::Callbacks tire do mapping do indexes :id, :type => 'string', :index => :not_analyzed # ... end end end
在内部,Tire只使用这些代理方法。当你遇到问题,使用代理方法,如 article.tire.mapping。
当你想要牢牢控制属性的方式添加到索引中,在你的模型只要实现toindexedjson方法。
最简单的方法是自定义模型的to_json序列化支持:
lass Article < ActiveRecord::Base # ... self.include_root_in_json = false def to_indexed_json to_json :except => ['updated_at'], :methods => ['length'] end end
当然,这很可能是合理定义JSON的:
class Article < ActiveRecord::Base # ... def to_indexed_json names = author.split(/\W/) last_name = names.pop first_name = names.join { :title => title, :content => content, :author => { :first_name => first_name, :last_name => last_name } }.to_json end end
请注意,在特殊情况下,您很可能想跳过包括Tire::Model::Callbacks,当你的记录是通过一些外部机制所创建,比如CouchDB或RabbitMQ,或者当你需要更好的控制文件被添加或从索引移除:
class Article < ActiveRecord::Base include Tire::Model::Search after_save do update_index if state == 'published' end end
有时,您可能想要完全控制索引过程。在这种情况下,就下降一层,并使用Tire::Index#store和Tire::Index#remove方法:
class Article < ActiveRecord::Base acts_as_paranoid include Tire::Model::Search after_save do if deleted_at.nil? self.index.store self else self.index.remove self end end end
当然,通过这种方式,你仍然执行http请求在你的数据库事务,不适合大规模应用。在这种情况下,一个更好的选择是选择在后台处理索引操作,类似resque或Sidekiq:
class Article < ActiveRecord::Base include Tire::Model::Search after_save { Indexer::Index.perform_async(document) } after_destroy { Indexer::Remove.perform_async(document) } end
当你把Tire内置进ActiveRecord模型中,你应该使用aftercommit和afterrollback钩子保持索引与数据库同步。
默认情况下,Article.search返回默认包装类结果。这种方式,我们有一个快速和灵活的访问从Elasticsearch返回的属性(通过_source或JSON字段属性)。通过这种方式,我们可以在Elasticsearch索引,通过.符号检索。
articles = Article.search 'love' articles.each do |article| puts article.title puts article.author.last_name end
该Item实例伪装自己作为Rails应用程式的模型的实例(基于type属性),所以你可以无忧无虑的使用,所有的urlfor或dom_id都能正常工作。
如果你需要访问“真正”的模型(访问没有存储在Elasticsearch中的关联或方法),那么需要在数据库中加载:
puts article.load(:include => 'comments').comments.size
你可以看到,Tire尽可能远离数据库成为可能。那是因为你有你想要显示的数据存储在Elasticsearch中。当你急切需要从数据库本身加载记录,无论什么原因,你都可以这么做,通过在搜索时使用:load选项。
# Will call `Article.search [1, 2, 3]` Article.search 'love', :load => true
不只是这样,你能够传递所有选项在模型中查找:
# Will call `Article.search [1, 2, 3], :include => 'comments'` Article.search :load => { :include => 'comments' } do query { string 'love' } end
如果你想访问由Elasticsearch返回的属性(例如 score),除了模型实例,使用eachwith_hit方法:
results = Article.search 'One', :load => true results.each_with_hit do |result, hit| puts "#{result.title} (score: #{hit['_score']})" end # One (score: 0.300123)
需要注意的是,Tire的搜索结果与Will_Paginate和Kaminari完全兼容,所以你可以通过常用的参数,在控制中的搜索方法:
@articles = Article.search params[:q], :page => (params[:page] || 1)
好的,当你有很多存储在数据库中的记录,你如何将他们放入Elasticsearch?很简单:
Article.index.import Article.all
通过这种方式,所有的记录都被加载到内存中,序列化成JSON,并写入Elasticsearch。不使用,你说呢?你是对的。
当你的模型是ActiveRecord::Base或Mongoid::Document,或者当它实现了某种分页的,你可以运行:
Article.import
根据模型的建立,无论是findinbatches, limit..skip,或使用分页导入你的数据。
难道我们说你有这个东西在Rails控制台或愚蠢的Ruby脚本里弄吗?你只需要调用在命令行中包含的Rake任务:
$ rake environment tire:import:all
你还可以强制导入数据,通过先删除索引(以及由映射块在模型中提供正确的设置和/或映射创建它):
$ rake environment tire:import CLASS='Article' FORCE=true
当你使用Elasticsearch时间越来越多,你会注意到倒排索引是个不错的想法。你可以索引你的数据到一个新的索引(也可能更新一个别名)
$ rake environment tire:import CLASS='Article' INDEX='articles-2011-05'
最后,考虑导入功能只是一个方便的起点。如果你加载大量数据,想要更好的控制,数据将被索引,等等。如使用较低级别的Tire API,直接使用ActiveRecord::Base#findinbatches:
Article.where("published_on > ?", Time.parse("2012-10-01")).find_in_batches(include: authors) do |batch| Tire.index("articles").import batch end
如果你正在使用一个不同的数据库,例如MongoDB, 其他的ORM库,例如Mongoid或MongoMapper,这一切是相同的:
class Article include Mongoid::Document field :title, :type => String field :content, :type => String include Tire::Model::Search include Tire::Model::Callbacks # These Mongo guys sure do get funky with their IDs in +serializable_hash+, let's fix it. # def to_indexed_json self.to_json end end Article.create :title => 'I Love Elasticsearch' Article.tire.search 'love'
Tire不在乎你的主数据库的存储解决方案,如果他有ActiveModel-compatible适配器。还有很多。
Tire实现不仅可搜索的特性,而且有持久性特征。这意味着你可以使用Tire模型数据库,不仅对搜索数据库。为什么你要这么做?
嗯,因为你厌倦了数据库迁移和大量的拿着你的数据库来存储东西,如{ :name => 'Tire', :tags => [ 'ruby', 'search' ] }。因为你需要的,真的,是将一个json表示的数据到数据库并加载它回来。因为你有很多的数据库和想使用Elasticsearch先进的分布式特性。
所有的理由使用Elasticsearch作为独立和高度可伸缩为您的数据存储和检索/聚合引擎。
使用持久性模式,我们将导入 Tire::Persistence模块,并定义它的属性,我们可以添加标准的映射声明,设置默认值,或定义属性的铸造,以创建轻量级的模型之间的关联。
class Article include Tire::Model::Persistence validates_presence_of :title, :author property :title, :analyzer => 'snowball' property :published_on, :type => 'date' property :tags, :default => [], :analyzer => 'keyword' property :author, :class => Author property :comments, :class => [Comment] end
请务必细读API和集成ActiveModel范例整合测试套件。