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.
- checksums.yaml +7 -0
- data/Gemfile +2 -1
- data/README.rdoc +37 -9
- data/elastic_record.gemspec +2 -2
- data/lib/elastic_record/callbacks.rb +1 -1
- data/lib/elastic_record/connection.rb +36 -13
- data/lib/elastic_record/index.rb +33 -4
- data/lib/elastic_record/index/configurator.rb +14 -0
- data/lib/elastic_record/index/documents.rb +21 -13
- data/lib/elastic_record/index/manage.rb +4 -9
- data/lib/elastic_record/index/mapping.rb +12 -0
- data/lib/elastic_record/index/percolator.rb +1 -0
- data/lib/elastic_record/index/settings.rb +4 -0
- data/lib/elastic_record/lucene.rb +40 -22
- data/lib/elastic_record/model.rb +8 -5
- data/lib/elastic_record/relation/batches.rb +8 -1
- data/lib/elastic_record/relation/finder_methods.rb +2 -2
- data/lib/elastic_record/relation/none.rb +3 -3
- data/lib/elastic_record/relation/search_methods.rb +33 -3
- data/lib/elastic_record/relation/value_methods.rb +1 -1
- data/lib/elastic_record/searches_many.rb +4 -0
- data/lib/elastic_record/searches_many/association.rb +25 -14
- data/lib/elastic_record/searches_many/reflection.rb +1 -1
- data/lib/elastic_record/tasks/index.rake +5 -21
- data/test/elastic_record/callbacks_test.rb +9 -0
- data/test/elastic_record/connection_test.rb +19 -1
- data/test/elastic_record/index/configurator_test.rb +18 -0
- data/test/elastic_record/index/documents_test.rb +22 -3
- data/test/elastic_record/index/manage_test.rb +7 -0
- data/test/elastic_record/index/mapping_test.rb +11 -0
- data/test/elastic_record/index/percolator_test.rb +11 -9
- data/test/elastic_record/index_test.rb +23 -6
- data/test/elastic_record/lucene_test.rb +21 -13
- data/test/elastic_record/model_test.rb +9 -9
- data/test/elastic_record/relation/batches_test.rb +8 -0
- data/test/elastic_record/relation/finder_methods_test.rb +6 -7
- data/test/elastic_record/relation/none_test.rb +3 -0
- data/test/elastic_record/relation/search_methods_test.rb +17 -41
- data/test/elastic_record/relation_test.rb +2 -0
- data/test/elastic_record/searches_many/reflection_test.rb +7 -0
- metadata +15 -19
data/lib/elastic_record/model.rb
CHANGED
@@ -9,13 +9,16 @@ module ElasticRecord
|
|
9
9
|
end
|
10
10
|
|
11
11
|
module ClassMethods
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
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
|
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'] }
|
@@ -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
|
-
|
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
|
@@ -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
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
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
|
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
|
-
|
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.
|
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
|
@@ -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
|
-
|
18
|
-
|
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
|
-
|
34
|
-
|
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
|
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(:@
|
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(:@
|
52
|
+
assert_equal expected, index.instance_variable_get(:@_batch)
|
47
53
|
end
|
48
54
|
|
49
|
-
assert_nil index.instance_variable_get(:@
|
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
|
|