tire-erez 0.5.4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (136) hide show
  1. data/.gitignore +14 -0
  2. data/.travis.yml +32 -0
  3. data/.yardopts +1 -0
  4. data/Gemfile +10 -0
  5. data/MIT-LICENSE +20 -0
  6. data/README.markdown +775 -0
  7. data/Rakefile +51 -0
  8. data/examples/rails-application-template.rb +263 -0
  9. data/examples/tire-dsl.rb +932 -0
  10. data/lib/tire.rb +59 -0
  11. data/lib/tire/alias.rb +296 -0
  12. data/lib/tire/configuration.rb +38 -0
  13. data/lib/tire/count.rb +85 -0
  14. data/lib/tire/dsl.rb +114 -0
  15. data/lib/tire/http/client.rb +62 -0
  16. data/lib/tire/http/clients/curb.rb +61 -0
  17. data/lib/tire/http/clients/faraday.rb +71 -0
  18. data/lib/tire/http/response.rb +27 -0
  19. data/lib/tire/index.rb +443 -0
  20. data/lib/tire/logger.rb +60 -0
  21. data/lib/tire/model/callbacks.rb +40 -0
  22. data/lib/tire/model/import.rb +27 -0
  23. data/lib/tire/model/indexing.rb +134 -0
  24. data/lib/tire/model/naming.rb +100 -0
  25. data/lib/tire/model/percolate.rb +99 -0
  26. data/lib/tire/model/persistence.rb +72 -0
  27. data/lib/tire/model/persistence/attributes.rb +148 -0
  28. data/lib/tire/model/persistence/finders.rb +54 -0
  29. data/lib/tire/model/persistence/storage.rb +77 -0
  30. data/lib/tire/model/search.rb +322 -0
  31. data/lib/tire/multi_search.rb +263 -0
  32. data/lib/tire/results/collection.rb +156 -0
  33. data/lib/tire/results/item.rb +94 -0
  34. data/lib/tire/results/pagination.rb +68 -0
  35. data/lib/tire/rubyext/hash.rb +8 -0
  36. data/lib/tire/rubyext/ruby_1_8.rb +1 -0
  37. data/lib/tire/rubyext/symbol.rb +11 -0
  38. data/lib/tire/rubyext/uri_escape.rb +74 -0
  39. data/lib/tire/search.rb +211 -0
  40. data/lib/tire/search/facet.rb +81 -0
  41. data/lib/tire/search/filter.rb +28 -0
  42. data/lib/tire/search/highlight.rb +37 -0
  43. data/lib/tire/search/queries/match.rb +40 -0
  44. data/lib/tire/search/query.rb +250 -0
  45. data/lib/tire/search/scan.rb +114 -0
  46. data/lib/tire/search/script_field.rb +23 -0
  47. data/lib/tire/search/sort.rb +25 -0
  48. data/lib/tire/tasks.rb +138 -0
  49. data/lib/tire/utils.rb +17 -0
  50. data/lib/tire/version.rb +18 -0
  51. data/test/fixtures/articles/1.json +1 -0
  52. data/test/fixtures/articles/2.json +1 -0
  53. data/test/fixtures/articles/3.json +1 -0
  54. data/test/fixtures/articles/4.json +1 -0
  55. data/test/fixtures/articles/5.json +1 -0
  56. data/test/integration/active_model_indexing_test.rb +51 -0
  57. data/test/integration/active_model_searchable_test.rb +114 -0
  58. data/test/integration/active_record_searchable_test.rb +620 -0
  59. data/test/integration/boolean_queries_test.rb +43 -0
  60. data/test/integration/boosting_queries_test.rb +32 -0
  61. data/test/integration/bulk_test.rb +86 -0
  62. data/test/integration/count_test.rb +64 -0
  63. data/test/integration/custom_score_queries_test.rb +89 -0
  64. data/test/integration/dis_max_queries_test.rb +68 -0
  65. data/test/integration/dsl_search_test.rb +30 -0
  66. data/test/integration/explanation_test.rb +44 -0
  67. data/test/integration/facets_test.rb +311 -0
  68. data/test/integration/filtered_queries_test.rb +66 -0
  69. data/test/integration/filters_test.rb +75 -0
  70. data/test/integration/fuzzy_queries_test.rb +20 -0
  71. data/test/integration/highlight_test.rb +64 -0
  72. data/test/integration/index_aliases_test.rb +122 -0
  73. data/test/integration/index_mapping_test.rb +43 -0
  74. data/test/integration/index_store_test.rb +112 -0
  75. data/test/integration/index_update_document_test.rb +121 -0
  76. data/test/integration/match_query_test.rb +79 -0
  77. data/test/integration/mongoid_searchable_test.rb +309 -0
  78. data/test/integration/multi_search_test.rb +114 -0
  79. data/test/integration/nested_query_test.rb +135 -0
  80. data/test/integration/percolator_test.rb +111 -0
  81. data/test/integration/persistent_model_test.rb +205 -0
  82. data/test/integration/prefix_query_test.rb +43 -0
  83. data/test/integration/query_return_version_test.rb +70 -0
  84. data/test/integration/query_string_test.rb +52 -0
  85. data/test/integration/range_queries_test.rb +36 -0
  86. data/test/integration/reindex_test.rb +56 -0
  87. data/test/integration/results_test.rb +58 -0
  88. data/test/integration/scan_test.rb +56 -0
  89. data/test/integration/script_fields_test.rb +38 -0
  90. data/test/integration/sort_test.rb +52 -0
  91. data/test/integration/text_query_test.rb +39 -0
  92. data/test/models/active_model_article.rb +31 -0
  93. data/test/models/active_model_article_with_callbacks.rb +49 -0
  94. data/test/models/active_model_article_with_custom_document_type.rb +7 -0
  95. data/test/models/active_model_article_with_custom_index_name.rb +7 -0
  96. data/test/models/active_record_models.rb +131 -0
  97. data/test/models/article.rb +15 -0
  98. data/test/models/mongoid_models.rb +85 -0
  99. data/test/models/persistent_article.rb +11 -0
  100. data/test/models/persistent_article_in_index.rb +16 -0
  101. data/test/models/persistent_article_in_namespace.rb +12 -0
  102. data/test/models/persistent_article_with_casting.rb +28 -0
  103. data/test/models/persistent_article_with_defaults.rb +12 -0
  104. data/test/models/persistent_article_with_percolation.rb +5 -0
  105. data/test/models/persistent_articles_with_custom_index_name.rb +10 -0
  106. data/test/models/supermodel_article.rb +17 -0
  107. data/test/models/validated_model.rb +11 -0
  108. data/test/test_helper.rb +118 -0
  109. data/test/unit/active_model_lint_test.rb +17 -0
  110. data/test/unit/configuration_test.rb +84 -0
  111. data/test/unit/count_test.rb +67 -0
  112. data/test/unit/http_client_test.rb +79 -0
  113. data/test/unit/http_response_test.rb +49 -0
  114. data/test/unit/index_alias_test.rb +335 -0
  115. data/test/unit/index_test.rb +1098 -0
  116. data/test/unit/logger_test.rb +125 -0
  117. data/test/unit/model_callbacks_test.rb +116 -0
  118. data/test/unit/model_import_test.rb +75 -0
  119. data/test/unit/model_initialization_test.rb +31 -0
  120. data/test/unit/model_persistence_test.rb +548 -0
  121. data/test/unit/model_search_test.rb +964 -0
  122. data/test/unit/multi_search_test.rb +304 -0
  123. data/test/unit/results_collection_test.rb +372 -0
  124. data/test/unit/results_item_test.rb +173 -0
  125. data/test/unit/rubyext_test.rb +66 -0
  126. data/test/unit/search_facet_test.rb +186 -0
  127. data/test/unit/search_filter_test.rb +42 -0
  128. data/test/unit/search_highlight_test.rb +46 -0
  129. data/test/unit/search_query_test.rb +419 -0
  130. data/test/unit/search_scan_test.rb +113 -0
  131. data/test/unit/search_script_field_test.rb +26 -0
  132. data/test/unit/search_sort_test.rb +50 -0
  133. data/test/unit/search_test.rb +556 -0
  134. data/test/unit/tire_test.rb +144 -0
  135. data/tire.gemspec +83 -0
  136. metadata +586 -0
@@ -0,0 +1,51 @@
1
+ require 'bundler'
2
+ Bundler::GemHelper.install_tasks
3
+
4
+ task :default => :test
5
+
6
+ require 'rake/testtask'
7
+ Rake::TestTask.new(:test) do |test|
8
+ test.libs << 'lib' << 'test'
9
+ test.test_files = FileList['test/unit/*_test.rb', 'test/integration/*_test.rb']
10
+ test.verbose = true
11
+ # test.warning = true
12
+ end
13
+
14
+ namespace :test do
15
+ Rake::TestTask.new(:unit) do |test|
16
+ test.libs << 'lib' << 'test'
17
+ test.test_files = FileList["test/unit/*_test.rb"]
18
+ test.verbose = true
19
+ end
20
+ Rake::TestTask.new(:integration) do |test|
21
+ test.libs << 'lib' << 'test'
22
+ test.test_files = FileList["test/integration/*_test.rb"]
23
+ test.verbose = true
24
+ end
25
+ end
26
+
27
+ namespace :web do
28
+
29
+ desc "Update the Github website"
30
+ task :update => :generate do
31
+ current_branch = `git branch --no-color`.
32
+ split("\n").
33
+ select { |line| line =~ /^\* / }.
34
+ first.to_s.
35
+ gsub(/\* (.*)/, '\1')
36
+ (puts "Unable to determine current branch"; exit(1) ) unless current_branch
37
+ system "git checkout web"
38
+ system "cp examples/tire-dsl.html index.html"
39
+ system "git add index.html && git co -m 'Updated Tire website'"
40
+ system "git push origin web:gh-pages -f"
41
+ system "git checkout #{current_branch}"
42
+ end
43
+
44
+ desc "Generate the Rocco documentation page"
45
+ task :generate do
46
+ system "rocco examples/tire-dsl.rb"
47
+ html = File.read('examples/tire-dsl.html').gsub!(/>tire\-dsl\.rb</, '>tire.rb<')
48
+ File.open('examples/tire-dsl.html', 'w') { |f| f.write html }
49
+ system "open examples/tire-dsl.html"
50
+ end
51
+ end
@@ -0,0 +1,263 @@
1
+ # ===================================================================================================================
2
+ # Template for generating a no-frills Rails application with support for Elasticsearch full-text search via Tire
3
+ # ===================================================================================================================
4
+ #
5
+ # This file creates a basic, fully working Rails application with support for Elasticsearch full-text search
6
+ # via the Tire gem [http://github.com/karmi/tire].
7
+ #
8
+ # You DON'T NEED ELASTICSEARCH INSTALLED, it is installed and launched automatically by this script.
9
+ #
10
+ # Requirements
11
+ # ------------
12
+ #
13
+ # * Git
14
+ # * Ruby >= 1.8.7
15
+ # * Rubygems
16
+ # * Rails >= 3.0.7
17
+ # * Sun Java 6 (for Elasticsearch)
18
+ #
19
+ #
20
+ # Usage
21
+ # -----
22
+ #
23
+ # $ rails new tired -m https://github.com/karmi/tire/raw/master/examples/rails-application-template.rb
24
+ #
25
+ # ===================================================================================================================
26
+
27
+ require 'rubygems'
28
+
29
+ begin
30
+ require 'restclient'
31
+ rescue LoadError
32
+ puts "\n"
33
+ say_status "ERROR", "Rubygem 'rest-client' not installed\n", :red
34
+ puts '-'*80
35
+ say_status "", "gem install rest-client"
36
+ puts "\n"
37
+
38
+ if yes?("Should I install it for you?", :bold)
39
+ say_status "gem", "install rest-client", :yellow
40
+ system "gem install rest-client"
41
+ else
42
+ exit(1)
43
+ end
44
+ end
45
+
46
+ at_exit do
47
+ pid = File.read("#{destination_root}/tmp/pids/elasticsearch.pid") rescue nil
48
+ if pid
49
+ say_status "Stop", "Elasticsearch", :yellow
50
+ run "kill #{pid}"
51
+ end
52
+ end
53
+
54
+ run "rm public/index.html"
55
+ run "rm public/images/rails.png"
56
+ run "touch tmp/.gitignore log/.gitignore vendor/.gitignore"
57
+
58
+ run "rm -f .gitignore"
59
+ file ".gitignore", <<-END.gsub(/ /, '')
60
+ .DS_Store
61
+ log/*.log
62
+ tmp/**/*
63
+ config/database.yml
64
+ db/*.sqlite3
65
+ vendor/elasticsearch-0.20.2/
66
+ END
67
+
68
+ git :init
69
+ git :add => '.'
70
+ git :commit => "-m 'Initial commit: Clean application'"
71
+
72
+ unless (RestClient.get('http://localhost:9200') rescue false)
73
+ COMMAND = <<-COMMAND.gsub(/^ /, '')
74
+ curl -k -L -# -o elasticsearch-0.20.2.tar.gz \
75
+ "http://github.com/downloads/elasticsearch/elasticsearch/elasticsearch-0.20.2.tar.gz"
76
+ tar -zxf elasticsearch-0.20.2.tar.gz
77
+ rm -f elasticsearch-0.20.2.tar.gz
78
+ ./elasticsearch-0.20.2/bin/elasticsearch -p #{destination_root}/tmp/pids/elasticsearch.pid
79
+ COMMAND
80
+
81
+ puts "\n"
82
+ say_status "ERROR", "Elasticsearch not running!\n", :red
83
+ puts '-'*80
84
+ say_status '', "It appears that Elasticsearch is not running on this machine."
85
+ say_status '', "Is it installed? Do you want me to install it for you with this command?\n\n"
86
+ COMMAND.each_line { |l| say_status '', "$ #{l}" }
87
+ puts
88
+ say_status '', "(To uninstall, just remove the generated application directory.)"
89
+ puts '-'*80, ''
90
+
91
+ if yes?("Install Elasticsearch?", :bold)
92
+ puts
93
+ say_status "Install", "Elasticsearch", :yellow
94
+
95
+ commands = COMMAND.split("\n")
96
+ exec = commands.pop
97
+ inside("vendor") do
98
+ commands.each { |command| run command }
99
+ run "(#{exec})" # Launch Elasticsearch in subshell
100
+ end
101
+ end
102
+ end
103
+
104
+ puts
105
+ say_status "Rubygems", "Adding Rubygems into Gemfile...\n", :yellow
106
+ puts '-'*80, ''; sleep 1
107
+
108
+ gem 'tire', :git => 'git://github.com/karmi/tire.git'
109
+ gem 'will_paginate', '~> 3.0'
110
+
111
+ git :add => '.'
112
+ git :commit => "-m 'Added gems'"
113
+
114
+ puts
115
+ say_status "Rubygems", "Installing Rubygems...", :yellow
116
+ puts '-'*80, ''
117
+
118
+ puts "********************************************************************************"
119
+ puts " Running `bundle install`. Let's watch a movie!"
120
+ puts "********************************************************************************", ""
121
+
122
+ run "bundle install"
123
+
124
+ puts
125
+ say_status "Model", "Adding the Article resource...", :yellow
126
+ puts '-'*80, ''; sleep 1
127
+
128
+ generate :scaffold, "Article title:string content:text published_on:date"
129
+ route "root :to => 'articles#index'"
130
+ rake "db:migrate"
131
+
132
+ git :add => '.'
133
+ git :commit => "-m 'Added the Article resource'"
134
+
135
+ puts
136
+ say_status "Database", "Seeding the database with data...", :yellow
137
+ puts '-'*80, ''; sleep 0.25
138
+
139
+ run "rm -f db/seeds.rb"
140
+ file 'db/seeds.rb', %q{
141
+ contents = [
142
+ 'Lorem ipsum dolor sit amet.',
143
+ 'Consectetur adipisicing elit, sed do eiusmod tempor incididunt.',
144
+ 'Labore et dolore magna aliqua.',
145
+ 'Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris.',
146
+ 'Excepteur sint occaecat cupidatat non proident.'
147
+ ]
148
+
149
+ puts "Deleting all articles..."
150
+ Article.delete_all
151
+
152
+ unless ENV['COUNT']
153
+
154
+ puts "Creating articles..."
155
+ %w[ One Two Three Four Five ].each_with_index do |title, i|
156
+ Article.create :title => title, :content => contents[i], :published_on => i.days.ago.utc
157
+ end
158
+
159
+ else
160
+
161
+ puts "Creating 10,000 articles..."
162
+ (1..ENV['COUNT'].to_i).each_with_index do |title, i|
163
+ Article.create :title => "Title #{title}", :content => 'Lorem', :published_on => i.days.ago.utc
164
+ print '.'
165
+ end
166
+
167
+ end
168
+ }
169
+
170
+ rake "db:seed"
171
+
172
+ git :add => "db/seeds.rb"
173
+ git :commit => "-m 'Added database seeding script'"
174
+
175
+ puts
176
+ say_status "Model", "Adding search support into the Article model...", :yellow
177
+ puts '-'*80, ''; sleep 1
178
+
179
+ run "rm -f app/models/article.rb"
180
+ file 'app/models/article.rb', <<-CODE
181
+ class Article < ActiveRecord::Base
182
+ include Tire::Model::Search
183
+ include Tire::Model::Callbacks
184
+
185
+ attr_accessible :title, :content, :published_on
186
+ end
187
+ CODE
188
+
189
+ initializer 'tire.rb', <<-CODE
190
+ Tire.configure do
191
+ logger STDERR
192
+ end
193
+ CODE
194
+
195
+ git :commit => "-a -m 'Added Tire support into the Article class and an initializer'"
196
+
197
+ puts
198
+ say_status "Controller", "Adding controller action, route, and HTML for search...", :yellow
199
+ puts '-'*80, ''; sleep 1
200
+
201
+ gsub_file 'app/controllers/articles_controller.rb', %r{# GET /articles/1$}, <<-CODE
202
+ # GET /articles/search
203
+ def search
204
+ @articles = Article.search params[:q]
205
+
206
+ render :action => "index"
207
+ end
208
+
209
+ # GET /articles/1
210
+ CODE
211
+
212
+ gsub_file 'app/views/articles/index.html.erb', %r{<h1>Listing articles</h1>}, <<-CODE
213
+ <h1>Listing articles</h1>
214
+
215
+ <%= form_tag search_articles_path, :method => 'get' do %>
216
+ <%= label_tag :query %>
217
+ <%= text_field_tag :q, params[:q] %>
218
+ <%= submit_tag :search %>
219
+ <% end %>
220
+
221
+ <hr>
222
+ CODE
223
+
224
+ gsub_file 'app/views/articles/index.html.erb', %r{<%= link_to 'New Article', new_article_path %>}, <<-CODE
225
+ <%= link_to 'New Article', new_article_path %>
226
+ <%= link_to 'Back', articles_path if params[:q] %>
227
+ CODE
228
+
229
+ gsub_file 'config/routes.rb', %r{resources :articles}, <<-CODE
230
+ resources :articles do
231
+ collection { get :search }
232
+ end
233
+ CODE
234
+
235
+ git :commit => "-a -m 'Added Tire support into the frontend of application'"
236
+
237
+ puts
238
+ say_status "Index", "Indexing the database...", :yellow
239
+ puts '-'*80, ''; sleep 0.5
240
+
241
+ rake "environment tire:import CLASS='Article' FORCE=true"
242
+
243
+ puts
244
+ say_status "Git", "Details about the application:", :yellow
245
+ puts '-'*80, ''
246
+
247
+ run "git log --reverse --pretty=format:'%Cblue%h%Creset | %s'"
248
+
249
+ if (begin; RestClient.get('http://localhost:3000'); rescue Errno::ECONNREFUSED; false; rescue Exception; true; end)
250
+ puts "\n"
251
+ say_status "ERROR", "Some other application is running on port 3000!\n", :red
252
+ puts '-'*80
253
+
254
+ port = ask("Please provide free port:", :bold)
255
+ else
256
+ port = '3000'
257
+ end
258
+
259
+ puts "", "="*80
260
+ say_status "DONE", "\e[1mStarting the application. Open http://localhost:#{port}\e[0m", :yellow
261
+ puts "="*80, ""
262
+
263
+ run "rails server --port=#{port}"
@@ -0,0 +1,932 @@
1
+ # encoding: UTF-8
2
+ #
3
+ # **Tire** provides rich and comfortable Ruby API for the
4
+ # [_Elasticsearch_](http://www.elasticsearch.org/) search engine/database.
5
+ #
6
+ # _Elasticsearch_ is a scalable, distributed, cloud-ready, highly-available
7
+ # full-text search engine and database, communicating by JSON over RESTful HTTP,
8
+ # based on [Lucene](http://lucene.apache.org/), written in Java.
9
+ #
10
+ # <img src="http://github.com/favicon.ico" style="position:relative; top:2px">
11
+ # _Tire_ is open source, and you can download or clone the source code
12
+ # from <https://github.com/karmi/tire>.
13
+ #
14
+ # By following these instructions you should have the search running
15
+ # on a sane operating system in less then 10 minutes.
16
+
17
+ # Note, that this file can be executed directly:
18
+ #
19
+ # ruby -I lib examples/tire-dsl.rb
20
+ #
21
+
22
+
23
+ #### Installation
24
+
25
+ # Install _Tire_ with _Rubygems_:
26
+
27
+ #
28
+ # gem install tire
29
+ #
30
+ require 'rubygems'
31
+
32
+ # _Tire_ uses the [_multi_json_](https://github.com/intridea/multi_json) gem as a generic JSON library.
33
+ # We want to use the [_yajl-ruby_](https://github.com/brianmario/yajl-ruby) gem in its full on mode here.
34
+ #
35
+ require 'yajl/json_gem'
36
+
37
+ # Now, let's require the _Tire_ gem itself, and we're ready to go.
38
+ #
39
+ require 'tire'
40
+
41
+ #### Prerequisites
42
+
43
+ # We'll need a working and running _Elasticsearch_ server, of course. Thankfully, that's easy.
44
+ ( puts <<-"INSTALL" ; exit(1) ) unless (RestClient.get('http://localhost:9200') rescue false)
45
+
46
+ [ERROR] You don’t appear to have Elasticsearch installed. Please install and launch it with the following commands:
47
+
48
+ curl -k -L -o elasticsearch-0.20.2.tar.gz http://github.com/downloads/elasticsearch/elasticsearch/elasticsearch-0.20.2.tar.gz
49
+ tar -zxvf elasticsearch-0.20.2.tar.gz
50
+ ./elasticsearch-0.20.2/bin/elasticsearch -f
51
+ INSTALL
52
+
53
+ ### Storing and indexing documents
54
+
55
+ # Let's initialize an index named “articles”.
56
+ #
57
+ Tire.index 'articles' do
58
+ # To make sure it's fresh, let's delete any existing index with the same name.
59
+ #
60
+ delete
61
+ # And then, let's create it.
62
+ #
63
+ create
64
+
65
+ # We want to store and index some articles with `title`, `tags` and `published_on` properties.
66
+ # Simple Hashes are OK. The default type is „document”.
67
+ #
68
+ store :title => 'One', :tags => ['ruby'], :published_on => '2011-01-01'
69
+ store :title => 'Two', :tags => ['ruby', 'python'], :published_on => '2011-01-02'
70
+
71
+ # We usually want to set a specific _type_ for the document in _Elasticsearch_.
72
+ # Simply setting a `type` property is OK.
73
+ #
74
+ store :type => 'article',
75
+ :title => 'Three',
76
+ :tags => ['java'],
77
+ :published_on => '2011-01-02'
78
+
79
+ # We may want to wrap your data in a Ruby class, and use it when storing data.
80
+ # The contract required of such a class is very simple.
81
+ #
82
+ class Article
83
+
84
+ #
85
+ attr_reader :title, :tags, :published_on
86
+ def initialize(attributes={})
87
+ @attributes = attributes
88
+ @attributes.each_pair { |name,value| instance_variable_set :"@#{name}", value }
89
+ end
90
+
91
+ # It must provide a `type`, `_type` or `document_type` method for propper mapping.
92
+ #
93
+ def type
94
+ 'article'
95
+ end
96
+
97
+ # And it must provide a `to_indexed_json` method for conversion to JSON.
98
+ #
99
+ def to_indexed_json
100
+ @attributes.to_json
101
+ end
102
+ end
103
+
104
+ # Note: Since our class takes a Hash of attributes on initialization, we may even
105
+ # wrap the results in instances of this class; we'll see how to do that further below.
106
+ #
107
+ article = Article.new :title => 'Four',
108
+ :tags => ['ruby', 'php'],
109
+ :published_on => '2011-01-03'
110
+
111
+ # Let's store the `article`, now.
112
+ #
113
+ store article
114
+
115
+ # And let's „force refresh“ the index, so we can query it immediately.
116
+ #
117
+ refresh
118
+ end
119
+
120
+ # We may want to define a specific [mapping](http://www.elasticsearch.org/guide/reference/api/admin-indices-create-index.html)
121
+ # for the index.
122
+
123
+ Tire.index 'articles' do
124
+ delete
125
+
126
+ # To do so, let's just pass a Hash containing the specified mapping to the `Index#create` method.
127
+ #
128
+ create :mappings => {
129
+
130
+ # Let's specify for which _type_ of documents this mapping should be used:
131
+ # „article”, in our case.
132
+ #
133
+ :article => {
134
+ :properties => {
135
+
136
+ # Let's specify the type of the field, whether it should be analyzed, ...
137
+ #
138
+ :id => { :type => 'string', :index => 'not_analyzed', :include_in_all => false },
139
+
140
+ # ... set the boost or analyzer settings for the field, etc. The _Elasticsearch_ guide
141
+ # has [more information](http://elasticsearch.org/guide/reference/mapping/index.html).
142
+ # Don't forget, that proper mapping is key to efficient and effective search.
143
+ # But don't fret about getting the mapping right the first time, you won't.
144
+ # In most cases, the default, dynamic mapping is just fine for prototyping.
145
+ #
146
+ :title => { :type => 'string', :analyzer => 'snowball', :boost => 2.0 },
147
+ :tags => { :type => 'string', :analyzer => 'keyword' },
148
+ :content => { :type => 'string', :analyzer => 'czech' }
149
+ }
150
+ }
151
+ }
152
+ end
153
+
154
+ #### Bulk Indexing
155
+
156
+ # Of course, we may have large amounts of data, and adding them to the index one by one really isn't the best idea.
157
+ # We can use _Elasticsearch's_ [bulk API](http://www.elasticsearch.org/guide/reference/api/bulk.html)
158
+ # for importing the data.
159
+
160
+ # So, for demonstration purposes, let's suppose we have a simple collection of hashes to store.
161
+ #
162
+ articles = [
163
+
164
+ # Notice that such objects must have an `id` property!
165
+ #
166
+ { :id => '1', :type => 'article', :title => 'one', :tags => ['ruby'], :published_on => '2011-01-01' },
167
+
168
+ # And, of course, they should contain the `type` property for the mapping to work!
169
+ #
170
+ { :id => '2', :type => 'article', :title => 'two', :tags => ['ruby', 'python'], :published_on => '2011-01-02' },
171
+ { :id => '3', :type => 'article', :title => 'three', :tags => ['java'], :published_on => '2011-01-02' },
172
+ { :id => '4', :type => 'article', :title => 'four', :tags => ['ruby', 'php'], :published_on => '2011-01-03' }
173
+ ]
174
+
175
+ # We can just push them into the index in one go.
176
+ #
177
+ Tire.index 'articles' do
178
+ import articles
179
+ end
180
+
181
+ # Of course, we can easily manipulate the documents before storing them in the index.
182
+ #
183
+ Tire.index 'articles' do
184
+ # ... by passing a block to the `import` method. The collection will
185
+ # be available in the block argument.
186
+ #
187
+ import articles do |documents|
188
+
189
+ # We will capitalize every _title_ and return the manipulated collection
190
+ # back to the `import` method.
191
+ #
192
+ documents.map { |document| document.update(:title => document[:title].capitalize) }
193
+ end
194
+
195
+ refresh
196
+ end
197
+
198
+ ### Searching
199
+
200
+ # With the documents indexed and stored in the _Elasticsearch_ database, we can search them, finally.
201
+ #
202
+ # _Tire_ exposes the search interface via simple domain-specific language.
203
+
204
+ #### Simple Query String Searches
205
+
206
+ # We can do simple searches, like searching for articles containing “One” in their title.
207
+ #
208
+ s = Tire.search('articles') do
209
+ query do
210
+ string "title:one"
211
+ end
212
+ end
213
+
214
+ # The results:
215
+ # * One [tags: ruby]
216
+ #
217
+ s.results.each do |document|
218
+ puts "* #{ document.title } [tags: #{document.tags.join(', ')}]"
219
+ end
220
+
221
+ # Or, we can search for articles published between January, 1st and January, 2nd.
222
+ #
223
+ s = Tire.search('articles') do
224
+ query do
225
+ string "published_on:[2011-01-01 TO 2011-01-02]"
226
+ end
227
+ end
228
+
229
+ # The results:
230
+ # * One [published: 2011-01-01]
231
+ # * Two [published: 2011-01-02]
232
+ # * Three [published: 2011-01-02]
233
+ #
234
+ s.results.each do |document|
235
+ puts "* #{ document.title } [published: #{document.published_on}]"
236
+ end
237
+
238
+ # Notice, that we can access local variables from the _enclosing scope_.
239
+ # (Of course, we may write the blocks in shorter notation.)
240
+
241
+ # We will define the query in a local variable named `q`...
242
+ #
243
+ q = "title:T*"
244
+ # ... and we can use it inside the `query` block.
245
+ #
246
+ s = Tire.search('articles') { query { string q } }
247
+
248
+ # The results:
249
+ # * Two [tags: ruby, python]
250
+ # * Three [tags: java]
251
+ #
252
+ s.results.each do |document|
253
+ puts "* #{ document.title } [tags: #{document.tags.join(', ')}]"
254
+ end
255
+
256
+ # Often, we need to access variables or methods defined in the _outer scope_.
257
+ # To do that, we have to use a slight variation of the DSL.
258
+ #
259
+
260
+ # Let's assume we have a plain Ruby class, named `Article`.
261
+ #
262
+ class Article
263
+
264
+ # We will define the query in a class method...
265
+ #
266
+ def self.q
267
+ "title:T*"
268
+ end
269
+
270
+ # ... and wrap the _Tire_ search method in another one.
271
+ def self.search
272
+
273
+ # Notice how we pass the `search` object around as a block argument.
274
+ #
275
+ Tire.search('articles') do |search|
276
+
277
+ # And we pass the query object in a similar matter.
278
+ #
279
+ search.query do |query|
280
+
281
+ # Which means we can access the `q` class method.
282
+ #
283
+ query.string self.q
284
+ end
285
+ end.results
286
+ end
287
+ end
288
+
289
+ # We may use any valid [Lucene query syntax](http://lucene.apache.org/java/3_0_3/queryparsersyntax.html)
290
+ # for the `query_string` queries.
291
+
292
+ # For debugging our queries, we can display the JSON which is being sent to _Elasticsearch_.
293
+ #
294
+ # {"query":{"query_string":{"query":"title:T*"}}}
295
+ #
296
+ puts "", "Query:", "-"*80
297
+ puts s.to_json
298
+
299
+ # Or better yet, we may display a complete `curl` command to recreate the request in terminal,
300
+ # so we can see the naked response, tweak request parameters and meditate on problems.
301
+ #
302
+ # curl -X POST "http://localhost:9200/articles/_search?pretty=true" \
303
+ # -d '{"query":{"query_string":{"query":"title:T*"}}}'
304
+ #
305
+ puts "", "Try the query in Curl:", "-"*80
306
+ puts s.to_curl
307
+
308
+
309
+ ### Logging
310
+
311
+ # For debugging more complex situations, we can enable logging, so requests and responses
312
+ # will be logged using this `curl`-friendly format.
313
+
314
+ Tire.configure do
315
+
316
+ # By default, at the _info_ level, only the `curl`-format of request and
317
+ # basic information about the response will be logged:
318
+ #
319
+ # # 2011-04-24 11:34:01:150 [CREATE] ("articles")
320
+ # #
321
+ # curl -X POST "http://localhost:9200/articles"
322
+ #
323
+ # # 2011-04-24 11:34:01:152 [200]
324
+ #
325
+ logger 'elasticsearch.log'
326
+
327
+ # For debugging, we can switch to the _debug_ level, which will log the complete JSON responses.
328
+ #
329
+ # That's very convenient if we want to post a recreation of some problem or solution
330
+ # to the mailing list, IRC channel, etc.
331
+ #
332
+ logger 'elasticsearch.log', :level => 'debug'
333
+
334
+ # Note that we can pass any [`IO`](http://www.ruby-doc.org/core/classes/IO.html)-compatible Ruby object as a logging device.
335
+ #
336
+ logger STDERR
337
+ end
338
+
339
+ ### Configuration
340
+
341
+ # As we have just seen with logging, we can configure various parts of _Tire_.
342
+ #
343
+ Tire.configure do
344
+
345
+ # First of all, we can configure the URL for _Elasticsearch_.
346
+ #
347
+ url "http://search.example.com"
348
+
349
+ # Second, we may want to wrap the result items in our own class, for instance
350
+ # the `Article` class set above.
351
+ #
352
+ wrapper Article
353
+
354
+ # Finally, we can reset one or all configuration settings to their defaults.
355
+ #
356
+ reset :url
357
+ reset
358
+
359
+ end
360
+
361
+
362
+ ### Complex Searching
363
+
364
+ # Query strings are convenient for simple searches, but we may want to define our queries more expressively,
365
+ # using the _Elasticsearch_ [Query DSL](http://www.elasticsearch.org/guide/reference/query-dsl/index.html).
366
+ #
367
+ s = Tire.search('articles') do
368
+
369
+ # Let's suppose we want to search for articles with specific _tags_, in our case “ruby” _or_ “python”.
370
+ #
371
+ query do
372
+
373
+ # That's a great excuse to use a [_terms_](http://elasticsearch.org/guide/reference/query-dsl/terms-query.html)
374
+ # query.
375
+ #
376
+ terms :tags, ['ruby', 'python']
377
+ end
378
+ end
379
+
380
+ # The search, as expected, returns three articles, all tagged “ruby” — among other tags:
381
+ #
382
+ # * Two [tags: ruby, python]
383
+ # * One [tags: ruby]
384
+ # * Four [tags: ruby, php]
385
+ #
386
+ s.results.each do |document|
387
+ puts "* #{ document.title } [tags: #{document.tags.join(', ')}]"
388
+ end
389
+
390
+ # What if we wanted to search for articles tagged both “ruby” _and_ “python”?
391
+ #
392
+ s = Tire.search('articles') do
393
+ query do
394
+
395
+ # That's a great excuse to specify `minimum_match` for the query.
396
+ #
397
+ terms :tags, ['ruby', 'python'], :minimum_match => 2
398
+ end
399
+ end
400
+
401
+ # The search, as expected, returns one article, tagged with _both_ “ruby” and “python”:
402
+ #
403
+ # * Two [tags: ruby, python]
404
+ #
405
+ s.results.each do |document|
406
+ puts "* #{ document.title } [tags: #{document.tags.join(', ')}]"
407
+ end
408
+
409
+ #### Boolean Queries
410
+
411
+ # Quite often, we need complex queries with boolean logic.
412
+ # Instead of composing long query strings such as `tags:ruby OR tags:java AND NOT tags:python`,
413
+ # we can use the [_bool_](http://www.elasticsearch.org/guide/reference/query-dsl/bool-query.html)
414
+ # query.
415
+
416
+ s = Tire.search('articles') do
417
+ query do
418
+
419
+ # In _Tire_, we can build `bool` queries declaratively, as usual.
420
+ boolean do
421
+
422
+ # Let's define a `should` (`OR`) query for _ruby_,
423
+ #
424
+ should { string 'tags:ruby' }
425
+
426
+ # as well as for _java_,
427
+ should { string 'tags:java' }
428
+
429
+ # while defining a `must_not` (`AND NOT`) query for _python_.
430
+ must_not { string 'tags:python' }
431
+ end
432
+ end
433
+ end
434
+
435
+ # The search returns these documents:
436
+ #
437
+ # * One [tags: ruby]
438
+ # * Three [tags: java]
439
+ # * Four [tags: ruby, php]
440
+ #
441
+ s.results.each do |document|
442
+ puts "* #{ document.title } [tags: #{document.tags.join(', ')}]"
443
+ end
444
+
445
+ # The best thing about `boolean` queries is that we can very easily save these partial queries as Ruby blocks,
446
+ # to mix and reuse them later, since we can call the `boolean` method multiple times.
447
+ #
448
+
449
+ # Let's define the query for the _tags_ property,
450
+ #
451
+ tags_query = lambda do |boolean|
452
+ boolean.should { string 'tags:ruby' }
453
+ boolean.should { string 'tags:java' }
454
+ end
455
+
456
+ # ... and a query for the _published_on_ property.
457
+ published_on_query = lambda do |boolean|
458
+ boolean.must { string 'published_on:[2011-01-01 TO 2011-01-02]' }
459
+ end
460
+
461
+ # Now, we can use the `tags_query` on its own.
462
+ #
463
+ Tire.search('articles') { query { boolean &tags_query } }
464
+
465
+ # Or, we can combine it with the `published_on` query.
466
+ #
467
+ Tire.search('articles') do
468
+ query do
469
+ boolean &tags_query
470
+ boolean &published_on_query
471
+ end
472
+ end
473
+
474
+ # _Elasticsearch_ supports many types of [queries](http://www.elasticsearch.org/guide/reference/query-dsl/).
475
+ #
476
+ # Eventually, _Tire_ will support all of them. So far, only these are supported:
477
+ #
478
+ # * [string](http://www.elasticsearch.org/guide/reference/query-dsl/query-string-query.html)
479
+ # * [text](http://www.elasticsearch.org/guide/reference/query-dsl/text-query.html)
480
+ # * [term](http://elasticsearch.org/guide/reference/query-dsl/term-query.html)
481
+ # * [terms](http://elasticsearch.org/guide/reference/query-dsl/terms-query.html)
482
+ # * [bool](http://www.elasticsearch.org/guide/reference/query-dsl/bool-query.html)
483
+ # * [custom_score](http://www.elasticsearch.org/guide/reference/query-dsl/custom-score-query.html)
484
+ # * [fuzzy](http://www.elasticsearch.org/guide/reference/query-dsl/fuzzy-query.html)
485
+ # * [all](http://www.elasticsearch.org/guide/reference/query-dsl/match-all-query.html)
486
+ # * [ids](http://www.elasticsearch.org/guide/reference/query-dsl/ids-query.html)
487
+
488
+ #### Faceted Search
489
+
490
+ # _Elasticsearch_ makes it trivial to retrieve complex aggregated data from our index/database,
491
+ # so called [_facets_](http://www.elasticsearch.org/guide/reference/api/search/facets/index.html).
492
+
493
+ # Let's say we want to display article counts for every tag in the database.
494
+ # For that, we'll use a _terms_ facet.
495
+
496
+ #
497
+ s = Tire.search 'articles' do
498
+
499
+ # We will search for articles whose title begins with letter “T”,
500
+ #
501
+ query { string 'title:T*' }
502
+
503
+ # and retrieve the counts “bucketed” by `tags`.
504
+ #
505
+ facet 'tags' do
506
+ terms :tags
507
+ end
508
+ end
509
+
510
+ # As we see, our query has found two articles, and if you recall our articles from above,
511
+ # _Two_ is tagged with “ruby” and “python”, while _Three_ is tagged with “java”.
512
+ #
513
+ # Found 2 articles: Three, Two
514
+ #
515
+ # The counts shouldn't surprise us:
516
+ #
517
+ # Counts by tag:
518
+ # -------------------------
519
+ # ruby 1
520
+ # python 1
521
+ # java 1
522
+ #
523
+ puts "Found #{s.results.count} articles: #{s.results.map(&:title).join(', ')}"
524
+ puts "Counts by tag:", "-"*25
525
+ s.results.facets['tags']['terms'].each do |f|
526
+ puts "#{f['term'].ljust(10)} #{f['count']}"
527
+ end
528
+
529
+ # These counts are based on the scope of our current query.
530
+ # What if we wanted to display aggregated counts by `tags` across the whole database?
531
+
532
+ #
533
+ s = Tire.search 'articles' do
534
+
535
+ # Let's repeat the search for “T”...
536
+ #
537
+ query { string 'title:T*' }
538
+
539
+ facet 'global-tags', :global => true do
540
+
541
+ # ...but set the `global` scope for the facet in this case.
542
+ #
543
+ terms :tags
544
+ end
545
+
546
+ # We can even _combine_ facets scoped to the current query
547
+ # with globally scoped facets — we'll just use a different name.
548
+ #
549
+ facet 'current-tags' do
550
+ terms :tags
551
+ end
552
+ end
553
+
554
+ # Aggregated results for the current query are the same as previously:
555
+ #
556
+ # Current query facets:
557
+ # -------------------------
558
+ # ruby 1
559
+ # python 1
560
+ # java 1
561
+ #
562
+ puts "Current query facets:", "-"*25
563
+ s.results.facets['current-tags']['terms'].each do |f|
564
+ puts "#{f['term'].ljust(10)} #{f['count']}"
565
+ end
566
+
567
+ # On the other hand, aggregated results for the global scope include also
568
+ # tags for articles not matched by the query, such as “java” or “php”:
569
+ #
570
+ # Global facets:
571
+ # -------------------------
572
+ # ruby 3
573
+ # python 1
574
+ # php 1
575
+ # java 1
576
+ #
577
+ puts "Global facets:", "-"*25
578
+ s.results.facets['global-tags']['terms'].each do |f|
579
+ puts "#{f['term'].ljust(10)} #{f['count']}"
580
+ end
581
+
582
+ # _Elasticsearch_ supports many advanced types of facets, such as those for computing statistics or geographical distance.
583
+ #
584
+ # Eventually, _Tire_ will support all of them. So far, only these are supported:
585
+ #
586
+ # * [terms](http://www.elasticsearch.org/guide/reference/api/search/facets/terms-facet.html)
587
+ # * [date](http://www.elasticsearch.org/guide/reference/api/search/facets/date-histogram-facet.html)
588
+ # * [range](http://www.elasticsearch.org/guide/reference/api/search/facets/range-facet.html)
589
+ # * [histogram](http://www.elasticsearch.org/guide/reference/api/search/facets/histogram-facet.html)
590
+ # * [statistical](http://www.elasticsearch.org/guide/reference/api/search/facets/statistical-facet.html)
591
+ # * [terms_stats](http://www.elasticsearch.org/guide/reference/api/search/facets/terms-stats-facet.html)
592
+ # * [query](http://www.elasticsearch.org/guide/reference/api/search/facets/query-facet.html)
593
+
594
+ # We have seen that _Elasticsearch_ facets enable us to fetch complex aggregations from our data.
595
+ #
596
+ # They are frequently used for another feature, „faceted navigation“.
597
+ # We can be combine query and facets with
598
+ # [filters](http://elasticsearch.org/guide/reference/api/search/filter.html),
599
+ # so the returned documents are restricted by certain criteria — for example to a specific category —,
600
+ # but the aggregation calculations are still based on the original query.
601
+
602
+
603
+ #### Filtered Search
604
+
605
+ # So, let's make our search a bit more complex. Let's search for articles whose titles begin
606
+ # with letter “T”, again, but filter the results, so only the articles tagged “ruby”
607
+ # are returned.
608
+ #
609
+ s = Tire.search 'articles' do
610
+
611
+ # We will use just the same **query** as before.
612
+ #
613
+ query { string 'title:T*' }
614
+
615
+ # But we will add a _terms_ **filter** based on tags.
616
+ #
617
+ filter :terms, :tags => ['ruby']
618
+
619
+ # And, of course, our facet definition.
620
+ #
621
+ facet('tags') { terms :tags }
622
+
623
+ end
624
+
625
+ # We see that only the article _Two_ (tagged “ruby” and “python”) is returned,
626
+ # _not_ the article _Three_ (tagged “java”):
627
+ #
628
+ # * Two [tags: ruby, python]
629
+ #
630
+ s.results.each do |document|
631
+ puts "* #{ document.title } [tags: #{document.tags.join(', ')}]"
632
+ end
633
+
634
+ # The _count_ for article _Three_'s tags, “java”, on the other hand, _is_ in fact included:
635
+ #
636
+ # Counts by tag:
637
+ # -------------------------
638
+ # ruby 1
639
+ # python 1
640
+ # java 1
641
+ #
642
+ puts "Counts by tag:", "-"*25
643
+ s.results.facets['tags']['terms'].each do |f|
644
+ puts "#{f['term'].ljust(10)} #{f['count']}"
645
+ end
646
+
647
+ #### Sorting
648
+
649
+ # By default, the results are sorted according to their relevancy.
650
+ #
651
+ s = Tire.search('articles') { query { string 'tags:ruby' } }
652
+
653
+ s.results.each do |document|
654
+ puts "* #{ document.title } " +
655
+ "[tags: #{document.tags.join(', ')}; " +
656
+
657
+ # The score is available as the `_score` property.
658
+ #
659
+ "score: #{document._score}]"
660
+ end
661
+
662
+ # The results:
663
+ #
664
+ # * One [tags: ruby; score: 0.30685282]
665
+ # * Four [tags: ruby, php; score: 0.19178301]
666
+ # * Two [tags: ruby, python; score: 0.19178301]
667
+
668
+ # But, what if we want to sort the results based on some other criteria,
669
+ # such as published date or product price? We can do that.
670
+ #
671
+ s = Tire.search 'articles' do
672
+
673
+ # We will search for articles tagged “ruby”, again, ...
674
+ #
675
+ query { string 'tags:ruby' }
676
+
677
+ # ... but will sort them by their `title`, in descending order.
678
+ #
679
+ sort { by :title, 'desc' }
680
+ end
681
+
682
+ # The results:
683
+ #
684
+ # * Two
685
+ # * One
686
+ # * Four
687
+ #
688
+ s.results.each do |document|
689
+ puts "* #{ document.title }"
690
+ end
691
+
692
+ # Of course, it's possible to combine more fields in the sorting definition.
693
+
694
+ s = Tire.search 'articles' do
695
+
696
+ # We will just get all articles in this case.
697
+ #
698
+ query { all }
699
+
700
+ sort do
701
+
702
+ # We will sort the results by their `published_on` property in _ascending_ order (the default),
703
+ #
704
+ by :published_on
705
+
706
+ # and by their `title` property, in _descending_ order.
707
+ #
708
+ by :title, 'desc'
709
+ end
710
+ end
711
+
712
+ # The results:
713
+ #
714
+ # * One (Published on: 2011-01-01)
715
+ # * Two (Published on: 2011-01-02)
716
+ # * Three (Published on: 2011-01-02)
717
+ # * Four (Published on: 2011-01-03)
718
+ #
719
+ s.results.each do |document|
720
+ puts "* #{ document.title.ljust(10) } (Published on: #{ document.published_on })"
721
+ end
722
+
723
+ #### Nested Documents and Queries
724
+
725
+ # Often, we want to store more complex entities in _Elasticsearch_;
726
+ # for example, we may want to store the information about comments for each article,
727
+ # and then search for articles where a certain person left a certain note.
728
+
729
+ # In the simplest case, we can store the comments as an Array of JSON documents in the
730
+ # article document. If we do that naively, our search results will be incorrect, though.
731
+ # That's because a match in just one field will be enough to match a document.
732
+ # We need to query parts of the document as if they were separate entities.
733
+
734
+ # _Elasticsearch_ provides a specific `nested`
735
+ # [field type](http://www.elasticsearch.org/guide/reference/mapping/nested-type.html) and
736
+ # [query](http://www.elasticsearch.org/guide/reference/query-dsl/nested-query.html)
737
+ # for working with "embedded" documents like these.
738
+
739
+ # So, let's update the mapping for the index first, adding the `comments` property as a `nested` type:
740
+ #
741
+ Tire::Configuration.client.put Tire.index('articles').url+'/article/_mapping',
742
+ { :article => { :properties => { :comments => { :type => 'nested' } } } }.to_json
743
+
744
+ # And let's add comments to articles (notice that both articles contain a comment with the _Cool!_ message,
745
+ # though by different authors):
746
+ #
747
+ Tire.index 'articles' do
748
+ update :article, 1,
749
+ :doc => { :comments => [ { :author => 'John', :message => 'Great!' }, { :author => 'Bob', :message => 'Cool!' } ] }
750
+ update :article, 2,
751
+ :doc => { :comments => [ { :author => 'John', :message => 'Cool!' }, { :author => 'Mary', :message => 'Thanks!' } ] }
752
+ refresh
753
+ end
754
+
755
+ # We'll use the `nested` query to search for articles where _John_ left a _"Cool"_ message:
756
+ #
757
+ s = Tire.search 'articles' do
758
+ query do
759
+ nested :path => 'comments' do
760
+ query do
761
+ match 'comments.author', 'John'
762
+ match 'comments.message', 'cool'
763
+ end
764
+ end
765
+ end
766
+ end
767
+
768
+ # The results contain just the second document, correctly:
769
+ #
770
+ # * Two (comments: 2)
771
+ #
772
+ s.results.each do |document|
773
+ puts "* #{ document.title } (comments: #{document.comments.size})"
774
+ end
775
+
776
+
777
+ #### Highlighting
778
+
779
+ # Often, we want to highlight the snippets matching our query in the displayed results.
780
+ # _Elasticsearch_ provides rich
781
+ # [highlighting](http://www.elasticsearch.org/guide/reference/api/search/highlighting.html)
782
+ # features, and _Tire_ makes them trivial to use.
783
+ #
784
+ s = Tire.search 'articles' do
785
+
786
+ # Let's search for documents containing word “Two” in their titles,
787
+ query { string 'title:Two' }
788
+
789
+ # and instruct _Elasticsearch_ to highlight relevant snippets.
790
+ #
791
+ highlight :title
792
+ end
793
+
794
+ # The results:
795
+ # Title: Two; Highlighted: <em>Two</em>
796
+ #
797
+ s.results.each do |document|
798
+ puts "Title: #{ document.title }; Highlighted: #{document.highlight.title}"
799
+ end
800
+
801
+ # We can configure many options for highlighting, such as:
802
+ #
803
+ s = Tire.search 'articles' do
804
+ query { string 'title:Two' }
805
+
806
+ # • specify the fields to highlight
807
+ #
808
+ highlight :title, :body
809
+
810
+ # • specify their individual options
811
+ #
812
+ highlight :title, :body => { :number_of_fragments => 0 }
813
+
814
+ # • or specify global highlighting options, such as the wrapper tag
815
+ #
816
+ highlight :title, :body, :options => { :tag => '<strong class="highlight">' }
817
+ end
818
+
819
+ #### Percolation
820
+
821
+ # _Elasticsearch_ comes with one very interesting, and rather unique feature:
822
+ # [_percolation_](http://www.elasticsearch.org/guide/reference/api/percolate.html).
823
+
824
+ # It works in a „reverse search“ manner to regular search workflow of adding
825
+ # documents to the index and then querying them.
826
+ # Percolation allows us to register a query, and ask if a specific document
827
+ # matches it, either on demand, or immediately as the document is being indexed.
828
+
829
+ # Let's review an example for an index named _weather_.
830
+ # We will register three queries for percolation against this index.
831
+ #
832
+ index = Tire.index('weather') do
833
+ delete
834
+ create
835
+
836
+ # First, a query named _warning_,
837
+ #
838
+ register_percolator_query('warning', :tags => ['warning']) { string 'warning OR severe OR extreme' }
839
+
840
+ # a query named _tsunami_,
841
+ #
842
+ register_percolator_query('tsunami', :tags => ['tsunami']) { string 'tsunami' }
843
+
844
+ # and a query named _floods_.
845
+ #
846
+ register_percolator_query('floods', :tags => ['floods']) { string 'flood*' }
847
+
848
+ end
849
+
850
+ # Notice, that we have added a _tags_ field to the query document, because it behaves
851
+ # just like any other document in _Elasticsearch_.
852
+
853
+ # We will refresh the `_percolator` index for immediate access.
854
+ #
855
+ Tire.index('_percolator').refresh
856
+
857
+ # Now, let's _percolate_ a document containing some trigger words against all registered queries.
858
+ #
859
+ tire_matches = index.percolate(:message => '[Warning] Extreme flooding expected after tsunami wave.')
860
+
861
+ # The result will contain, unsurprisingly, names of all the three registered queries:
862
+ #
863
+ # Matching queries: ["floods", "tsunami", "warning"]
864
+ #
865
+ puts "Matching queries: " + tire_matches.inspect
866
+
867
+ # We can filter the executed queries with a regular _Elasticsearch_ query passed as a block to
868
+ # the `percolate` method.
869
+ #
870
+ tire_matches = index.percolate(:message => '[Warning] Extreme flooding expected after tsunami wave.') do
871
+ # Let's use a _terms_ query against the `tags` field.
872
+ term :tags, 'tsunami'
873
+ end
874
+
875
+ # In this case, the result will contain only the name of the “tsunami” query.
876
+ #
877
+ # Matching queries: ["tsunami"]
878
+ #
879
+ puts "Matching queries: " + tire_matches.inspect
880
+
881
+ # What if we percolate another document, without the “tsunami” trigger word?
882
+ #
883
+ tire_matches = index.percolate(:message => '[Warning] Extreme temperatures expected.') { term :tags, 'tsunami' }
884
+
885
+ # As expected, we will get an empty array:
886
+ #
887
+ # Matching queries: []
888
+ #
889
+ puts "Matching queries: " + tire_matches.inspect
890
+
891
+ # Well, that's of course immensely useful for real-time search systems. But, there's more.
892
+ # We can _percolate_ a document _at the same time_ it is being stored in the index,
893
+ # getting back a list of matching queries.
894
+
895
+ # Let's store a document with some trigger words in the index, and mark it for percolation.
896
+ #
897
+ response = index.store( { :message => '[Warning] Severe floods expected after tsunami wave.' },
898
+ { :percolate => true } )
899
+
900
+ # We will get the names of all matching queries in response.
901
+ #
902
+ # Matching queries: ["floods", "tsunami", "warning"]
903
+ #
904
+ puts "Matching queries: " + response['tire_matches'].inspect
905
+
906
+ # As with the _percolate_ example, we can filter the executed queries.
907
+ #
908
+ response = index.store( { :message => '[Warning] Severe floods expected after tsunami wave.' },
909
+ # Let's use a simple string query for the “tsunami” tag.
910
+ { :percolate => 'tags:tsunami' } )
911
+
912
+ # Unsurprisingly, the response will contain just the name of the “tsunami” query.
913
+ #
914
+ # Matching queries: ["tsunami"]
915
+ #
916
+ puts "Matching queries: " + response['tire_matches'].inspect
917
+
918
+ ### ActiveModel Integration
919
+
920
+ # As you can see, [_Tire_](https://github.com/karmi/tire) supports the
921
+ # main features of _Elasticsearch_ in Ruby.
922
+ #
923
+ # It allows you to create and delete indices, add documents, search them, retrieve the facets, highlight the results,
924
+ # and comes with a usable logging facility.
925
+ #
926
+ # Of course, the holy grail of any search library is easy, painless integration with your Ruby classes, and,
927
+ # most importantly, with ActiveRecord/ActiveModel classes.
928
+ #
929
+ # Please, check out the [README](https://github.com/karmi/tire/tree/master#readme) file for instructions
930
+ # how to include _Tire_-based search in your models..
931
+ #
932
+ # Send any feedback via Github issues, or ask questions in the [#elasticsearch](irc://irc.freenode.net/#elasticsearch) IRC channel.