ssickles-tire 0.4.2.7 → 0.4.3

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 (59) hide show
  1. data/lib/tire.rb +18 -3
  2. data/lib/tire/alias.rb +11 -35
  3. data/lib/tire/index.rb +34 -76
  4. data/lib/tire/model/callbacks.rb +40 -0
  5. data/lib/tire/model/import.rb +26 -0
  6. data/lib/tire/model/indexing.rb +128 -0
  7. data/lib/tire/model/naming.rb +100 -0
  8. data/lib/tire/model/percolate.rb +99 -0
  9. data/lib/tire/model/persistence.rb +72 -0
  10. data/lib/tire/model/persistence/attributes.rb +143 -0
  11. data/lib/tire/model/persistence/finders.rb +66 -0
  12. data/lib/tire/model/persistence/storage.rb +71 -0
  13. data/lib/tire/model/search.rb +305 -0
  14. data/lib/tire/results/collection.rb +38 -13
  15. data/lib/tire/results/item.rb +19 -0
  16. data/lib/tire/rubyext/hash.rb +8 -0
  17. data/lib/tire/rubyext/ruby_1_8.rb +54 -0
  18. data/lib/tire/rubyext/symbol.rb +11 -0
  19. data/lib/tire/search.rb +7 -8
  20. data/lib/tire/search/scan.rb +8 -8
  21. data/lib/tire/search/sort.rb +1 -1
  22. data/lib/tire/utils.rb +17 -0
  23. data/lib/tire/version.rb +7 -38
  24. data/test/integration/active_model_indexing_test.rb +51 -0
  25. data/test/integration/active_model_searchable_test.rb +114 -0
  26. data/test/integration/active_record_searchable_test.rb +446 -0
  27. data/test/integration/mongoid_searchable_test.rb +309 -0
  28. data/test/integration/persistent_model_test.rb +117 -0
  29. data/test/integration/reindex_test.rb +2 -2
  30. data/test/integration/scan_test.rb +1 -1
  31. data/test/models/active_model_article.rb +31 -0
  32. data/test/models/active_model_article_with_callbacks.rb +49 -0
  33. data/test/models/active_model_article_with_custom_document_type.rb +7 -0
  34. data/test/models/active_model_article_with_custom_index_name.rb +7 -0
  35. data/test/models/active_record_models.rb +122 -0
  36. data/test/models/mongoid_models.rb +97 -0
  37. data/test/models/persistent_article.rb +11 -0
  38. data/test/models/persistent_article_in_namespace.rb +12 -0
  39. data/test/models/persistent_article_with_casting.rb +28 -0
  40. data/test/models/persistent_article_with_defaults.rb +11 -0
  41. data/test/models/persistent_articles_with_custom_index_name.rb +10 -0
  42. data/test/models/supermodel_article.rb +17 -0
  43. data/test/models/validated_model.rb +11 -0
  44. data/test/test_helper.rb +27 -3
  45. data/test/unit/active_model_lint_test.rb +17 -0
  46. data/test/unit/index_alias_test.rb +3 -17
  47. data/test/unit/index_test.rb +30 -18
  48. data/test/unit/model_callbacks_test.rb +116 -0
  49. data/test/unit/model_import_test.rb +71 -0
  50. data/test/unit/model_persistence_test.rb +516 -0
  51. data/test/unit/model_search_test.rb +899 -0
  52. data/test/unit/results_collection_test.rb +60 -0
  53. data/test/unit/results_item_test.rb +37 -0
  54. data/test/unit/rubyext_test.rb +3 -3
  55. data/test/unit/search_test.rb +1 -6
  56. data/test/unit/tire_test.rb +15 -0
  57. data/tire.gemspec +30 -13
  58. metadata +153 -41
  59. data/lib/tire/rubyext/to_json.rb +0 -21
data/lib/tire.rb CHANGED
@@ -1,12 +1,17 @@
1
1
  require 'rest_client'
2
2
  require 'multi_json'
3
+ #require 'active_model'
3
4
  require 'hashr'
4
5
  require 'cgi'
5
- require 'escape_utils'
6
6
 
7
- require 'active_support/core_ext'
7
+ require 'active_support'
8
8
 
9
- require 'tire/rubyext/to_json'
9
+ # Ruby 1.8 compatibility
10
+ require 'tire/rubyext/ruby_1_8' if defined?(RUBY_VERSION) && RUBY_VERSION < '1.9'
11
+
12
+ require 'tire/rubyext/hash'
13
+ require 'tire/rubyext/symbol'
14
+ require 'tire/utils'
10
15
  require 'tire/logger'
11
16
  require 'tire/configuration'
12
17
  require 'tire/http/response'
@@ -24,6 +29,16 @@ require 'tire/results/item'
24
29
  require 'tire/index'
25
30
  require 'tire/alias'
26
31
  require 'tire/dsl'
32
+ require 'tire/model/naming'
33
+ require 'tire/model/callbacks'
34
+ require 'tire/model/percolate'
35
+ require 'tire/model/indexing'
36
+ require 'tire/model/import'
37
+ require 'tire/model/search'
38
+ require 'tire/model/persistence/finders'
39
+ require 'tire/model/persistence/attributes'
40
+ require 'tire/model/persistence/storage'
41
+ require 'tire/model/persistence'
27
42
  require 'tire/tasks'
28
43
 
29
44
  module Tire
data/lib/tire/alias.rb CHANGED
@@ -74,7 +74,6 @@ module Tire
74
74
  def initialize(attributes={}, &block)
75
75
  raise ArgumentError, "Please pass a Hash-like object" unless attributes.respond_to?(:each_pair)
76
76
 
77
- @url = attributes.delete(:url) || Configuration.url
78
77
  @attributes = { :indices => IndexCollection.new([]) }
79
78
 
80
79
  attributes.each_pair do |key, value|
@@ -88,57 +87,34 @@ module Tire
88
87
  block.arity < 1 ? instance_eval(&block) : block.call(self) if block_given?
89
88
  end
90
89
 
91
- attr_accessor :url
92
-
93
90
  # Returns a collection of Tire::Alias objects for all aliases defined in the cluster, or for a specific index.
94
91
  #
95
- def self.all(url=nil, index=nil)
96
- if url and url !~ /^https?:\/\//i
97
- index, url = url, Configuration.url
98
- elsif url.nil?
99
- url = Configuration.url
100
- end
92
+ def self.all(index=nil)
93
+ @response = Configuration.client.get [Configuration.url, index, '_aliases'].compact.join('/')
101
94
 
102
- @response = Configuration.client.get [url, index, '_aliases'].compact.join('/')
103
-
104
- aliases = MultiJson.decode(@response.body).inject({}) do |result, (name, value)|
95
+ aliases = MultiJson.decode(@response.body).inject({}) do |result, (index, value)|
105
96
  # 1] Skip indices without aliases
106
97
  next result if value['aliases'].empty?
107
98
 
108
99
  # 2] Build a reverse map of hashes (alias => indices, config)
109
- value['aliases'].each do |key, v| (result[key] ||= { 'indices' => [] }).update(v)['indices'].push(name) end
100
+ value['aliases'].each do |key, value| (result[key] ||= { 'indices' => [] }).update(value)['indices'].push(index) end
110
101
  result
111
102
  end
112
103
 
113
104
  # 3] Build a collection of Alias objects from hashes
114
105
  aliases.map do |key, value|
115
- self.new(value.update('name' => key, :url => url))
106
+ self.new(value.update('name' => key))
116
107
  end
117
108
 
118
109
  ensure
119
110
  # FIXME: Extract the `logged` method
120
- Alias.new.logged '_aliases', %Q|curl "#{url}/_aliases"|
111
+ Alias.new.logged '_aliases', %Q|curl "#{Configuration.url}/_aliases"|
121
112
  end
122
113
 
123
114
  # Returns an alias by name
124
115
  #
125
- # Examples
126
- #
127
- # Alias.find( url, name )
128
- # Alias.find( name )
129
- #
130
- def self.find(*args, &block)
131
- case args.length
132
- when 2
133
- url, name = args
134
- when 1
135
- url = Configuration.url
136
- name = args.first
137
- else
138
- raise ArgumentError, "wrong number of arguments (#{args.length} for 2)"
139
- end
140
-
141
- a = all(url).select { |b| b.name == name }.first
116
+ def self.find(name, &block)
117
+ a = all.select { |a| a.name == name }.first
142
118
  block.call(a) if block_given?
143
119
  return a
144
120
  end
@@ -183,10 +159,10 @@ module Tire
183
159
  # Save the alias in _ElasticSearch_
184
160
  #
185
161
  def save
186
- @response = Configuration.client.post "#{url}/_aliases", to_json
162
+ @response = Configuration.client.post "#{Configuration.url}/_aliases", to_json
187
163
 
188
164
  ensure
189
- logged '_aliases', %Q|curl -X POST "#{url}/_aliases" -d '#{to_json}'|
165
+ logged '_aliases', %Q|curl -X POST "#{Configuration.url}/_aliases" -d '#{to_json}'|
190
166
  end
191
167
 
192
168
  # Return a Hash suitable for JSON serialization
@@ -211,7 +187,7 @@ module Tire
211
187
  # Return alias serialized in JSON for _ElasticSearch_
212
188
  #
213
189
  def to_json(options=nil)
214
- MultiJson.encode(as_json)
190
+ as_json.to_json
215
191
  end
216
192
 
217
193
  def inspect
data/lib/tire/index.rb CHANGED
@@ -1,27 +1,15 @@
1
1
  module Tire
2
2
  class Index
3
3
 
4
- attr_reader :name, :response, :url
5
-
6
- def initialize(*args, &block)
7
- case args.length
8
- when 2
9
- @name = args.last
10
- self.url = args.first
11
- when 1
12
- @name = args.first
13
- self.url = Configuration.url
14
- else
15
- raise ArgumentError, "wrong number of arguments (#{args.length} for 2)"
16
- end
4
+ attr_reader :name, :response
17
5
 
6
+ def initialize(name, &block)
7
+ @name = name
18
8
  block.arity < 1 ? instance_eval(&block) : block.call(self) if block_given?
19
9
  end
20
10
 
21
- def url=( url )
22
- @base_url = url.dup
23
- @url = "#@base_url/#@name"
24
- @percolator_url = "#@base_url/_percolator/#@name"
11
+ def url
12
+ "#{Configuration.url}/#{@name}"
25
13
  end
26
14
 
27
15
  def exists?
@@ -53,15 +41,15 @@ module Tire
53
41
  end
54
42
 
55
43
  def add_alias(alias_name, configuration={})
56
- Alias.create(configuration.merge( :name => alias_name, :index => @name, :url => @base_url ) )
44
+ Alias.create(configuration.merge( :name => alias_name, :index => @name ) )
57
45
  end
58
46
 
59
47
  def remove_alias(alias_name)
60
- Alias.find(@base_url, alias_name) { |a| a.indices.delete @name }.save
48
+ Alias.find(alias_name) { |a| a.indices.delete @name }.save
61
49
  end
62
50
 
63
51
  def aliases(alias_name=nil)
64
- alias_name ? Alias.all(@base_url, @name).select { |a| a.name == alias_name }.first : Alias.all(@base_url, @name)
52
+ alias_name ? Alias.all(@name).select { |a| a.name == alias_name }.first : Alias.all(@name)
65
53
  end
66
54
 
67
55
  def mapping
@@ -75,24 +63,23 @@ module Tire
75
63
  end
76
64
 
77
65
  def store(*args)
78
- document, params = args
79
- id = get_id_from_document(document)
80
- type = get_type_from_document(document)
81
- routing = get_routing_from_document(document)
82
- parent = get_parent_from_document(document)
83
-
84
- params ||= {}
85
- params[:routing] = routing if routing
86
- params[:parent] = parent if parent
87
- params[:percolate] = '*' if params[:percolate] === true
66
+ document, options = args
67
+ type = get_type_from_document(document)
88
68
 
89
- url = id ? "#{self.url}/#{type}/#{id}" : "#{self.url}/#{type}"
90
- url << "?#{params.to_param}" unless params.empty?
69
+ if options
70
+ percolate = options[:percolate]
71
+ percolate = "*" if percolate === true
72
+ end
91
73
 
74
+ id = get_id_from_document(document)
92
75
  document = convert_document_to_json(document)
93
76
 
77
+ url = id ? "#{self.url}/#{type}/#{id}" : "#{self.url}/#{type}/"
78
+ url += "?percolate=#{percolate}" if percolate
79
+
94
80
  @response = Configuration.client.post url, document
95
81
  MultiJson.decode(@response.body)
82
+
96
83
  ensure
97
84
  curl = %Q|curl -X POST "#{url}" -d '#{document}'|
98
85
  logged([type, id].join('/'), curl)
@@ -172,7 +159,7 @@ module Tire
172
159
  def remove(*args)
173
160
  if args.size > 1
174
161
  type, document = args
175
- type = EscapeUtils.escape_url(type.to_s)
162
+ type = Utils.escape(type)
176
163
  id = get_id_from_document(document) || document
177
164
  else
178
165
  document = args.pop
@@ -181,25 +168,20 @@ module Tire
181
168
  end
182
169
  raise ArgumentError, "Please pass a document ID" unless id
183
170
 
184
- routing = get_routing_from_document(document)
185
-
186
- url = "#{self.url}/#{type}/#{id}"
187
- url << "?routing=#{EscapeUtils.escape_url(routing)}" if routing
188
-
171
+ url = "#{self.url}/#{type}/#{id}"
189
172
  result = Configuration.client.delete url
190
173
  MultiJson.decode(result.body) if result.success?
174
+
191
175
  ensure
192
176
  curl = %Q|curl -X DELETE "#{url}"|
193
177
  logged(id, curl)
194
178
  end
195
179
 
196
- def retrieve(type, id, params = {})
180
+ def retrieve(type, id)
197
181
  raise ArgumentError, "Please pass a document ID" unless id
198
182
 
199
- type = EscapeUtils.escape_url(type.to_s)
200
- url = "#{self.url}/#{type}/#{id}"
201
- url << "?#{params.to_param}" unless params.empty?
202
-
183
+ type = Utils.escape(type)
184
+ url = "#{self.url}/#{type}/#{id}"
203
185
  @response = Configuration.client.get url
204
186
 
205
187
  h = MultiJson.decode(@response.body)
@@ -257,22 +239,20 @@ module Tire
257
239
  def register_percolator_query(name, options={}, &block)
258
240
  options[:query] = Search::Query.new(&block).to_hash if block_given?
259
241
 
260
- url = "#@percolator_url/#{name}"
261
- @response = Configuration.client.put url, MultiJson.encode(options)
242
+ @response = Configuration.client.put "#{Configuration.url}/_percolator/#{@name}/#{name}", MultiJson.encode(options)
262
243
  MultiJson.decode(@response.body)['ok']
263
244
 
264
245
  ensure
265
- curl = %Q|curl -X PUT "#{url}?pretty=true" -d '#{MultiJson.encode(options)}'|
246
+ curl = %Q|curl -X PUT "#{Configuration.url}/_percolator/#{@name}/?pretty=1" -d '#{MultiJson.encode(options)}'|
266
247
  logged('_percolator', curl)
267
248
  end
268
249
 
269
250
  def unregister_percolator_query(name)
270
- url = "#@percolator_url/#{name}"
271
- @response = Configuration.client.delete url
251
+ @response = Configuration.client.delete "#{Configuration.url}/_percolator/#{@name}/#{name}"
272
252
  MultiJson.decode(@response.body)['ok']
273
253
 
274
254
  ensure
275
- curl = %Q|curl -X DELETE "#{url}"|
255
+ curl = %Q|curl -X DELETE "#{Configuration.url}/_percolator/#{@name}"|
276
256
  logged('_percolator', curl)
277
257
  end
278
258
 
@@ -322,10 +302,10 @@ module Tire
322
302
 
323
303
  old_verbose, $VERBOSE = $VERBOSE, nil # Silence Object#type deprecation warnings
324
304
  type = case
325
- when document.is_a?(Hash)
326
- document.delete(:_type) || document.delete('_type') || document[:type] || document['type']
327
305
  when document.respond_to?(:document_type)
328
306
  document.document_type
307
+ when document.is_a?(Hash)
308
+ document[:_type] || document['_type'] || document[:type] || document['type']
329
309
  when document.respond_to?(:_type)
330
310
  document._type
331
311
  when document.respond_to?(:type) && document.type != document.class
@@ -333,15 +313,15 @@ module Tire
333
313
  end
334
314
  $VERBOSE = old_verbose
335
315
 
336
- type = type ? type.to_s : 'document'
337
- options[:escape] ? EscapeUtils.escape_url(type) : type
316
+ type ||= 'document'
317
+ options[:escape] ? Utils.escape(type) : type
338
318
  end
339
319
 
340
320
  def get_id_from_document(document)
341
321
  old_verbose, $VERBOSE = $VERBOSE, nil # Silence Object#id deprecation warnings
342
322
  id = case
343
323
  when document.is_a?(Hash)
344
- document.delete(:_id) || document.delete('_id') || document[:id] || document['id']
324
+ document[:_id] || document['_id'] || document[:id] || document['id']
345
325
  when document.respond_to?(:id) && document.id != document.object_id
346
326
  document.id
347
327
  end
@@ -349,28 +329,6 @@ module Tire
349
329
  id
350
330
  end
351
331
 
352
- def get_routing_from_document(document)
353
- case
354
- when document.is_a?(Hash)
355
- document.delete(:_routing) || document.delete('_routing')
356
- when document.respond_to?(:routing)
357
- document.routing
358
- when document.respond_to?(:_routing)
359
- document._routing
360
- end
361
- end
362
-
363
- def get_parent_from_document(document)
364
- case
365
- when document.is_a?(Hash)
366
- document.delete(:_parent) || document.delete('_parent')
367
- when document.respond_to?(:parent)
368
- document.parent
369
- when document.respond_to?(:_parent)
370
- document._parent
371
- end
372
- end
373
-
374
332
  def convert_document_to_json(document)
375
333
  document = case
376
334
  when document.is_a?(String)
@@ -0,0 +1,40 @@
1
+ module Tire
2
+ module Model
3
+
4
+ # Main module containing the infrastructure for automatic updating
5
+ # of the _ElasticSearch_ index on model instance create, update or delete.
6
+ #
7
+ # Include it in your model: `include Tire::Model::Callbacks`
8
+ #
9
+ # The model must respond to `after_save` and `after_destroy` callbacks
10
+ # (ActiveModel and ActiveRecord models do so, by default).
11
+ #
12
+ module Callbacks
13
+
14
+ # A hook triggered by the `include Tire::Model::Callbacks` statement in the model.
15
+ #
16
+ def self.included(base)
17
+ # Update index on model instance change or destroy.
18
+ #
19
+ if base.respond_to?(:after_save) && base.respond_to?(:after_destroy)
20
+ base.class_eval do
21
+ after_save lambda { tire.update_index }
22
+ after_destroy lambda { tire.update_index }
23
+ end
24
+ end
25
+
26
+ # Add neccessary infrastructure for the model, when missing in
27
+ # some half-baked ActiveModel implementations.
28
+ #
29
+ if base.respond_to?(:before_destroy) && !base.instance_methods.map(&:to_sym).include?(:destroyed?)
30
+ base.class_eval do
31
+ before_destroy { @destroyed = true }
32
+ def destroyed?; !!@destroyed; end
33
+ end
34
+ end
35
+ end
36
+
37
+ end
38
+
39
+ end
40
+ end
@@ -0,0 +1,26 @@
1
+
2
+ module Tire
3
+ module Model
4
+
5
+ # Provides support for easy importing of large ActiveRecord- and ActiveModel-bound
6
+ # recordsets into model index.
7
+ #
8
+ # Relies on pagination support in your model, namely the `paginate` class method.
9
+ #
10
+ # Please refer to the relevant of the README for more information.
11
+ #
12
+ module Import
13
+
14
+ module ClassMethods
15
+
16
+ def import options={}, &block
17
+ options = { :method => 'paginate' }.update options
18
+ index.import klass, options, &block
19
+ end
20
+
21
+ end
22
+
23
+ end
24
+
25
+ end
26
+ end
@@ -0,0 +1,128 @@
1
+ module Tire
2
+ module Model
3
+
4
+ # Contains logic for definition of index settings and mappings.
5
+ #
6
+ module Indexing
7
+
8
+ module ClassMethods
9
+
10
+ # Define [_settings_](http://www.elasticsearch.org/guide/reference/api/admin-indices-create-index.html)
11
+ # for the corresponding index, such as number of shards and replicas, custom analyzers, etc.
12
+ #
13
+ # Usage:
14
+ #
15
+ # class Article
16
+ # # ...
17
+ # settings :number_of_shards => 1 do
18
+ # mapping do
19
+ # # ...
20
+ # end
21
+ # end
22
+ # end
23
+ #
24
+ def settings(*args)
25
+ @settings ||= {}
26
+ args.empty? ? (return @settings) : @settings = args.pop
27
+ yield if block_given?
28
+ end
29
+
30
+ # Define the [_mapping_](http://www.elasticsearch.org/guide/reference/mapping/index.html)
31
+ # for the corresponding index, telling _ElasticSearch_ how to understand your documents:
32
+ # what type is which property, whether it is analyzed or no, which analyzer to use, etc.
33
+ #
34
+ # You may pass the top level mapping properties (such as `_source` or `_all`) as a Hash.
35
+ #
36
+ # Usage:
37
+ #
38
+ # class Article
39
+ # # ...
40
+ # mapping :_source => { :compress => true } do
41
+ # indexes :id, :index => :not_analyzed
42
+ # indexes :title, :analyzer => 'snowball', :boost => 100
43
+ # indexes :words, :as => 'content.split(/\W/).length'
44
+ #
45
+ # indexes :comments do
46
+ # indexes :body
47
+ # indexes :author do
48
+ # indexes :name
49
+ # end
50
+ # end
51
+ #
52
+ # # ...
53
+ # end
54
+ # end
55
+ #
56
+ def mapping(*args)
57
+ @mapping ||= {}
58
+ if block_given?
59
+ @mapping_options = args.pop
60
+ yield
61
+ create_elasticsearch_index
62
+ else
63
+ @mapping
64
+ end
65
+ end
66
+
67
+ # Define mapping for the property passed as the first argument (`name`)
68
+ # using definition from the second argument (`options`).
69
+ #
70
+ # `:type` is optional and defaults to `'string'`.
71
+ #
72
+ # Usage:
73
+ #
74
+ # * Index property but do not analyze it: `indexes :id, :index => :not_analyzed`
75
+ #
76
+ # * Use different analyzer for indexing a property: `indexes :title, :analyzer => 'snowball'`
77
+ #
78
+ # * Use the `:as` option to dynamically define the serialized property value, eg:
79
+ #
80
+ # :as => 'content.split(/\W/).length'
81
+ #
82
+ # Please refer to the
83
+ # [_mapping_ documentation](http://www.elasticsearch.org/guide/reference/mapping/index.html)
84
+ # for more information.
85
+ #
86
+ def indexes(name, options = {}, &block)
87
+ mapping[name] = options
88
+
89
+ if block_given?
90
+ mapping[name][:type] ||= 'object'
91
+ mapping[name][:properties] ||= {}
92
+
93
+ previous = @mapping
94
+ @mapping = mapping[name][:properties]
95
+ yield
96
+ @mapping = previous
97
+ end
98
+
99
+ mapping[name][:type] ||= 'string'
100
+
101
+ self
102
+ end
103
+
104
+ # Creates the corresponding index with desired settings and mappings, when it does not exists yet.
105
+ #
106
+ def create_elasticsearch_index
107
+ unless index.exists?
108
+ index.create :mappings => mapping_to_hash, :settings => settings
109
+ end
110
+ rescue Errno::ECONNREFUSED => e
111
+ STDERR.puts "Skipping index creation, cannot connect to ElasticSearch",
112
+ "(The original exception was: #{e.inspect})"
113
+ end
114
+
115
+ def mapping_options
116
+ @mapping_options || {}
117
+ end
118
+
119
+ def mapping_to_hash
120
+ { document_type.to_sym => mapping_options.merge({ :properties => mapping }) }
121
+ end
122
+
123
+ end
124
+
125
+ end
126
+
127
+ end
128
+ end