elastic_record 2.0.2 → 3.0.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.
- checksums.yaml +4 -4
- data/.gitignore +1 -0
- data/.travis.yml +15 -9
- data/Gemfile +5 -4
- data/README.md +214 -0
- data/elastic_record.gemspec +7 -7
- data/lib/elastic_record.rb +1 -0
- data/lib/elastic_record/callbacks.rb +46 -14
- data/lib/elastic_record/config.rb +1 -21
- data/lib/elastic_record/connection.rb +24 -14
- data/lib/elastic_record/errors.rb +5 -0
- data/lib/elastic_record/index.rb +11 -1
- data/lib/elastic_record/index/deferred.rb +1 -0
- data/lib/elastic_record/index/documents.rb +95 -18
- data/lib/elastic_record/index/manage.rb +0 -8
- data/lib/elastic_record/index/mapping.rb +1 -10
- data/lib/elastic_record/json.rb +29 -0
- data/lib/elastic_record/relation.rb +9 -5
- data/lib/elastic_record/relation/batches.rb +4 -40
- data/lib/elastic_record/relation/none.rb +0 -4
- data/lib/elastic_record/relation/search_methods.rb +48 -38
- data/lib/elastic_record/relation/value_methods.rb +2 -2
- data/lib/elastic_record/tasks/index.rake +2 -2
- data/test/dummy/.env.example +1 -0
- data/test/dummy/.env.test +1 -0
- data/test/dummy/app/models/project.rb +1 -1
- data/test/dummy/app/models/test_model.rb +3 -2
- data/test/dummy/app/models/widget.rb +3 -3
- data/test/dummy/config/initializers/elastic_record.rb +1 -1
- data/test/dummy/db/migrate/20151211225259_create_projects.rb +7 -0
- data/test/dummy/db/schema.rb +8 -1
- data/test/elastic_record/callbacks_test.rb +16 -2
- data/test/elastic_record/config_test.rb +1 -2
- data/test/elastic_record/connection_test.rb +52 -9
- data/test/elastic_record/index/documents_test.rb +55 -21
- data/test/elastic_record/index/mapping_test.rb +0 -10
- data/test/elastic_record/integration/active_record_test.rb +3 -3
- data/test/elastic_record/log_subscriber_test.rb +4 -4
- data/test/elastic_record/relation/batches_test.rb +5 -24
- data/test/elastic_record/relation/delegation_test.rb +4 -3
- data/test/elastic_record/relation/finder_methods_test.rb +1 -0
- data/test/elastic_record/relation/search_methods_test.rb +47 -45
- data/test/elastic_record/relation_test.rb +18 -10
- data/test/helper.rb +4 -3
- metadata +21 -12
- data/README.rdoc +0 -146
- data/test/dummy/config/database.yml +0 -15
data/lib/elastic_record/index.rb
CHANGED
@@ -40,8 +40,10 @@ module ElasticRecord
|
|
40
40
|
include Deferred
|
41
41
|
|
42
42
|
attr_accessor :model
|
43
|
+
|
43
44
|
attr_accessor :disabled
|
44
45
|
attr_accessor :has_percolator
|
46
|
+
attr_accessor :partial_updates
|
45
47
|
|
46
48
|
def initialize(model)
|
47
49
|
@model = model
|
@@ -53,10 +55,18 @@ module ElasticRecord
|
|
53
55
|
@mapping = mapping.deep_dup
|
54
56
|
end
|
55
57
|
|
58
|
+
def alias_name=(name)
|
59
|
+
@alias_name = name
|
60
|
+
end
|
61
|
+
|
56
62
|
def alias_name
|
57
63
|
@alias_name ||= model.base_class.name.demodulize.underscore.pluralize
|
58
64
|
end
|
59
65
|
|
66
|
+
def type=(name)
|
67
|
+
@type = name
|
68
|
+
end
|
69
|
+
|
60
70
|
def type
|
61
71
|
@type ||= model.base_class.name.demodulize.underscore
|
62
72
|
end
|
@@ -84,7 +94,7 @@ module ElasticRecord
|
|
84
94
|
private
|
85
95
|
|
86
96
|
def new_index_name
|
87
|
-
"#{alias_name}_#{
|
97
|
+
"#{alias_name}_#{Time.now.utc.strftime('%Y%m%d_%H%M%S')}"
|
88
98
|
end
|
89
99
|
|
90
100
|
end
|
@@ -2,38 +2,105 @@ require 'active_support/core_ext/object/to_query'
|
|
2
2
|
|
3
3
|
module ElasticRecord
|
4
4
|
class Index
|
5
|
-
|
6
|
-
|
7
|
-
|
5
|
+
class ScanSearch
|
6
|
+
attr_reader :scroll_id
|
7
|
+
attr_accessor :total_hits
|
8
|
+
|
9
|
+
def initialize(elastic_index, scroll_id, options = {})
|
10
|
+
@elastic_index = elastic_index
|
11
|
+
@scroll_id = scroll_id
|
12
|
+
@options = options
|
13
|
+
end
|
8
14
|
|
9
|
-
|
15
|
+
def each_slice(&block)
|
16
|
+
while (hit_ids = request_more_ids).any?
|
17
|
+
hit_ids.each_slice(requested_batch_size, &block)
|
18
|
+
end
|
10
19
|
end
|
11
20
|
|
12
|
-
def
|
13
|
-
|
21
|
+
def request_more_ids
|
22
|
+
json = @elastic_index.scroll(@scroll_id, keep_alive)
|
23
|
+
json['hits']['hits'].map { |hit| hit['_id'] }
|
24
|
+
end
|
14
25
|
|
15
|
-
|
26
|
+
def keep_alive
|
27
|
+
@options[:keep_alive] || (raise "Must provide a :keep_alive option")
|
28
|
+
end
|
16
29
|
|
30
|
+
def requested_batch_size
|
31
|
+
@options[:batch_size]
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
module Documents
|
36
|
+
def index_record(record, index_name: alias_name)
|
37
|
+
unless disabled
|
38
|
+
index_document(record.send(record.class.primary_key), record.as_search, index_name: index_name)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def update_record(record, index_name: alias_name)
|
43
|
+
unless disabled
|
44
|
+
update_document(record.send(record.class.primary_key), record.as_partial_update_document, index_name: index_name)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def index_document(id, document, parent: nil, index_name: alias_name)
|
17
49
|
if batch = current_bulk_batch
|
18
|
-
|
50
|
+
instructions = { _index: index_name, _type: type, _id: id }
|
51
|
+
instructions[:parent] = parent if parent
|
52
|
+
|
53
|
+
batch << { index: instructions }
|
19
54
|
batch << document
|
20
55
|
else
|
21
|
-
|
56
|
+
path = "/#{index_name}/#{type}/#{id}"
|
57
|
+
path << "?parent=#{parent}" if parent
|
58
|
+
|
59
|
+
connection.json_put path, document
|
22
60
|
end
|
23
61
|
end
|
24
62
|
|
25
|
-
def
|
63
|
+
def update_document(id, document, parent: nil, index_name: alias_name)
|
64
|
+
params = {doc: document, doc_as_upsert: true}
|
65
|
+
|
66
|
+
if batch = current_bulk_batch
|
67
|
+
instructions = { _index: index_name, _type: type, _id: id, _retry_on_conflict: 3 }
|
68
|
+
instructions[:parent] = parent if parent
|
69
|
+
|
70
|
+
batch << { update: instructions }
|
71
|
+
batch << params
|
72
|
+
else
|
73
|
+
path = "/#{index_name}/#{type}/#{id}/_update?retry_on_conflict=3"
|
74
|
+
path << "&parent=#{parent}" if parent
|
75
|
+
|
76
|
+
connection.json_post path, params
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
def delete_document(id, parent: nil, index_name: alias_name)
|
81
|
+
raise "Cannot delete document with empty id" if id.blank?
|
26
82
|
index_name ||= alias_name
|
27
83
|
|
28
84
|
if batch = current_bulk_batch
|
29
|
-
|
85
|
+
instructions = { _index: index_name, _type: type, _id: id, _retry_on_conflict: 3 }
|
86
|
+
instructions[:parent] = parent if parent
|
87
|
+
batch << { delete: instructions }
|
30
88
|
else
|
31
|
-
|
89
|
+
path = "/#{index_name}/#{type}/#{id}"
|
90
|
+
path << "&parent=#{parent}" if parent
|
91
|
+
|
92
|
+
connection.json_delete path
|
32
93
|
end
|
33
94
|
end
|
34
95
|
|
35
96
|
def delete_by_query(query)
|
36
|
-
|
97
|
+
scan_search = create_scan_search query
|
98
|
+
|
99
|
+
scan_search.each_slice do |ids|
|
100
|
+
bulk do
|
101
|
+
ids.each { |id| delete_document(id) }
|
102
|
+
end
|
103
|
+
end
|
37
104
|
end
|
38
105
|
|
39
106
|
def record_exists?(id)
|
@@ -53,6 +120,18 @@ module ElasticRecord
|
|
53
120
|
get "_explain", elastic_query
|
54
121
|
end
|
55
122
|
|
123
|
+
def create_scan_search(elastic_query, options = {})
|
124
|
+
options[:batch_size] ||= 100
|
125
|
+
options[:keep_alive] ||= ElasticRecord::Config.scroll_keep_alive
|
126
|
+
|
127
|
+
search_options = {search_type: 'scan', size: options[:batch_size], scroll: options[:keep_alive]}
|
128
|
+
json = search(elastic_query, search_options)
|
129
|
+
|
130
|
+
ScanSearch.new(self, json['_scroll_id'], options).tap do |scan_search|
|
131
|
+
scan_search.total_hits = json['hits']['total']
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
56
135
|
def scroll(scroll_id, scroll_keep_alive)
|
57
136
|
options = {scroll_id: scroll_id, scroll: scroll_keep_alive}
|
58
137
|
connection.json_get("/_search/scroll?#{options.to_query}")
|
@@ -64,7 +143,7 @@ module ElasticRecord
|
|
64
143
|
yield
|
65
144
|
|
66
145
|
if current_bulk_batch.any?
|
67
|
-
body = current_bulk_batch.map { |action| "#{
|
146
|
+
body = current_bulk_batch.map { |action| "#{ElasticRecord::JSON.encode(action)}\n" }.join
|
68
147
|
results = connection.json_post("/_bulk?#{options.to_query}", body)
|
69
148
|
verify_bulk_results(results)
|
70
149
|
end
|
@@ -72,12 +151,10 @@ module ElasticRecord
|
|
72
151
|
connection.bulk_stack.pop
|
73
152
|
end
|
74
153
|
|
75
|
-
def bulk_add(batch, index_name
|
76
|
-
index_name ||= alias_name
|
77
|
-
|
154
|
+
def bulk_add(batch, index_name: alias_name)
|
78
155
|
bulk do
|
79
156
|
batch.each do |record|
|
80
|
-
index_record(record, index_name)
|
157
|
+
index_record(record, index_name: index_name)
|
81
158
|
end
|
82
159
|
end
|
83
160
|
end
|
@@ -23,14 +23,6 @@ module ElasticRecord
|
|
23
23
|
end
|
24
24
|
end
|
25
25
|
|
26
|
-
def open(index_name)
|
27
|
-
connection.json_post("/#{index_name}/_open")
|
28
|
-
end
|
29
|
-
|
30
|
-
def close(index_name)
|
31
|
-
connection.json_post("/#{index_name}/_close")
|
32
|
-
end
|
33
|
-
|
34
26
|
def exists?(index_name)
|
35
27
|
connection.head("/#{index_name}") == '200'
|
36
28
|
end
|
@@ -16,22 +16,13 @@ module ElasticRecord
|
|
16
16
|
end
|
17
17
|
end
|
18
18
|
|
19
|
-
def delete_mapping(index_name = alias_name)
|
20
|
-
connection.json_delete "/#{index_name}/#{type}"
|
21
|
-
end
|
22
|
-
|
23
19
|
def mapping
|
24
20
|
@mapping ||= {
|
25
|
-
|
26
|
-
enabled: false
|
21
|
+
properties: {
|
27
22
|
},
|
28
23
|
_all: {
|
29
24
|
enabled: false
|
30
25
|
},
|
31
|
-
properties: {
|
32
|
-
created_at: {type: "date", index: "not_analyzed", format: "dateOptionalTime"},
|
33
|
-
updated_at: {type: "date", index: "not_analyzed", format: "dateOptionalTime"}
|
34
|
-
},
|
35
26
|
dynamic_templates: [
|
36
27
|
{
|
37
28
|
no_string_analyzing: {
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module ElasticRecord
|
2
|
+
module JSON
|
3
|
+
class << self
|
4
|
+
def parser
|
5
|
+
@@parser ||= :active_support
|
6
|
+
end
|
7
|
+
|
8
|
+
def parser=(value)
|
9
|
+
@@parser = value
|
10
|
+
end
|
11
|
+
|
12
|
+
def decode(json)
|
13
|
+
if ElasticRecord::JSON.parser == :oj
|
14
|
+
Oj.compat_load(json)
|
15
|
+
else
|
16
|
+
ActiveSupport::JSON.decode(json)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def encode(data)
|
21
|
+
if ElasticRecord::JSON.parser == :oj
|
22
|
+
Oj.dump(data, mode: :compat)
|
23
|
+
else
|
24
|
+
ActiveSupport::JSON.encode(data)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -22,10 +22,6 @@ module ElasticRecord
|
|
22
22
|
search_results['hits']['total']
|
23
23
|
end
|
24
24
|
|
25
|
-
def facets
|
26
|
-
search_results['facets']
|
27
|
-
end
|
28
|
-
|
29
25
|
def aggregations
|
30
26
|
search_results['aggregations']
|
31
27
|
end
|
@@ -47,6 +43,11 @@ module ElasticRecord
|
|
47
43
|
search_hits.map { |hit| hit['_id'] }
|
48
44
|
end
|
49
45
|
|
46
|
+
def delete_all
|
47
|
+
find_ids_in_batches { |ids| klass.delete(ids) }
|
48
|
+
klass.elastic_index.delete_by_query(as_elastic)
|
49
|
+
end
|
50
|
+
|
50
51
|
def ==(other)
|
51
52
|
to_a == other
|
52
53
|
end
|
@@ -72,7 +73,10 @@ module ElasticRecord
|
|
72
73
|
end
|
73
74
|
|
74
75
|
def search_results
|
75
|
-
@search_results ||=
|
76
|
+
@search_results ||= begin
|
77
|
+
options = search_type_value ? {search_type: search_type_value} : {}
|
78
|
+
klass.elastic_index.search(as_elastic, options.update(fields: ''))
|
79
|
+
end
|
76
80
|
end
|
77
81
|
|
78
82
|
def load_hits(ids)
|
@@ -1,29 +1,5 @@
|
|
1
1
|
module ElasticRecord
|
2
2
|
class Relation
|
3
|
-
class ScanSearch
|
4
|
-
attr_reader :scroll_id
|
5
|
-
attr_accessor :total_hits
|
6
|
-
|
7
|
-
def initialize(model, scroll_id, options = {})
|
8
|
-
@model = model
|
9
|
-
@scroll_id = scroll_id
|
10
|
-
@options = options
|
11
|
-
end
|
12
|
-
|
13
|
-
def request_more_ids
|
14
|
-
json = @model.elastic_index.scroll(@scroll_id, keep_alive)
|
15
|
-
json['hits']['hits'].map { |hit| hit['_id'] }
|
16
|
-
end
|
17
|
-
|
18
|
-
def keep_alive
|
19
|
-
@options[:keep_alive] || (raise "Must provide a :keep_alive option")
|
20
|
-
end
|
21
|
-
|
22
|
-
def requested_batch_size
|
23
|
-
@options[:batch_size]
|
24
|
-
end
|
25
|
-
end
|
26
|
-
|
27
3
|
module Batches
|
28
4
|
def find_each(options = {})
|
29
5
|
find_in_batches(options) do |records|
|
@@ -38,11 +14,11 @@ module ElasticRecord
|
|
38
14
|
end
|
39
15
|
|
40
16
|
def find_ids_in_batches(options = {}, &block)
|
41
|
-
|
17
|
+
create_scan_search(options).each_slice(&block)
|
18
|
+
end
|
42
19
|
|
43
|
-
|
44
|
-
|
45
|
-
end
|
20
|
+
def create_scan_search(options)
|
21
|
+
elastic_index.create_scan_search(as_elastic, options)
|
46
22
|
end
|
47
23
|
|
48
24
|
def reindex
|
@@ -50,18 +26,6 @@ module ElasticRecord
|
|
50
26
|
elastic_index.bulk_add(batch)
|
51
27
|
end
|
52
28
|
end
|
53
|
-
|
54
|
-
def create_scan_search(options = {})
|
55
|
-
options[:batch_size] ||= 100
|
56
|
-
options[:keep_alive] ||= ElasticRecord::Config.scroll_keep_alive
|
57
|
-
|
58
|
-
search_options = {search_type: 'scan', size: options[:batch_size], scroll: options[:keep_alive]}
|
59
|
-
json = klass.elastic_index.search(as_elastic, search_options)
|
60
|
-
|
61
|
-
ElasticRecord::Relation::ScanSearch.new(klass, json['_scroll_id'], options).tap do |scan_search|
|
62
|
-
scan_search.total_hits = json['hits']['total']
|
63
|
-
end
|
64
|
-
end
|
65
29
|
end
|
66
30
|
end
|
67
31
|
end
|
@@ -1,6 +1,33 @@
|
|
1
1
|
module ElasticRecord
|
2
2
|
class Relation
|
3
3
|
module SearchMethods
|
4
|
+
class FilterChain
|
5
|
+
def initialize(scope)
|
6
|
+
@scope = scope
|
7
|
+
end
|
8
|
+
|
9
|
+
def not(*filters)
|
10
|
+
add_filter_nodes_to_scope(filters) do |filter_node|
|
11
|
+
Arelastic::Filters::Not.new(filter_node)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
def nested(path, *filters)
|
16
|
+
add_filter_nodes_to_scope(filters) do |filter_node|
|
17
|
+
Arelastic::Filters::Nested.new(path, filter_node)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def add_filter_nodes_to_scope(filters)
|
22
|
+
filter_value = @scope.send(:build_filter_nodes, filters).map do |filter_node|
|
23
|
+
yield filter_node
|
24
|
+
end
|
25
|
+
|
26
|
+
@scope.filter_values += filter_value
|
27
|
+
@scope
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
4
31
|
Relation::MULTI_VALUE_METHODS.each do |name|
|
5
32
|
define_method "#{name}_values" do
|
6
33
|
@values[name] || []
|
@@ -35,8 +62,12 @@ module ElasticRecord
|
|
35
62
|
self
|
36
63
|
end
|
37
64
|
|
38
|
-
def filter(*
|
39
|
-
|
65
|
+
def filter(opts = :chain, *rest)
|
66
|
+
if opts == :chain
|
67
|
+
FilterChain.new(clone)
|
68
|
+
else
|
69
|
+
clone.filter!(opts, *rest)
|
70
|
+
end
|
40
71
|
end
|
41
72
|
|
42
73
|
def find_by(*args)
|
@@ -78,18 +109,13 @@ module ElasticRecord
|
|
78
109
|
end
|
79
110
|
end
|
80
111
|
|
81
|
-
def
|
82
|
-
|
83
|
-
self.facet_values += [arelastic.facet[name_or_facet].terms(name_or_facet, options)]
|
84
|
-
else
|
85
|
-
self.facet_values += [name_or_facet]
|
86
|
-
end
|
87
|
-
|
112
|
+
def search_type!(type)
|
113
|
+
self.search_type_value = type
|
88
114
|
self
|
89
115
|
end
|
90
116
|
|
91
|
-
def
|
92
|
-
clone.
|
117
|
+
def search_type(type)
|
118
|
+
clone.search_type! type
|
93
119
|
end
|
94
120
|
|
95
121
|
def aggregate!(aggregation)
|
@@ -149,7 +175,6 @@ module ElasticRecord
|
|
149
175
|
build_query_and_filter(query_value, filter_values),
|
150
176
|
build_limit(limit_value),
|
151
177
|
build_offset(offset_value),
|
152
|
-
build_facets(facet_values),
|
153
178
|
build_aggregations(aggregation_values),
|
154
179
|
build_orders(order_values)
|
155
180
|
].compact
|
@@ -178,9 +203,17 @@ module ElasticRecord
|
|
178
203
|
end
|
179
204
|
|
180
205
|
def build_filter(filters)
|
181
|
-
nodes =
|
206
|
+
nodes = build_filter_nodes(filters)
|
182
207
|
|
183
|
-
|
208
|
+
if nodes.size == 1
|
209
|
+
nodes.first
|
210
|
+
elsif nodes.size > 1
|
211
|
+
Arelastic::Filters::And.new(nodes)
|
212
|
+
end
|
213
|
+
end
|
214
|
+
|
215
|
+
def build_filter_nodes(filters)
|
216
|
+
filters.each_with_object([]) do |filter, nodes|
|
184
217
|
if filter.is_a?(Arelastic::Filters::Filter)
|
185
218
|
nodes << filter
|
186
219
|
elsif filter.is_a?(ElasticRecord::Relation)
|
@@ -200,27 +233,8 @@ module ElasticRecord
|
|
200
233
|
end
|
201
234
|
end
|
202
235
|
end
|
203
|
-
|
204
|
-
if nodes.size == 1
|
205
|
-
nodes.first
|
206
|
-
elsif nodes.size > 1
|
207
|
-
Arelastic::Filters::And.new(nodes)
|
208
|
-
end
|
209
236
|
end
|
210
237
|
|
211
|
-
# def normalize_value(value)
|
212
|
-
# case value
|
213
|
-
# when true
|
214
|
-
# "T"
|
215
|
-
# when false
|
216
|
-
# "F"
|
217
|
-
# when Time
|
218
|
-
# value.iso8601
|
219
|
-
# else
|
220
|
-
# value
|
221
|
-
# end
|
222
|
-
# end
|
223
|
-
|
224
238
|
def build_limit(limit)
|
225
239
|
if limit
|
226
240
|
Arelastic::Searches::Size.new(limit)
|
@@ -237,10 +251,6 @@ module ElasticRecord
|
|
237
251
|
Arelastic::Searches::Aggregations.new(aggregations) unless aggregations.empty?
|
238
252
|
end
|
239
253
|
|
240
|
-
def build_facets(facets)
|
241
|
-
Arelastic::Searches::Facets.new(facets) unless facets.empty?
|
242
|
-
end
|
243
|
-
|
244
254
|
def build_orders(orders)
|
245
255
|
return if orders.empty?
|
246
256
|
|
@@ -248,7 +258,7 @@ module ElasticRecord
|
|
248
258
|
if order.is_a?(Arelastic::Sorts::Sort)
|
249
259
|
order
|
250
260
|
else
|
251
|
-
Arelastic::Sorts::
|
261
|
+
Arelastic::Sorts::Field.new(order)
|
252
262
|
end
|
253
263
|
end
|
254
264
|
|