elasticsearch-model 0.1.6 → 0.1.7

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