tire 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. data/.gitignore +9 -0
  2. data/Gemfile +4 -0
  3. data/MIT-LICENSE +20 -0
  4. data/README.markdown +435 -0
  5. data/Rakefile +75 -0
  6. data/examples/dsl.rb +73 -0
  7. data/examples/rails-application-template.rb +144 -0
  8. data/examples/tire-dsl.rb +617 -0
  9. data/lib/tire.rb +35 -0
  10. data/lib/tire/client.rb +40 -0
  11. data/lib/tire/configuration.rb +29 -0
  12. data/lib/tire/dsl.rb +33 -0
  13. data/lib/tire/index.rb +209 -0
  14. data/lib/tire/logger.rb +60 -0
  15. data/lib/tire/model/callbacks.rb +23 -0
  16. data/lib/tire/model/import.rb +18 -0
  17. data/lib/tire/model/indexing.rb +50 -0
  18. data/lib/tire/model/naming.rb +30 -0
  19. data/lib/tire/model/persistence.rb +34 -0
  20. data/lib/tire/model/persistence/attributes.rb +60 -0
  21. data/lib/tire/model/persistence/finders.rb +61 -0
  22. data/lib/tire/model/persistence/storage.rb +75 -0
  23. data/lib/tire/model/search.rb +97 -0
  24. data/lib/tire/results/collection.rb +56 -0
  25. data/lib/tire/results/item.rb +39 -0
  26. data/lib/tire/results/pagination.rb +30 -0
  27. data/lib/tire/rubyext/hash.rb +3 -0
  28. data/lib/tire/rubyext/symbol.rb +11 -0
  29. data/lib/tire/search.rb +117 -0
  30. data/lib/tire/search/facet.rb +41 -0
  31. data/lib/tire/search/filter.rb +28 -0
  32. data/lib/tire/search/highlight.rb +37 -0
  33. data/lib/tire/search/query.rb +42 -0
  34. data/lib/tire/search/sort.rb +29 -0
  35. data/lib/tire/tasks.rb +88 -0
  36. data/lib/tire/version.rb +3 -0
  37. data/test/fixtures/articles/1.json +1 -0
  38. data/test/fixtures/articles/2.json +1 -0
  39. data/test/fixtures/articles/3.json +1 -0
  40. data/test/fixtures/articles/4.json +1 -0
  41. data/test/fixtures/articles/5.json +1 -0
  42. data/test/integration/active_model_searchable_test.rb +80 -0
  43. data/test/integration/active_record_searchable_test.rb +193 -0
  44. data/test/integration/facets_test.rb +65 -0
  45. data/test/integration/filters_test.rb +46 -0
  46. data/test/integration/highlight_test.rb +52 -0
  47. data/test/integration/index_mapping_test.rb +44 -0
  48. data/test/integration/index_store_test.rb +68 -0
  49. data/test/integration/persistent_model_test.rb +35 -0
  50. data/test/integration/query_string_test.rb +43 -0
  51. data/test/integration/results_test.rb +28 -0
  52. data/test/integration/sort_test.rb +36 -0
  53. data/test/models/active_model_article.rb +31 -0
  54. data/test/models/active_model_article_with_callbacks.rb +49 -0
  55. data/test/models/active_model_article_with_custom_index_name.rb +5 -0
  56. data/test/models/active_record_article.rb +12 -0
  57. data/test/models/article.rb +15 -0
  58. data/test/models/persistent_article.rb +11 -0
  59. data/test/models/persistent_articles_with_custom_index_name.rb +10 -0
  60. data/test/models/supermodel_article.rb +22 -0
  61. data/test/models/validated_model.rb +11 -0
  62. data/test/test_helper.rb +52 -0
  63. data/test/unit/active_model_lint_test.rb +17 -0
  64. data/test/unit/client_test.rb +43 -0
  65. data/test/unit/configuration_test.rb +71 -0
  66. data/test/unit/index_test.rb +390 -0
  67. data/test/unit/logger_test.rb +114 -0
  68. data/test/unit/model_callbacks_test.rb +90 -0
  69. data/test/unit/model_import_test.rb +71 -0
  70. data/test/unit/model_persistence_test.rb +400 -0
  71. data/test/unit/model_search_test.rb +289 -0
  72. data/test/unit/results_collection_test.rb +131 -0
  73. data/test/unit/results_item_test.rb +59 -0
  74. data/test/unit/rubyext_hash_test.rb +19 -0
  75. data/test/unit/search_facet_test.rb +69 -0
  76. data/test/unit/search_filter_test.rb +36 -0
  77. data/test/unit/search_highlight_test.rb +46 -0
  78. data/test/unit/search_query_test.rb +55 -0
  79. data/test/unit/search_sort_test.rb +50 -0
  80. data/test/unit/search_test.rb +204 -0
  81. data/test/unit/tire_test.rb +55 -0
  82. data/tire.gemspec +54 -0
  83. metadata +372 -0
@@ -0,0 +1,75 @@
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.pattern = 'test/unit/*_test.rb'
18
+ test.verbose = true
19
+ end
20
+ Rake::TestTask.new(:integration) do |test|
21
+ test.libs << 'lib' << 'test'
22
+ test.pattern = 'test/integration/*_test.rb'
23
+ test.verbose = true
24
+ end
25
+ end
26
+
27
+ # Generate documentation
28
+ begin
29
+ require 'sdoc'
30
+ rescue LoadError
31
+ end
32
+ require 'rake/rdoctask'
33
+ Rake::RDocTask.new do |rdoc|
34
+ rdoc.rdoc_dir = 'rdoc'
35
+ rdoc.title = "Tire"
36
+ rdoc.rdoc_files.include('README.rdoc')
37
+ rdoc.rdoc_files.include('lib/**/*.rb')
38
+ end
39
+
40
+ # Generate coverage reports
41
+ begin
42
+ require 'rcov/rcovtask'
43
+ Rcov::RcovTask.new do |test|
44
+ test.libs << 'test'
45
+ test.rcov_opts = ['--exclude', 'gems/*']
46
+ test.pattern = 'test/**/*_test.rb'
47
+ test.verbose = true
48
+ end
49
+ rescue LoadError
50
+ task :rcov do
51
+ abort "RCov is not available. In order to run rcov, you must: sudo gem install rcov"
52
+ end
53
+ end
54
+
55
+ namespace :web do
56
+
57
+ desc "Update the Github website"
58
+ task :update => :generate do
59
+ current_branch = `git branch --no-color`.split("\n").select { |line| line =~ /^\* / }.to_s.gsub(/\* (.*)/, '\1')
60
+ (puts "Unable to determine current branch"; exit(1) ) unless current_branch
61
+ system "git stash save && git checkout web"
62
+ system "cp examples/tire-dsl.html index.html"
63
+ system "git add index.html && git co -m 'Updated Tire website'"
64
+ system "git push origin web:gh-pages -f"
65
+ system "git checkout #{current_branch} && git stash pop"
66
+ end
67
+
68
+ desc "Generate the Rocco documentation page"
69
+ task :generate do
70
+ system "rocco examples/tire-dsl.rb"
71
+ html = File.read('examples/tire-dsl.html').gsub!(/tire\-dsl\.rb/, 'tire.rb')
72
+ File.open('examples/tire-dsl.html', 'w') { |f| f.write html }
73
+ system "open examples/tire-dsl.html"
74
+ end
75
+ end
@@ -0,0 +1,73 @@
1
+ $LOAD_PATH.unshift File.expand_path('../../lib', __FILE__)
2
+
3
+ require 'rubygems'
4
+ require 'tire'
5
+
6
+ extend Tire::DSL
7
+
8
+ configure do
9
+ url "http://localhost:9200"
10
+ end
11
+
12
+ index 'articles' do
13
+ delete
14
+ create
15
+
16
+ puts "Documents:", "-"*80
17
+ [
18
+ { :title => 'One', :tags => ['ruby'] },
19
+ { :title => 'Two', :tags => ['ruby', 'python'] },
20
+ { :title => 'Three', :tags => ['java'] },
21
+ { :title => 'Four', :tags => ['ruby', 'php'] }
22
+ ].each do |article|
23
+ puts "Indexing article: #{article.to_json}"
24
+ store article
25
+ end
26
+
27
+ refresh
28
+ end
29
+
30
+ s = search 'articles' do
31
+ query do
32
+ string 'T*'
33
+ end
34
+
35
+ filter :terms, :tags => ['ruby']
36
+
37
+ sort do
38
+ title 'desc'
39
+ end
40
+
41
+ facet 'global-tags' do
42
+ terms :tags, :global => true
43
+ end
44
+
45
+ facet 'current-tags' do
46
+ terms :tags
47
+ end
48
+ end
49
+
50
+ puts "", "Query:", "-"*80
51
+ puts s.to_json
52
+
53
+ puts "", "Raw JSON result:", "-"*80
54
+ puts JSON.pretty_generate(s.response)
55
+
56
+ puts "", "Try the query in Curl:", "-"*80
57
+ puts s.to_curl
58
+
59
+ puts "", "Results:", "-"*80
60
+ s.results.each_with_index do |document, i|
61
+ puts "#{i+1}. #{ document.title.ljust(10) } [id] #{document._id}"
62
+ end
63
+
64
+ puts "", "Facets: tags distribution across the whole database:", "-"*80
65
+ s.results.facets['global-tags']['terms'].each do |f|
66
+ puts "#{f['term'].ljust(13)} #{f['count']}"
67
+ end
68
+
69
+ puts "", "Facets: tags distribution for the current query ",
70
+ "(Notice that 'java' is included, because of the filter)", "-"*80
71
+ s.results.facets['current-tags']['terms'].each do |f|
72
+ puts "#{f['term'].ljust(13)} #{f['count']}"
73
+ end
@@ -0,0 +1,144 @@
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 Rails application with support for ElasticSearch full-text via the Tire gem
6
+ #
7
+ # Run it like this:
8
+ #
9
+ # rails new searchapp -m https://github.com/karmi/tire/raw/master/examples/rails-application-template.rb
10
+ #
11
+
12
+ run "rm public/index.html"
13
+ run "rm public/images/rails.png"
14
+ run "touch tmp/.gitignore log/.gitignore vendor/.gitignore"
15
+
16
+ git :init
17
+ git :add => '.'
18
+ git :commit => "-m 'Initial commit: Clean application'"
19
+
20
+ puts
21
+ say_status "Rubygems", "Adding Rubygems into Gemfile...\n", :yellow
22
+ puts '-'*80, ''
23
+
24
+ gem 'tire', :git => 'https://github.com/karmi/tire.git', :branch => 'activemodel'
25
+ gem 'will_paginate', '~>3.0.pre'
26
+ git :add => '.'
27
+ git :commit => "-m 'Added gems'"
28
+
29
+ puts
30
+ say_status "Rubygems", "Installing Rubygems...", :yellow
31
+
32
+ puts
33
+ puts "********************************************************************************"
34
+ puts " Running `bundle install`. Let's watch a movie!"
35
+ puts "********************************************************************************", ""
36
+
37
+ run "bundle install"
38
+
39
+ puts
40
+ say_status "Model", "Adding search support into the Article model...", :yellow
41
+ puts '-'*80, ''
42
+
43
+ generate :scaffold, "Article title:string content:text published_on:date"
44
+ route "root :to => 'articles#index'"
45
+ rake "db:migrate"
46
+
47
+ git :add => '.'
48
+ git :commit => "-m 'Added the Article resource'"
49
+
50
+ run "rm -f app/models/article.rb"
51
+ file 'app/models/article.rb', <<-CODE
52
+ class Article < ActiveRecord::Base
53
+ include Tire::Model::Search
54
+ include Tire::Model::Callbacks
55
+ end
56
+ CODE
57
+
58
+ initializer 'tire.rb', <<-CODE
59
+ Tire.configure do
60
+ logger STDERR
61
+ end
62
+ CODE
63
+
64
+ git :commit => "-a -m 'Added Tire support into the Article class and an initializer'"
65
+
66
+ puts
67
+ say_status "Controller", "Adding controller action, route, and neccessary HTML for search...", :yellow
68
+ puts '-'*80, ''
69
+
70
+ gsub_file 'app/controllers/articles_controller.rb', %r{# GET /articles/1$}, <<-CODE
71
+ # GET /articles/search
72
+ def search
73
+ @articles = Article.search params[:q]
74
+
75
+ render :action => "index"
76
+ end
77
+
78
+ # GET /articles/1
79
+ CODE
80
+
81
+ gsub_file 'app/views/articles/index.html.erb', %r{<h1>Listing articles</h1>}, <<-CODE
82
+ <h1>Listing articles</h1>
83
+
84
+ <%= form_tag search_articles_path, :method => 'get' do %>
85
+ <%= label_tag :query %>
86
+ <%= text_field_tag :q, params[:q] %>
87
+ <%= submit_tag :search %>
88
+ <% end %>
89
+
90
+ <hr>
91
+ CODE
92
+
93
+ gsub_file 'app/views/articles/index.html.erb', %r{<%= link_to 'New Article', new_article_path %>}, <<-CODE
94
+ <%= link_to 'New Article', new_article_path %>
95
+ <%= link_to 'Back', articles_path if params[:q] %>
96
+ CODE
97
+
98
+ gsub_file 'config/routes.rb', %r{resources :articles}, <<-CODE
99
+ resources :articles do
100
+ collection { get :search }
101
+ end
102
+ CODE
103
+
104
+ git :commit => "-a -m 'Added Tire support into the frontend of application'"
105
+
106
+ puts
107
+ say_status "Database", "Seeding the database with data...", :yellow
108
+ puts '-'*80, ''
109
+
110
+ run "rm -f db/seeds.rb"
111
+ file 'db/seeds.rb', <<-CODE
112
+ contents = [
113
+ 'Lorem ipsum dolor sit amet.',
114
+ 'Consectetur adipisicing elit, sed do eiusmod tempor incididunt.',
115
+ 'Labore et dolore magna aliqua.',
116
+ 'Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris.',
117
+ 'Excepteur sint occaecat cupidatat non proident.'
118
+ ]
119
+
120
+ puts "Deleting all articles..."
121
+ Article.delete_all
122
+
123
+ puts "Creating articles:"
124
+ %w[ One Two Three Four Five ].each_with_index do |title, i|
125
+ Article.create :title => title, :content => contents[i], :published_on => i.days.ago.utc
126
+ end
127
+ CODE
128
+
129
+ rake "db:seed"
130
+
131
+ git :add => "db/seeds.rb"
132
+ git :commit => "-m 'Added database seeding script'"
133
+
134
+ puts
135
+ say_status "Index", "Indexing database...", :yellow
136
+ puts '-'*80, ''
137
+
138
+ rake "environment tire:import CLASS='Article' FORCE=true"
139
+
140
+ puts "", "="*80
141
+ say_status "DONE", "\e[1mStarting the application. Open http://localhost:3000 and search for something...\e[0m", :yellow
142
+ puts "="*80, ""
143
+
144
+ run "rails server"
@@ -0,0 +1,617 @@
1
+ # **Tire** provides rich and comfortable Ruby API for the
2
+ # [_ElasticSearch_](http://www.elasticsearch.org/) search engine/database.
3
+ #
4
+ # _ElasticSearch_ is a scalable, distributed, cloud-ready, highly-available
5
+ # full-text search engine and database, communicating by JSON over RESTful HTTP,
6
+ # based on [Lucene](http://lucene.apache.org/), written in Java.
7
+ #
8
+ # <img src="http://github.com/favicon.ico" style="position:relative; top:2px">
9
+ # _Tire_ is open source, and you can download or clone the source code
10
+ # from <https://github.com/karmi/tire>.
11
+ #
12
+ # By following these instructions you should have the search running
13
+ # on a sane operation system in less then 10 minutes.
14
+
15
+ # Note, that this file can be executed directly:
16
+ #
17
+ # ruby examples/tire-dsl.rb
18
+ #
19
+
20
+
21
+ #### Installation
22
+
23
+ # Install _Tire_ with Rubygems.
24
+
25
+ #
26
+ # gem install tire
27
+ #
28
+ require 'rubygems'
29
+ require 'tire'
30
+
31
+ #### Prerequisites
32
+
33
+ # You'll need a working and running _ElasticSearch_ server. Thankfully, that's easy.
34
+ ( puts <<-"INSTALL" ; exit(1) ) unless (RestClient.get('http://localhost:9200') rescue false)
35
+
36
+ [ERROR] You don’t appear to have ElasticSearch installed. Please install and launch it with the following commands:
37
+
38
+ curl -k -L -o elasticsearch-0.16.0.tar.gz http://github.com/downloads/elasticsearch/elasticsearch/elasticsearch-0.16.0.tar.gz
39
+ tar -zxvf elasticsearch-0.16.0.tar.gz
40
+ ./elasticsearch-0.16.0/bin/elasticsearch -f
41
+ INSTALL
42
+
43
+ ### Storing and indexing documents
44
+
45
+ # Let's initialize an index named “articles”.
46
+ #
47
+ Tire.index 'articles' do
48
+ # To make sure it's fresh, let's delete any existing index with the same name.
49
+ #
50
+ delete
51
+ # And then, let's create it.
52
+ #
53
+ create
54
+
55
+ # We want to store and index some articles with `title`, `tags` and `published_on` properties.
56
+ # Simple Hashes are OK.
57
+ #
58
+ store :title => 'One', :tags => ['ruby'], :published_on => '2011-01-01'
59
+ store :title => 'Two', :tags => ['ruby', 'python'], :published_on => '2011-01-02'
60
+ store :title => 'Three', :tags => ['java'], :published_on => '2011-01-02'
61
+ store :title => 'Four', :tags => ['ruby', 'php'], :published_on => '2011-01-03'
62
+
63
+ # We force refreshing the index, so we can query it immediately.
64
+ #
65
+ refresh
66
+ end
67
+
68
+ # We may want to define a specific [mapping](http://www.elasticsearch.org/guide/reference/api/admin-indices-create-index.html)
69
+ # for the index.
70
+
71
+ Tire.index 'articles' do
72
+ # To do so, just pass a Hash containing the specified mapping to the `Index#create` method.
73
+ #
74
+ create :mappings => {
75
+
76
+ # Specify for which type of documents this mapping should be used.
77
+ # (The documents must provide a `type` method or property then.)
78
+ #
79
+ :article => {
80
+ :properties => {
81
+
82
+ # Specify the type of the field, whether it should be analyzed, etc.
83
+ #
84
+ :id => { :type => 'string', :index => 'not_analyzed', :include_in_all => false },
85
+
86
+ # Set the boost or analyzer settings for the field, ... The _ElasticSearch_ guide
87
+ # has [more information](http://elasticsearch.org/guide/reference/mapping/index.html)
88
+ # about this. Proper mapping is key to efficient and effective search.
89
+ # But don't fret about getting the mapping right the first time, you won't.
90
+ # In most cases, the default mapping is just fine for prototyping.
91
+ #
92
+ :title => { :type => 'string', :analyzer => 'snowball', :boost => 2.0 },
93
+ :tags => { :type => 'string', :analyzer => 'keyword' },
94
+ :content => { :type => 'string', :analyzer => 'czech' }
95
+ }
96
+ }
97
+ }
98
+ end
99
+
100
+ #### Bulk Storage
101
+
102
+ # Of course, we may have large amounts of data, and adding them to the index one by one really isn't the best idea.
103
+ # We can use _ElasticSearch's_ [bulk storage](http://www.elasticsearch.org/guide/reference/api/bulk.html)
104
+ # for importing the data.
105
+
106
+ # So, for demonstration purposes, let's suppose we have a plain collection of hashes to store.
107
+ #
108
+ articles = [
109
+
110
+ # Notice that such objects must have an `id` property!
111
+ #
112
+ { :id => '1', :title => 'one', :tags => ['ruby'], :published_on => '2011-01-01' },
113
+ { :id => '2', :title => 'two', :tags => ['ruby', 'python'], :published_on => '2011-01-02' },
114
+ { :id => '3', :title => 'three', :tags => ['java'], :published_on => '2011-01-02' },
115
+ { :id => '4', :title => 'four', :tags => ['ruby', 'php'], :published_on => '2011-01-03' }
116
+ ]
117
+
118
+ # We can just push them into the index in one go.
119
+ #
120
+ Tire.index 'articles' do
121
+ import articles
122
+ end
123
+
124
+ # Of course, we can easily manipulate the documents before storing them in the index.
125
+ #
126
+ Tire.index 'articles' do
127
+ delete
128
+
129
+ # ... by just passing a block to the `import` method. The collection will
130
+ # be available in the block argument.
131
+ #
132
+ import articles do |documents|
133
+
134
+ # We will capitalize every _title_ and return the manipulated collection
135
+ # back to the `import` method.
136
+ #
137
+ documents.map { |document| document.update(:title => document[:title].capitalize) }
138
+ end
139
+
140
+ refresh
141
+ end
142
+
143
+ ### Searching
144
+
145
+ # With the documents indexed and stored in the _ElasticSearch_ database, we can search them, finally.
146
+ #
147
+ # Tire exposes the search interface via simple domain-specific language.
148
+
149
+
150
+ #### Simple Query String Searches
151
+
152
+ # We can do simple searches, like searching for articles containing “One” in their title.
153
+ #
154
+ s = Tire.search('articles') do
155
+ query do
156
+ string "title:One"
157
+ end
158
+ end
159
+
160
+ # The results:
161
+ # * One [tags: ruby]
162
+ #
163
+ s.results.each do |document|
164
+ puts "* #{ document.title } [tags: #{document.tags.join(', ')}]"
165
+ end
166
+
167
+ # Or, we can search for articles published between January, 1st and January, 2nd.
168
+ #
169
+ s = Tire.search('articles') do
170
+ query do
171
+ string "published_on:[2011-01-01 TO 2011-01-02]"
172
+ end
173
+ end
174
+
175
+ # The results:
176
+ # * One [published: 2011-01-01]
177
+ # * Two [published: 2011-01-02]
178
+ # * Three [published: 2011-01-02]
179
+ #
180
+ s.results.each do |document|
181
+ puts "* #{ document.title } [published: #{document.published_on}]"
182
+ end
183
+
184
+ # Of course, we may write the blocks in shorter notation.
185
+ # Local variables from outer scope are passed down the chain.
186
+
187
+ # Let's search for articles whose titles begin with letter “T”.
188
+ #
189
+ q = "title:T*"
190
+ s = Tire.search('articles') { query { string q } }
191
+
192
+ # The results:
193
+ # * Two [tags: ruby, python]
194
+ # * Three [tags: java]
195
+ #
196
+ s.results.each do |document|
197
+ puts "* #{ document.title } [tags: #{document.tags.join(', ')}]"
198
+ end
199
+
200
+ # In fact, we can use any valid [Lucene query syntax](http://lucene.apache.org/java/3_0_3/queryparsersyntax.html)
201
+ # for the query string queries.
202
+
203
+ # For debugging, we can display the JSON which is being sent to _ElasticSearch_.
204
+ #
205
+ # {"query":{"query_string":{"query":"title:T*"}}}
206
+ #
207
+ puts "", "Query:", "-"*80
208
+ puts s.to_json
209
+
210
+ # Or better, we may display a complete `curl` command to recreate the request in terminal,
211
+ # so we can see the naked response, tweak request parameters and meditate on problems.
212
+ #
213
+ # curl -X POST "http://localhost:9200/articles/_search?pretty=true" \
214
+ # -d '{"query":{"query_string":{"query":"title:T*"}}}'
215
+ #
216
+ puts "", "Try the query in Curl:", "-"*80
217
+ puts s.to_curl
218
+
219
+
220
+ ### Logging
221
+
222
+ # For debugging more complex situations, we can enable logging, so requests and responses
223
+ # will be logged using this `curl`-friendly format.
224
+
225
+ Tire.configure do
226
+
227
+ # By default, at the _info_ level, only the `curl`-format of request and
228
+ # basic information about the response will be logged:
229
+ #
230
+ # # 2011-04-24 11:34:01:150 [CREATE] ("articles")
231
+ # #
232
+ # curl -X POST "http://localhost:9200/articles"
233
+ #
234
+ # # 2011-04-24 11:34:01:152 [200]
235
+ #
236
+ logger 'elasticsearch.log'
237
+
238
+ # For debugging, we can switch to the _debug_ level, which will log the complete JSON responses.
239
+ #
240
+ # That's very convenient if we want to post a recreation of some problem or solution
241
+ # to the mailing list, IRC channel, etc.
242
+ #
243
+ logger 'elasticsearch.log', :level => 'debug'
244
+
245
+ # Note that we can pass any [`IO`](http://www.ruby-doc.org/core/classes/IO.html)-compatible Ruby object as a logging device.
246
+ #
247
+ logger STDERR
248
+ end
249
+
250
+ ### Configuration
251
+
252
+ # As we have just seen with logging, we can configure various parts of _Tire_.
253
+ #
254
+ Tire.configure do
255
+
256
+ # First of all, we can configure the URL for _ElasticSearch_.
257
+ #
258
+ url "http://search.example.com"
259
+
260
+ # Second, we may want to wrap the result items in our own class.
261
+ #
262
+ class MySpecialWrapper; end
263
+ wrapper MySpecialWrapper
264
+
265
+ # Finally, we can reset one or all configuration settings to their defaults.
266
+ #
267
+ reset
268
+
269
+ end
270
+
271
+
272
+ ### Complex Searching
273
+
274
+ #### Other Types of Queries
275
+
276
+ # Query strings are convenient for simple searches, but we may want to define our queries more expressively,
277
+ # using the _ElasticSearch_ [Query DSL](http://www.elasticsearch.org/guide/reference/query-dsl/index.html).
278
+ #
279
+ s = Tire.search('articles') do
280
+
281
+ # Let's suppose we want to search for articles with specific _tags_, in our case “ruby” _or_ “python”.
282
+ #
283
+ query do
284
+
285
+ # That's a great excuse to use a [_terms_](http://elasticsearch.org/guide/reference/query-dsl/terms-query.html)
286
+ # query.
287
+ #
288
+ terms :tags, ['ruby', 'python']
289
+ end
290
+ end
291
+
292
+ # The search, as expected, returns three articles, all tagged “ruby” — among other tags:
293
+ #
294
+ # * Two [tags: ruby, python]
295
+ # * One [tags: ruby]
296
+ # * Four [tags: ruby, php]
297
+ #
298
+ s.results.each do |document|
299
+ puts "* #{ document.title } [tags: #{document.tags.join(', ')}]"
300
+ end
301
+
302
+ # What if we wanted to search for articles tagged both “ruby” _and_ “python”?
303
+ #
304
+ s = Tire.search('articles') do
305
+ query do
306
+
307
+ # That's a great excuse to specify `minimum_match` for the query.
308
+ #
309
+ terms :tags, ['ruby', 'python'], :minimum_match => 2
310
+ end
311
+ end
312
+
313
+ # The search, as expected, returns one article, tagged with _both_ “ruby” and “python”:
314
+ #
315
+ # * Two [tags: ruby, python]
316
+ #
317
+ s.results.each do |document|
318
+ puts "* #{ document.title } [tags: #{document.tags.join(', ')}]"
319
+ end
320
+
321
+ # _ElasticSearch_ supports many types of [queries](http://www.elasticsearch.org/guide/reference/query-dsl/).
322
+ #
323
+ # Eventually, _Tire_ will support all of them. So far, only these are supported:
324
+ #
325
+ # * [string](http://www.elasticsearch.org/guide/reference/query-dsl/query-string-query.html)
326
+ # * [term](http://elasticsearch.org/guide/reference/query-dsl/term-query.html)
327
+ # * [terms](http://elasticsearch.org/guide/reference/query-dsl/terms-query.html)
328
+ # * [all](http://www.elasticsearch.org/guide/reference/query-dsl/match-all-query.html)
329
+ # * [ids](http://www.elasticsearch.org/guide/reference/query-dsl/ids-query.html)
330
+
331
+ #### Faceted Search
332
+
333
+ # _ElasticSearch_ makes it trivial to retrieve complex aggregated data from our index/database,
334
+ # so called [_facets_](http://www.elasticsearch.org/guide/reference/api/search/facets/index.html).
335
+
336
+ # Let's say we want to display article counts for every tag in the database.
337
+ # For that, we'll use a _terms_ facet.
338
+
339
+ #
340
+ s = Tire.search 'articles' do
341
+
342
+ # We will search for articles whose title begins with letter “T”,
343
+ #
344
+ query { string 'title:T*' }
345
+
346
+ # and retrieve the counts “bucketed” by `tags`.
347
+ #
348
+ facet 'tags' do
349
+ terms :tags
350
+ end
351
+ end
352
+
353
+ # As we see, our query has found two articles, and if you recall our articles from above,
354
+ # _Two_ is tagged with “ruby” and “python”, while _Three_ is tagged with “java”.
355
+ #
356
+ # Found 2 articles: Three, Two
357
+ #
358
+ # The counts shouldn't surprise us:
359
+ #
360
+ # Counts by tag:
361
+ # -------------------------
362
+ # ruby 1
363
+ # python 1
364
+ # java 1
365
+ #
366
+ puts "Found #{s.results.count} articles: #{s.results.map(&:title).join(', ')}"
367
+ puts "Counts by tag:", "-"*25
368
+ s.results.facets['tags']['terms'].each do |f|
369
+ puts "#{f['term'].ljust(10)} #{f['count']}"
370
+ end
371
+
372
+ # These counts are based on the scope of our current query.
373
+ # What if we wanted to display aggregated counts by `tags` across the whole database?
374
+
375
+ #
376
+ s = Tire.search 'articles' do
377
+
378
+ # Let's repeat the search for “T”...
379
+ #
380
+ query { string 'title:T*' }
381
+
382
+ facet 'global-tags' do
383
+
384
+ # ...but set the `global` scope for the facet in this case.
385
+ #
386
+ terms :tags, :global => true
387
+ end
388
+
389
+ # We can even _combine_ facets scoped to the current query
390
+ # with globally scoped facets — we'll just use a different name.
391
+ #
392
+ facet 'current-tags' do
393
+ terms :tags
394
+ end
395
+ end
396
+
397
+ # Aggregated results for the current query are the same as previously:
398
+ #
399
+ # Current query facets:
400
+ # -------------------------
401
+ # ruby 1
402
+ # python 1
403
+ # java 1
404
+ #
405
+ puts "Current query facets:", "-"*25
406
+ s.results.facets['current-tags']['terms'].each do |f|
407
+ puts "#{f['term'].ljust(10)} #{f['count']}"
408
+ end
409
+
410
+ # On the other hand, aggregated results for the global scope include also
411
+ # tags for articles not matched by the query, such as “java” or “php”:
412
+ #
413
+ # Global facets:
414
+ # -------------------------
415
+ # ruby 3
416
+ # python 1
417
+ # php 1
418
+ # java 1
419
+ #
420
+ puts "Global facets:", "-"*25
421
+ s.results.facets['global-tags']['terms'].each do |f|
422
+ puts "#{f['term'].ljust(10)} #{f['count']}"
423
+ end
424
+
425
+ # _ElasticSearch_ supports many advanced types of facets, such as those for computing statistics or geographical distance.
426
+ #
427
+ # Eventually, _Tire_ will support all of them. So far, only these are supported:
428
+ #
429
+ # * [terms](http://www.elasticsearch.org/guide/reference/api/search/facets/terms-facet.html)
430
+ # * [date](http://www.elasticsearch.org/guide/reference/api/search/facets/date-histogram-facet.html)
431
+
432
+ # We have seen that _ElasticSearch_ facets enable us to fetch complex aggregations from our data.
433
+ #
434
+ # They are frequently used for another feature, „faceted navigation“.
435
+ # We can be combine query and facets with
436
+ # [filters](http://elasticsearch.org/guide/reference/api/search/filter.html),
437
+ # so the returned documents are restricted by certain criteria — for example to a specific category —,
438
+ # but the aggregation calculations are still based on the original query.
439
+
440
+
441
+ #### Filtered Search
442
+
443
+ # So, let's make our search a bit more complex. Let's search for articles whose titles begin
444
+ # with letter “T”, again, but filter the results, so only the articles tagged “ruby”
445
+ # are returned.
446
+ #
447
+ s = Tire.search 'articles' do
448
+
449
+ # We will use just the same **query** as before.
450
+ #
451
+ query { string 'title:T*' }
452
+
453
+ # But we will add a _terms_ **filter** based on tags.
454
+ #
455
+ filter :terms, :tags => ['ruby']
456
+
457
+ # And, of course, our facet definition.
458
+ #
459
+ facet('tags') { terms :tags }
460
+
461
+ end
462
+
463
+ # We see that only the article _Two_ (tagged “ruby” and “python”) is returned,
464
+ # _not_ the article _Three_ (tagged “java”):
465
+ #
466
+ # * Two [tags: ruby, python]
467
+ #
468
+ s.results.each do |document|
469
+ puts "* #{ document.title } [tags: #{document.tags.join(', ')}]"
470
+ end
471
+
472
+ # The _count_ for article _Three_'s tags, “java”, on the other hand, _is_ in fact included:
473
+ #
474
+ # Counts by tag:
475
+ # -------------------------
476
+ # ruby 1
477
+ # python 1
478
+ # java 1
479
+ #
480
+ puts "Counts by tag:", "-"*25
481
+ s.results.facets['tags']['terms'].each do |f|
482
+ puts "#{f['term'].ljust(10)} #{f['count']}"
483
+ end
484
+
485
+ #### Sorting
486
+
487
+ # By default, the results are sorted according to their relevancy.
488
+ #
489
+ s = Tire.search('articles') { query { string 'tags:ruby' } }
490
+
491
+ s.results.each do |document|
492
+ puts "* #{ document.title } " +
493
+ "[tags: #{document.tags.join(', ')}; " +
494
+
495
+ # The score is available as the `_score` property.
496
+ #
497
+ "score: #{document._score}]"
498
+ end
499
+
500
+ # The results:
501
+ #
502
+ # * One [tags: ruby; score: 0.30685282]
503
+ # * Four [tags: ruby, php; score: 0.19178301]
504
+ # * Two [tags: ruby, python; score: 0.19178301]
505
+
506
+ # But, what if we want to sort the results based on some other criteria,
507
+ # such as published date or product price? We can do that.
508
+ #
509
+ s = Tire.search 'articles' do
510
+
511
+ # We will search for articles tagged “ruby”, again, ...
512
+ #
513
+ query { string 'tags:ruby' }
514
+
515
+ # ... but will sort them by their `title`, in descending order.
516
+ #
517
+ sort { title 'desc' }
518
+ end
519
+
520
+ # The results:
521
+ #
522
+ # * Two
523
+ # * One
524
+ # * Four
525
+ #
526
+ s.results.each do |document|
527
+ puts "* #{ document.title }"
528
+ end
529
+
530
+ # Of course, it's possible to combine more fields in the sorting definition.
531
+
532
+ s = Tire.search 'articles' do
533
+
534
+ # We will just get all articles in this case.
535
+ #
536
+ query { all }
537
+
538
+ sort do
539
+
540
+ # We will sort the results by their `published_on` property in _ascending_ order (the default),
541
+ #
542
+ published_on
543
+
544
+ # and by their `title` property, in _descending_ order.
545
+ #
546
+ title 'desc'
547
+ end
548
+ end
549
+
550
+ # The results:
551
+ # * One (Published on: 2011-01-01)
552
+ # * Two (Published on: 2011-01-02)
553
+ # * Three (Published on: 2011-01-02)
554
+ # * Four (Published on: 2011-01-03)
555
+ #
556
+ s.results.each do |document|
557
+ puts "* #{ document.title.ljust(10) } (Published on: #{ document.published_on })"
558
+ end
559
+
560
+ #### Highlighting
561
+
562
+ # Often, we want to highlight the snippets matching our query in the displayed results.
563
+ # _ElasticSearch_ provides rich
564
+ # [highlighting](http://www.elasticsearch.org/guide/reference/api/search/highlighting.html)
565
+ # features, and _Tire_ makes them trivial to use.
566
+ #
567
+ s = Tire.search 'articles' do
568
+
569
+ # Let's search for documents containing word “Two” in their titles,
570
+ query { string 'title:Two' }
571
+
572
+ # and instruct _ElasticSearch_ to highlight relevant snippets.
573
+ #
574
+ highlight :title
575
+ end
576
+
577
+ # The results:
578
+ # Title: Two; Highlighted: <em>Two</em>
579
+ #
580
+ s.results.each do |document|
581
+ puts "Title: #{ document.title }; Highlighted: #{document.highlight.title}"
582
+ end
583
+
584
+ # We can configure many options for highlighting, such as:
585
+ #
586
+ s = Tire.search 'articles' do
587
+ query { string 'title:Two' }
588
+
589
+ # • specify the fields to highlight
590
+ #
591
+ highlight :title, :body
592
+
593
+ # • specify their individual options
594
+ #
595
+ highlight :title, :body => { :number_of_fragments => 0 }
596
+
597
+ # • or specify global highlighting options, such as the wrapper tag
598
+ #
599
+ highlight :title, :body, :options => { :tag => '<strong class="highlight">' }
600
+ end
601
+
602
+
603
+ ### ActiveModel Integration
604
+
605
+ # As you can see, [_Tire_](https://github.com/karmi/tire) supports the
606
+ # main features of _ElasticSearch_ in Ruby.
607
+ #
608
+ # It allows you to create and delete indices, add documents, search them, retrieve the facets, highlight the results,
609
+ # and comes with a usable logging facility.
610
+ #
611
+ # Of course, the holy grail of any search library is easy, painless integration with your Ruby classes, and,
612
+ # most importantly, with ActiveRecord/ActiveModel classes.
613
+ #
614
+ # Please, check out the [README](https://github.com/karmi/tire/tree/master#readme) file for instructions
615
+ # how to include _Tire_-based search in your models..
616
+ #
617
+ # Send any feedback via Github issues, or ask questions in the [#elasticsearch](irc://irc.freenode.net/#elasticsearch) IRC channel.