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 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