elastic_record 0.11.1 → 0.12.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 (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