elastic_record 4.0.0 → 4.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.travis.yml +3 -6
- data/Gemfile +0 -2
- data/README.md +25 -9
- data/elastic_record.gemspec +1 -1
- data/lib/elastic_record/as_document.rb +41 -0
- data/lib/elastic_record/callbacks.rb +11 -56
- data/lib/elastic_record/connection.rb +1 -1
- data/lib/elastic_record/doctype.rb +43 -0
- data/lib/elastic_record/index/deferred.rb +13 -15
- data/lib/elastic_record/index/documents.rb +23 -13
- data/lib/elastic_record/index/manage.rb +4 -6
- data/lib/elastic_record/index/mapping.rb +7 -26
- data/lib/elastic_record/index/settings.rb +17 -2
- data/lib/elastic_record/index.rb +12 -24
- data/lib/elastic_record/model.rb +14 -4
- data/lib/elastic_record/percolator_model.rb +44 -0
- data/lib/elastic_record/relation.rb +1 -2
- data/lib/elastic_record/searching.rb +5 -1
- data/lib/elastic_record/tasks/index.rake +0 -10
- data/lib/elastic_record.rb +3 -0
- data/test/dummy/app/models/mock_model.rb +108 -0
- data/test/dummy/app/models/project.rb +1 -1
- data/test/dummy/app/models/test_model.rb +1 -102
- data/test/dummy/app/models/test_percolator_model.rb +8 -0
- data/test/dummy/app/models/widget.rb +2 -5
- data/test/dummy/app/models/widget_query.rb +14 -0
- data/test/dummy/config/environments/test.rb +3 -3
- data/test/dummy/config/initializers/elastic_record.rb +1 -1
- data/test/elastic_record/as_document_test.rb +65 -0
- data/test/elastic_record/callbacks_test.rb +6 -81
- data/test/elastic_record/config_test.rb +1 -1
- data/test/elastic_record/doctype_test.rb +45 -0
- data/test/elastic_record/index/documents_test.rb +1 -1
- data/test/elastic_record/index/mapping_test.rb +16 -13
- data/test/elastic_record/index/settings_test.rb +34 -1
- data/test/elastic_record/index_test.rb +7 -15
- data/test/elastic_record/model_test.rb +1 -7
- data/test/elastic_record/percolator_model_test.rb +54 -0
- data/test/elastic_record/relation/batches_test.rb +0 -1
- data/test/elastic_record/relation/delegation_test.rb +0 -1
- data/test/elastic_record/relation/finder_methods_test.rb +0 -1
- data/test/elastic_record/relation_test.rb +0 -2
- data/test/elastic_record/searching_test.rb +7 -0
- metadata +11 -10
- data/lib/elastic_record/index/configurator.rb +0 -18
- data/lib/elastic_record/index/percolator.rb +0 -50
- data/lib/elastic_record/index/warmer.rb +0 -24
- data/lib/elastic_record/relation/admin.rb +0 -13
- data/test/elastic_record/index/configurator_test.rb +0 -18
- data/test/elastic_record/index/percolator_test.rb +0 -45
- data/test/elastic_record/index/warmer_test.rb +0 -20
- data/test/elastic_record/relation/admin_test.rb +0 -28
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: d074472d548bd216e7bfb7d92c9bebae0c396570
|
4
|
+
data.tar.gz: a3ec5801c91b65775d27d59d7eaf57fc13adb131
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: b611bc325d5b09e2adac8f34450898c32a23a4eb50050a23673efcee27a44b1ea938c2034fe92d2c29104e9068295df4c2c3abda6d28a813e5e7afe4d11c2365
|
7
|
+
data.tar.gz: 5ac3ce49becf9c7d1d13fd12ee5396375c16c37df7ba0339be1f9bae57a88f58f5fc5e2823e2872cc6e185cd7422baacd92c983c439a064e316f093921b003b0
|
data/.travis.yml
CHANGED
@@ -1,14 +1,11 @@
|
|
1
|
-
rvm:
|
2
|
-
- 2.4.1
|
1
|
+
rvm: 2.4.1
|
3
2
|
cache: bundler
|
4
|
-
|
5
|
-
# https://github.com/travis-ci/travis-ci/issues/6842
|
6
|
-
sudo: required
|
3
|
+
sudo: false
|
7
4
|
dist: trusty
|
8
5
|
addons:
|
9
6
|
apt:
|
10
7
|
sources:
|
11
|
-
- elasticsearch-
|
8
|
+
- elasticsearch-5.x
|
12
9
|
packages:
|
13
10
|
- elasticsearch
|
14
11
|
before_script:
|
data/Gemfile
CHANGED
data/README.md
CHANGED
@@ -153,26 +153,42 @@ end
|
|
153
153
|
Product.filter(color: 'red').increase_prices
|
154
154
|
```
|
155
155
|
|
156
|
-
##
|
156
|
+
## Percolators ##
|
157
|
+
|
158
|
+
ElasticRecord supports representing query documents as a model. Queries are registered and unregistered as query models are created and destroyed.
|
157
159
|
|
158
|
-
|
160
|
+
First, include `ElasticRecord::PercolatorModel` into your model. Specify the target model to percolate and how the model should be indexed as an ElasticSearch query.
|
159
161
|
|
160
162
|
```ruby
|
161
|
-
class
|
162
|
-
|
163
|
-
|
163
|
+
class ProductQuery
|
164
|
+
include ElasticRecord::PercolatorModel
|
165
|
+
|
166
|
+
self.percolates_model = Product
|
167
|
+
|
168
|
+
def as_search_document
|
169
|
+
Product.filter(status: status).as_elastic
|
164
170
|
end
|
165
171
|
end
|
166
172
|
```
|
167
173
|
|
168
|
-
|
174
|
+
Use the `percolate` method to find records with queries that match.
|
175
|
+
|
176
|
+
```
|
177
|
+
product = Product.new(price: 5.99)
|
178
|
+
matching_product_queries = ProductQuery.percolate(product)
|
179
|
+
```
|
180
|
+
|
181
|
+
## Index Configuration
|
182
|
+
|
183
|
+
To avoid elasticsearch dynamically mapping fields, you can directly configure Product.doctype.mapping
|
184
|
+
and Product.elastic_index.settings:
|
169
185
|
|
170
186
|
```ruby
|
171
187
|
class Product
|
172
|
-
|
188
|
+
doctype.mapping = {
|
173
189
|
properties: {
|
174
|
-
name: {type: "
|
175
|
-
status: {type: "
|
190
|
+
name: {type: "text"},
|
191
|
+
status: {type: "keyword"}
|
176
192
|
}
|
177
193
|
}
|
178
194
|
end
|
data/elastic_record.gemspec
CHANGED
@@ -0,0 +1,41 @@
|
|
1
|
+
module ElasticRecord
|
2
|
+
module AsDocument
|
3
|
+
def as_search_document
|
4
|
+
doctype.mapping[:properties].each_with_object({}) do |(field, mapping), result|
|
5
|
+
value = elastic_search_value field, mapping
|
6
|
+
|
7
|
+
unless value.nil?
|
8
|
+
result[field] = value
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
def as_partial_update_document
|
14
|
+
mappings = doctype.mapping[:properties]
|
15
|
+
|
16
|
+
changed.each_with_object({}) do |field, result|
|
17
|
+
if field_mapping = mappings[field]
|
18
|
+
result[field] = elastic_search_value field, field_mapping
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def elastic_search_value(field, mapping)
|
24
|
+
value = try field
|
25
|
+
return if value.nil?
|
26
|
+
|
27
|
+
value = case mapping[:type]
|
28
|
+
when :object
|
29
|
+
value.as_search_document
|
30
|
+
when :nested
|
31
|
+
value.map(&:as_search_document)
|
32
|
+
else
|
33
|
+
value
|
34
|
+
end
|
35
|
+
|
36
|
+
if value.present? || value == false
|
37
|
+
value
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -4,71 +4,26 @@ module ElasticRecord
|
|
4
4
|
return unless base.respond_to?(:after_save) && base.respond_to?(:after_destroy)
|
5
5
|
|
6
6
|
base.class_eval do
|
7
|
-
after_create
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
after_update if: :changed? do
|
12
|
-
method = self.class.elastic_index.partial_updates ? :update_record : :index_record
|
13
|
-
self.class.elastic_index.send(method, self)
|
14
|
-
end
|
15
|
-
|
16
|
-
after_destroy do
|
17
|
-
self.class.elastic_index.delete_document id
|
18
|
-
end
|
7
|
+
after_create :create_index_document
|
8
|
+
after_update :update_index_document, if: :changed?
|
9
|
+
after_destroy :delete_index_document
|
19
10
|
end
|
20
11
|
end
|
21
12
|
|
22
|
-
|
23
|
-
json = {}
|
13
|
+
private
|
24
14
|
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
unless value.nil?
|
29
|
-
json[field] = value
|
30
|
-
end
|
15
|
+
def create_index_document
|
16
|
+
self.class.elastic_index.index_record self
|
31
17
|
end
|
32
18
|
|
33
|
-
|
19
|
+
def update_index_document
|
20
|
+
method = self.class.elastic_index.partial_updates ? :update_record : :index_record
|
34
21
|
|
35
|
-
|
36
|
-
end
|
37
|
-
|
38
|
-
def as_partial_update_document
|
39
|
-
json = {}
|
40
|
-
|
41
|
-
mappings = elastic_index.mapping[:properties]
|
42
|
-
changed.each do |field|
|
43
|
-
if field_mapping = mappings[field]
|
44
|
-
json[field] = elastic_search_value field, field_mapping
|
45
|
-
end
|
22
|
+
self.class.elastic_index.send(method, self)
|
46
23
|
end
|
47
24
|
|
48
|
-
|
49
|
-
|
50
|
-
json
|
51
|
-
end
|
52
|
-
|
53
|
-
def elastic_search_value(field, mapping)
|
54
|
-
value = try field
|
55
|
-
return if value.nil?
|
56
|
-
|
57
|
-
value = case mapping[:type]
|
58
|
-
when :object
|
59
|
-
value.as_search
|
60
|
-
when :nested
|
61
|
-
value.map(&:as_search)
|
62
|
-
else
|
63
|
-
value
|
64
|
-
end
|
65
|
-
|
66
|
-
if value.present? || value == false
|
67
|
-
value
|
25
|
+
def delete_index_document
|
26
|
+
self.class.elastic_index.delete_document id
|
68
27
|
end
|
69
|
-
|
70
|
-
rescue
|
71
|
-
raise "Field not found for #{field.inspect}"
|
72
|
-
end
|
73
28
|
end
|
74
29
|
end
|
@@ -76,7 +76,7 @@ module ElasticRecord
|
|
76
76
|
delete: Net::HTTP::Delete
|
77
77
|
}
|
78
78
|
def new_request(method, path, body)
|
79
|
-
request = METHODS[method].new(path)
|
79
|
+
request = METHODS[method].new(path.starts_with?('/') ? path : "/#{path}")
|
80
80
|
request.basic_auth(options[:username], options[:password]) if options[:username].present?
|
81
81
|
request.body = body
|
82
82
|
request.content_type = 'application/json'
|
@@ -0,0 +1,43 @@
|
|
1
|
+
module ElasticRecord
|
2
|
+
class Doctype
|
3
|
+
attr_accessor :name, :mapping, :analysis
|
4
|
+
|
5
|
+
DEFAULT_MAPPING = {
|
6
|
+
properties: {
|
7
|
+
},
|
8
|
+
_all: {
|
9
|
+
enabled: false
|
10
|
+
}
|
11
|
+
}
|
12
|
+
|
13
|
+
PERCOLATOR_MAPPING = {
|
14
|
+
"properties" => {
|
15
|
+
"query" => {
|
16
|
+
"type" => "percolator"
|
17
|
+
}
|
18
|
+
}
|
19
|
+
}
|
20
|
+
|
21
|
+
def self.percolator_doctype
|
22
|
+
new('queries', PERCOLATOR_MAPPING)
|
23
|
+
end
|
24
|
+
|
25
|
+
def initialize(name, mapping = DEFAULT_MAPPING.deep_dup)
|
26
|
+
@name = name
|
27
|
+
@mapping = mapping
|
28
|
+
@analysis = {}
|
29
|
+
end
|
30
|
+
|
31
|
+
def mapping=(custom_mapping)
|
32
|
+
mapping.deep_merge!(custom_mapping)
|
33
|
+
end
|
34
|
+
|
35
|
+
def analysis=(custom_analysis)
|
36
|
+
analysis.deep_merge!(custom_analysis)
|
37
|
+
end
|
38
|
+
|
39
|
+
def ==(other)
|
40
|
+
name == other.name && mapping == other.mapping && analysis == other.analysis
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -14,9 +14,8 @@ module ElasticRecord
|
|
14
14
|
attr_accessor :bulk_stack
|
15
15
|
|
16
16
|
def initialize(index)
|
17
|
-
|
18
|
-
|
19
|
-
@deferring_enabled = false
|
17
|
+
@index = index
|
18
|
+
@bulk_stack = []
|
20
19
|
reset!
|
21
20
|
end
|
22
21
|
|
@@ -34,30 +33,29 @@ module ElasticRecord
|
|
34
33
|
self.writes_made = false
|
35
34
|
end
|
36
35
|
|
37
|
-
def flush!
|
38
|
-
deferred_actions.each do |queued_action|
|
39
|
-
self.writes_made = true
|
40
|
-
queued_action.run(index.real_connection)
|
41
|
-
end
|
42
|
-
deferred_actions.clear
|
43
|
-
end
|
44
|
-
|
45
36
|
private
|
46
37
|
READ_METHODS = [:json_get, :head]
|
47
38
|
def method_missing(method, *args, &block)
|
48
39
|
super unless index.real_connection.respond_to?(method)
|
49
40
|
|
50
41
|
if READ_METHODS.include?(method)
|
51
|
-
|
52
|
-
|
42
|
+
flush_deferred_actions!
|
43
|
+
if method == :json_get && args.first =~ /^\/(.*)\/_search/
|
44
|
+
index.real_connection.json_post("/#{$1.partition('/').first}/_refresh")
|
45
|
+
end
|
46
|
+
|
53
47
|
index.real_connection.send(method, *args, &block)
|
54
48
|
else
|
55
49
|
deferred_actions << DeferredAction.new(method, args, block)
|
56
50
|
end
|
57
51
|
end
|
58
52
|
|
59
|
-
def
|
60
|
-
|
53
|
+
def flush_deferred_actions!
|
54
|
+
deferred_actions.each do |queued_action|
|
55
|
+
self.writes_made = true
|
56
|
+
queued_action.run(index.real_connection)
|
57
|
+
end
|
58
|
+
deferred_actions.clear
|
61
59
|
end
|
62
60
|
end
|
63
61
|
|
@@ -53,58 +53,68 @@ module ElasticRecord
|
|
53
53
|
module Documents
|
54
54
|
def index_record(record, index_name: alias_name)
|
55
55
|
unless disabled
|
56
|
-
index_document(
|
56
|
+
index_document(
|
57
|
+
record.send(record.class.primary_key),
|
58
|
+
record.as_search_document,
|
59
|
+
doctype: record.doctype,
|
60
|
+
index_name: index_name
|
61
|
+
)
|
57
62
|
end
|
58
63
|
end
|
59
64
|
|
60
65
|
def update_record(record, index_name: alias_name)
|
61
66
|
unless disabled
|
62
|
-
update_document(
|
67
|
+
update_document(
|
68
|
+
record.send(record.class.primary_key),
|
69
|
+
record.as_partial_update_document,
|
70
|
+
doctype: record.doctype,
|
71
|
+
index_name: index_name
|
72
|
+
)
|
63
73
|
end
|
64
74
|
end
|
65
75
|
|
66
|
-
def index_document(id, document, parent: nil, index_name: alias_name)
|
76
|
+
def index_document(id, document, doctype: model.doctype, parent: nil, index_name: alias_name)
|
67
77
|
if batch = current_bulk_batch
|
68
|
-
instructions = { _index: index_name, _type:
|
78
|
+
instructions = { _index: index_name, _type: doctype.name, _id: id }
|
69
79
|
instructions[:parent] = parent if parent
|
70
80
|
|
71
81
|
batch << { index: instructions }
|
72
82
|
batch << document
|
73
83
|
else
|
74
|
-
path = "/#{index_name}/#{
|
84
|
+
path = "/#{index_name}/#{doctype.name}/#{id}"
|
75
85
|
path << "?parent=#{parent}" if parent
|
76
86
|
|
77
87
|
connection.json_put path, document
|
78
88
|
end
|
79
89
|
end
|
80
90
|
|
81
|
-
def update_document(id, document, parent: nil, index_name: alias_name)
|
91
|
+
def update_document(id, document, doctype: model.doctype, parent: nil, index_name: alias_name)
|
82
92
|
params = {doc: document, doc_as_upsert: true}
|
83
93
|
|
84
94
|
if batch = current_bulk_batch
|
85
|
-
instructions = { _index: index_name, _type:
|
95
|
+
instructions = { _index: index_name, _type: doctype.name, _id: id, _retry_on_conflict: 3 }
|
86
96
|
instructions[:parent] = parent if parent
|
87
97
|
|
88
98
|
batch << { update: instructions }
|
89
99
|
batch << params
|
90
100
|
else
|
91
|
-
path = "/#{index_name}/#{
|
101
|
+
path = "/#{index_name}/#{doctype.name}/#{id}/_update?retry_on_conflict=3"
|
92
102
|
path << "&parent=#{parent}" if parent
|
93
103
|
|
94
104
|
connection.json_post path, params
|
95
105
|
end
|
96
106
|
end
|
97
107
|
|
98
|
-
def delete_document(id,
|
108
|
+
def delete_document(id, doctype: model.doctype, parent: nil, index_name: alias_name)
|
99
109
|
raise "Cannot delete document with empty id" if id.blank?
|
100
110
|
index_name ||= alias_name
|
101
111
|
|
102
112
|
if batch = current_bulk_batch
|
103
|
-
instructions = { _index: index_name, _type:
|
113
|
+
instructions = { _index: index_name, _type: doctype.name, _id: id, _retry_on_conflict: 3 }
|
104
114
|
instructions[:parent] = parent if parent
|
105
115
|
batch << { delete: instructions }
|
106
116
|
else
|
107
|
-
path = "/#{index_name}/#{
|
117
|
+
path = "/#{index_name}/#{doctype.name}/#{id}"
|
108
118
|
path << "&parent=#{parent}" if parent
|
109
119
|
|
110
120
|
connection.json_delete path
|
@@ -122,7 +132,7 @@ module ElasticRecord
|
|
122
132
|
end
|
123
133
|
|
124
134
|
def record_exists?(id)
|
125
|
-
get(id)['found']
|
135
|
+
get(id, model.doctype)['found']
|
126
136
|
end
|
127
137
|
|
128
138
|
def search(elastic_query, options = {})
|
@@ -131,7 +141,7 @@ module ElasticRecord
|
|
131
141
|
url += "?#{options.to_query}"
|
132
142
|
end
|
133
143
|
|
134
|
-
get url, elastic_query
|
144
|
+
get url, model.doctype, elastic_query
|
135
145
|
end
|
136
146
|
|
137
147
|
def explain(id, elastic_query)
|
@@ -9,9 +9,7 @@ module ElasticRecord
|
|
9
9
|
|
10
10
|
def create(index_name = new_index_name)
|
11
11
|
connection.json_put "/#{index_name}", {
|
12
|
-
"mappings" =>
|
13
|
-
type => mapping
|
14
|
-
},
|
12
|
+
"mappings" => mapping_body,
|
15
13
|
"settings" => settings
|
16
14
|
}
|
17
15
|
index_name
|
@@ -31,8 +29,8 @@ module ElasticRecord
|
|
31
29
|
connection.head("/#{index_name}") == '200'
|
32
30
|
end
|
33
31
|
|
34
|
-
def type_exists?(index_name = alias_name)
|
35
|
-
connection.head("/#{index_name}/#{type}") == '200'
|
32
|
+
def type_exists?(index_name = alias_name, type = model.doctype.name)
|
33
|
+
connection.head("/#{index_name}/_mapping/#{type}") == '200'
|
36
34
|
end
|
37
35
|
|
38
36
|
def deploy(index_name)
|
@@ -78,7 +76,7 @@ module ElasticRecord
|
|
78
76
|
end
|
79
77
|
|
80
78
|
def all_names
|
81
|
-
connection.json_get("/#{alias_name}/_mapping/#{
|
79
|
+
connection.json_get("/#{alias_name}/_mapping/#{model.doctype.name}/").keys
|
82
80
|
rescue
|
83
81
|
# TODO: In ES 1.4, this returns empty rather than a 404
|
84
82
|
[]
|