elasticsearch-model 0.1.6 → 0.1.7

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: 6b66840bcd1e2b9f877f0c21c6e6c69c61bcc988
4
- data.tar.gz: 7ddc2b0fc4089fa9079cdaf771d6f947950d90c4
3
+ metadata.gz: 62d28c182c4b21b048ad496387e6cc139a79ee1b
4
+ data.tar.gz: f4bdb161bc448d38ba4fd2da0e7eb8c74fdebd15
5
5
  SHA512:
6
- metadata.gz: bf83de14e369c4acb429559fd1e6f4522e45b0b275fcf594df185a96593ae9d4791ff3f7214d6a19f7a8e10ee073da897424c0d44571f5369cca2b39abf4b131
7
- data.tar.gz: 345b76b6c9b3bacc5eab1538040188d7a622a9469dbfc89e4495392448d8a5c77dbf0cca0e0af678c916ebceb0fc2347eaf08d5aec26da7fe5bc8ffff26ebcf3
6
+ metadata.gz: ce7abb8673ae21d515c48f64e07a057ea82302132cedce60eeade565a44f9743fc0e811903df29aa93fa7cb55b96011d52ec25287064df5c846d5474da5125a4
7
+ data.tar.gz: 0eb48de6a8a1f7b264582b764de7013727d8909fa024a1777a9bad4304fa57649957dbc54b92fff24aa140364ee38f1ad35ddfc35da552cbab1ee4b0064e07f2
@@ -1,3 +1,11 @@
1
+ ## 0.1.7
2
+
3
+ * Improved examples and instructions in README and code annotations
4
+ * Prevented index methods to swallow all exceptions
5
+ * Added the `:validate` option to the `save` method for models
6
+ * Added support for searching across multiple models (elastic/elasticsearch-rails#345),
7
+ including documentation, examples and tests
8
+
1
9
  ## 0.1.6
2
10
 
3
11
  * Improved documentation
data/README.md CHANGED
@@ -216,8 +216,9 @@ response.records.to_a
216
216
  ```
217
217
 
218
218
  The returned object is the genuine collection of model instances returned by your database,
219
- i.e. `ActiveRecord::Relation` for ActiveRecord, or `Mongoid::Criteria` in case of MongoDB. This allows you to
220
- chain other methods on top of search results, as you would normally do:
219
+ i.e. `ActiveRecord::Relation` for ActiveRecord, or `Mongoid::Criteria` in case of MongoDB.
220
+
221
+ This allows you to chain other methods on top of search results, as you would normally do:
221
222
 
222
223
  ```ruby
223
224
  response.records.where(title: 'Quick brown fox').to_a
@@ -252,11 +253,35 @@ response.records.each_with_hit { |record, hit| puts "* #{record.title}: #{hit._s
252
253
  # * Fast black dogs: 0.02250402
253
254
  ```
254
255
 
256
+ #### Searching multiple models
257
+
258
+ It is possible to search across multiple models with the module method:
259
+
260
+ ```ruby
261
+ Elasticsearch::Model.search('fox', [Article, Comment]).results.to_a.map(&:to_hash)
262
+ # => [
263
+ # {"_index"=>"articles", "_type"=>"article", "_id"=>"1", "_score"=>0.35136628, "_source"=>...},
264
+ # {"_index"=>"comments", "_type"=>"comment", "_id"=>"1", "_score"=>0.35136628, "_source"=>...}
265
+ # ]
266
+
267
+ Elasticsearch::Model.search('fox', [Article, Comment]).records.to_a
268
+ # Article Load (0.3ms) SELECT "articles".* FROM "articles" WHERE "articles"."id" IN (1)
269
+ # Comment Load (0.2ms) SELECT "comments".* FROM "comments" WHERE "comments"."id" IN (1,5)
270
+ # => [#<Article id: 1, title: "Quick brown fox">, #<Comment id: 1, body: "Fox News">, ...]
271
+ ```
272
+
273
+ By default, all models which include the `Elasticsearch::Model` module are searched.
274
+
275
+ NOTE: It is _not_ possible to chain other methods on top of the `records` object, since it
276
+ is a heterogenous collection, with models potentially backed by different databases.
277
+
255
278
  #### Pagination
256
279
 
257
280
  You can implement pagination with the `from` and `size` search parameters. However, search results
258
281
  can be automatically paginated with the [`kaminari`](http://rubygems.org/gems/kaminari) or
259
282
  [`will_paginate`](https://github.com/mislav/will_paginate) gems.
283
+ (The pagination gems must be added before the Elasticsearch gems in your Gemfile,
284
+ or loaded first in your application.)
260
285
 
261
286
  If Kaminari or WillPaginate is loaded, use the familiar paging methods:
262
287
 
@@ -430,15 +455,15 @@ class Article < ActiveRecord::Base
430
455
  include Elasticsearch::Model
431
456
 
432
457
  after_commit on: [:create] do
433
- index_document if self.published?
458
+ __elasticsearch__.index_document if self.published?
434
459
  end
435
460
 
436
461
  after_commit on: [:update] do
437
- update_document if self.published?
462
+ __elasticsearch__.update_document if self.published?
438
463
  end
439
464
 
440
465
  after_commit on: [:destroy] do
441
- delete_document if self.published?
466
+ __elasticsearch__.delete_document if self.published?
442
467
  end
443
468
  end
444
469
  ```
@@ -59,9 +59,43 @@ ActiveRecord::Schema.define(version: 1) do
59
59
  add_index(:comments, :article_id)
60
60
  end
61
61
 
62
+ # ----- Elasticsearch client setup ----------------------------------------------------------------
63
+
64
+ Elasticsearch::Model.client = Elasticsearch::Client.new log: true
65
+ Elasticsearch::Model.client.transport.logger.formatter = proc { |s, d, p, m| "\e[32m#{m}\n\e[0m" }
66
+
67
+ # ----- Search integration ------------------------------------------------------------------------
68
+
69
+ module Searchable
70
+ extend ActiveSupport::Concern
71
+
72
+ included do
73
+ include Elasticsearch::Model
74
+ include Elasticsearch::Model::Callbacks
75
+
76
+ include Indexing
77
+ after_touch() { __elasticsearch__.index_document }
78
+ end
79
+
80
+ module Indexing
81
+
82
+ # Customize the JSON serialization for Elasticsearch
83
+ def as_indexed_json(options={})
84
+ self.as_json(
85
+ include: { categories: { only: :title},
86
+ authors: { methods: [:full_name], only: [:full_name] },
87
+ comments: { only: :text }
88
+ })
89
+ end
90
+ end
91
+ end
92
+
62
93
  # ----- Model definitions -------------------------------------------------------------------------
63
94
 
64
95
  class Category < ActiveRecord::Base
96
+ include Elasticsearch::Model
97
+ include Elasticsearch::Model::Callbacks
98
+
65
99
  has_and_belongs_to_many :articles
66
100
  end
67
101
 
@@ -81,6 +115,8 @@ class Authorship < ActiveRecord::Base
81
115
  end
82
116
 
83
117
  class Article < ActiveRecord::Base
118
+ include Searchable
119
+
84
120
  has_and_belongs_to_many :categories, after_add: [ lambda { |a,c| a.__elasticsearch__.index_document } ],
85
121
  after_remove: [ lambda { |a,c| a.__elasticsearch__.index_document } ]
86
122
  has_many :authorships
@@ -88,43 +124,13 @@ class Article < ActiveRecord::Base
88
124
  has_many :comments
89
125
  end
90
126
 
91
- class Article < ActiveRecord::Base; delegate :size, to: :comments, prefix: true; end
92
-
93
127
  class Comment < ActiveRecord::Base
94
- belongs_to :article, touch: true
95
- end
96
-
97
- # ----- Search integration ------------------------------------------------------------------------
98
-
99
- module Searchable
100
- extend ActiveSupport::Concern
101
-
102
- included do
103
- include Elasticsearch::Model
104
- include Elasticsearch::Model::Callbacks
105
-
106
- __elasticsearch__.client = Elasticsearch::Client.new log: true
107
- __elasticsearch__.client.transport.logger.formatter = proc { |s, d, p, m| "\e[32m#{m}\n\e[0m" }
108
-
109
- include Indexing
110
- after_touch() { __elasticsearch__.index_document }
111
- end
112
-
113
- module Indexing
128
+ include Elasticsearch::Model
129
+ include Elasticsearch::Model::Callbacks
114
130
 
115
- # Customize the JSON serialization for Elasticsearch
116
- def as_indexed_json(options={})
117
- self.as_json(
118
- include: { categories: { only: :title},
119
- authors: { methods: [:full_name], only: [:full_name] },
120
- comments: { only: :text }
121
- })
122
- end
123
- end
131
+ belongs_to :article, touch: true
124
132
  end
125
133
 
126
- Article.__send__ :include, Searchable
127
-
128
134
  # ----- Insert data -------------------------------------------------------------------------------
129
135
 
130
136
  # Create category
@@ -149,14 +155,23 @@ article.authors << author
149
155
 
150
156
  # Add comment
151
157
  #
152
- article.comments.create text: 'First comment'
158
+ article.comments.create text: 'First comment for article One'
159
+ article.comments.create text: 'Second comment for article One'
153
160
 
154
- # Load
161
+ Elasticsearch::Model.client.indices.refresh index: Elasticsearch::Model::Registry.all.map(&:index_name)
162
+
163
+ puts "\n\e[1mArticles containing 'one':\e[0m", Article.search('one').records.to_a.map(&:inspect), ""
164
+
165
+ puts "\n\e[1mModels containing 'one':\e[0m", Elasticsearch::Model.search('one').records.to_a.map(&:inspect), ""
166
+
167
+ # Load model
155
168
  #
156
169
  article = Article.all.includes(:categories, :authors, :comments).first
157
170
 
158
171
  # ----- Pry ---------------------------------------------------------------------------------------
159
172
 
173
+ puts '', '-'*Pry::Terminal.width!
174
+
160
175
  Pry.start(binding, prompt: lambda { |obj, nest_level, _| '> ' },
161
- input: StringIO.new('puts "\n\narticle.as_indexed_json\n"; article.as_indexed_json'),
176
+ input: StringIO.new("article.as_indexed_json\n"),
162
177
  quiet: true)
@@ -8,10 +8,13 @@ require 'elasticsearch/model/version'
8
8
 
9
9
  require 'elasticsearch/model/client'
10
10
 
11
+ require 'elasticsearch/model/multimodel'
12
+
11
13
  require 'elasticsearch/model/adapter'
12
14
  require 'elasticsearch/model/adapters/default'
13
15
  require 'elasticsearch/model/adapters/active_record'
14
16
  require 'elasticsearch/model/adapters/mongoid'
17
+ require 'elasticsearch/model/adapters/multiple'
15
18
 
16
19
  require 'elasticsearch/model/importing'
17
20
  require 'elasticsearch/model/indexing'
@@ -119,6 +122,9 @@ module Elasticsearch
119
122
  include Elasticsearch::Model::Importing::ClassMethods
120
123
  include Adapter.from_class(base).importing_mixin
121
124
  end
125
+
126
+ # Add to the registry if it's a class (and not in intermediate module)
127
+ Registry.add(base) if base.is_a?(Class)
122
128
  end
123
129
  end
124
130
 
@@ -149,6 +155,30 @@ module Elasticsearch
149
155
  @client = client
150
156
  end
151
157
 
158
+ # Search across multiple models
159
+ #
160
+ # By default, all models which include the `Elasticsearch::Model` module are searched
161
+ #
162
+ # @param query_or_payload [String,Hash,Object] The search request definition
163
+ # (string, JSON, Hash, or object responding to `to_hash`)
164
+ # @param models [Array] The Array of Model objects to search
165
+ # @param options [Hash] Optional parameters to be passed to the Elasticsearch client
166
+ #
167
+ # @return [Elasticsearch::Model::Response::Response]
168
+ #
169
+ # @example Search across specific models
170
+ #
171
+ # Elasticsearch::Model.search('foo', [Author, Article])
172
+ #
173
+ # @example Search across all models which include the `Elasticsearch::Model` module
174
+ #
175
+ # Elasticsearch::Model.search('foo')
176
+ #
177
+ def search(query_or_payload, models=[], options={})
178
+ models = Multimodel.new(models)
179
+ request = Searching::SearchRequest.new(models, query_or_payload, options)
180
+ Response::Response.new(models, request)
181
+ end
152
182
  end
153
183
  extend ClassMethods
154
184
 
@@ -7,7 +7,7 @@ module Elasticsearch
7
7
  module ActiveRecord
8
8
 
9
9
  Adapter.register self,
10
- lambda { |klass| !!defined?(::ActiveRecord::Base) && klass.ancestors.include?(::ActiveRecord::Base) }
10
+ lambda { |klass| !!defined?(::ActiveRecord::Base) && klass.respond_to?(:ancestors) && klass.ancestors.include?(::ActiveRecord::Base) }
11
11
 
12
12
  module Records
13
13
  # Returns an `ActiveRecord::Relation` instance
@@ -9,7 +9,7 @@ module Elasticsearch
9
9
  module Mongoid
10
10
 
11
11
  Adapter.register self,
12
- lambda { |klass| !!defined?(::Mongoid::Document) && klass.ancestors.include?(::Mongoid::Document) }
12
+ lambda { |klass| !!defined?(::Mongoid::Document) && klass.respond_to?(:ancestors) && klass.ancestors.include?(::Mongoid::Document) }
13
13
 
14
14
  module Records
15
15
 
@@ -0,0 +1,110 @@
1
+ module Elasticsearch
2
+ module Model
3
+ module Adapter
4
+
5
+ # An adapter to be used for deserializing results from multiple models,
6
+ # retrieved through `Elasticsearch::Model.search`
7
+ #
8
+ # @see Elasticsearch::Model.search
9
+ #
10
+ module Multiple
11
+ Adapter.register self, lambda { |klass| klass.is_a? Multimodel }
12
+
13
+ module Records
14
+ # Returns a collection of model instances, possibly of different classes (ActiveRecord, Mongoid, ...)
15
+ #
16
+ # @note The order of results in the Elasticsearch response is preserved
17
+ #
18
+ def records
19
+ records_by_type = __records_by_type
20
+
21
+ response.response["hits"]["hits"].map do |hit|
22
+ records_by_type[ __type_for_hit(hit) ][ hit[:_id] ]
23
+ end
24
+ end
25
+
26
+ # Returns the collection of records grouped by class based on `_type`
27
+ #
28
+ # Example:
29
+ #
30
+ # {
31
+ # Foo => {"1"=> #<Foo id: 1, title: "ABC"}, ...},
32
+ # Bar => {"1"=> #<Bar id: 1, name: "XYZ"}, ...}
33
+ # }
34
+ #
35
+ # @api private
36
+ #
37
+ def __records_by_type
38
+ result = __ids_by_type.map do |klass, ids|
39
+ records = __records_for_klass(klass, ids)
40
+ ids = records.map(&:id).map(&:to_s)
41
+ [ klass, Hash[ids.zip(records)] ]
42
+ end
43
+
44
+ Hash[result]
45
+ end
46
+
47
+ # Returns the collection of records for a specific type based on passed `klass`
48
+ #
49
+ # @api private
50
+ #
51
+ def __records_for_klass(klass, ids)
52
+ adapter = __adapter_name_for_klass(klass)
53
+
54
+ case adapter
55
+ when Elasticsearch::Model::Adapter::ActiveRecord
56
+ klass.where(klass.primary_key => ids)
57
+ when Elasticsearch::Model::Adapter::Mongoid
58
+ klass.where(:id.in => ids)
59
+ else
60
+ klass.find(ids)
61
+ end
62
+ end
63
+
64
+ # Returns the record IDs grouped by class based on type `_type`
65
+ #
66
+ # Example:
67
+ #
68
+ # { Foo => ["1"], Bar => ["1", "5"] }
69
+ #
70
+ # @api private
71
+ #
72
+ def __ids_by_type
73
+ ids_by_type = {}
74
+
75
+ response.response["hits"]["hits"].each do |hit|
76
+ type = __type_for_hit(hit)
77
+ ids_by_type[type] ||= []
78
+ ids_by_type[type] << hit[:_id]
79
+ end
80
+ ids_by_type
81
+ end
82
+
83
+ # Returns the class of the model corresponding to a specific `hit` in Elasticsearch results
84
+ #
85
+ # @see Elasticsearch::Model::Registry
86
+ #
87
+ # @api private
88
+ #
89
+ def __type_for_hit(hit)
90
+ @@__types ||= {}
91
+
92
+ @@__types[ "#{hit[:_index]}::#{hit[:_type]}" ] ||= begin
93
+ Registry.all.detect do |model|
94
+ model.index_name == hit[:_index] && model.document_type == hit[:_type]
95
+ end
96
+ end
97
+ end
98
+
99
+ # Returns the adapter registered for a particular `klass` or `nil` if not available
100
+ #
101
+ # @api private
102
+ #
103
+ def __adapter_name_for_klass(klass)
104
+ Adapter.adapters.select { |name, checker| checker.call(klass) }.keys.first
105
+ end
106
+ end
107
+ end
108
+ end
109
+ end
110
+ end
@@ -187,17 +187,10 @@ module Elasticsearch
187
187
  delete_index!(options.merge index: target_index) if options[:force]
188
188
 
189
189
  unless ( self.client.indices.exists(index: target_index) rescue false )
190
- begin
191
- self.client.indices.create index: target_index,
192
- body: {
193
- settings: self.settings.to_hash,
194
- mappings: self.mappings.to_hash }
195
- rescue Exception => e
196
- unless e.class.to_s =~ /NotFound/ && options[:force]
197
- STDERR.puts "[!!!] Error when creating the index: #{e.class}", "#{e.message}"
198
- end
199
- end
200
- else
190
+ self.client.indices.create index: target_index,
191
+ body: {
192
+ settings: self.settings.to_hash,
193
+ mappings: self.mappings.to_hash }
201
194
  end
202
195
  end
203
196
 
@@ -217,8 +210,10 @@ module Elasticsearch
217
210
  begin
218
211
  self.client.indices.delete index: target_index
219
212
  rescue Exception => e
220
- unless e.class.to_s =~ /NotFound/ && options[:force]
221
- STDERR.puts "[!!!] Error when deleting the index: #{e.class}", "#{e.message}"
213
+ if e.class.to_s =~ /NotFound/ && options[:force]
214
+ STDERR.puts "[!!!] Index does not exist (#{e.class})"
215
+ else
216
+ raise e
222
217
  end
223
218
  end
224
219
  end
@@ -241,8 +236,10 @@ module Elasticsearch
241
236
  begin
242
237
  self.client.indices.refresh index: target_index
243
238
  rescue Exception => e
244
- unless e.class.to_s =~ /NotFound/ && options[:force]
245
- STDERR.puts "[!!!] Error when refreshing the index: #{e.class}", "#{e.message}"
239
+ if e.class.to_s =~ /NotFound/ && options[:force]
240
+ STDERR.puts "[!!!] Index does not exist (#{e.class})"
241
+ else
242
+ raise e
246
243
  end
247
244
  end
248
245
  end
@@ -0,0 +1,83 @@
1
+ module Elasticsearch
2
+ module Model
3
+
4
+ # Keeps a global registry of classes that include `Elasticsearch::Model`
5
+ #
6
+ class Registry
7
+ def initialize
8
+ @models = []
9
+ end
10
+
11
+ # Returns the unique instance of the registry (Singleton)
12
+ #
13
+ # @api private
14
+ #
15
+ def self.__instance
16
+ @instance ||= new
17
+ end
18
+
19
+ # Adds a model to the registry
20
+ #
21
+ def self.add(klass)
22
+ __instance.add(klass)
23
+ end
24
+
25
+ # Returns an Array of registered models
26
+ #
27
+ def self.all
28
+ __instance.models
29
+ end
30
+
31
+ # Adds a model to the registry
32
+ #
33
+ def add(klass)
34
+ @models << klass
35
+ end
36
+
37
+ # Returns a copy of the registered models
38
+ #
39
+ def models
40
+ @models.dup
41
+ end
42
+ end
43
+
44
+ # Wraps a collection of models when querying multiple indices
45
+ #
46
+ # @see Elasticsearch::Model.search
47
+ #
48
+ class Multimodel
49
+ attr_reader :models
50
+
51
+ # @param models [Class] The list of models across which the search will be performed
52
+ #
53
+ def initialize(*models)
54
+ @models = models.flatten
55
+ @models = Model::Registry.all if @models.empty?
56
+ end
57
+
58
+ # Get an Array of index names used for retrieving documents when doing a search across multiple models
59
+ #
60
+ # @return [Array] the list of index names used for retrieving documents
61
+ #
62
+ def index_name
63
+ models.map { |m| m.index_name }
64
+ end
65
+
66
+ # Get an Array of document types used for retrieving documents when doing a search across multiple models
67
+ #
68
+ # @return [Array] the list of document types used for retrieving documents
69
+ #
70
+ def document_type
71
+ models.map { |m| m.document_type }
72
+ end
73
+
74
+ # Get the client common for all models
75
+ #
76
+ # @return Elasticsearch::Transport::Client
77
+ #
78
+ def client
79
+ Elasticsearch::Model.client
80
+ end
81
+ end
82
+ end
83
+ end
@@ -79,7 +79,8 @@ module Elasticsearch
79
79
  # fields: {
80
80
  # title: {}
81
81
  # }
82
- # }
82
+ # },
83
+ # size: 50
83
84
  #
84
85
  # response.results.first.title
85
86
  # # => "Foo"
@@ -1,5 +1,5 @@
1
1
  module Elasticsearch
2
2
  module Model
3
- VERSION = "0.1.6"
3
+ VERSION = "0.1.7"
4
4
  end
5
5
  end
@@ -1,25 +1,9 @@
1
1
  require 'test_helper'
2
2
 
3
- begin
4
- require 'mongoid'
5
- session = Moped::Connection.new("localhost", 27017, 0.5)
6
- session.connect
7
- ENV["MONGODB_AVAILABLE"] = 'yes'
8
- rescue LoadError, Moped::Errors::ConnectionFailure => e
9
- $stderr.puts "MongoDB not installed or running: #{e}"
10
- end
11
-
12
- if ENV["MONGODB_AVAILABLE"]
13
- $stderr.puts "Mongoid #{Mongoid::VERSION}", '-'*80
14
-
15
- logger = ::Logger.new($stderr)
16
- logger.formatter = lambda { |s, d, p, m| " #{m.ansi(:faint, :cyan)}\n" }
17
- logger.level = ::Logger::DEBUG
18
-
19
- Mongoid.logger = logger unless ENV['QUIET']
20
- Moped.logger = logger unless ENV['QUIET']
3
+ Mongo.setup!
21
4
 
22
- Mongoid.connect_to 'mongoid_articles'
5
+ if Mongo.available?
6
+ Mongo.connect_to 'mongoid_articles'
23
7
 
24
8
  module Elasticsearch
25
9
  module Model
@@ -50,7 +34,7 @@ if ENV["MONGODB_AVAILABLE"]
50
34
  setup do
51
35
  Elasticsearch::Model::Adapter.register \
52
36
  Elasticsearch::Model::Adapter::Mongoid,
53
- lambda { |klass| !!defined?(::Mongoid::Document) && klass.ancestors.include?(::Mongoid::Document) }
37
+ lambda { |klass| !!defined?(::Mongoid::Document) && klass.respond_to?(:ancestors) && klass.ancestors.include?(::Mongoid::Document) }
54
38
 
55
39
  MongoidArticle.__elasticsearch__.create_index! force: true
56
40
 
@@ -0,0 +1,154 @@
1
+ require 'test_helper'
2
+ require 'active_record'
3
+
4
+ Mongo.setup!
5
+
6
+ module Elasticsearch
7
+ module Model
8
+ class MultipleModelsIntegration < Elasticsearch::Test::IntegrationTestCase
9
+ context "Multiple models" do
10
+ setup do
11
+ ActiveRecord::Schema.define(:version => 1) do
12
+ create_table :episodes do |t|
13
+ t.string :name
14
+ t.datetime :created_at, :default => 'NOW()'
15
+ end
16
+
17
+ create_table :series do |t|
18
+ t.string :name
19
+ t.datetime :created_at, :default => 'NOW()'
20
+ end
21
+ end
22
+
23
+ class ::Episode < ActiveRecord::Base
24
+ include Elasticsearch::Model
25
+ include Elasticsearch::Model::Callbacks
26
+
27
+ settings index: {number_of_shards: 1, number_of_replicas: 0} do
28
+ mapping do
29
+ indexes :name, type: 'string', analyzer: 'snowball'
30
+ indexes :created_at, type: 'date'
31
+ end
32
+ end
33
+ end
34
+
35
+ class ::Series < ActiveRecord::Base
36
+ include Elasticsearch::Model
37
+ include Elasticsearch::Model::Callbacks
38
+
39
+ settings index: {number_of_shards: 1, number_of_replicas: 0} do
40
+ mapping do
41
+ indexes :name, type: 'string', analyzer: 'snowball'
42
+ indexes :created_at, type: 'date'
43
+ end
44
+ end
45
+ end
46
+
47
+ [::Episode, ::Series].each do |model|
48
+ model.delete_all
49
+ model.__elasticsearch__.create_index! force: true
50
+ model.create name: "The #{model.name}"
51
+ model.create name: "A great #{model.name}"
52
+ model.create name: "The greatest #{model.name}"
53
+ model.__elasticsearch__.refresh_index!
54
+ end
55
+
56
+ end
57
+
58
+ should "find matching documents across multiple models" do
59
+ response = Elasticsearch::Model.search("greatest", [Series, Episode])
60
+
61
+ assert response.any?, "Response should not be empty: #{response.to_a.inspect}"
62
+
63
+ assert_equal 2, response.results.size
64
+ assert_equal 2, response.records.size
65
+
66
+ assert_instance_of Elasticsearch::Model::Response::Result, response.results.first
67
+ assert_instance_of Episode, response.records.first
68
+ assert_instance_of Series, response.records.last
69
+
70
+ assert_equal 'The greatest Episode', response.results[0].name
71
+ assert_equal 'The greatest Episode', response.records[0].name
72
+
73
+ assert_equal 'The greatest Series', response.results[1].name
74
+ assert_equal 'The greatest Series', response.records[1].name
75
+ end
76
+
77
+ should "provide access to results" do
78
+ q = {query: {query_string: {query: 'A great *'}}, highlight: {fields: {name: {}}}}
79
+ response = Elasticsearch::Model.search(q, [Series, Episode])
80
+
81
+ assert_equal 'A great Episode', response.results[0].name
82
+ assert_equal true, response.results[0].name?
83
+ assert_equal false, response.results[0].boo?
84
+ assert_equal true, response.results[0].highlight?
85
+ assert_equal true, response.results[0].highlight.name?
86
+ assert_equal false, response.results[0].highlight.boo?
87
+
88
+ assert_equal 'A great Series', response.results[1].name
89
+ assert_equal true, response.results[1].name?
90
+ assert_equal false, response.results[1].boo?
91
+ assert_equal true, response.results[1].highlight?
92
+ assert_equal true, response.results[1].highlight.name?
93
+ assert_equal false, response.results[1].highlight.boo?
94
+ end
95
+
96
+ if Mongo.available?
97
+ Mongo.connect_to 'mongoid_collections'
98
+
99
+ context "Across mongoid models" do
100
+ setup do
101
+ class ::Image
102
+ include Mongoid::Document
103
+ include Elasticsearch::Model
104
+ include Elasticsearch::Model::Callbacks
105
+
106
+ field :name, type: String
107
+ attr_accessible :name if respond_to? :attr_accessible
108
+
109
+ settings index: {number_of_shards: 1, number_of_replicas: 0} do
110
+ mapping do
111
+ indexes :name, type: 'string', analyzer: 'snowball'
112
+ indexes :created_at, type: 'date'
113
+ end
114
+ end
115
+
116
+ def as_indexed_json(options={})
117
+ as_json(except: [:_id])
118
+ end
119
+ end
120
+
121
+ Image.delete_all
122
+ Image.__elasticsearch__.create_index! force: true
123
+ Image.create! name: "The Image"
124
+ Image.create! name: "A great Image"
125
+ Image.create! name: "The greatest Image"
126
+ Image.__elasticsearch__.refresh_index!
127
+ Image.__elasticsearch__.client.cluster.health wait_for_status: 'yellow'
128
+ end
129
+
130
+ should "find matching documents across multiple models" do
131
+ response = Elasticsearch::Model.search("greatest", [Episode, Image])
132
+
133
+ assert response.any?, "Response should not be empty: #{response.to_a.inspect}"
134
+
135
+ assert_equal 2, response.results.size
136
+ assert_equal 2, response.records.size
137
+
138
+ assert_instance_of Elasticsearch::Model::Response::Result, response.results.first
139
+ assert_instance_of Image, response.records.first
140
+ assert_instance_of Episode, response.records.last
141
+
142
+ assert_equal 'The greatest Image', response.results[0].name
143
+ assert_equal 'The greatest Image', response.records[0].name
144
+
145
+ assert_equal 'The greatest Episode', response.results[1].name
146
+ assert_equal 'The greatest Episode', response.records[1].name
147
+ end
148
+ end
149
+ end
150
+
151
+ end
152
+ end
153
+ end
154
+ end
@@ -61,3 +61,33 @@ module Elasticsearch
61
61
  end
62
62
  end
63
63
  end
64
+
65
+ class Mongo
66
+ def self.setup!
67
+ begin
68
+ require 'mongoid'
69
+ session = Moped::Connection.new("localhost", 27017, 0.5)
70
+ session.connect
71
+ ENV['MONGODB_AVAILABLE'] = 'yes'
72
+ rescue LoadError, Moped::Errors::ConnectionFailure => e
73
+ $stderr.puts "MongoDB not installed or running: #{e}"
74
+ end
75
+ end
76
+
77
+ def self.available?
78
+ !!ENV['MONGODB_AVAILABLE']
79
+ end
80
+
81
+ def self.connect_to(source)
82
+ $stderr.puts "Mongoid #{Mongoid::VERSION}", '-'*80
83
+
84
+ logger = ::Logger.new($stderr)
85
+ logger.formatter = lambda { |s, d, p, m| " #{m.ansi(:faint, :cyan)}\n" }
86
+ logger.level = ::Logger::DEBUG
87
+
88
+ Mongoid.logger = logger unless ENV['QUIET']
89
+ Moped.logger = logger unless ENV['QUIET']
90
+
91
+ Mongoid.connect_to source
92
+ end
93
+ end
@@ -0,0 +1,106 @@
1
+ require 'test_helper'
2
+
3
+ class Elasticsearch::Model::MultipleTest < Test::Unit::TestCase
4
+
5
+ context "Adapter for multiple models" do
6
+
7
+ class ::DummyOne
8
+ include Elasticsearch::Model
9
+
10
+ index_name 'dummy'
11
+ document_type 'dummy_one'
12
+
13
+ def self.find(ids)
14
+ ids.map { |id| new(id) }
15
+ end
16
+
17
+ attr_reader :id
18
+
19
+ def initialize(id)
20
+ @id = id.to_i
21
+ end
22
+ end
23
+
24
+ module ::Namespace
25
+ class DummyTwo
26
+ include Elasticsearch::Model
27
+
28
+ index_name 'dummy'
29
+ document_type 'dummy_two'
30
+
31
+ def self.find(ids)
32
+ ids.map { |id| new(id) }
33
+ end
34
+
35
+ attr_reader :id
36
+
37
+ def initialize(id)
38
+ @id = id.to_i
39
+ end
40
+ end
41
+ end
42
+
43
+ class ::DummyTwo
44
+ include Elasticsearch::Model
45
+
46
+ index_name 'other_index'
47
+ document_type 'dummy_two'
48
+
49
+ def self.find(ids)
50
+ ids.map { |id| new(id) }
51
+ end
52
+
53
+ attr_reader :id
54
+
55
+ def initialize(id)
56
+ @id = id.to_i
57
+ end
58
+ end
59
+
60
+ HITS = [{_index: 'dummy',
61
+ _type: 'dummy_two',
62
+ _id: '2',
63
+ }, {
64
+ _index: 'dummy',
65
+ _type: 'dummy_one',
66
+ _id: '2',
67
+ }, {
68
+ _index: 'other_index',
69
+ _type: 'dummy_two',
70
+ _id: '1',
71
+ }, {
72
+ _index: 'dummy',
73
+ _type: 'dummy_two',
74
+ _id: '1',
75
+ }, {
76
+ _index: 'dummy',
77
+ _type: 'dummy_one',
78
+ _id: '3'}]
79
+
80
+ setup do
81
+ @multimodel = Elasticsearch::Model::Multimodel.new(DummyOne, DummyTwo, Namespace::DummyTwo)
82
+ end
83
+
84
+ context "when returning records" do
85
+ setup do
86
+ @multimodel.class.send :include, Elasticsearch::Model::Adapter::Multiple::Records
87
+ @multimodel.expects(:response).at_least_once.returns(stub(response: { 'hits' => { 'hits' => HITS } }))
88
+ end
89
+
90
+ should "keep the order from response" do
91
+ assert_instance_of Module, Elasticsearch::Model::Adapter::Multiple::Records
92
+ records = @multimodel.records
93
+
94
+ assert_equal 5, records.count
95
+
96
+ assert_kind_of ::Namespace::DummyTwo, records[0]
97
+ assert_kind_of ::DummyOne, records[1]
98
+ assert_kind_of ::DummyTwo, records[2]
99
+ assert_kind_of ::Namespace::DummyTwo, records[3]
100
+ assert_kind_of ::DummyOne, records[4]
101
+
102
+ assert_equal [2, 2, 1, 1, 3], records.map(&:id)
103
+ end
104
+ end
105
+ end
106
+ end
@@ -12,6 +12,8 @@ class Elasticsearch::Model::IndexingTest < Test::Unit::TestCase
12
12
  end
13
13
  end
14
14
 
15
+ class NotFound < Exception; end
16
+
15
17
  context "Settings class" do
16
18
  should "be convertible to hash" do
17
19
  hash = { foo: 'bar' }
@@ -336,6 +338,7 @@ class Elasticsearch::Model::IndexingTest < Test::Unit::TestCase
336
338
  assert_equal 'bar', payload[:type]
337
339
  assert_equal '1', payload[:id]
338
340
  assert_equal({title: 'green'}, payload[:body][:doc])
341
+ true
339
342
  end
340
343
 
341
344
  instance.expects(:client).returns(client)
@@ -356,6 +359,7 @@ class Elasticsearch::Model::IndexingTest < Test::Unit::TestCase
356
359
  assert_equal '1', payload[:id]
357
360
  assert_equal({title: 'green'}, payload[:body][:doc])
358
361
  assert_equal true, payload[:refresh]
362
+ true
359
363
  end
360
364
 
361
365
  instance.expects(:client).returns(client)
@@ -380,19 +384,40 @@ class Elasticsearch::Model::IndexingTest < Test::Unit::TestCase
380
384
  end
381
385
  end
382
386
 
383
- should "delete the index without raising exception" do
387
+ should "delete the index without raising exception when the index is not found" do
384
388
  client = stub('client')
385
389
  indices = stub('indices')
386
390
  client.stubs(:indices).returns(indices)
387
391
 
388
- indices.expects(:delete).returns({}).then.raises(Exception).at_least_once
392
+ indices.expects(:delete).returns({}).then.raises(NotFound).at_least_once
389
393
 
390
394
  DummyIndexingModelForRecreate.expects(:client).returns(client).at_least_once
391
395
 
392
- assert_nothing_raised do
393
- DummyIndexingModelForRecreate.delete_index!
394
- DummyIndexingModelForRecreate.delete_index!
395
- end
396
+ assert_nothing_raised { DummyIndexingModelForRecreate.delete_index! force: true }
397
+ end
398
+
399
+ should "raise an exception without the force option" do
400
+ client = stub('client')
401
+ indices = stub('indices')
402
+ client.stubs(:indices).returns(indices)
403
+
404
+ indices.expects(:delete).raises(NotFound)
405
+
406
+ DummyIndexingModelForRecreate.expects(:client).returns(client)
407
+
408
+ assert_raise(NotFound) { DummyIndexingModelForRecreate.delete_index! }
409
+ end
410
+
411
+ should "raise a regular exception when deleting the index" do
412
+ client = stub('client')
413
+
414
+ indices = stub('indices')
415
+ indices.expects(:delete).raises(Exception)
416
+ client.stubs(:indices).returns(indices)
417
+
418
+ DummyIndexingModelForRecreate.expects(:client).returns(client)
419
+
420
+ assert_raise(Exception) { DummyIndexingModelForRecreate.delete_index! force: true }
396
421
  end
397
422
 
398
423
  should "create the index with correct settings and mappings when it doesn't exist" do
@@ -428,19 +453,18 @@ class Elasticsearch::Model::IndexingTest < Test::Unit::TestCase
428
453
  assert_nothing_raised { DummyIndexingModelForRecreate.create_index! }
429
454
  end
430
455
 
431
- should "not raise exception during index creation" do
456
+ should "raise exception during index creation" do
432
457
  client = stub('client')
433
458
  indices = stub('indices')
434
459
  client.stubs(:indices).returns(indices)
435
460
 
461
+ indices.expects(:delete).returns({})
436
462
  indices.expects(:exists).returns(false)
437
- indices.expects(:create).raises(Exception).at_least_once
463
+ indices.expects(:create).raises(Exception)
438
464
 
439
465
  DummyIndexingModelForRecreate.expects(:client).returns(client).at_least_once
440
466
 
441
- assert_nothing_raised do
442
- DummyIndexingModelForRecreate.create_index!
443
- end
467
+ assert_raise(Exception) { DummyIndexingModelForRecreate.create_index! force: true }
444
468
  end
445
469
 
446
470
  should "delete the index first with the force option" do
@@ -459,7 +483,19 @@ class Elasticsearch::Model::IndexingTest < Test::Unit::TestCase
459
483
  end
460
484
  end
461
485
 
462
- should "refresh the index without raising exception" do
486
+ should "refresh the index without raising exception with the force option" do
487
+ client = stub('client')
488
+ indices = stub('indices')
489
+ client.stubs(:indices).returns(indices)
490
+
491
+ indices.expects(:refresh).returns({}).then.raises(NotFound).at_least_once
492
+
493
+ DummyIndexingModelForRecreate.expects(:client).returns(client).at_least_once
494
+
495
+ assert_nothing_raised { DummyIndexingModelForRecreate.refresh_index! force: true }
496
+ end
497
+
498
+ should "raise a regular exception when refreshing the index" do
463
499
  client = stub('client')
464
500
  indices = stub('indices')
465
501
  client.stubs(:indices).returns(indices)
@@ -468,10 +504,7 @@ class Elasticsearch::Model::IndexingTest < Test::Unit::TestCase
468
504
 
469
505
  DummyIndexingModelForRecreate.expects(:client).returns(client).at_least_once
470
506
 
471
- assert_nothing_raised do
472
- DummyIndexingModelForRecreate.refresh_index!
473
- DummyIndexingModelForRecreate.refresh_index!
474
- end
507
+ assert_nothing_raised { DummyIndexingModelForRecreate.refresh_index! force: true }
475
508
  end
476
509
 
477
510
  context "with a custom index name" do
@@ -0,0 +1,38 @@
1
+ require 'test_helper'
2
+
3
+ class Elasticsearch::Model::MultimodelTest < Test::Unit::TestCase
4
+
5
+ context "Multimodel class" do
6
+ setup do
7
+ title = stub('Foo', index_name: 'foo_index', document_type: 'foo')
8
+ series = stub('Bar', index_name: 'bar_index', document_type: 'bar')
9
+ @multimodel = Elasticsearch::Model::Multimodel.new(title, series)
10
+ end
11
+
12
+ should "have an index_name" do
13
+ assert_equal ['foo_index', 'bar_index'], @multimodel.index_name
14
+ end
15
+
16
+ should "have a document_type" do
17
+ assert_equal ['foo', 'bar'], @multimodel.document_type
18
+ end
19
+
20
+ should "have a client" do
21
+ assert_equal Elasticsearch::Model.client, @multimodel.client
22
+ end
23
+
24
+ should "include models in the registry" do
25
+ class ::JustAModel
26
+ include Elasticsearch::Model
27
+ end
28
+
29
+ class ::JustAnotherModel
30
+ include Elasticsearch::Model
31
+ end
32
+
33
+ multimodel = Elasticsearch::Model::Multimodel.new
34
+ assert multimodel.models.include?(::JustAModel)
35
+ assert multimodel.models.include?(::JustAnotherModel)
36
+ end
37
+ end
38
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: elasticsearch-model
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.6
4
+ version: 0.1.7
5
5
  platform: ruby
6
6
  authors:
7
7
  - Karel Minarik
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2014-10-01 00:00:00.000000000 Z
11
+ date: 2015-04-08 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: elasticsearch
@@ -348,11 +348,13 @@ files:
348
348
  - lib/elasticsearch/model/adapters/active_record.rb
349
349
  - lib/elasticsearch/model/adapters/default.rb
350
350
  - lib/elasticsearch/model/adapters/mongoid.rb
351
+ - lib/elasticsearch/model/adapters/multiple.rb
351
352
  - lib/elasticsearch/model/callbacks.rb
352
353
  - lib/elasticsearch/model/client.rb
353
354
  - lib/elasticsearch/model/ext/active_record.rb
354
355
  - lib/elasticsearch/model/importing.rb
355
356
  - lib/elasticsearch/model/indexing.rb
357
+ - lib/elasticsearch/model/multimodel.rb
356
358
  - lib/elasticsearch/model/naming.rb
357
359
  - lib/elasticsearch/model/proxy.rb
358
360
  - lib/elasticsearch/model/response.rb
@@ -373,16 +375,19 @@ files:
373
375
  - test/integration/active_record_pagination_test.rb
374
376
  - test/integration/dynamic_index_name_test.rb
375
377
  - test/integration/mongoid_basic_test.rb
378
+ - test/integration/multiple_models_test.rb
376
379
  - test/test_helper.rb
377
380
  - test/unit/adapter_active_record_test.rb
378
381
  - test/unit/adapter_default_test.rb
379
382
  - test/unit/adapter_mongoid_test.rb
383
+ - test/unit/adapter_multiple_test.rb
380
384
  - test/unit/adapter_test.rb
381
385
  - test/unit/callbacks_test.rb
382
386
  - test/unit/client_test.rb
383
387
  - test/unit/importing_test.rb
384
388
  - test/unit/indexing_test.rb
385
389
  - test/unit/module_test.rb
390
+ - test/unit/multimodel_test.rb
386
391
  - test/unit/naming_test.rb
387
392
  - test/unit/proxy_test.rb
388
393
  - test/unit/response_base_test.rb
@@ -430,16 +435,19 @@ test_files:
430
435
  - test/integration/active_record_pagination_test.rb
431
436
  - test/integration/dynamic_index_name_test.rb
432
437
  - test/integration/mongoid_basic_test.rb
438
+ - test/integration/multiple_models_test.rb
433
439
  - test/test_helper.rb
434
440
  - test/unit/adapter_active_record_test.rb
435
441
  - test/unit/adapter_default_test.rb
436
442
  - test/unit/adapter_mongoid_test.rb
443
+ - test/unit/adapter_multiple_test.rb
437
444
  - test/unit/adapter_test.rb
438
445
  - test/unit/callbacks_test.rb
439
446
  - test/unit/client_test.rb
440
447
  - test/unit/importing_test.rb
441
448
  - test/unit/indexing_test.rb
442
449
  - test/unit/module_test.rb
450
+ - test/unit/multimodel_test.rb
443
451
  - test/unit/naming_test.rb
444
452
  - test/unit/proxy_test.rb
445
453
  - test/unit/response_base_test.rb