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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 62d28c182c4b21b048ad496387e6cc139a79ee1b
4
- data.tar.gz: f4bdb161bc448d38ba4fd2da0e7eb8c74fdebd15
3
+ metadata.gz: a357cdf1cb6d365afa38ed6631734804545d9cac
4
+ data.tar.gz: d11d799cc4c42be14d094e4561ca826f235d8399
5
5
  SHA512:
6
- metadata.gz: ce7abb8673ae21d515c48f64e07a057ea82302132cedce60eeade565a44f9743fc0e811903df29aa93fa7cb55b96011d52ec25287064df5c846d5474da5125a4
7
- data.tar.gz: 0eb48de6a8a1f7b264582b764de7013727d8909fa024a1777a9bad4304fa57649957dbc54b92fff24aa140364ee38f1ad35ddfc35da552cbab1ee4b0064e07f2
6
+ metadata.gz: c6608ecda81dc60edd8a18e814ad2baa758d879ed861b9e413b9123c91c648037d719635c7875a4e95e0199dfdad660a34297a4fa759e00eec23c0bf2eefcde3
7
+ data.tar.gz: 34e2112aa52d77b7b8d3fd688f11618cf2aa34634004217c613b31e65cc3b7cbd884b2614cdfb3b63d60e68ec9b2e4b0a3ff664b142287d170d9097cb9318530
@@ -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
- * Added support for searching across multiple models (elastic/elasticsearch-rails#345),
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 you application bootstrap process, e.g. in a Rails initializer.
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;
@@ -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:9250', log: true
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:9250', log:true
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',
@@ -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
- items = []
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 = __adapter_name_for_klass(klass)
54
+ adapter = __adapter_for_klass(klass)
53
55
 
54
- case adapter
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 __adapter_name_for_klass(klass)
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 = {}, &block)
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] == 'multi_field' ? :fields : :properties
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 defines them. Use the `__elasticsearch__` proxy otherwise.
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
- if block_given?
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 ( self.client.indices.exists(index: target_index) rescue false )
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 ||= klass.default_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
- search.klass.default_per_page
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
- page = [options[:page].to_i, 1].max
126
- per_page = (options[:per_page] || klass.per_page).to_i
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