elasticsearch-model 0.1.7 → 0.1.8
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/CHANGELOG.md +26 -1
- data/README.md +1 -1
- data/examples/activerecord_mapping_completion.rb +69 -0
- data/examples/mongoid_article.rb +2 -2
- data/lib/elasticsearch/model.rb +1 -1
- data/lib/elasticsearch/model/adapters/mongoid.rb +2 -12
- data/lib/elasticsearch/model/adapters/multiple.rb +8 -6
- data/lib/elasticsearch/model/importing.rb +3 -0
- data/lib/elasticsearch/model/indexing.rb +62 -8
- data/lib/elasticsearch/model/proxy.rb +2 -1
- data/lib/elasticsearch/model/response.rb +6 -0
- data/lib/elasticsearch/model/response/pagination.rb +25 -6
- data/lib/elasticsearch/model/response/results.rb +1 -1
- data/lib/elasticsearch/model/version.rb +1 -1
- data/test/integration/active_record_basic_test.rb +29 -4
- data/test/integration/multiple_models_test.rb +44 -26
- data/test/support/model.json +1 -0
- data/test/support/model.yml +2 -0
- data/test/unit/adapter_mongoid_test.rb +3 -1
- data/test/unit/importing_test.rb +39 -12
- data/test/unit/indexing_test.rb +111 -22
- data/test/unit/response_pagination_kaminari_test.rb +208 -8
- data/test/unit/response_pagination_will_paginate_test.rb +204 -14
- data/test/unit/response_test.rb +11 -1
- metadata +7 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: a357cdf1cb6d365afa38ed6631734804545d9cac
|
4
|
+
data.tar.gz: d11d799cc4c42be14d094e4561ca826f235d8399
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: c6608ecda81dc60edd8a18e814ad2baa758d879ed861b9e413b9123c91c648037d719635c7875a4e95e0199dfdad660a34297a4fa759e00eec23c0bf2eefcde3
|
7
|
+
data.tar.gz: 34e2112aa52d77b7b8d3fd688f11618cf2aa34634004217c613b31e65cc3b7cbd884b2614cdfb3b63d60e68ec9b2e4b0a3ff664b142287d170d9097cb9318530
|
data/CHANGELOG.md
CHANGED
@@ -1,9 +1,34 @@
|
|
1
|
+
## 0.1.8
|
2
|
+
|
3
|
+
* Added "default per page" methods for pagination with multi model searches
|
4
|
+
* Added a convenience accessor for the `aggregations` part of response
|
5
|
+
* Added a full example with mapping for the completion suggester
|
6
|
+
* Added an integration test for paginating multiple models
|
7
|
+
* Added proper support for the new "multi_fields" in the mapping DSL
|
8
|
+
* Added the `no_timeout` option for `__find_in_batches` in the Mongoid adapter
|
9
|
+
* Added, that index settings can be loaded from any object that responds to `:read`
|
10
|
+
* Added, that index settings/mappings can be loaded from a YAML or JSON file
|
11
|
+
* Added, that String pagination parameters are converted to numbers
|
12
|
+
* Added, that empty block is not required for setting mapping options
|
13
|
+
* Added, that on MyModel#import, an exception is raised if the index does not exists
|
14
|
+
* Changed the Elasticsearch port in the Mongoid example to 9200
|
15
|
+
* Cleaned up the tests for multiple fields/properties in mapping DSL
|
16
|
+
* Fixed a bug where continuous `#save` calls emptied the `@__changed_attributes` variable
|
17
|
+
* Fixed a buggy test introduced in #335
|
18
|
+
* Fixed incorrect deserialization of records in the Multiple adapter
|
19
|
+
* Fixed incorrect examples and documentation
|
20
|
+
* Fixed unreliable order of returned results/records in the integration test for the multiple adapter
|
21
|
+
* Fixed, that `param_name` is used when paginating with WillPaginate
|
22
|
+
* Fixed the problem where `document_type` configuration was not propagated to mapping [6 months ago by Miguel Ferna
|
23
|
+
* Refactored the code in `__find_in_batches` to use Enumerable#each_slice
|
24
|
+
* Refactored the string queries in multiple_models_test.rb to avoid quote escaping
|
25
|
+
|
1
26
|
## 0.1.7
|
2
27
|
|
3
28
|
* Improved examples and instructions in README and code annotations
|
4
29
|
* Prevented index methods to swallow all exceptions
|
5
30
|
* Added the `:validate` option to the `save` method for models
|
6
|
-
*
|
31
|
+
* Added support for searching across multiple models (elastic/elasticsearch-rails#345),
|
7
32
|
including documentation, examples and tests
|
8
33
|
|
9
34
|
## 0.1.6
|
data/README.md
CHANGED
@@ -129,7 +129,7 @@ Or configure the client for all models:
|
|
129
129
|
Elasticsearch::Model.client = Elasticsearch::Client.new log: true
|
130
130
|
```
|
131
131
|
|
132
|
-
You might want to do this during
|
132
|
+
You might want to do this during your application bootstrap process, e.g. in a Rails initializer.
|
133
133
|
|
134
134
|
Please refer to the
|
135
135
|
[`elasticsearch-transport`](https://github.com/elasticsearch/elasticsearch-ruby/tree/master/elasticsearch-transport)
|
@@ -0,0 +1,69 @@
|
|
1
|
+
require 'ansi'
|
2
|
+
require 'active_record'
|
3
|
+
require 'elasticsearch/model'
|
4
|
+
|
5
|
+
ActiveRecord::Base.logger = ActiveSupport::Logger.new(STDOUT)
|
6
|
+
ActiveRecord::Base.establish_connection( adapter: 'sqlite3', database: ":memory:" )
|
7
|
+
|
8
|
+
ActiveRecord::Schema.define(version: 1) do
|
9
|
+
create_table :articles do |t|
|
10
|
+
t.string :title
|
11
|
+
t.date :published_at
|
12
|
+
t.timestamps
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
class Article < ActiveRecord::Base
|
17
|
+
include Elasticsearch::Model
|
18
|
+
include Elasticsearch::Model::Callbacks
|
19
|
+
|
20
|
+
mapping do
|
21
|
+
indexes :title
|
22
|
+
indexes :title_suggest, type: 'completion', payloads: true
|
23
|
+
end
|
24
|
+
|
25
|
+
def as_indexed_json(options={})
|
26
|
+
as_json.merge \
|
27
|
+
title_suggest: {
|
28
|
+
input: title,
|
29
|
+
output: title,
|
30
|
+
payload: { url: "/articles/#{id}" }
|
31
|
+
}
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
Article.__elasticsearch__.client = Elasticsearch::Client.new log: true
|
36
|
+
|
37
|
+
# Create index
|
38
|
+
|
39
|
+
Article.__elasticsearch__.create_index! force: true
|
40
|
+
|
41
|
+
# Store data
|
42
|
+
|
43
|
+
Article.delete_all
|
44
|
+
Article.create title: 'Foo'
|
45
|
+
Article.create title: 'Bar'
|
46
|
+
Article.create title: 'Foo Foo'
|
47
|
+
Article.__elasticsearch__.refresh_index!
|
48
|
+
|
49
|
+
# Search and suggest
|
50
|
+
|
51
|
+
response_1 = Article.search 'foo';
|
52
|
+
|
53
|
+
puts "Article search:".ansi(:bold),
|
54
|
+
response_1.to_a.map { |d| "Title: #{d.title}" }.inspect.ansi(:bold, :yellow)
|
55
|
+
|
56
|
+
response_2 = Article.__elasticsearch__.client.suggest \
|
57
|
+
index: Article.index_name,
|
58
|
+
body: {
|
59
|
+
articles: {
|
60
|
+
text: 'foo',
|
61
|
+
completion: { field: 'title_suggest', size: 25 }
|
62
|
+
}
|
63
|
+
};
|
64
|
+
|
65
|
+
puts "Article suggest:".ansi(:bold),
|
66
|
+
response_2['articles'].first['options'].map { |d| "#{d['text']} -> #{d['payload']['url']}" }.
|
67
|
+
inspect.ansi(:bold, :green)
|
68
|
+
|
69
|
+
require 'pry'; binding.pry;
|
data/examples/mongoid_article.rb
CHANGED
@@ -21,7 +21,7 @@ Moped.logger.level = Logger::DEBUG
|
|
21
21
|
|
22
22
|
Mongoid.connect_to 'articles'
|
23
23
|
|
24
|
-
Elasticsearch::Model.client = Elasticsearch::Client.new host: 'localhost:
|
24
|
+
Elasticsearch::Model.client = Elasticsearch::Client.new host: 'localhost:9200', log: true
|
25
25
|
|
26
26
|
class Article
|
27
27
|
include Mongoid::Document
|
@@ -49,7 +49,7 @@ Article.create id: '3', title: 'Foo Foo'
|
|
49
49
|
|
50
50
|
# Index data
|
51
51
|
#
|
52
|
-
client = Elasticsearch::Client.new host:'localhost:
|
52
|
+
client = Elasticsearch::Client.new host:'localhost:9200', log:true
|
53
53
|
|
54
54
|
client.indices.delete index: 'articles' rescue nil
|
55
55
|
client.bulk index: 'articles',
|
data/lib/elasticsearch/model.rb
CHANGED
@@ -145,7 +145,7 @@ module Elasticsearch
|
|
145
145
|
#
|
146
146
|
# @example Configure (set) the client for all models
|
147
147
|
#
|
148
|
-
# Elasticsearch::Model.client Elasticsearch::Client.new host: 'http://localhost:9200', tracer: true
|
148
|
+
# Elasticsearch::Model.client = Elasticsearch::Client.new host: 'http://localhost:9200', tracer: true
|
149
149
|
# => #<Elasticsearch::Transport::Client:0x007f96a6dd0d80 @transport=... >
|
150
150
|
#
|
151
151
|
# @note You have to set the client before you call Elasticsearch methods on the model,
|
@@ -64,18 +64,8 @@ module Elasticsearch
|
|
64
64
|
#
|
65
65
|
def __find_in_batches(options={}, &block)
|
66
66
|
options[:batch_size] ||= 1_000
|
67
|
-
|
68
|
-
|
69
|
-
all.each do |item|
|
70
|
-
items << item
|
71
|
-
|
72
|
-
if items.length % options[:batch_size] == 0
|
73
|
-
yield items
|
74
|
-
items = []
|
75
|
-
end
|
76
|
-
end
|
77
|
-
|
78
|
-
unless items.empty?
|
67
|
+
|
68
|
+
all.no_timeout.each_slice(options[:batch_size]) do |items|
|
79
69
|
yield items
|
80
70
|
end
|
81
71
|
end
|
@@ -18,9 +18,11 @@ module Elasticsearch
|
|
18
18
|
def records
|
19
19
|
records_by_type = __records_by_type
|
20
20
|
|
21
|
-
response.response["hits"]["hits"].map do |hit|
|
21
|
+
records = response.response["hits"]["hits"].map do |hit|
|
22
22
|
records_by_type[ __type_for_hit(hit) ][ hit[:_id] ]
|
23
23
|
end
|
24
|
+
|
25
|
+
records.compact
|
24
26
|
end
|
25
27
|
|
26
28
|
# Returns the collection of records grouped by class based on `_type`
|
@@ -49,12 +51,12 @@ module Elasticsearch
|
|
49
51
|
# @api private
|
50
52
|
#
|
51
53
|
def __records_for_klass(klass, ids)
|
52
|
-
adapter =
|
54
|
+
adapter = __adapter_for_klass(klass)
|
53
55
|
|
54
|
-
case
|
55
|
-
when Elasticsearch::Model::Adapter::ActiveRecord
|
56
|
+
case
|
57
|
+
when Elasticsearch::Model::Adapter::ActiveRecord.equal?(adapter)
|
56
58
|
klass.where(klass.primary_key => ids)
|
57
|
-
when Elasticsearch::Model::Adapter::Mongoid
|
59
|
+
when Elasticsearch::Model::Adapter::Mongoid.equal?(adapter)
|
58
60
|
klass.where(:id.in => ids)
|
59
61
|
else
|
60
62
|
klass.find(ids)
|
@@ -100,7 +102,7 @@ module Elasticsearch
|
|
100
102
|
#
|
101
103
|
# @api private
|
102
104
|
#
|
103
|
-
def
|
105
|
+
def __adapter_for_klass(klass)
|
104
106
|
Adapter.adapters.select { |name, checker| checker.call(klass) }.keys.first
|
105
107
|
end
|
106
108
|
end
|
@@ -114,6 +114,9 @@ module Elasticsearch
|
|
114
114
|
|
115
115
|
if options.delete(:force)
|
116
116
|
self.create_index! force: true, index: target_index
|
117
|
+
elsif !self.index_exists? index: target_index
|
118
|
+
raise ArgumentError,
|
119
|
+
"#{target_index} does not exist to be imported into. Use create_index! or the :force option to create it."
|
117
120
|
end
|
118
121
|
|
119
122
|
__find_in_batches(options) do |batch|
|
@@ -34,7 +34,10 @@ module Elasticsearch
|
|
34
34
|
# Wraps the [index mappings](http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/mapping.html)
|
35
35
|
#
|
36
36
|
class Mappings
|
37
|
-
attr_accessor :options
|
37
|
+
attr_accessor :options, :type
|
38
|
+
|
39
|
+
# @private
|
40
|
+
TYPES_WITH_EMBEDDED_PROPERTIES = %w(object nested)
|
38
41
|
|
39
42
|
def initialize(type, options={})
|
40
43
|
raise ArgumentError, "`type` is missing" if type.nil?
|
@@ -44,12 +47,12 @@ module Elasticsearch
|
|
44
47
|
@mapping = {}
|
45
48
|
end
|
46
49
|
|
47
|
-
def indexes(name, options
|
50
|
+
def indexes(name, options={}, &block)
|
48
51
|
@mapping[name] = options
|
49
52
|
|
50
53
|
if block_given?
|
51
54
|
@mapping[name][:type] ||= 'object'
|
52
|
-
properties = @mapping[name][:type]
|
55
|
+
properties = TYPES_WITH_EMBEDDED_PROPERTIES.include?(@mapping[name][:type]) ? :properties : :fields
|
53
56
|
|
54
57
|
@mapping[name][properties] ||= {}
|
55
58
|
|
@@ -63,7 +66,6 @@ module Elasticsearch
|
|
63
66
|
end
|
64
67
|
|
65
68
|
# Set the type to `string` by default
|
66
|
-
#
|
67
69
|
@mapping[name][:type] ||= 'string'
|
68
70
|
|
69
71
|
self
|
@@ -128,14 +130,14 @@ module Elasticsearch
|
|
128
130
|
# # => {:article=>{:dynamic=>"strict", :properties=>{:foo=>{:type=>"long"}}}}
|
129
131
|
#
|
130
132
|
# The `mappings` and `settings` methods are accessible directly on the model class,
|
131
|
-
# when it doesn't already
|
133
|
+
# when it doesn't already define them. Use the `__elasticsearch__` proxy otherwise.
|
132
134
|
#
|
133
135
|
def mapping(options={}, &block)
|
134
136
|
@mapping ||= Mappings.new(document_type, options)
|
135
137
|
|
136
|
-
|
137
|
-
@mapping.options.update(options)
|
138
|
+
@mapping.options.update(options) unless options.empty?
|
138
139
|
|
140
|
+
if block_given?
|
139
141
|
@mapping.instance_eval(&block)
|
140
142
|
return self
|
141
143
|
else
|
@@ -153,7 +155,39 @@ module Elasticsearch
|
|
153
155
|
#
|
154
156
|
# # => {:index=>{:number_of_shards=>1}}
|
155
157
|
#
|
158
|
+
# You can read settings from any object that responds to :read
|
159
|
+
# as long as its return value can be parsed as either YAML or JSON.
|
160
|
+
#
|
161
|
+
# @example Define index settings from YAML file
|
162
|
+
#
|
163
|
+
# # config/elasticsearch/articles.yml:
|
164
|
+
# #
|
165
|
+
# # index:
|
166
|
+
# # number_of_shards: 1
|
167
|
+
# #
|
168
|
+
#
|
169
|
+
# Article.settings File.open("config/elasticsearch/articles.yml")
|
170
|
+
#
|
171
|
+
# Article.settings.to_hash
|
172
|
+
#
|
173
|
+
# # => { "index" => { "number_of_shards" => 1 } }
|
174
|
+
#
|
175
|
+
#
|
176
|
+
# @example Define index settings from JSON file
|
177
|
+
#
|
178
|
+
# # config/elasticsearch/articles.json:
|
179
|
+
# #
|
180
|
+
# # { "index": { "number_of_shards": 1 } }
|
181
|
+
# #
|
182
|
+
#
|
183
|
+
# Article.settings File.open("config/elasticsearch/articles.json")
|
184
|
+
#
|
185
|
+
# Article.settings.to_hash
|
186
|
+
#
|
187
|
+
# # => { "index" => { "number_of_shards" => 1 } }
|
188
|
+
#
|
156
189
|
def settings(settings={}, &block)
|
190
|
+
settings = YAML.load(settings.read) if settings.respond_to?(:read)
|
157
191
|
@settings ||= Settings.new(settings)
|
158
192
|
|
159
193
|
@settings.settings.update(settings) unless settings.empty?
|
@@ -166,6 +200,10 @@ module Elasticsearch
|
|
166
200
|
end
|
167
201
|
end
|
168
202
|
|
203
|
+
def load_settings_from_io(settings)
|
204
|
+
YAML.load(settings.read)
|
205
|
+
end
|
206
|
+
|
169
207
|
# Creates an index with correct name, automatically passing
|
170
208
|
# `settings` and `mappings` defined in the model
|
171
209
|
#
|
@@ -186,7 +224,7 @@ module Elasticsearch
|
|
186
224
|
|
187
225
|
delete_index!(options.merge index: target_index) if options[:force]
|
188
226
|
|
189
|
-
unless (
|
227
|
+
unless index_exists?(index: target_index)
|
190
228
|
self.client.indices.create index: target_index,
|
191
229
|
body: {
|
192
230
|
settings: self.settings.to_hash,
|
@@ -194,6 +232,22 @@ module Elasticsearch
|
|
194
232
|
end
|
195
233
|
end
|
196
234
|
|
235
|
+
# Returns true if the index exists
|
236
|
+
#
|
237
|
+
# @example Check whether the model's index exists
|
238
|
+
#
|
239
|
+
# Article.__elasticsearch__.index_exists?
|
240
|
+
#
|
241
|
+
# @example Check whether a specific index exists
|
242
|
+
#
|
243
|
+
# Article.__elasticsearch__.index_exists? index: 'my-index'
|
244
|
+
#
|
245
|
+
def index_exists?(options={})
|
246
|
+
target_index = options[:index] || self.index_name
|
247
|
+
|
248
|
+
self.client.indices.exists(index: target_index) rescue false
|
249
|
+
end
|
250
|
+
|
197
251
|
# Deletes the index with corresponding name
|
198
252
|
#
|
199
253
|
# @example Delete the index for the `Article` model
|
@@ -59,8 +59,9 @@ module Elasticsearch
|
|
59
59
|
# @see http://api.rubyonrails.org/classes/ActiveModel/Dirty.html
|
60
60
|
#
|
61
61
|
before_save do |i|
|
62
|
+
changed_attr = i.__elasticsearch__.instance_variable_get(:@__changed_attributes) || {}
|
62
63
|
i.__elasticsearch__.instance_variable_set(:@__changed_attributes,
|
63
|
-
Hash[ i.changes.map { |key, value| [key, value.last] } ])
|
64
|
+
changed_attr.merge(Hash[ i.changes.map { |key, value| [key, value.last] } ]))
|
64
65
|
end if respond_to?(:before_save) && instance_methods.include?(:changed_attributes)
|
65
66
|
end
|
66
67
|
end
|
@@ -65,6 +65,12 @@ module Elasticsearch
|
|
65
65
|
def shards
|
66
66
|
Hashie::Mash.new(response['_shards'])
|
67
67
|
end
|
68
|
+
|
69
|
+
# Returns a Hashie::Mash of the aggregations
|
70
|
+
#
|
71
|
+
def aggregations
|
72
|
+
response['aggregations'] ? Hashie::Mash.new(response['aggregations']) : nil
|
73
|
+
end
|
68
74
|
end
|
69
75
|
end
|
70
76
|
end
|
@@ -31,7 +31,7 @@ module Elasticsearch
|
|
31
31
|
@records = nil
|
32
32
|
@response = nil
|
33
33
|
@page = [num.to_i, 1].max
|
34
|
-
@per_page ||=
|
34
|
+
@per_page ||= __default_per_page
|
35
35
|
|
36
36
|
self.search.definition.update size: @per_page,
|
37
37
|
from: @per_page * (@page - 1)
|
@@ -48,7 +48,7 @@ module Elasticsearch
|
|
48
48
|
when search.definition[:size]
|
49
49
|
search.definition[:size]
|
50
50
|
else
|
51
|
-
|
51
|
+
__default_per_page
|
52
52
|
end
|
53
53
|
end
|
54
54
|
|
@@ -66,10 +66,11 @@ module Elasticsearch
|
|
66
66
|
# Set the "limit" (`size`) value
|
67
67
|
#
|
68
68
|
def limit(value)
|
69
|
+
return self if value.to_i <= 0
|
69
70
|
@results = nil
|
70
71
|
@records = nil
|
71
72
|
@response = nil
|
72
|
-
@per_page = value
|
73
|
+
@per_page = value.to_i
|
73
74
|
|
74
75
|
search.definition.update :size => @per_page
|
75
76
|
search.definition.update :from => @per_page * (@page - 1) if @page
|
@@ -79,11 +80,12 @@ module Elasticsearch
|
|
79
80
|
# Set the "offset" (`from`) value
|
80
81
|
#
|
81
82
|
def offset(value)
|
83
|
+
return self if value.to_i < 0
|
82
84
|
@results = nil
|
83
85
|
@records = nil
|
84
86
|
@response = nil
|
85
87
|
@page = nil
|
86
|
-
search.definition.update :from => value
|
88
|
+
search.definition.update :from => value.to_i
|
87
89
|
self
|
88
90
|
end
|
89
91
|
|
@@ -92,6 +94,14 @@ module Elasticsearch
|
|
92
94
|
def total_count
|
93
95
|
results.total
|
94
96
|
end
|
97
|
+
|
98
|
+
# Returns the models's `per_page` value or the default
|
99
|
+
#
|
100
|
+
# @api private
|
101
|
+
#
|
102
|
+
def __default_per_page
|
103
|
+
klass.respond_to?(:default_per_page) && klass.default_per_page || ::Kaminari.config.default_per_page
|
104
|
+
end
|
95
105
|
end
|
96
106
|
|
97
107
|
# Allow models to be paginated with the "will_paginate" gem [https://github.com/mislav/will_paginate]
|
@@ -122,8 +132,9 @@ module Elasticsearch
|
|
122
132
|
# Article.search('foo').paginate(page: 1, per_page: 30)
|
123
133
|
#
|
124
134
|
def paginate(options)
|
125
|
-
|
126
|
-
|
135
|
+
param_name = options[:param_name] || :page
|
136
|
+
page = [options[param_name].to_i, 1].max
|
137
|
+
per_page = (options[:per_page] || __default_per_page).to_i
|
127
138
|
|
128
139
|
search.definition.update size: per_page,
|
129
140
|
from: (page - 1) * per_page
|
@@ -165,6 +176,14 @@ module Elasticsearch
|
|
165
176
|
def total_entries
|
166
177
|
results.total
|
167
178
|
end
|
179
|
+
|
180
|
+
# Returns the models's `per_page` value or the default
|
181
|
+
#
|
182
|
+
# @api private
|
183
|
+
#
|
184
|
+
def __default_per_page
|
185
|
+
klass.respond_to?(:per_page) && klass.per_page || ::WillPaginate.per_page
|
186
|
+
end
|
168
187
|
end
|
169
188
|
end
|
170
189
|
|