elastic_record 0.11.1 → 0.12.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (41) hide show
  1. checksums.yaml +7 -0
  2. data/Gemfile +2 -1
  3. data/README.rdoc +37 -9
  4. data/elastic_record.gemspec +2 -2
  5. data/lib/elastic_record/callbacks.rb +1 -1
  6. data/lib/elastic_record/connection.rb +36 -13
  7. data/lib/elastic_record/index.rb +33 -4
  8. data/lib/elastic_record/index/configurator.rb +14 -0
  9. data/lib/elastic_record/index/documents.rb +21 -13
  10. data/lib/elastic_record/index/manage.rb +4 -9
  11. data/lib/elastic_record/index/mapping.rb +12 -0
  12. data/lib/elastic_record/index/percolator.rb +1 -0
  13. data/lib/elastic_record/index/settings.rb +4 -0
  14. data/lib/elastic_record/lucene.rb +40 -22
  15. data/lib/elastic_record/model.rb +8 -5
  16. data/lib/elastic_record/relation/batches.rb +8 -1
  17. data/lib/elastic_record/relation/finder_methods.rb +2 -2
  18. data/lib/elastic_record/relation/none.rb +3 -3
  19. data/lib/elastic_record/relation/search_methods.rb +33 -3
  20. data/lib/elastic_record/relation/value_methods.rb +1 -1
  21. data/lib/elastic_record/searches_many.rb +4 -0
  22. data/lib/elastic_record/searches_many/association.rb +25 -14
  23. data/lib/elastic_record/searches_many/reflection.rb +1 -1
  24. data/lib/elastic_record/tasks/index.rake +5 -21
  25. data/test/elastic_record/callbacks_test.rb +9 -0
  26. data/test/elastic_record/connection_test.rb +19 -1
  27. data/test/elastic_record/index/configurator_test.rb +18 -0
  28. data/test/elastic_record/index/documents_test.rb +22 -3
  29. data/test/elastic_record/index/manage_test.rb +7 -0
  30. data/test/elastic_record/index/mapping_test.rb +11 -0
  31. data/test/elastic_record/index/percolator_test.rb +11 -9
  32. data/test/elastic_record/index_test.rb +23 -6
  33. data/test/elastic_record/lucene_test.rb +21 -13
  34. data/test/elastic_record/model_test.rb +9 -9
  35. data/test/elastic_record/relation/batches_test.rb +8 -0
  36. data/test/elastic_record/relation/finder_methods_test.rb +6 -7
  37. data/test/elastic_record/relation/none_test.rb +3 -0
  38. data/test/elastic_record/relation/search_methods_test.rb +17 -41
  39. data/test/elastic_record/relation_test.rb +2 -0
  40. data/test/elastic_record/searches_many/reflection_test.rb +7 -0
  41. metadata +15 -19
@@ -9,13 +9,16 @@ module ElasticRecord
9
9
  end
10
10
 
11
11
  module ClassMethods
12
- # def inherited(child)
13
- # super
14
- # child.elastic_index = elastic_index.dup
15
- # end
12
+ def inherited(child)
13
+ super
14
+
15
+ if child < child.base_class
16
+ child.elastic_index = elastic_index.dup
17
+ end
18
+ end
16
19
 
17
20
  def elastic_connection
18
- @elastic_connection ||= ElasticRecord::Connection.new(ElasticRecord::Config.servers)
21
+ @elastic_connection ||= ElasticRecord::Connection.new(ElasticRecord::Config.servers, ElasticRecord::Config.connection_options)
19
22
  end
20
23
 
21
24
  def elastic_connection=(connection)
@@ -8,6 +8,12 @@ module ElasticRecord
8
8
  end
9
9
 
10
10
  def find_in_batches(options = {})
11
+ find_ids_in_batches(options) do |ids|
12
+ yield klass.find(ids)
13
+ end
14
+ end
15
+
16
+ def find_ids_in_batches(options = {})
11
17
  scroll_keep_alive = '10m'
12
18
 
13
19
  options = {
@@ -19,7 +25,7 @@ module ElasticRecord
19
25
  scroll_id = klass.elastic_index.search(as_elastic, options)['_scroll_id']
20
26
 
21
27
  while (hit_ids = get_scroll_hit_ids(scroll_id, scroll_keep_alive)).any?
22
- yield klass.find(hit_ids)
28
+ yield hit_ids
23
29
  end
24
30
  end
25
31
 
@@ -30,6 +36,7 @@ module ElasticRecord
30
36
  end
31
37
 
32
38
  private
39
+
33
40
  def get_scroll_hit_ids(scroll_id, scroll_keep_alive)
34
41
  json = klass.elastic_index.scroll(scroll_id, scroll_keep_alive)
35
42
  json['hits']['hits'].map { |hit| hit['_id'] }
@@ -6,11 +6,11 @@ module ElasticRecord
6
6
  end
7
7
 
8
8
  def first
9
- find_one order('_uid')
9
+ find_one self
10
10
  end
11
11
 
12
12
  def last
13
- find_one order('color' => 'reverse')
13
+ find_one reverse_order
14
14
  end
15
15
 
16
16
  def all
@@ -12,10 +12,10 @@ module ElasticRecord
12
12
  def facets
13
13
  {}
14
14
  end
15
-
15
+
16
16
  def as_elastic
17
- {}
17
+ Arelastic::Filters::Not.new(Arelastic::Queries::MatchAll.new).as_elastic
18
18
  end
19
19
  end
20
20
  end
21
- end
21
+ end
@@ -85,10 +85,10 @@ module ElasticRecord
85
85
  end
86
86
 
87
87
  def facet(facet_or_name, options = {})
88
- clone.facet! facet_or_name, options = {}
88
+ clone.facet! facet_or_name, options
89
89
  end
90
90
 
91
- def order!(*args)
91
+ def order!(*args) # :nodoc:
92
92
  self.order_values += args.flatten
93
93
  self
94
94
  end
@@ -97,6 +97,18 @@ module ElasticRecord
97
97
  clone.order! *args
98
98
  end
99
99
 
100
+ # Reverse the existing order clause on the relation.
101
+ #
102
+ # User.order('name').reverse_order # generated search has 'sort: {'name' => :desc}
103
+ def reverse_order
104
+ clone.reverse_order!
105
+ end
106
+
107
+ def reverse_order! # :nodoc:
108
+ self.reverse_order_value = !reverse_order_value
109
+ self
110
+ end
111
+
100
112
  def extending!(*modules, &block)
101
113
  modules << Module.new(&block) if block_given?
102
114
 
@@ -214,7 +226,25 @@ module ElasticRecord
214
226
  end
215
227
 
216
228
  def build_orders(orders)
217
- Arelastic::Searches::Sort.new(orders) unless orders.empty?
229
+ unless orders.empty?
230
+ orders = reverse_query_order(orders) if reverse_order_value
231
+ Arelastic::Searches::Sort.new(orders) unless orders.empty?
232
+ end
233
+ end
234
+
235
+ def reverse_query_order(orders)
236
+ orders.reverse.map do |o|
237
+ case o
238
+ when String, Symbol
239
+ {o => :desc}
240
+ when Hash
241
+ o.each_with_object({}) do |(field, dir), memo|
242
+ memo[field] = (dir.to_sym == :asc ? :desc : :asc )
243
+ end
244
+ else
245
+ o
246
+ end
247
+ end
218
248
  end
219
249
  end
220
250
  end
@@ -1,6 +1,6 @@
1
1
  module ElasticRecord
2
2
  class Relation
3
3
  MULTI_VALUE_METHODS = [:extending, :facet, :filter, :order, :select]
4
- SINGLE_VALUE_METHODS = [:query, :limit, :offset]
4
+ SINGLE_VALUE_METHODS = [:query, :limit, :offset, :reverse_order]
5
5
  end
6
6
  end
@@ -43,6 +43,10 @@ module ElasticRecord
43
43
  # is used on the associate class (such as a Post class). You can also specify a custom counter
44
44
  # cache column by providing a column name instead of a +true+/+false+ value to this
45
45
  # option (e.g., <tt>:counter_cache => :my_custom_counter</tt>.)
46
+ # [:class_name]
47
+ # Specify the class name of the association. Use it only if that name can't be inferred
48
+ # from the association name. So <tt>has_one :manager</tt> will by default be linked to the Manager class, but
49
+ # if the real class name is Person, you'll have to specify it with this option.
46
50
  #
47
51
  # === Example
48
52
  #
@@ -19,6 +19,10 @@ module ElasticRecord
19
19
  other_record.is_a?(Hash) ? klass.new(other_record) : other_record
20
20
  end
21
21
 
22
+ delete(load_collection - other_records)
23
+ merge_collections(load_collection, other_records)
24
+ concat(other_records - load_collection)
25
+
22
26
  if reflection.counter_cache_column
23
27
  owner.send("#{reflection.counter_cache_column}=", other_records.size)
24
28
  end
@@ -26,10 +30,6 @@ module ElasticRecord
26
30
  if reflection.touch_column
27
31
  owner.send("#{reflection.touch_column}=", Time.current)
28
32
  end
29
-
30
- delete(load_collection - other_records)
31
- merge_collections(load_collection, other_records)
32
- concat(other_records - load_collection)
33
33
  end
34
34
 
35
35
  def reader
@@ -55,15 +55,21 @@ module ElasticRecord
55
55
  end
56
56
 
57
57
  def delete(records)
58
- if options[:autosave] || owner.new_record?
59
- records.each(&:mark_for_destruction)
60
- else
61
- record.destroy
58
+ records.each do |record|
59
+ callback(:before_remove, record)
60
+
61
+ if options[:autosave] || owner.new_record?
62
+ record.mark_for_destruction
63
+ else
64
+ record.destroy
65
+ end
66
+
67
+ callback(:after_remove, record)
62
68
  end
63
69
  end
64
70
 
65
71
  def scope
66
- search = klass.elastic_search.filter "#{reflection.belongs_to}_id" => owner.id
72
+ search = klass.elastic_search.filter("#{reflection.belongs_to}_id" => owner.id).limit(1000)
67
73
  if options[:as]
68
74
  search.filter! "#{reflection.belongs_to}_type" => owner.class.name
69
75
  end
@@ -76,7 +82,6 @@ module ElasticRecord
76
82
  @loaded = true
77
83
  end
78
84
 
79
- loaded = true
80
85
  collection
81
86
  end
82
87
 
@@ -86,7 +91,13 @@ module ElasticRecord
86
91
  end
87
92
 
88
93
  def persisted_collection
89
- scope.to_a
94
+ @persisted_collection ||= begin
95
+ if reflection.counter_cache_column && (owner.send(reflection.counter_cache_column).nil? || owner.send(reflection.counter_cache_column) == 0)
96
+ []
97
+ else
98
+ scope.to_a
99
+ end
100
+ end
90
101
  end
91
102
 
92
103
  def merge_collections(existing_records, new_records)
@@ -122,10 +133,10 @@ module ElasticRecord
122
133
 
123
134
  def callback(method, record)
124
135
  reflection.callbacks[method].each do |callback|
125
- if callback.is_a?(Symbol)
126
- owner.send(callback, record)
127
- else
136
+ if callback.respond_to?(:call)
128
137
  callback.call(owner, record)
138
+ else
139
+ owner.send(callback, record)
129
140
  end
130
141
  end
131
142
  end
@@ -13,7 +13,7 @@ module ElasticRecord
13
13
  end
14
14
 
15
15
  def klass_name
16
- name.to_s.classify
16
+ options[:class_name] || name.to_s.classify
17
17
  end
18
18
 
19
19
  def belongs_to
@@ -14,32 +14,16 @@ namespace :index do
14
14
  desc "Create index for CLASS or all models."
15
15
  task create: :environment do
16
16
  ElasticRecord::Task.get_models.each do |model|
17
- # begin
18
- index_name = model.elastic_index.create_and_deploy
19
- logger.info "Created #{model.name} index (#{index_name})"
20
- # rescue => e
21
- # if e.message =~ /IndexAlreadyExistsException/
22
- # logger.info "#{model.name} index already exists"
23
- # else
24
- # raise e
25
- # end
26
- # end
17
+ index_name = model.elastic_index.create_and_deploy
18
+ logger.info "Created #{model.name} index (#{index_name})"
27
19
  end
28
20
  end
29
21
 
30
22
  desc "Drop index for CLASS or all models."
31
23
  task drop: :environment do
32
24
  ElasticRecord::Task.get_models.each do |model|
33
- # begin
34
- model.elastic_index.delete_all
35
- logger.info "Dropped #{model.name} index"
36
- # rescue => e
37
- # if e.message =~ /IndexMissingException/
38
- # logger.info "#{model.name} index does not exist"
39
- # else
40
- # raise e
41
- # end
42
- # end
25
+ model.elastic_index.delete_all
26
+ logger.info "Dropped #{model.name} index"
43
27
  end
44
28
  end
45
29
 
@@ -76,4 +60,4 @@ namespace :index do
76
60
  logger.info " Done."
77
61
  end
78
62
  end
79
- end
63
+ end
@@ -10,6 +10,15 @@ class ElasticRecord::CallbacksTest < MiniTest::Spec
10
10
  assert Widget.elastic_index.record_exists?(widget.id)
11
11
  end
12
12
 
13
+ def test_not_added_to_index_if_not_dirty
14
+ widget = Widget.new id: '10', color: 'green'
15
+ widget.changed_attributes.clear
16
+
17
+ widget.save
18
+
19
+ refute Widget.elastic_index.record_exists?(widget.id)
20
+ end
21
+
13
22
  def test_deleted_from_index
14
23
  widget = Widget.new id: '10', color: 'green'
15
24
  Widget.elastic_index.index_document(widget.id, widget.as_search)
@@ -24,7 +24,7 @@ class ElasticRecord::ConnectionTest < MiniTest::Spec
24
24
  assert_equal expected, connection.json_put("/test")
25
25
  end
26
26
 
27
- def test_json_request_with_error
27
+ def test_json_request_with_error_status
28
28
  response_json = {'error' => 'Doing it wrong'}
29
29
  FakeWeb.register_uri(:get, %r[/error], status: ["404", "Not Found"], body: ActiveSupport::JSON.encode(response_json))
30
30
 
@@ -35,6 +35,24 @@ class ElasticRecord::ConnectionTest < MiniTest::Spec
35
35
  assert_equal 'Doing it wrong', error.message
36
36
  end
37
37
 
38
+ def test_execute_retries
39
+ responses = [
40
+ {exception: Errno::ECONNREFUSED},
41
+ {status: ["200", "OK"], body: ActiveSupport::JSON.encode('hello' => 'world')}
42
+ ]
43
+
44
+ ElasticRecord::Connection.new(ElasticRecord::Config.servers, retries: 0).tap do |connection|
45
+ FakeWeb.register_uri :get, %r[/error], responses
46
+ assert_raises(Errno::ECONNREFUSED) { connection.json_get("/error") }
47
+ end
48
+
49
+ ElasticRecord::Connection.new(ElasticRecord::Config.servers, retries: 1).tap do |connection|
50
+ FakeWeb.register_uri :get, %r[/error], responses
51
+ json = connection.json_get("/error")
52
+ assert_equal({'hello' => 'world'}, json)
53
+ end
54
+ end
55
+
38
56
  private
39
57
  def connection
40
58
  ElasticRecord::Connection.new(ElasticRecord::Config.servers)
@@ -0,0 +1,18 @@
1
+ require 'helper'
2
+
3
+ class ElasticRecord::Index::ConfiguratorTest < MiniTest::Spec
4
+ def test_property
5
+ configurator.property :am_i_cool, type: "boolean"
6
+
7
+ expected = {type: "boolean"}
8
+ assert_equal expected, configurator.index.mapping[:properties][:am_i_cool]
9
+ end
10
+
11
+ private
12
+ def configurator
13
+ @configurator ||= begin
14
+ index = ElasticRecord::Index.new(Widget)
15
+ ElasticRecord::Index::Configurator.new(index)
16
+ end
17
+ end
18
+ end
@@ -1,6 +1,12 @@
1
1
  require 'helper'
2
2
 
3
3
  class ElasticRecord::Index::DocumentsTest < MiniTest::Spec
4
+ class InheritedWidget < Widget
5
+ def self.base_class
6
+ Widget
7
+ end
8
+ end
9
+
4
10
  def setup
5
11
  super
6
12
  index.disable_deferring!
@@ -32,7 +38,7 @@ class ElasticRecord::Index::DocumentsTest < MiniTest::Spec
32
38
  end
33
39
 
34
40
  def test_bulk
35
- assert_nil index.instance_variable_get(:@batch)
41
+ assert_nil index.instance_variable_get(:@_batch)
36
42
 
37
43
  index.bulk do
38
44
  index.index_document '5', color: 'green'
@@ -43,13 +49,26 @@ class ElasticRecord::Index::DocumentsTest < MiniTest::Spec
43
49
  {color: "green"},
44
50
  {delete: {_index: "widgets", _type: "widget", _id: "3"}}
45
51
  ]
46
- assert_equal expected, index.instance_variable_get(:@batch)
52
+ assert_equal expected, index.instance_variable_get(:@_batch)
47
53
  end
48
54
 
49
- assert_nil index.instance_variable_get(:@batch)
55
+ assert_nil index.instance_variable_get(:@_batch)
56
+ end
57
+
58
+ def test_bulk_inheritence
59
+ index.bulk do
60
+ InheritedWidget.elastic_index.index_document '5', color: 'green'
61
+
62
+ expected = [
63
+ {index: {_index: "widgets", _type: "widget", _id: "5"}},
64
+ {color: "green"}
65
+ ]
66
+ assert_equal expected, index.instance_variable_get(:@_batch)
67
+ end
50
68
  end
51
69
 
52
70
  private
71
+
53
72
  def index
54
73
  @index ||= Widget.elastic_index
55
74
  end
@@ -26,6 +26,13 @@ class ElasticRecord::Index::ManageTest < MiniTest::Spec
26
26
  assert !index.exists?('widgets_bar')
27
27
  end
28
28
 
29
+ def test_type_exists
30
+ index.create 'widgets_foo'
31
+
32
+ assert index.type_exists?('widgets_foo')
33
+ assert !index.type_exists?('widgets_bar')
34
+ end
35
+
29
36
  def test_deploy
30
37
  index.create 'widgets_foo'
31
38
 
@@ -1,6 +1,17 @@
1
1
  require 'helper'
2
2
 
3
3
  class ElasticRecord::Index::MappingTest < MiniTest::Spec
4
+ def test_delete_mapping
5
+ index_name = index.create
6
+ index.get_mapping(index_name)
7
+
8
+ index.delete_mapping(index_name)
9
+
10
+ assert_raises ElasticRecord::ConnectionError do
11
+ index.get_mapping(index_name)
12
+ end
13
+ end
14
+
4
15
  def test_default_mapping
5
16
  mapping = index.mapping
6
17