elastic_record 4.0.0 → 4.1.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/.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
|
[]
|