tire 0.4.3 → 0.5.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 (57) hide show
  1. data/.gitignore +1 -1
  2. data/.yardopts +1 -0
  3. data/README.markdown +2 -2
  4. data/examples/rails-application-template.rb +20 -6
  5. data/lib/tire.rb +2 -0
  6. data/lib/tire/alias.rb +1 -1
  7. data/lib/tire/configuration.rb +8 -0
  8. data/lib/tire/dsl.rb +69 -2
  9. data/lib/tire/index.rb +33 -20
  10. data/lib/tire/model/indexing.rb +7 -1
  11. data/lib/tire/model/persistence.rb +7 -4
  12. data/lib/tire/model/persistence/attributes.rb +1 -1
  13. data/lib/tire/model/persistence/finders.rb +4 -16
  14. data/lib/tire/model/search.rb +21 -8
  15. data/lib/tire/multi_search.rb +263 -0
  16. data/lib/tire/results/collection.rb +78 -49
  17. data/lib/tire/results/item.rb +6 -3
  18. data/lib/tire/results/pagination.rb +15 -1
  19. data/lib/tire/rubyext/ruby_1_8.rb +1 -7
  20. data/lib/tire/rubyext/uri_escape.rb +74 -0
  21. data/lib/tire/search.rb +33 -11
  22. data/lib/tire/search/facet.rb +8 -3
  23. data/lib/tire/search/filter.rb +1 -1
  24. data/lib/tire/search/highlight.rb +1 -1
  25. data/lib/tire/search/queries/match.rb +40 -0
  26. data/lib/tire/search/query.rb +42 -6
  27. data/lib/tire/search/scan.rb +1 -1
  28. data/lib/tire/search/script_field.rb +1 -1
  29. data/lib/tire/search/sort.rb +1 -1
  30. data/lib/tire/tasks.rb +17 -14
  31. data/lib/tire/version.rb +26 -8
  32. data/test/integration/active_record_searchable_test.rb +248 -129
  33. data/test/integration/boosting_queries_test.rb +32 -0
  34. data/test/integration/custom_score_queries_test.rb +1 -0
  35. data/test/integration/dsl_search_test.rb +9 -1
  36. data/test/integration/facets_test.rb +19 -6
  37. data/test/integration/match_query_test.rb +79 -0
  38. data/test/integration/multi_search_test.rb +114 -0
  39. data/test/integration/persistent_model_test.rb +58 -0
  40. data/test/models/article.rb +1 -1
  41. data/test/models/persistent_article_in_index.rb +16 -0
  42. data/test/models/persistent_article_with_defaults.rb +4 -3
  43. data/test/test_helper.rb +3 -1
  44. data/test/unit/configuration_test.rb +10 -0
  45. data/test/unit/index_test.rb +69 -27
  46. data/test/unit/model_initialization_test.rb +31 -0
  47. data/test/unit/model_persistence_test.rb +21 -7
  48. data/test/unit/model_search_test.rb +56 -5
  49. data/test/unit/multi_search_test.rb +304 -0
  50. data/test/unit/results_collection_test.rb +42 -2
  51. data/test/unit/results_item_test.rb +4 -0
  52. data/test/unit/search_facet_test.rb +35 -11
  53. data/test/unit/search_query_test.rb +96 -0
  54. data/test/unit/search_test.rb +60 -3
  55. data/test/unit/tire_test.rb +14 -0
  56. data/tire.gemspec +0 -1
  57. metadata +75 -44
data/.gitignore CHANGED
@@ -11,4 +11,4 @@ examples/*.html
11
11
  *.log
12
12
  .rvmrc
13
13
  .rbenv-version
14
- tags
14
+ tmp/
data/.yardopts ADDED
@@ -0,0 +1 @@
1
+ --markup markdown
data/README.markdown CHANGED
@@ -247,7 +247,7 @@ The best thing about `boolean` queries is that we can easily save these partial
247
247
  to mix and reuse them later. So, we may define a query for the _tags_ property:
248
248
 
249
249
  ```ruby
250
- tags_query = lambda do
250
+ tags_query = lambda do |boolean|
251
251
  boolean.should { string 'tags:ruby' }
252
252
  boolean.should { string 'tags:java' }
253
253
  end
@@ -256,7 +256,7 @@ to mix and reuse them later. So, we may define a query for the _tags_ property:
256
256
  And a query for the _published_on_ property:
257
257
 
258
258
  ```ruby
259
- published_on_query = lambda do
259
+ published_on_query = lambda do |boolean|
260
260
  boolean.must { string 'published_on:[2011-01-01 TO 2011-01-02]' }
261
261
  end
262
262
  ```
@@ -105,7 +105,7 @@ puts
105
105
  say_status "Rubygems", "Adding Rubygems into Gemfile...\n", :yellow
106
106
  puts '-'*80, ''; sleep 1
107
107
 
108
- gem 'tire'
108
+ gem 'tire', :git => 'git://github.com/karmi/tire.git'
109
109
  gem 'will_paginate', '~> 3.0'
110
110
 
111
111
  git :add => '.'
@@ -137,7 +137,7 @@ say_status "Database", "Seeding the database with data...", :yellow
137
137
  puts '-'*80, ''; sleep 0.25
138
138
 
139
139
  run "rm -f db/seeds.rb"
140
- file 'db/seeds.rb', <<-CODE
140
+ file 'db/seeds.rb', %q{
141
141
  contents = [
142
142
  'Lorem ipsum dolor sit amet.',
143
143
  'Consectetur adipisicing elit, sed do eiusmod tempor incididunt.',
@@ -149,11 +149,23 @@ contents = [
149
149
  puts "Deleting all articles..."
150
150
  Article.delete_all
151
151
 
152
- puts "Creating articles..."
153
- %w[ One Two Three Four Five ].each_with_index do |title, i|
154
- Article.create :title => title, :content => contents[i], :published_on => i.days.ago.utc
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
+
155
167
  end
156
- CODE
168
+ }
157
169
 
158
170
  rake "db:seed"
159
171
 
@@ -169,6 +181,8 @@ file 'app/models/article.rb', <<-CODE
169
181
  class Article < ActiveRecord::Base
170
182
  include Tire::Model::Search
171
183
  include Tire::Model::Callbacks
184
+
185
+ attr_accessible :title, :content, :published_on
172
186
  end
173
187
  CODE
174
188
 
data/lib/tire.rb CHANGED
@@ -20,12 +20,14 @@ require 'tire/http/response'
20
20
  require 'tire/http/client'
21
21
  require 'tire/search'
22
22
  require 'tire/search/query'
23
+ require 'tire/search/queries/match'
23
24
  require 'tire/search/sort'
24
25
  require 'tire/search/facet'
25
26
  require 'tire/search/filter'
26
27
  require 'tire/search/highlight'
27
28
  require 'tire/search/scan'
28
29
  require 'tire/search/script_field'
30
+ require 'tire/multi_search'
29
31
  require 'tire/results/pagination'
30
32
  require 'tire/results/collection'
31
33
  require 'tire/results/item'
data/lib/tire/alias.rb CHANGED
@@ -209,7 +209,7 @@ module Tire
209
209
 
210
210
  if Configuration.logger.level.to_s == 'debug'
211
211
  body = if @response
212
- defined?(Yajl) ? Yajl::Encoder.encode(@response.body, :pretty => true) : MultiJson.encode(@response.body)
212
+ MultiJson.encode(@response.body, :pretty => Configuration.pretty)
213
213
  else
214
214
  error.message rescue ''
215
215
  end
@@ -19,6 +19,14 @@ module Tire
19
19
  @logger || nil
20
20
  end
21
21
 
22
+ def self.pretty(value=nil, options={})
23
+ if value === false
24
+ return @pretty = false
25
+ else
26
+ @pretty.nil? ? true : @pretty
27
+ end
28
+ end
29
+
22
30
  def self.reset(*properties)
23
31
  reset_variables = properties.empty? ? instance_variables : instance_variables.map { |p| p.to_s} & \
24
32
  properties.map { |p| "@#{p}" }
data/lib/tire/dsl.rb CHANGED
@@ -18,8 +18,11 @@ module Tire
18
18
  options
19
19
  else raise ArgumentError, "Please pass a Ruby Hash or String with JSON"
20
20
  end
21
-
22
- Search::Search.new(indices, :payload => payload)
21
+ unless options.empty?
22
+ Search::Search.new(indices, :payload => payload)
23
+ else
24
+ Search::Search.new(indices)
25
+ end
23
26
  end
24
27
  rescue Exception => error
25
28
  STDERR.puts "[REQUEST FAILED] #{error.class} #{error.message rescue nil}\n"
@@ -27,6 +30,70 @@ module Tire
27
30
  ensure
28
31
  end
29
32
 
33
+ # Build and perform a [multi-search](http://elasticsearch.org/guide/reference/api/multi-search.html)
34
+ # request.
35
+ #
36
+ # s = Tire.search 'clients' do
37
+ # search :names do
38
+ # query { match :name, 'carpenter' }
39
+ # end
40
+ # search :counts, search_type: 'count' do
41
+ # query { match [:name, :street, :occupation], 'carpenter' }
42
+ # end
43
+ # search :vip, index: 'clients-vip' do
44
+ # query { string "last_name:carpenter" }
45
+ # end
46
+ # search() { query {all} }
47
+ # end
48
+ #
49
+ # The DSL allows you to perform multiple searches and get corresponding results
50
+ # in a single HTTP request, saving network roundtrips.
51
+ #
52
+ # Use the `search` method in the block to define a search request with the
53
+ # regular Tire's DSL (`query`, `facet`, etc).
54
+ #
55
+ # You can pass options such as `search_type`, `routing`, etc.,
56
+ # as well as a different `index` and/or `type` to individual searches.
57
+ #
58
+ # You can give single searches names, to be able to refer to them later.
59
+ #
60
+ # The results are returned as an enumerable collection of {Tire::Results::Collection} instances.
61
+ #
62
+ # You may simply iterate over them with `each`:
63
+ #
64
+ # s.results.each do |results|
65
+ # puts results.map(&:name)
66
+ # end
67
+ #
68
+ # To iterate over named results, use the `each_pair` method:
69
+ #
70
+ # s.results.each_pair do |name,results|
71
+ # puts "Search #{name} got #{results.size} results"
72
+ # end
73
+ #
74
+ # You can get a specific named result:
75
+ #
76
+ # search.results[:vip]
77
+ #
78
+ # You can mix & match named and non-named searches in the definition; the non-named
79
+ # searches will be zero-based numbered, so you can refer to them:
80
+ #
81
+ # search.results[3] # Results for the last query
82
+ #
83
+ # To log the multi-search request, use the standard `to_curl` method (or set up a logger):
84
+ #
85
+ # print search.to_curl
86
+ #
87
+ def multi_search(indices=nil, options={}, &block)
88
+ Search::Multi::Search.new(indices, options, &block)
89
+ rescue Exception => error
90
+ STDERR.puts "[REQUEST FAILED] #{error.class} #{error.message rescue nil}\n"
91
+ raise
92
+ ensure
93
+ end
94
+ alias :multisearch :multi_search
95
+ alias :msearch :multi_search
96
+
30
97
  def index(name, &block)
31
98
  Index.new(name, &block)
32
99
  end
data/lib/tire/index.rb CHANGED
@@ -36,7 +36,7 @@ module Tire
36
36
  @response.success? ? @response : false
37
37
 
38
38
  ensure
39
- curl = %Q|curl -X POST #{url} -d '#{MultiJson.encode(options)}'|
39
+ curl = %Q|curl -X POST #{url} -d '#{MultiJson.encode(options, :pretty => Configuration.pretty)}'|
40
40
  logged('CREATE', curl)
41
41
  end
42
42
 
@@ -64,22 +64,27 @@ module Tire
64
64
 
65
65
  def store(*args)
66
66
  document, options = args
67
- type = get_type_from_document(document)
68
-
69
- if options
70
- percolate = options[:percolate]
71
- percolate = "*" if percolate === true
72
- end
73
67
 
74
68
  id = get_id_from_document(document)
69
+ type = get_type_from_document(document)
75
70
  document = convert_document_to_json(document)
76
71
 
77
- url = id ? "#{self.url}/#{type}/#{id}" : "#{self.url}/#{type}/"
78
- url += "?percolate=#{percolate}" if percolate
72
+ options ||= {}
73
+ params = {}
74
+
75
+ if options[:percolate]
76
+ params[:percolate] = options[:percolate]
77
+ params[:percolate] = "*" if params[:percolate] === true
78
+ end
79
+
80
+ params[:parent] = options[:parent] if options[:parent]
81
+
82
+ params_encoded = params.empty? ? '' : "?#{params.to_param}"
83
+
84
+ url = id ? "#{self.url}/#{type}/#{id}#{params_encoded}" : "#{self.url}/#{type}/#{params_encoded}"
79
85
 
80
86
  @response = Configuration.client.post url, document
81
87
  MultiJson.decode(@response.body)
82
-
83
88
  ensure
84
89
  curl = %Q|curl -X POST "#{url}" -d '#{document}'|
85
90
  logged([type, id].join('/'), curl)
@@ -117,7 +122,7 @@ module Tire
117
122
  end
118
123
 
119
124
  ensure
120
- curl = %Q|curl -X POST "#{url}/_bulk" -d '{... data omitted ...}'|
125
+ curl = %Q|curl -X POST "#{url}/_bulk" --data-binary '{... data omitted ...}'|
121
126
  logged('BULK', curl)
122
127
  end
123
128
  end
@@ -184,20 +189,28 @@ module Tire
184
189
  logged(id, curl)
185
190
  end
186
191
 
187
- def retrieve(type, id)
192
+ def retrieve(type, id, options={})
188
193
  raise ArgumentError, "Please pass a document ID" unless id
189
194
 
190
195
  type = Utils.escape(type)
191
196
  url = "#{self.url}/#{type}/#{id}"
192
- @response = Configuration.client.get url
197
+
198
+ params = {}
199
+ params[:routing] = options[:routing] if options[:routing]
200
+ params[:fields] = options[:fields] if options[:fields]
201
+ params[:preference] = options[:preference] if options[:preference]
202
+ params_encoded = params.empty? ? '' : "?#{params.to_param}"
203
+
204
+ @response = Configuration.client.get "#{url}#{params_encoded}"
193
205
 
194
206
  h = MultiJson.decode(@response.body)
195
- if Configuration.wrapper == Hash then h
207
+ wrapper = options[:wrapper] || Configuration.wrapper
208
+ if wrapper == Hash then h
196
209
  else
197
210
  return nil if h['exists'] == false
198
211
  document = h['_source'] || h['fields'] || {}
199
212
  document.update('id' => h['_id'], '_type' => h['_type'], '_index' => h['_index'], '_version' => h['_version'])
200
- Configuration.wrapper.new(document)
213
+ wrapper.new(document)
201
214
  end
202
215
 
203
216
  ensure
@@ -217,7 +230,7 @@ module Tire
217
230
  MultiJson.decode(@response.body)
218
231
 
219
232
  ensure
220
- curl = %Q|curl -X POST "#{url}" -d '#{MultiJson.encode(payload)}'|
233
+ curl = %Q|curl -X POST "#{url}" -d '#{MultiJson.encode(payload, :pretty => Configuration.pretty)}'|
221
234
  logged(id, curl)
222
235
  end
223
236
 
@@ -266,7 +279,7 @@ module Tire
266
279
  MultiJson.decode(@response.body)['ok']
267
280
 
268
281
  ensure
269
- curl = %Q|curl -X PUT "#{Configuration.url}/_percolator/#{@name}/#{name}?pretty=1" -d '#{MultiJson.encode(options)}'|
282
+ curl = %Q|curl -X PUT "#{Configuration.url}/_percolator/#{@name}/#{name}?pretty" -d '#{MultiJson.encode(options, :pretty => Configuration.pretty)}'|
270
283
  logged('_percolator', curl)
271
284
  end
272
285
 
@@ -294,7 +307,7 @@ module Tire
294
307
  MultiJson.decode(@response.body)['matches']
295
308
 
296
309
  ensure
297
- curl = %Q|curl -X GET "#{url}/#{type}/_percolate?pretty=1" -d '#{payload.to_json}'|
310
+ curl = %Q|curl -X GET "#{url}/#{type}/_percolate?pretty" -d '#{MultiJson.encode(payload, :pretty => Configuration.pretty)}'|
298
311
  logged('_percolate', curl)
299
312
  end
300
313
 
@@ -308,9 +321,9 @@ module Tire
308
321
 
309
322
  if Configuration.logger.level.to_s == 'debug'
310
323
  body = if @response
311
- defined?(Yajl) ? Yajl::Encoder.encode(@response.body, :pretty => true) : MultiJson.encode(@response.body)
324
+ MultiJson.encode( MultiJson.load(@response.body), :pretty => Configuration.pretty)
312
325
  else
313
- error.message rescue ''
326
+ MultiJson.encode( MultiJson.load(error.message), :pretty => Configuration.pretty) rescue ''
314
327
  end
315
328
  else
316
329
  body = ''
@@ -105,11 +105,17 @@ module Tire
105
105
  #
106
106
  def create_elasticsearch_index
107
107
  unless index.exists?
108
- index.create :mappings => mapping_to_hash, :settings => settings
108
+ new_index = index
109
+ unless result = new_index.create(:mappings => mapping_to_hash, :settings => settings)
110
+ STDERR.puts "[ERROR] There has been an error when creating the index -- elasticsearch returned:",
111
+ new_index.response
112
+ result
113
+ end
109
114
  end
110
115
  rescue Errno::ECONNREFUSED => e
111
116
  STDERR.puts "Skipping index creation, cannot connect to ElasticSearch",
112
117
  "(The original exception was: #{e.inspect})"
118
+ false
113
119
  end
114
120
 
115
121
  def mapping_options
@@ -11,7 +11,7 @@ module Tire
11
11
  #
12
12
  # class Article
13
13
  # include Tire::Model::Persistence
14
- #
14
+ #
15
15
  # property :title
16
16
  # end
17
17
  #
@@ -54,11 +54,14 @@ module Tire
54
54
  args.last.update(:wrapper => self, :version => true) if args.last.is_a? Hash
55
55
  args << { :wrapper => self, :version => true } unless args.any? { |a| a.is_a? Hash }
56
56
 
57
- self.__search_without_persistence(*args, &block)
57
+ self.tire.search(*args, &block)
58
58
  end
59
59
 
60
- def self.__search_without_persistence(*args, &block)
61
- self.tire.search(*args, &block)
60
+ def self.multi_search(*args, &block)
61
+ args.last.update(:wrapper => self, :version => true) if args.last.is_a? Hash
62
+ args << { :wrapper => self, :version => true } unless args.any? { |a| a.is_a? Hash }
63
+
64
+ self.tire.multi_search(*args, &block)
62
65
  end
63
66
 
64
67
  end
@@ -46,7 +46,7 @@ module Tire
46
46
 
47
47
  # Save property default value (when relevant):
48
48
  unless (default_value = options.delete(:default)).nil?
49
- property_defaults[name.to_sym] = default_value
49
+ property_defaults[name.to_sym] = default_value.respond_to?(:call) ? default_value.call : default_value
50
50
  end
51
51
 
52
52
  # Save property casting (when relevant):
@@ -11,12 +11,10 @@ module Tire
11
11
 
12
12
  def find *args
13
13
  # TODO: Options like `sort`
14
- old_wrapper = Tire::Configuration.wrapper
15
- Tire::Configuration.wrapper self
16
14
  options = args.pop if args.last.is_a?(Hash)
17
15
  args.flatten!
18
16
  if args.size > 1
19
- Tire::Search::Search.new(index.name) do |search|
17
+ Tire::Search::Search.new(index.name, :wrapper => self) do |search|
20
18
  search.query do |query|
21
19
  query.ids(args, document_type)
22
20
  end
@@ -25,35 +23,25 @@ module Tire
25
23
  else
26
24
  case args = args.pop
27
25
  when Fixnum, String
28
- index.retrieve document_type, args
26
+ index.retrieve document_type, args, :wrapper => self
29
27
  when :all, :first
30
28
  send(args)
31
29
  else
32
30
  raise ArgumentError, "Please pass either ID as Fixnum or String, or :all, :first as an argument"
33
31
  end
34
32
  end
35
- ensure
36
- Tire::Configuration.wrapper old_wrapper
37
33
  end
38
34
 
39
35
  def all
40
36
  # TODO: Options like `sort`; Possibly `filters`
41
- old_wrapper = Tire::Configuration.wrapper
42
- Tire::Configuration.wrapper self
43
- s = Tire::Search::Search.new(index.name).query { all }
37
+ s = Tire::Search::Search.new(index.name, :type => document_type, :wrapper => self).query { all }
44
38
  s.version(true).results
45
- ensure
46
- Tire::Configuration.wrapper old_wrapper
47
39
  end
48
40
 
49
41
  def first
50
42
  # TODO: Options like `sort`; Possibly `filters`
51
- old_wrapper = Tire::Configuration.wrapper
52
- Tire::Configuration.wrapper self
53
- s = Tire::Search::Search.new(index.name).query { all }.size(1)
43
+ s = Tire::Search::Search.new(index.name, :type => document_type, :wrapper => self).query { all }.size(1)
54
44
  s.version(true).results.first
55
- ensure
56
- Tire::Configuration.wrapper old_wrapper
57
45
  end
58
46
 
59
47
  end
@@ -72,20 +72,26 @@ module Tire
72
72
  options ||= {}
73
73
  end
74
74
 
75
- sort = Array( options[:order] || options[:sort] )
76
75
  options = default_options.update(options)
76
+ sort = Array( options.delete(:order) || options.delete(:sort) )
77
77
 
78
78
  s = Tire::Search::Search.new(options.delete(:index), options)
79
- s.size( options[:per_page].to_i ) if options[:per_page]
80
- s.from( options[:page].to_i <= 1 ? 0 : (options[:per_page].to_i * (options[:page].to_i-1)) ) if options[:page] && options[:per_page]
79
+
80
+ page = options.delete(:page)
81
+ per_page = options.delete(:per_page) || Tire::Results::Pagination::default_per_page
82
+
83
+ s.size( per_page.to_i ) if per_page
84
+ s.from( page.to_i <= 1 ? 0 : (per_page.to_i * (page.to_i-1)) ) if page && per_page
85
+
81
86
  s.sort do
82
87
  sort.each do |t|
83
- field_name, direction = t.split(' ')
88
+ field_name, direction = t.split(':')
84
89
  by field_name, direction
85
90
  end
86
91
  end unless sort.empty?
87
92
 
88
- if version = options.delete(:version); s.version(version); end
93
+ version = options.delete(:version)
94
+ s.version(version) if version
89
95
 
90
96
  if block_given?
91
97
  block.arity < 1 ? s.instance_eval(&block) : block.call(s)
@@ -99,6 +105,12 @@ module Tire
99
105
  s.results
100
106
  end
101
107
 
108
+ def multi_search(options={}, &block)
109
+ default_options = {:type => document_type}
110
+ options = default_options.update(options)
111
+ Tire::Search::Multi::Search.new(index.name, options, &block).results
112
+ end
113
+
102
114
  # Returns a Tire::Index instance for this model.
103
115
  #
104
116
  # Example usage: `Article.index.refresh`.
@@ -154,7 +166,7 @@ module Tire
154
166
  # declared in the mapping are serialized.
155
167
  #
156
168
  # For properties declared with the `:as` option, the passed String or Proc
157
- # is evaluated in the instance context.
169
+ # is evaluated in the instance context. Other objects are indexed "as is".
158
170
  #
159
171
  def to_indexed_json
160
172
  if instance.class.tire.mapping.empty?
@@ -172,7 +184,9 @@ module Tire
172
184
  hash[key] = instance.instance_eval(options[:as])
173
185
  when Proc
174
186
  hash[key] = instance.instance_eval(&options[:as])
175
- end
187
+ else
188
+ hash[key] = options[:as]
189
+ end if options[:as]
176
190
  end
177
191
 
178
192
  hash.to_json
@@ -300,7 +314,6 @@ module Tire
300
314
  Results::Item.send :include, Loader
301
315
  end
302
316
 
303
-
304
317
  end
305
318
 
306
319
  end