elasticsearch-model 0.1.7 → 0.1.8
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
|