elastic_record 2.0.2 → 3.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
|