elasticsearch-model 5.1.0 → 6.0.0

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.
Files changed (33) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +4 -4
  3. data/elasticsearch-model.gemspec +4 -4
  4. data/examples/activerecord_custom_analyzer.rb +135 -0
  5. data/examples/activerecord_mapping_completion.rb +1 -3
  6. data/examples/datamapper_article.rb +11 -1
  7. data/gemfiles/3.0.gemfile +1 -1
  8. data/gemfiles/4.0.gemfile +1 -1
  9. data/gemfiles/5.0.gemfile +1 -1
  10. data/lib/elasticsearch/model/adapters/active_record.rb +4 -1
  11. data/lib/elasticsearch/model/adapters/mongoid.rb +10 -3
  12. data/lib/elasticsearch/model/importing.rb +1 -1
  13. data/lib/elasticsearch/model/indexing.rb +1 -1
  14. data/lib/elasticsearch/model/naming.rb +3 -1
  15. data/lib/elasticsearch/model/response/aggregations.rb +1 -1
  16. data/lib/elasticsearch/model/response/base.rb +3 -2
  17. data/lib/elasticsearch/model/response/suggestions.rb +1 -1
  18. data/lib/elasticsearch/model/response.rb +11 -10
  19. data/lib/elasticsearch/model/version.rb +1 -1
  20. data/test/integration/active_record_associations_parent_child_test.rb +75 -34
  21. data/test/integration/active_record_basic_test.rb +4 -0
  22. data/test/integration/active_record_custom_serialization_test.rb +2 -2
  23. data/test/integration/active_record_import_test.rb +65 -12
  24. data/test/integration/active_record_namespaced_model_test.rb +2 -0
  25. data/test/integration/mongoid_basic_test.rb +64 -0
  26. data/test/test_helper.rb +1 -1
  27. data/test/unit/adapter_mongoid_test.rb +58 -1
  28. data/test/unit/importing_test.rb +21 -0
  29. data/test/unit/indexing_test.rb +35 -2
  30. data/test/unit/naming_inheritance_test.rb +2 -2
  31. data/test/unit/naming_test.rb +2 -2
  32. data/test/unit/response_results_test.rb +3 -0
  33. metadata +8 -7
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d3f9a1aed30a6f21b7c51f5f73ab01e9f00ea7b3d2d3d6b2203b5ba922e6dd63
4
- data.tar.gz: 65bcb7ba51e908ada809d47d9aea04d2c78091182061b2d83488735696c25c93
3
+ metadata.gz: 726d16096fd6ca31266bab9b42c4f878cba6942eada1649de7fbf946af2575f0
4
+ data.tar.gz: 8ce1faffcdda9dae38ff2beddab31f0b161853c4fd9919b699cdb5e5a27f2baa
5
5
  SHA512:
6
- metadata.gz: 81f261b7d86e68d6b23e811a5053bb300982d4536ac3a7026850b098f08237e46a51c847cc2d12736fee7544a3984cae45a0ebef034a6cb38dd41c3bc96c8b14
7
- data.tar.gz: e53472c07599acae594fdcdc19405eb4f03cbfe77ee242429d539dc09cce5b03011d3e74f2b777642e0018895ba0e57551f385d37dbb20c6ce62877e8393819f
6
+ metadata.gz: 63904bd61ec508918ee8a0d6cbe7cd089dc0b6df7c6f4d8ea1fef7538b2aea3a88962724e6a5fc486f5ec10146350e4176655b3fbeec868a66b6b3e00e767307
7
+ data.tar.gz: 36fccab94cf1f3cdfa599ad0a6d74cecc1b503c80da296e73381f3b3f2130244d57a9b51fabaff5e624e7f0955442e3b0e67d7006b6b94f705fdc4249252ac12
data/README.md CHANGED
@@ -321,7 +321,7 @@ Elasticsearch::Model::Response::Response.__send__ :include, Elasticsearch::Model
321
321
 
322
322
  #### The Elasticsearch DSL
323
323
 
324
- In most situation, you'll want to pass the search definition
324
+ In most situations, you'll want to pass the search definition
325
325
  in the Elasticsearch [domain-specific language](http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/query-dsl.html) to the client:
326
326
 
327
327
  ```ruby
@@ -425,7 +425,7 @@ Article.__elasticsearch__.refresh_index!
425
425
  ```
426
426
 
427
427
  By default, index name and document type will be inferred from your class name,
428
- you can set it explicitely, however:
428
+ you can set it explicitly, however:
429
429
 
430
430
  ```ruby
431
431
  class Article
@@ -534,7 +534,7 @@ class Indexer
534
534
  case operation.to_s
535
535
  when /index/
536
536
  record = Article.find(record_id)
537
- Client.index index: 'articles', type: 'article', id: record.id, body: record.as_indexed_json
537
+ Client.index index: 'articles', type: 'article', id: record.id, body: record.__elasticsearch__.as_indexed_json
538
538
  when /delete/
539
539
  Client.delete index: 'articles', type: 'article', id: record_id
540
540
  else raise ArgumentError, "Unknown operation '#{operation}'"
@@ -674,7 +674,7 @@ module DataMapperAdapter
674
674
  #
675
675
  module Records
676
676
  def records
677
- klass.all(id: @ids)
677
+ klass.all(id: ids)
678
678
  end
679
679
 
680
680
  # ...
@@ -23,7 +23,7 @@ Gem::Specification.new do |s|
23
23
 
24
24
  s.required_ruby_version = ">= 1.9.3"
25
25
 
26
- s.add_dependency "elasticsearch", '~> 5'
26
+ s.add_dependency "elasticsearch", '> 1'
27
27
  s.add_dependency "activesupport", '> 3'
28
28
  s.add_dependency "hashie"
29
29
 
@@ -32,10 +32,10 @@ Gem::Specification.new do |s|
32
32
 
33
33
  s.add_development_dependency "elasticsearch-extensions"
34
34
 
35
- s.add_development_dependency "sqlite3"
35
+ s.add_development_dependency "sqlite3" unless defined?(JRUBY_VERSION)
36
36
  s.add_development_dependency "activemodel", "> 3"
37
37
 
38
- s.add_development_dependency "oj"
38
+ s.add_development_dependency "oj" unless defined?(JRUBY_VERSION)
39
39
  s.add_development_dependency "kaminari"
40
40
  s.add_development_dependency "will_paginate"
41
41
 
@@ -45,7 +45,7 @@ Gem::Specification.new do |s|
45
45
  s.add_development_dependency "mocha"
46
46
  s.add_development_dependency "turn"
47
47
  s.add_development_dependency "yard"
48
- s.add_development_dependency "ruby-prof"
48
+ s.add_development_dependency "ruby-prof" unless defined?(JRUBY_VERSION)
49
49
  s.add_development_dependency "pry"
50
50
 
51
51
  s.add_development_dependency "simplecov"
@@ -0,0 +1,135 @@
1
+ # Custom Analyzer for ActiveRecord integration with Elasticsearch
2
+ # ===============================================================
3
+
4
+ $LOAD_PATH.unshift File.expand_path('../../lib', __FILE__)
5
+
6
+ require 'ansi'
7
+ require 'logger'
8
+
9
+ require 'active_record'
10
+ require 'elasticsearch/model'
11
+
12
+ ActiveRecord::Base.logger = ActiveSupport::Logger.new(STDOUT)
13
+ ActiveRecord::Base.establish_connection( adapter: 'sqlite3', database: ":memory:" )
14
+
15
+ ActiveRecord::Schema.define(version: 1) do
16
+ create_table :articles do |t|
17
+ t.string :title
18
+ t.date :published_at
19
+ t.timestamps
20
+ end
21
+ end
22
+
23
+ Elasticsearch::Model.client.transport.logger = ActiveSupport::Logger.new(STDOUT)
24
+ Elasticsearch::Model.client.transport.logger.formatter = lambda { |s, d, p, m| "#{m.ansi(:faint)}\n" }
25
+
26
+ class Article < ActiveRecord::Base
27
+ include Elasticsearch::Model
28
+
29
+ settings index: {
30
+ number_of_shards: 1,
31
+ number_of_replicas: 0,
32
+ analysis: {
33
+ analyzer: {
34
+ pattern: {
35
+ type: 'pattern',
36
+ pattern: "\\s|_|-|\\.",
37
+ lowercase: true
38
+ },
39
+ trigram: {
40
+ tokenizer: 'trigram'
41
+ }
42
+ },
43
+ tokenizer: {
44
+ trigram: {
45
+ type: 'ngram',
46
+ min_gram: 3,
47
+ max_gram: 3,
48
+ token_chars: ['letter', 'digit']
49
+ }
50
+ }
51
+ } } do
52
+ mapping do
53
+ indexes :title, type: 'text', analyzer: 'english' do
54
+ indexes :keyword, analyzer: 'keyword'
55
+ indexes :pattern, analyzer: 'pattern'
56
+ indexes :trigram, analyzer: 'trigram'
57
+ end
58
+ end
59
+ end
60
+ end
61
+
62
+ # Create example records
63
+ #
64
+ Article.delete_all
65
+ Article.create title: 'Foo'
66
+ Article.create title: 'Foo-Bar'
67
+ Article.create title: 'Foo_Bar_Bazooka'
68
+ Article.create title: 'Foo.Bar'
69
+
70
+ # Index records
71
+ #
72
+ errors = Article.import force: true, refresh: true, return: 'errors'
73
+ puts "[!] Errors importing records: #{errors.map { |d| d['index']['error'] }.join(', ')}".ansi(:red) && exit(1) unless errors.empty?
74
+
75
+ puts '', '-'*80
76
+
77
+ puts "English analyzer [Foo_Bar_1_Bazooka]".ansi(:bold),
78
+ "Tokens: " +
79
+ Article.__elasticsearch__.client.indices
80
+ .analyze(index: Article.index_name, body: { field: 'title', text: 'Foo_Bar_1_Bazooka' })['tokens']
81
+ .map { |d| "[#{d['token']}]" }.join(' '),
82
+ "\n"
83
+
84
+ puts "Keyword analyzer [Foo_Bar_1_Bazooka]".ansi(:bold),
85
+ "Tokens: " +
86
+ Article.__elasticsearch__.client.indices
87
+ .analyze(index: Article.index_name, body: { field: 'title.keyword', text: 'Foo_Bar_1_Bazooka' })['tokens']
88
+ .map { |d| "[#{d['token']}]" }.join(' '),
89
+ "\n"
90
+
91
+ puts "Pattern analyzer [Foo_Bar_1_Bazooka]".ansi(:bold),
92
+ "Tokens: " +
93
+ Article.__elasticsearch__.client.indices
94
+ .analyze(index: Article.index_name, body: { field: 'title.pattern', text: 'Foo_Bar_1_Bazooka' })['tokens']
95
+ .map { |d| "[#{d['token']}]" }.join(' '),
96
+ "\n"
97
+
98
+ puts "Trigram analyzer [Foo_Bar_1_Bazooka]".ansi(:bold),
99
+ "Tokens: " +
100
+ Article.__elasticsearch__.client.indices
101
+ .analyze(index: Article.index_name, body: { field: 'title.trigram', text: 'Foo_Bar_1_Bazooka' })['tokens']
102
+ .map { |d| "[#{d['token']}]" }.join(' '),
103
+ "\n"
104
+
105
+ puts '', '-'*80
106
+
107
+ response = Article.search query: { match: { 'title' => 'foo' } } ;
108
+
109
+ puts "English search for 'foo'".ansi(:bold),
110
+ "#{response.response.hits.total} matches: " +
111
+ response.records.map { |d| d.title }.join(', '),
112
+ "\n"
113
+
114
+ puts '', '-'*80
115
+
116
+ response = Article.search query: { match: { 'title.pattern' => 'foo' } } ;
117
+
118
+ puts "Pattern search for 'foo'".ansi(:bold),
119
+ "#{response.response.hits.total} matches: " +
120
+ response.records.map { |d| d.title }.join(', '),
121
+ "\n"
122
+
123
+ puts '', '-'*80
124
+
125
+ response = Article.search query: { match: { 'title.trigram' => 'zoo' } } ;
126
+
127
+ puts "Trigram search for 'zoo'".ansi(:bold),
128
+ "#{response.response.hits.total} matches: " +
129
+ response.records.map { |d| d.title }.join(', '),
130
+ "\n"
131
+
132
+ puts '', '-'*80
133
+
134
+
135
+ require 'pry'; binding.pry;
@@ -25,9 +25,7 @@ class Article < ActiveRecord::Base
25
25
  end
26
26
 
27
27
  def as_indexed_json(options={})
28
- as_json.merge \
29
- title_suggest: { input: title },
30
- url: "/articles/#{id}"
28
+ as_json.merge 'url' => "/articles/#{id}"
31
29
  end
32
30
  end
33
31
 
@@ -50,11 +50,21 @@ module DataMapperAdapter
50
50
  #
51
51
  module Records
52
52
  def records
53
- klass.all(id: @ids)
53
+ klass.all(id: ids)
54
54
  end
55
55
 
56
56
  # ...
57
57
  end
58
+
59
+ module Callbacks
60
+ def self.included(model)
61
+ model.class_eval do
62
+ after(:create) { __elasticsearch__.index_document }
63
+ after(:save) { __elasticsearch__.update_document }
64
+ after(:destroy) { __elasticsearch__.delete_document }
65
+ end
66
+ end
67
+ end
58
68
  end
59
69
 
60
70
  # Register the adapter
data/gemfiles/3.0.gemfile CHANGED
@@ -10,4 +10,4 @@ gemspec path: '../'
10
10
  gem 'activemodel', '>= 3.0'
11
11
  gem 'activerecord', '~> 3.2'
12
12
  gem 'mongoid', '>= 3.0'
13
- gem 'sqlite3'
13
+ gem 'sqlite3' unless defined?(JRUBY_VERSION)
data/gemfiles/4.0.gemfile CHANGED
@@ -9,4 +9,4 @@ gemspec path: '../'
9
9
 
10
10
  gem 'activemodel', '~> 4'
11
11
  gem 'activerecord', '~> 4'
12
- gem 'sqlite3'
12
+ gem 'sqlite3' unless defined?(JRUBY_VERSION)
data/gemfiles/5.0.gemfile CHANGED
@@ -9,4 +9,4 @@ gemspec path: '../'
9
9
 
10
10
  gem 'activemodel', '~> 5'
11
11
  gem 'activerecord', '~> 5'
12
- gem 'sqlite3'
12
+ gem 'sqlite3' unless defined?(JRUBY_VERSION)
@@ -56,7 +56,10 @@ module Elasticsearch
56
56
  # Redefine the `to_a` method to the original one
57
57
  #
58
58
  sql_records.instance_exec do
59
- define_singleton_method(:to_a) do
59
+ ar_records_method_name = :to_a
60
+ ar_records_method_name = :records if defined?(::ActiveRecord) && ::ActiveRecord::VERSION::MAJOR >= 5
61
+
62
+ define_singleton_method(ar_records_method_name) do
60
63
  if defined?(::ActiveRecord) && ::ActiveRecord::VERSION::MAJOR >= 4
61
64
  self.load
62
65
  else
@@ -63,10 +63,17 @@ module Elasticsearch
63
63
  # @see https://github.com/karmi/retire/pull/724
64
64
  #
65
65
  def __find_in_batches(options={}, &block)
66
- options[:batch_size] ||= 1_000
66
+ batch_size = options[:batch_size] || 1_000
67
+ query = options[:query]
68
+ named_scope = options[:scope]
69
+ preprocess = options[:preprocess]
70
+
71
+ scope = all
72
+ scope = scope.send(named_scope) if named_scope
73
+ scope = query.is_a?(Proc) ? scope.class_exec(&query) : scope.where(query) if query
67
74
 
68
- all.no_timeout.each_slice(options[:batch_size]) do |items|
69
- yield items
75
+ scope.no_timeout.each_slice(batch_size) do |items|
76
+ yield (preprocess ? self.__send__(preprocess, items) : items)
70
77
  end
71
78
  end
72
79
 
@@ -130,7 +130,7 @@ module Elasticsearch
130
130
  errors += response['items'].select { |k, v| k.values.first['error'] }
131
131
  end
132
132
 
133
- self.refresh_index! if refresh
133
+ self.refresh_index! index: target_index if refresh
134
134
 
135
135
  case return_value
136
136
  when 'errors'
@@ -397,7 +397,7 @@ module Elasticsearch
397
397
  # @see http://rubydoc.info/gems/elasticsearch-api/Elasticsearch/API/Actions:update
398
398
  #
399
399
  def update_document(options={})
400
- if attributes_in_database = self.instance_variable_get(:@__changed_model_attributes)
400
+ if attributes_in_database = self.instance_variable_get(:@__changed_model_attributes).presence
401
401
  attributes = if respond_to?(:as_indexed_json)
402
402
  self.as_indexed_json.select { |k,v| attributes_in_database.keys.map(&:to_s).include? k.to_s }
403
403
  else
@@ -5,6 +5,8 @@ module Elasticsearch
5
5
  #
6
6
  module Naming
7
7
 
8
+ DEFAULT_DOC_TYPE = '_doc'.freeze
9
+
8
10
  module ClassMethods
9
11
 
10
12
  # Get or set the name of the index
@@ -90,7 +92,7 @@ module Elasticsearch
90
92
  end
91
93
 
92
94
  def default_document_type
93
- self.model_name.element
95
+ DEFAULT_DOC_TYPE
94
96
  end
95
97
 
96
98
  end
@@ -2,7 +2,7 @@ module Elasticsearch
2
2
  module Model
3
3
  module Response
4
4
 
5
- class Aggregations < Hashie::Mash
5
+ class Aggregations < HashWrapper
6
6
  disable_warnings if respond_to?(:disable_warnings)
7
7
 
8
8
  def initialize(attributes={})
@@ -4,7 +4,7 @@ module Elasticsearch
4
4
  # Common funtionality for classes in the {Elasticsearch::Model::Response} module
5
5
  #
6
6
  module Base
7
- attr_reader :klass, :response
7
+ attr_reader :klass, :response, :raw_response
8
8
 
9
9
  # @param klass [Class] The name of the model class
10
10
  # @param response [Hash] The full response returned from Elasticsearch client
@@ -12,7 +12,8 @@ module Elasticsearch
12
12
  #
13
13
  def initialize(klass, response, options={})
14
14
  @klass = klass
15
- @response = response
15
+ @raw_response = response
16
+ @response = response
16
17
  end
17
18
 
18
19
  # @abstract Implement this method in specific class
@@ -2,7 +2,7 @@ module Elasticsearch
2
2
  module Model
3
3
  module Response
4
4
 
5
- class Suggestions < Hashie::Mash
5
+ class Suggestions < HashWrapper
6
6
  disable_warnings if respond_to?(:disable_warnings)
7
7
 
8
8
  def terms
@@ -10,8 +10,7 @@ module Elasticsearch
10
10
  # Implements Enumerable and forwards its methods to the {#results} object.
11
11
  #
12
12
  class Response
13
- attr_reader :klass, :search, :response,
14
- :took, :timed_out, :shards
13
+ attr_reader :klass, :search
15
14
 
16
15
  include Enumerable
17
16
 
@@ -27,9 +26,7 @@ module Elasticsearch
27
26
  # @return [Hash]
28
27
  #
29
28
  def response
30
- @response ||= begin
31
- HashWrapper.new(search.execute!)
32
- end
29
+ @response ||= HashWrapper.new(search.execute!)
33
30
  end
34
31
 
35
32
  # Returns the collection of "hits" from Elasticsearch
@@ -51,31 +48,35 @@ module Elasticsearch
51
48
  # Returns the "took" time
52
49
  #
53
50
  def took
54
- response['took']
51
+ raw_response['took']
55
52
  end
56
53
 
57
54
  # Returns whether the response timed out
58
55
  #
59
56
  def timed_out
60
- response['timed_out']
57
+ raw_response['timed_out']
61
58
  end
62
59
 
63
60
  # Returns the statistics on shards
64
61
  #
65
62
  def shards
66
- HashWrapper.new(response['_shards'])
63
+ @shards ||= HashWrapper.new(raw_response['_shards'])
67
64
  end
68
65
 
69
66
  # Returns a Hashie::Mash of the aggregations
70
67
  #
71
68
  def aggregations
72
- Aggregations.new(response['aggregations'])
69
+ @aggregations ||= Aggregations.new(raw_response['aggregations'])
73
70
  end
74
71
 
75
72
  # Returns a Hashie::Mash of the suggestions
76
73
  #
77
74
  def suggestions
78
- Suggestions.new(response['suggest'])
75
+ @suggestions ||= Suggestions.new(raw_response['suggest'])
76
+ end
77
+
78
+ def raw_response
79
+ @raw_response ||= search.execute!
79
80
  end
80
81
  end
81
82
  end
@@ -1,5 +1,5 @@
1
1
  module Elasticsearch
2
2
  module Model
3
- VERSION = "5.1.0"
3
+ VERSION = '6.0.0'
4
4
  end
5
5
  end
@@ -11,7 +11,11 @@ class Question < ActiveRecord::Base
11
11
 
12
12
  has_many :answers, dependent: :destroy
13
13
 
14
- index_name 'questions_and_answers'
14
+ JOIN_TYPE = 'question'.freeze
15
+ JOIN_METADATA = { join_field: JOIN_TYPE}.freeze
16
+
17
+ index_name 'questions_and_answers'.freeze
18
+ document_type 'doc'.freeze
15
19
 
16
20
  mapping do
17
21
  indexes :title
@@ -19,6 +23,12 @@ class Question < ActiveRecord::Base
19
23
  indexes :author
20
24
  end
21
25
 
26
+ def as_indexed_json(options={})
27
+ # This line is necessary for differences between ActiveModel::Serializers::JSON#as_json versions
28
+ json = as_json(options)[JOIN_TYPE] || as_json(options)
29
+ json.merge(JOIN_METADATA)
30
+ end
31
+
22
32
  after_commit lambda { __elasticsearch__.index_document }, on: :create
23
33
  after_commit lambda { __elasticsearch__.update_document }, on: :update
24
34
  after_commit lambda { __elasticsearch__.delete_document }, on: :destroy
@@ -29,32 +39,55 @@ class Answer < ActiveRecord::Base
29
39
 
30
40
  belongs_to :question
31
41
 
32
- index_name 'questions_and_answers'
42
+ JOIN_TYPE = 'answer'.freeze
43
+
44
+ index_name 'questions_and_answers'.freeze
45
+ document_type 'doc'.freeze
46
+
47
+ before_create :randomize_id
48
+
49
+ def randomize_id
50
+ begin
51
+ self.id = SecureRandom.random_number(1_000_000)
52
+ end while Answer.where(id: self.id).exists?
53
+ end
33
54
 
34
- mapping _parent: { type: 'question' }, _routing: { required: true } do
55
+ mapping do
35
56
  indexes :text
36
57
  indexes :author
37
58
  end
38
59
 
39
- after_commit lambda { __elasticsearch__.index_document(parent: question_id) }, on: :create
40
- after_commit lambda { __elasticsearch__.update_document(parent: question_id) }, on: :update
41
- after_commit lambda { __elasticsearch__.delete_document(parent: question_id) }, on: :destroy
60
+ def as_indexed_json(options={})
61
+ # This line is necessary for differences between ActiveModel::Serializers::JSON#as_json versions
62
+ json = as_json(options)[JOIN_TYPE] || as_json(options)
63
+ json.merge(join_field: { name: JOIN_TYPE, parent: question_id })
64
+ end
65
+
66
+ after_commit lambda { __elasticsearch__.index_document(routing: (question_id || 1)) }, on: :create
67
+ after_commit lambda { __elasticsearch__.update_document(routing: (question_id || 1)) }, on: :update
68
+ after_commit lambda {__elasticsearch__.delete_document(routing: (question_id || 1)) }, on: :destroy
42
69
  end
43
70
 
44
71
  module ParentChildSearchable
45
- INDEX_NAME = 'questions_and_answers'
72
+ INDEX_NAME = 'questions_and_answers'.freeze
73
+ JOIN = 'join'.freeze
46
74
 
47
75
  def create_index!(options={})
48
76
  client = Question.__elasticsearch__.client
49
77
  client.indices.delete index: INDEX_NAME rescue nil if options[:force]
50
78
 
51
79
  settings = Question.settings.to_hash.merge Answer.settings.to_hash
52
- mappings = Question.mappings.to_hash.merge Answer.mappings.to_hash
80
+ mapping_properties = { join_field: { type: JOIN,
81
+ relations: { Question::JOIN_TYPE => Answer::JOIN_TYPE } } }
82
+
83
+ merged_properties = mapping_properties.merge(Question.mappings.to_hash[:doc][:properties]).merge(
84
+ Answer.mappings.to_hash[:doc][:properties])
85
+ mappings = { doc: { properties: merged_properties }}
53
86
 
54
87
  client.indices.create index: INDEX_NAME,
55
88
  body: {
56
- settings: settings.to_hash,
57
- mappings: mappings.to_hash }
89
+ settings: settings.to_hash,
90
+ mappings: mappings }
58
91
  end
59
92
 
60
93
  extend self
@@ -100,34 +133,34 @@ module Elasticsearch
100
133
 
101
134
  should "find questions by matching answers" do
102
135
  response = Question.search(
103
- { query: {
104
- has_child: {
105
- type: 'answer',
106
- query: {
107
- match: {
108
- author: 'john'
109
- }
110
- }
111
- }
112
- }
113
- })
136
+ { query: {
137
+ has_child: {
138
+ type: 'answer',
139
+ query: {
140
+ match: {
141
+ author: 'john'
142
+ }
143
+ }
144
+ }
145
+ }
146
+ })
114
147
 
115
148
  assert_equal 'Second Question', response.records.first.title
116
149
  end
117
150
 
118
151
  should "find answers for matching questions" do
119
152
  response = Answer.search(
120
- { query: {
121
- has_parent: {
122
- parent_type: 'question',
123
- query: {
124
- match: {
125
- author: 'john'
126
- }
127
- }
128
- }
129
- }
130
- })
153
+ { query: {
154
+ has_parent: {
155
+ parent_type: 'question',
156
+ query: {
157
+ match: {
158
+ author: 'john'
159
+ }
160
+ }
161
+ }
162
+ }
163
+ })
131
164
 
132
165
  assert_same_elements ['Adam', 'Ryan'], response.records.map(&:author)
133
166
  end
@@ -136,12 +169,20 @@ module Elasticsearch
136
169
  Question.where(title: 'First Question').each(&:destroy)
137
170
  Question.__elasticsearch__.refresh_index!
138
171
 
139
- response = Answer.search query: { match_all: {} }
172
+ response = Answer.search(
173
+ { query: {
174
+ has_parent: {
175
+ parent_type: 'question',
176
+ query: {
177
+ match_all: {}
178
+ }
179
+ }
180
+ }
181
+ })
140
182
 
141
183
  assert_equal 1, response.results.total
142
184
  end
143
185
  end
144
-
145
186
  end
146
187
  end
147
188
  end
@@ -16,6 +16,8 @@ module Elasticsearch
16
16
  include Elasticsearch::Model
17
17
  include Elasticsearch::Model::Callbacks
18
18
 
19
+ document_type 'article'
20
+
19
21
  settings index: { number_of_shards: 1, number_of_replicas: 0 } do
20
22
  mapping do
21
23
  indexes :title, type: 'text', analyzer: 'snowball'
@@ -221,8 +223,10 @@ module Elasticsearch
221
223
 
222
224
  if defined?(::ActiveRecord) && ::ActiveRecord::VERSION::MAJOR >= 4
223
225
  assert_equal 'Testing Coding', response.records.order(title: :desc).first.title
226
+ assert_equal 'Testing Coding', response.records.order(title: :desc)[0].title
224
227
  else
225
228
  assert_equal 'Testing Coding', response.records.order('title DESC').first.title
229
+ assert_equal 'Testing Coding', response.records.order('title DESC')[0].title
226
230
  end
227
231
  end
228
232
 
@@ -41,7 +41,7 @@ module Elasticsearch
41
41
 
42
42
  a = ArticleWithCustomSerialization.__elasticsearch__.client.get \
43
43
  index: 'article_with_custom_serializations',
44
- type: 'article_with_custom_serialization',
44
+ type: '_doc',
45
45
  id: '1'
46
46
 
47
47
  assert_equal( { 'title' => 'Test' }, a['_source'] )
@@ -55,7 +55,7 @@ module Elasticsearch
55
55
 
56
56
  a = ArticleWithCustomSerialization.__elasticsearch__.client.get \
57
57
  index: 'article_with_custom_serializations',
58
- type: 'article_with_custom_serialization',
58
+ type: '_doc',
59
59
  id: '1'
60
60
 
61
61
  assert_equal( { 'title' => 'UPDATED' }, a['_source'] )
@@ -10,21 +10,22 @@ module Elasticsearch
10
10
  module Model
11
11
  class ActiveRecordImportIntegrationTest < Elasticsearch::Test::IntegrationTestCase
12
12
 
13
- class ::ImportArticle < ActiveRecord::Base
14
- include Elasticsearch::Model
13
+ context "ActiveRecord importing" do
14
+ setup do
15
+ Object.send(:remove_const, :ImportArticle) if defined?(ImportArticle)
16
+ class ::ImportArticle < ActiveRecord::Base
17
+ include Elasticsearch::Model
15
18
 
16
- scope :popular, -> { where('views >= 50') }
19
+ scope :popular, -> { where('views >= 50') }
17
20
 
18
- mapping do
19
- indexes :title, type: 'text'
20
- indexes :views, type: 'integer'
21
- indexes :numeric, type: 'integer'
22
- indexes :created_at, type: 'date'
23
- end
24
- end
21
+ mapping do
22
+ indexes :title, type: 'text'
23
+ indexes :views, type: 'integer'
24
+ indexes :numeric, type: 'integer'
25
+ indexes :created_at, type: 'date'
26
+ end
27
+ end
25
28
 
26
- context "ActiveRecord importing" do
27
- setup do
28
29
  ActiveRecord::Schema.define(:version => 1) do
29
30
  create_table :import_articles do |t|
30
31
  t.string :title
@@ -110,6 +111,58 @@ module Elasticsearch
110
111
  end
111
112
  end
112
113
 
114
+ context "ActiveRecord importing when the model has a default scope" do
115
+
116
+ setup do
117
+ Object.send(:remove_const, :ImportArticle) if defined?(ImportArticle)
118
+ class ::ImportArticle < ActiveRecord::Base
119
+ include Elasticsearch::Model
120
+
121
+ default_scope { where('views >= 8') }
122
+
123
+ mapping do
124
+ indexes :title, type: 'text'
125
+ indexes :views, type: 'integer'
126
+ indexes :numeric, type: 'integer'
127
+ indexes :created_at, type: 'date'
128
+ end
129
+ end
130
+
131
+ ActiveRecord::Schema.define(:version => 1) do
132
+ create_table :import_articles do |t|
133
+ t.string :title
134
+ t.integer :views
135
+ t.string :numeric # For the sake of invalid data sent to Elasticsearch
136
+ t.datetime :created_at, :default => 'NOW()'
137
+ end
138
+ end
139
+
140
+ ImportArticle.delete_all
141
+ ImportArticle.__elasticsearch__.delete_index! force: true
142
+ ImportArticle.__elasticsearch__.create_index! force: true
143
+ ImportArticle.__elasticsearch__.client.cluster.health wait_for_status: 'yellow'
144
+
145
+ 10.times { |i| ImportArticle.create! title: "Test #{i}", views: i }
146
+ end
147
+
148
+ should "import only documents from the default scope" do
149
+ assert_equal 2, ImportArticle.count
150
+
151
+ assert_equal 0, ImportArticle.import
152
+
153
+ ImportArticle.__elasticsearch__.refresh_index!
154
+ assert_equal 2, ImportArticle.search('*').results.total
155
+ end
156
+
157
+ should "import only documents from a specific query combined with the default scope" do
158
+ assert_equal 2, ImportArticle.count
159
+
160
+ assert_equal 0, ImportArticle.import(query: -> { where("title = 'Test 9'") })
161
+
162
+ ImportArticle.__elasticsearch__.refresh_index!
163
+ assert_equal 1, ImportArticle.search('*').results.total
164
+ end
165
+ end
113
166
  end
114
167
  end
115
168
  end
@@ -22,6 +22,8 @@ module Elasticsearch
22
22
  include Elasticsearch::Model
23
23
  include Elasticsearch::Model::Callbacks
24
24
 
25
+ document_type 'article'
26
+
25
27
  mapping { indexes :title }
26
28
  end
27
29
  end
@@ -167,6 +167,70 @@ if MongoDB.available?
167
167
  assert response.results.any?, "Search has not returned results: #{response.to_a}"
168
168
  end
169
169
  end
170
+
171
+ context "importing when the model has a default scope" do
172
+ class ::MongoidArticleWithDefaultScope
173
+ include Mongoid::Document
174
+ include Elasticsearch::Model
175
+
176
+ default_scope -> { where(status: 'active') }
177
+
178
+ field :id, type: String
179
+ field :title, type: String
180
+ field :status, type: String, default: 'active'
181
+
182
+ attr_accessible :title if respond_to? :attr_accessible
183
+ attr_accessible :status if respond_to? :attr_accessible
184
+
185
+ settings index: { number_of_shards: 1, number_of_replicas: 0 } do
186
+ mapping do
187
+ indexes :title, type: 'text', analyzer: 'snowball'
188
+ indexes :status, type: 'text'
189
+ indexes :created_at, type: 'date'
190
+ end
191
+ end
192
+
193
+ def as_indexed_json(options={})
194
+ as_json(except: [:id, :_id])
195
+ end
196
+ end
197
+
198
+ setup do
199
+ Elasticsearch::Model::Adapter.register \
200
+ Elasticsearch::Model::Adapter::Mongoid,
201
+ lambda { |klass| !!defined?(::Mongoid::Document) && klass.respond_to?(:ancestors) && klass.ancestors.include?(::Mongoid::Document) }
202
+
203
+ MongoidArticleWithDefaultScope.__elasticsearch__.create_index! force: true
204
+
205
+ MongoidArticleWithDefaultScope.delete_all
206
+
207
+ MongoidArticleWithDefaultScope.create! title: 'Test'
208
+ MongoidArticleWithDefaultScope.create! title: 'Testing Coding'
209
+ MongoidArticleWithDefaultScope.create! title: 'Coding'
210
+ MongoidArticleWithDefaultScope.create! title: 'Test legacy code', status: 'removed'
211
+
212
+ MongoidArticleWithDefaultScope.__elasticsearch__.refresh_index!
213
+ MongoidArticleWithDefaultScope.__elasticsearch__.client.cluster.health wait_for_status: 'yellow'
214
+ end
215
+
216
+ should "import only documents from the default scope" do
217
+ assert_equal 3, MongoidArticleWithDefaultScope.count
218
+
219
+ assert_equal 0, MongoidArticleWithDefaultScope.import
220
+
221
+ MongoidArticleWithDefaultScope.__elasticsearch__.refresh_index!
222
+ assert_equal 3, MongoidArticleWithDefaultScope.search('*').results.total
223
+ end
224
+
225
+ should "import only documents from a specific query combined with the default scope" do
226
+ assert_equal 3, MongoidArticleWithDefaultScope.count
227
+
228
+ assert_equal 0, MongoidArticleWithDefaultScope.import(query: -> { where(title: /^Test/) })
229
+
230
+ MongoidArticleWithDefaultScope.__elasticsearch__.refresh_index!
231
+ assert_equal 2, MongoidArticleWithDefaultScope.search('*').results.total
232
+ end
233
+ end
170
234
  end
171
235
 
172
236
  end
data/test/test_helper.rb CHANGED
@@ -23,7 +23,7 @@ require 'shoulda-context'
23
23
  require 'turn' unless ENV["TM_FILEPATH"] || ENV["NOTURN"] || defined?(RUBY_VERSION) && RUBY_VERSION > '2.2'
24
24
 
25
25
  require 'ansi'
26
- require 'oj'
26
+ require 'oj' unless defined?(JRUBY_VERSION)
27
27
 
28
28
  require 'active_model'
29
29
 
@@ -98,7 +98,64 @@ class Elasticsearch::Model::AdapterMongoidTest < Test::Unit::TestCase
98
98
  assert_equal @transform.call(model), { index: { _id: "1", data: {} } }
99
99
  end
100
100
  end
101
- end
102
101
 
102
+ should "limit the relation to a specific scope" do
103
+ relation = mock()
104
+ relation.stubs(:no_timeout).returns(relation)
105
+ relation.expects(:published).returns(relation)
106
+ relation.expects(:each_slice).returns([])
107
+ DummyClassForMongoid.expects(:all).returns(relation)
108
+
109
+ DummyClassForMongoid.__send__ :extend, Elasticsearch::Model::Adapter::Mongoid::Importing
110
+ DummyClassForMongoid.__find_in_batches(scope: :published) do; end
111
+ end
112
+
113
+ context "when limit the relation with proc" do
114
+ setup do
115
+ @query = Proc.new { where(color: "red") }
116
+ end
117
+ should "query with a specific criteria" do
118
+ relation = mock()
119
+ relation.stubs(:no_timeout).returns(relation)
120
+ relation.expects(:class_exec).returns(relation)
121
+ relation.expects(:each_slice).returns([])
122
+ DummyClassForMongoid.expects(:all).returns(relation)
123
+
124
+ DummyClassForMongoid.__find_in_batches(query: @query) do; end
125
+ end
126
+ end
127
+
128
+ context "when limit the relation with hash" do
129
+ setup do
130
+ @query = { color: "red" }
131
+ end
132
+ should "query with a specific criteria" do
133
+ relation = mock()
134
+ relation.stubs(:no_timeout).returns(relation)
135
+ relation.expects(:where).with(@query).returns(relation)
136
+ relation.expects(:each_slice).returns([])
137
+ DummyClassForMongoid.expects(:all).returns(relation)
138
+
139
+ DummyClassForMongoid.__find_in_batches(query: @query) do; end
140
+ end
141
+ end
142
+
143
+ should "preprocess the batch if option provided" do
144
+ class << DummyClassForMongoid
145
+ # Updates/transforms the batch while fetching it from the database
146
+ # (eg. with information from an external system)
147
+ #
148
+ def update_batch(batch)
149
+ batch.collect { |b| b.to_s + '!' }
150
+ end
151
+ end
152
+
153
+ DummyClassForMongoid.expects(:__find_in_batches).returns( [:a, :b] )
154
+
155
+ DummyClassForMongoid.__find_in_batches(preprocess: :update_batch) do |batch|
156
+ assert_same_elements ["a!", "b!"], batch
157
+ end
158
+ end
159
+ end
103
160
  end
104
161
  end
@@ -146,6 +146,27 @@ class Elasticsearch::Model::ImportingTest < Test::Unit::TestCase
146
146
  end
147
147
  end
148
148
 
149
+ context "with the refresh option" do
150
+ should "refresh the index" do
151
+ DummyImportingModel.expects(:__find_in_batches).with do |options|
152
+ assert_equal 'bar', options[:foo]
153
+ assert_nil options[:refresh]
154
+ true
155
+ end
156
+
157
+ DummyImportingModel.expects(:refresh_index!).with do |options|
158
+ assert_equal 'foo', options[:index]
159
+ true
160
+ end
161
+
162
+ DummyImportingModel.expects(:index_name).returns('foo')
163
+ DummyImportingModel.expects(:document_type).returns('foo')
164
+ DummyImportingModel.stubs(:index_exists?).returns(true)
165
+
166
+ DummyImportingModel.import refresh: true, foo: 'bar'
167
+ end
168
+ end
169
+
149
170
  should "allow passing a different index / type" do
150
171
  Elasticsearch::Model::Adapter.expects(:from_class)
151
172
  .with(DummyImportingModel)
@@ -149,7 +149,7 @@ class Elasticsearch::Model::IndexingTest < Test::Unit::TestCase
149
149
  should "update and return the index mappings" do
150
150
  DummyIndexingModel.mappings foo: 'boo'
151
151
  DummyIndexingModel.mappings bar: 'bam'
152
- assert_equal( { dummy_indexing_model: { foo: "boo", bar: "bam", properties: {} } },
152
+ assert_equal( { _doc: { foo: "boo", bar: "bam", properties: {} } },
153
153
  DummyIndexingModel.mappings.to_hash )
154
154
  end
155
155
 
@@ -176,6 +176,19 @@ class Elasticsearch::Model::IndexingTest < Test::Unit::TestCase
176
176
  end
177
177
  end
178
178
 
179
+ class ::DummyIndexingModelWithNoChanges
180
+ extend Elasticsearch::Model::Indexing::ClassMethods
181
+ include Elasticsearch::Model::Indexing::InstanceMethods
182
+
183
+ def self.before_save(&block)
184
+ (@callbacks ||= {})[block.hash] = block
185
+ end
186
+
187
+ def changes_to_save
188
+ {}
189
+ end
190
+ end
191
+
179
192
  class ::DummyIndexingModelWithCallbacksAndCustomAsIndexedJson
180
193
  extend Elasticsearch::Model::Indexing::ClassMethods
181
194
  include Elasticsearch::Model::Indexing::InstanceMethods
@@ -393,6 +406,26 @@ class Elasticsearch::Model::IndexingTest < Test::Unit::TestCase
393
406
  instance.update_document
394
407
  end
395
408
 
409
+ should "index instead of update when nothing was changed" do
410
+ client = mock('client')
411
+ instance = ::DummyIndexingModelWithNoChanges.new
412
+
413
+ # Set the fake `changes` hash
414
+ instance.instance_variable_set(:@__changed_model_attributes, {})
415
+ # Overload as_indexed_json for running index
416
+ instance.expects(:as_indexed_json).returns({ 'foo' => 'BAR' })
417
+
418
+ client.expects(:index)
419
+ client.expects(:update).never
420
+
421
+ instance.expects(:client).returns(client)
422
+ instance.expects(:index_name).returns('foo')
423
+ instance.expects(:document_type).returns('bar')
424
+ instance.expects(:id).returns('1')
425
+
426
+ instance.update_document({})
427
+ end
428
+
396
429
  should "update only the specific attributes" do
397
430
  client = mock('client')
398
431
  instance = ::DummyIndexingModelWithCallbacks.new
@@ -544,7 +577,7 @@ class Elasticsearch::Model::IndexingTest < Test::Unit::TestCase
544
577
  indices.expects(:create).with do |payload|
545
578
  assert_equal 'dummy_indexing_model_for_recreates', payload[:index]
546
579
  assert_equal 1, payload[:body][:settings][:index][:number_of_shards]
547
- assert_equal 'keyword', payload[:body][:mappings][:dummy_indexing_model_for_recreate][:properties][:foo][:analyzer]
580
+ assert_equal 'keyword', payload[:body][:mappings][:_doc][:properties][:foo][:analyzer]
548
581
  true
549
582
  end.returns({})
550
583
 
@@ -69,8 +69,8 @@ class Elasticsearch::Model::NamingInheritanceTest < Test::Unit::TestCase
69
69
  end
70
70
 
71
71
  should "return the default document_type" do
72
- assert_equal "test_base", TestBase.document_type
73
- assert_equal "test_base", TestBase.new.document_type
72
+ assert_equal "_doc", TestBase.document_type
73
+ assert_equal "_doc", TestBase.new.document_type
74
74
  end
75
75
 
76
76
  should "return the explicit document_type" do
@@ -29,8 +29,8 @@ class Elasticsearch::Model::NamingTest < Test::Unit::TestCase
29
29
  end
30
30
 
31
31
  should "return the default document_type" do
32
- assert_equal 'dummy_naming_model', DummyNamingModel.document_type
33
- assert_equal 'dummy_naming_model', DummyNamingModel.new.document_type
32
+ assert_equal '_doc', DummyNamingModel.document_type
33
+ assert_equal '_doc', DummyNamingModel.new.document_type
34
34
  end
35
35
 
36
36
  should "set and return the index_name" do
@@ -27,5 +27,8 @@ class Elasticsearch::Model::ResultsTest < Test::Unit::TestCase
27
27
  assert_equal 'bar', @results.first.foo
28
28
  end
29
29
 
30
+ should "provide access to the raw response" do
31
+ assert_equal RESPONSE, @response.raw_response
32
+ end
30
33
  end
31
34
  end
metadata CHANGED
@@ -1,29 +1,29 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: elasticsearch-model
3
3
  version: !ruby/object:Gem::Version
4
- version: 5.1.0
4
+ version: 6.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Karel Minarik
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2018-07-04 00:00:00.000000000 Z
11
+ date: 2018-09-06 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: elasticsearch
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
- - - "~>"
17
+ - - ">"
18
18
  - !ruby/object:Gem::Version
19
- version: '5'
19
+ version: '1'
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
- - - "~>"
24
+ - - ">"
25
25
  - !ruby/object:Gem::Version
26
- version: '5'
26
+ version: '1'
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: activesupport
29
29
  requirement: !ruby/object:Gem::Requirement
@@ -336,6 +336,7 @@ files:
336
336
  - elasticsearch-model.gemspec
337
337
  - examples/activerecord_article.rb
338
338
  - examples/activerecord_associations.rb
339
+ - examples/activerecord_custom_analyzer.rb
339
340
  - examples/activerecord_mapping_completion.rb
340
341
  - examples/activerecord_mapping_edge_ngram.rb
341
342
  - examples/couchbase_article.rb
@@ -432,7 +433,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
432
433
  version: '0'
433
434
  requirements: []
434
435
  rubyforge_project:
435
- rubygems_version: 2.7.6
436
+ rubygems_version: 2.7.7
436
437
  signing_key:
437
438
  specification_version: 4
438
439
  summary: ActiveModel/Record integrations for Elasticsearch.