elasticsearch-model 0.0.1 → 0.1.0.rc1

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 (64) hide show
  1. data/.gitignore +3 -0
  2. data/LICENSE.txt +1 -1
  3. data/README.md +669 -8
  4. data/Rakefile +52 -0
  5. data/elasticsearch-model.gemspec +48 -17
  6. data/examples/activerecord_article.rb +77 -0
  7. data/examples/activerecord_associations.rb +153 -0
  8. data/examples/couchbase_article.rb +66 -0
  9. data/examples/datamapper_article.rb +71 -0
  10. data/examples/mongoid_article.rb +68 -0
  11. data/examples/ohm_article.rb +70 -0
  12. data/examples/riak_article.rb +52 -0
  13. data/gemfiles/3.gemfile +11 -0
  14. data/gemfiles/4.gemfile +11 -0
  15. data/lib/elasticsearch/model.rb +151 -1
  16. data/lib/elasticsearch/model/adapter.rb +145 -0
  17. data/lib/elasticsearch/model/adapters/active_record.rb +97 -0
  18. data/lib/elasticsearch/model/adapters/default.rb +44 -0
  19. data/lib/elasticsearch/model/adapters/mongoid.rb +90 -0
  20. data/lib/elasticsearch/model/callbacks.rb +35 -0
  21. data/lib/elasticsearch/model/client.rb +61 -0
  22. data/lib/elasticsearch/model/importing.rb +94 -0
  23. data/lib/elasticsearch/model/indexing.rb +332 -0
  24. data/lib/elasticsearch/model/naming.rb +101 -0
  25. data/lib/elasticsearch/model/proxy.rb +127 -0
  26. data/lib/elasticsearch/model/response.rb +70 -0
  27. data/lib/elasticsearch/model/response/base.rb +44 -0
  28. data/lib/elasticsearch/model/response/pagination.rb +96 -0
  29. data/lib/elasticsearch/model/response/records.rb +71 -0
  30. data/lib/elasticsearch/model/response/result.rb +50 -0
  31. data/lib/elasticsearch/model/response/results.rb +32 -0
  32. data/lib/elasticsearch/model/searching.rb +107 -0
  33. data/lib/elasticsearch/model/serializing.rb +35 -0
  34. data/lib/elasticsearch/model/support/forwardable.rb +44 -0
  35. data/lib/elasticsearch/model/version.rb +1 -1
  36. data/test/integration/active_record_associations_parent_child.rb +138 -0
  37. data/test/integration/active_record_associations_test.rb +306 -0
  38. data/test/integration/active_record_basic_test.rb +139 -0
  39. data/test/integration/active_record_import_test.rb +74 -0
  40. data/test/integration/active_record_namespaced_model_test.rb +49 -0
  41. data/test/integration/active_record_pagination_test.rb +109 -0
  42. data/test/integration/mongoid_basic_test.rb +178 -0
  43. data/test/test_helper.rb +57 -0
  44. data/test/unit/adapter_active_record_test.rb +93 -0
  45. data/test/unit/adapter_default_test.rb +31 -0
  46. data/test/unit/adapter_mongoid_test.rb +87 -0
  47. data/test/unit/adapter_test.rb +69 -0
  48. data/test/unit/callbacks_test.rb +30 -0
  49. data/test/unit/client_test.rb +27 -0
  50. data/test/unit/importing_test.rb +97 -0
  51. data/test/unit/indexing_test.rb +364 -0
  52. data/test/unit/module_test.rb +46 -0
  53. data/test/unit/naming_test.rb +76 -0
  54. data/test/unit/proxy_test.rb +88 -0
  55. data/test/unit/response_base_test.rb +40 -0
  56. data/test/unit/response_pagination_test.rb +159 -0
  57. data/test/unit/response_records_test.rb +87 -0
  58. data/test/unit/response_result_test.rb +52 -0
  59. data/test/unit/response_results_test.rb +31 -0
  60. data/test/unit/response_test.rb +57 -0
  61. data/test/unit/searching_search_request_test.rb +73 -0
  62. data/test/unit/searching_test.rb +39 -0
  63. data/test/unit/serializing_test.rb +17 -0
  64. metadata +418 -11
@@ -0,0 +1,50 @@
1
+ module Elasticsearch
2
+ module Model
3
+ module Response
4
+
5
+ # Encapsulates the "hit" returned from the Elasticsearch client
6
+ #
7
+ # Wraps the raw Hash with in a `Hashie::Mash` instance, providing
8
+ # access to the Hash properties by calling Ruby methods.
9
+ #
10
+ # @see https://github.com/intridea/hashie
11
+ #
12
+ class Result
13
+
14
+ # @param attributes [Hash] A Hash with document properties
15
+ #
16
+ def initialize(attributes={})
17
+ @result = Hashie::Mash.new(attributes)
18
+ end
19
+
20
+ # Delegate methods to `@result` or `@result._source`
21
+ #
22
+ def method_missing(method_name, *arguments)
23
+ case
24
+ when @result.respond_to?(method_name.to_sym)
25
+ @result.__send__ method_name.to_sym, *arguments
26
+ when @result._source && @result._source.respond_to?(method_name.to_sym)
27
+ @result._source.__send__ method_name.to_sym, *arguments
28
+ else
29
+ super
30
+ end
31
+ end
32
+
33
+ # Respond to methods from `@result` or `@result._source`
34
+ #
35
+ def respond_to?(method_name, include_private = false)
36
+ @result.respond_to?(method_name.to_sym) || \
37
+ @result._source && @result._source.respond_to?(method_name.to_sym) || \
38
+ super
39
+ end
40
+
41
+ def as_json(options={})
42
+ @result.as_json(options)
43
+ end
44
+
45
+ # TODO: #to_s, #inspect, with support for Pry
46
+
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,32 @@
1
+ module Elasticsearch
2
+ module Model
3
+ module Response
4
+
5
+ # Encapsulates the collection of documents returned from Elasticsearch
6
+ #
7
+ # Implements Enumerable and forwards its methods to the {#results} object.
8
+ #
9
+ class Results
10
+ include Base
11
+ include Enumerable
12
+
13
+ extend Support::Forwardable
14
+ forward :results, :each, :empty?, :size, :slice, :[], :to_a, :to_ary
15
+
16
+ # @see Base#initialize
17
+ #
18
+ def initialize(klass, response, options={})
19
+ super
20
+ end
21
+
22
+ # Returns the {Results} collection
23
+ #
24
+ def results
25
+ # TODO: Configurable custom wrapper
26
+ @results = response.response['hits']['hits'].map { |hit| Result.new(hit) }
27
+ end
28
+
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,107 @@
1
+ module Elasticsearch
2
+ module Model
3
+
4
+ # Contains functionality related to searching.
5
+ #
6
+ module Searching
7
+
8
+ # Wraps a search request definition
9
+ #
10
+ class SearchRequest
11
+ attr_reader :klass, :definition
12
+
13
+ # @param klass [Class] The class of the model
14
+ # @param query_or_payload [String,Hash,Object] The search request definition
15
+ # (string, JSON, Hash, or object responding to `to_hash`)
16
+ # @param options [Hash] Optional parameters to be passed to the Elasticsearch client
17
+ #
18
+ def initialize(klass, query_or_payload, options={})
19
+ @klass = klass
20
+
21
+ __index_name = options[:index] || klass.index_name
22
+ __document_type = options[:type] || klass.document_type
23
+
24
+ case
25
+ # search query: ...
26
+ when query_or_payload.respond_to?(:to_hash)
27
+ body = query_or_payload.to_hash
28
+
29
+ # search '{ "query" : ... }'
30
+ when query_or_payload.is_a?(String) && query_or_payload =~ /^\s*{/
31
+ body = query_or_payload
32
+
33
+ # search '...'
34
+ else
35
+ q = query_or_payload
36
+ end
37
+
38
+ if body
39
+ @definition = { index: __index_name, type: __document_type, body: body }.update options
40
+ else
41
+ @definition = { index: __index_name, type: __document_type, q: q }.update options
42
+ end
43
+ end
44
+
45
+ # Performs the request and returns the response from client
46
+ #
47
+ # @return [Hash] The response from Elasticsearch
48
+ #
49
+ def execute!
50
+ klass.client.search(@definition)
51
+ end
52
+ end
53
+
54
+ module ClassMethods
55
+
56
+ # Provides a `search` method for the model to easily search within an index/type
57
+ # corresponding to the model settings.
58
+ #
59
+ # @param query_or_payload [String,Hash,Object] The search request definition
60
+ # (string, JSON, Hash, or object responding to `to_hash`)
61
+ # @param options [Hash] Optional parameters to be passed to the Elasticsearch client
62
+ #
63
+ # @return [Elasticsearch::Model::Response::Response]
64
+ #
65
+ # @example Simple search in `Article`
66
+ #
67
+ # Article.search 'foo'
68
+ #
69
+ # @example Search using a search definition as a Hash
70
+ #
71
+ # response = Article.search \
72
+ # query: {
73
+ # match: {
74
+ # title: 'foo'
75
+ # }
76
+ # },
77
+ # highlight: {
78
+ # fields: {
79
+ # title: {}
80
+ # }
81
+ # }
82
+ #
83
+ # response.results.first.title
84
+ # # => "Foo"
85
+ #
86
+ # response.results.first.highlight.title
87
+ # # => ["<em>Foo</em>"]
88
+ #
89
+ # response.records.first.title
90
+ # # Article Load (0.2ms) SELECT "articles".* FROM "articles" WHERE "articles"."id" IN (1, 3)
91
+ # # => "Foo"
92
+ #
93
+ # @example Search using a search definition as a JSON string
94
+ #
95
+ # Article.search '{"query" : { "match_all" : {} }}'
96
+ #
97
+ def search(query_or_payload, options={})
98
+ search = SearchRequest.new(self, query_or_payload, options={})
99
+
100
+ Response::Response.new(self, search)
101
+ end
102
+
103
+ end
104
+
105
+ end
106
+ end
107
+ end
@@ -0,0 +1,35 @@
1
+ module Elasticsearch
2
+ module Model
3
+
4
+ # Contains functionality for serializing model instances for the client
5
+ #
6
+ module Serializing
7
+
8
+ module ClassMethods
9
+ end
10
+
11
+ module InstanceMethods
12
+
13
+ # Serialize the record as a Hash, to be passed to the client.
14
+ #
15
+ # Re-define this method to customize the serialization.
16
+ #
17
+ # @return [Hash]
18
+ #
19
+ # @example Return the model instance as a Hash
20
+ #
21
+ # Article.first.__elasticsearch__.as_indexed_json
22
+ # => {"title"=>"Foo"}
23
+ #
24
+ # @see Elasticsearch::Model::Indexing
25
+ #
26
+ def as_indexed_json(options={})
27
+ # TODO: Play with the `MyModel.indexes` method -- reject non-mapped attributes, `:as` options, etc
28
+ self.as_json(options.merge root: false)
29
+ end
30
+
31
+ end
32
+
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,44 @@
1
+ module Elasticsearch
2
+ module Model
3
+ module Support
4
+
5
+ # Lightweight wrapper around "forwardable.rb" interface,
6
+ # to allow easy implementation changes in the future.
7
+ #
8
+ # Cf. https://github.com/mongoid/origin/blob/master/lib/origin/forwardable.rb
9
+ #
10
+ module Forwardable
11
+ def self.extended(base)
12
+ base.__send__ :extend, ::Forwardable
13
+ base.__send__ :extend, ::SingleForwardable
14
+ end
15
+
16
+ # Forwards specific method(s) to the provided receiver
17
+ #
18
+ # @example Forward the `each` method to `results` object
19
+ #
20
+ # MyClass.forward(:results, :each)
21
+ #
22
+ # @example Forward the `include?` method to `ancestors` class method
23
+ #
24
+ # MyClass.forward(:'self.ancestors', :include?)
25
+ #
26
+ # @param [ Symbol ] receiver The name of the receiver method
27
+ # @param [ Symbol, Array ] methods The forwarded methods
28
+ #
29
+ # @api private
30
+ #
31
+ def forward(receiver, *methods)
32
+ methods = Array(methods).flatten
33
+ target = self.__send__ :eval, receiver.to_s rescue nil
34
+
35
+ if target
36
+ single_delegate methods => receiver
37
+ else
38
+ instance_delegate methods => receiver
39
+ end
40
+ end; module_function :forward
41
+ end
42
+ end
43
+ end
44
+ end
@@ -1,5 +1,5 @@
1
1
  module Elasticsearch
2
2
  module Model
3
- VERSION = "0.0.1"
3
+ VERSION = "0.1.0.rc1"
4
4
  end
5
5
  end
@@ -0,0 +1,138 @@
1
+ require 'test_helper'
2
+
3
+ class Question < ActiveRecord::Base
4
+ include Elasticsearch::Model
5
+
6
+ has_many :answers, dependent: :destroy
7
+
8
+ index_name 'questions_and_answers'
9
+
10
+ mapping do
11
+ indexes :title
12
+ indexes :text
13
+ indexes :author
14
+ end
15
+
16
+ after_commit lambda { __elasticsearch__.index_document }, on: :create
17
+ after_commit lambda { __elasticsearch__.update_document }, on: :update
18
+ after_commit lambda { __elasticsearch__.delete_document }, on: :destroy
19
+ end
20
+
21
+ class Answer < ActiveRecord::Base
22
+ include Elasticsearch::Model
23
+
24
+ belongs_to :question
25
+
26
+ index_name 'questions_and_answers'
27
+
28
+ mapping _parent: { type: 'question', required: true } do
29
+ indexes :text
30
+ indexes :author
31
+ end
32
+
33
+ after_commit lambda { __elasticsearch__.index_document(parent: question_id) }, on: :create
34
+ after_commit lambda { __elasticsearch__.update_document(parent: question_id) }, on: :update
35
+ after_commit lambda { __elasticsearch__.delete_document(parent: question_id) }, on: :destroy
36
+ end
37
+
38
+ module ParentChildSearchable
39
+ INDEX_NAME = 'questions_and_answers'
40
+
41
+ def create_index!(options={})
42
+ client = Question.__elasticsearch__.client
43
+ client.indices.delete index: INDEX_NAME rescue nil if options[:force]
44
+
45
+ settings = Question.settings.to_hash.merge Answer.settings.to_hash
46
+ mappings = Question.mappings.to_hash.merge Answer.mappings.to_hash
47
+
48
+ client.indices.create index: INDEX_NAME,
49
+ body: {
50
+ settings: settings.to_hash,
51
+ mappings: mappings.to_hash }
52
+ end
53
+
54
+ extend self
55
+ end
56
+
57
+ module Elasticsearch
58
+ module Model
59
+ class ActiveRecordAssociationsParentChildIntegrationTest < Elasticsearch::Test::IntegrationTestCase
60
+
61
+ context "ActiveRecord associations with parent/child modelling" do
62
+ setup do
63
+ ActiveRecord::Schema.define(version: 1) do
64
+ create_table :questions do |t|
65
+ t.string :title
66
+ t.text :text
67
+ t.string :author
68
+ t.timestamps
69
+ end
70
+ create_table :answers do |t|
71
+ t.text :text
72
+ t.string :author
73
+ t.references :question
74
+ t.timestamps
75
+ end and add_index(:answers, :question_id)
76
+ end
77
+
78
+ Question.delete_all
79
+ ParentChildSearchable.create_index! force: true
80
+
81
+ q_1 = Question.create! title: 'First Question', author: 'John'
82
+ q_2 = Question.create! title: 'Second Question', author: 'Jody'
83
+
84
+ q_1.answers.create! text: 'Lorem Ipsum', author: 'Adam'
85
+ q_1.answers.create! text: 'Dolor Sit', author: 'Ryan'
86
+
87
+ q_2.answers.create! text: 'Amet Et', author: 'John'
88
+
89
+ Question.__elasticsearch__.refresh_index!
90
+ end
91
+
92
+ should "find questions by matching answers" do
93
+ response = Question.search(
94
+ { query: {
95
+ has_child: {
96
+ type: 'answer',
97
+ query: {
98
+ match: {
99
+ author: 'john'
100
+ }
101
+ }
102
+ }
103
+ }
104
+ })
105
+
106
+ assert_equal 'Second Question', response.records.first.title
107
+ end
108
+
109
+ should "find answers for matching questions" do
110
+ response = Answer.search(
111
+ { query: {
112
+ has_parent: {
113
+ parent_type: 'question',
114
+ query: {
115
+ match: {
116
+ author: 'john'
117
+ }
118
+ }
119
+ }
120
+ }
121
+ })
122
+
123
+ assert_same_elements ['Adam', 'Ryan'], response.records.map(&:author)
124
+ end
125
+
126
+ should "delete answers when the question is deleted" do
127
+ Question.where(title: 'First Question').each(&:destroy)
128
+ Question.__elasticsearch__.refresh_index!
129
+
130
+ response = Answer.search query: { match_all: {} }
131
+
132
+ assert_equal 1, response.results.total
133
+ end
134
+ end
135
+
136
+ end
137
+ end
138
+ end
@@ -0,0 +1,306 @@
1
+ require 'test_helper'
2
+
3
+ # ----- Models definition -------------------------------------------------------------------------
4
+
5
+ class Category < ActiveRecord::Base
6
+ has_and_belongs_to_many :posts
7
+ end
8
+
9
+ class Author < ActiveRecord::Base
10
+ has_many :authorships
11
+
12
+ def full_name
13
+ [first_name, last_name].compact.join(' ')
14
+ end
15
+ end
16
+
17
+ class Authorship < ActiveRecord::Base
18
+ belongs_to :author
19
+ belongs_to :post, touch: true
20
+ end
21
+
22
+ class Comment < ActiveRecord::Base
23
+ belongs_to :post, touch: true
24
+ end
25
+
26
+ class Post < ActiveRecord::Base
27
+ has_and_belongs_to_many :categories, after_add: [ lambda { |a,c| a.__elasticsearch__.index_document } ],
28
+ after_remove: [ lambda { |a,c| a.__elasticsearch__.index_document } ]
29
+ has_many :authorships
30
+ has_many :authors, through: :authorships
31
+ has_many :comments
32
+ end
33
+
34
+ # ----- Search integration via Concern module -----------------------------------------------------
35
+
36
+ module Searchable
37
+ extend ActiveSupport::Concern
38
+
39
+ included do
40
+ include Elasticsearch::Model
41
+ include Elasticsearch::Model::Callbacks
42
+
43
+ # Set up the mapping
44
+ #
45
+ settings index: { number_of_shards: 1, number_of_replicas: 0 } do
46
+ mapping do
47
+ indexes :title, analyzer: 'snowball'
48
+ indexes :created_at, type: 'date'
49
+
50
+ indexes :authors do
51
+ indexes :first_name
52
+ indexes :last_name
53
+ indexes :full_name, type: 'multi_field' do
54
+ indexes :full_name
55
+ indexes :raw, analyzer: 'keyword'
56
+ end
57
+ end
58
+
59
+ indexes :categories, analyzer: 'keyword'
60
+
61
+ indexes :comments, type: 'nested' do
62
+ indexes :text
63
+ indexes :author
64
+ end
65
+ end
66
+ end
67
+
68
+ # Customize the JSON serialization for Elasticsearch
69
+ #
70
+ def as_indexed_json(options={})
71
+ {
72
+ title: title,
73
+ text: text,
74
+ categories: categories.map(&:title),
75
+ authors: authors.as_json(methods: [:full_name], only: [:full_name, :first_name, :last_name]),
76
+ comments: comments.as_json(only: [:text, :author])
77
+ }
78
+ end
79
+
80
+ # Update document in the index after touch
81
+ #
82
+ after_touch() { __elasticsearch__.index_document }
83
+ end
84
+ end
85
+
86
+ # Include the search integration
87
+ #
88
+ Post.__send__ :include, Searchable
89
+
90
+ module Elasticsearch
91
+ module Model
92
+ class ActiveRecordAssociationsIntegrationTest < Elasticsearch::Test::IntegrationTestCase
93
+
94
+ context "ActiveRecord associations" do
95
+ setup do
96
+
97
+ # ----- Schema definition ---------------------------------------------------------------
98
+
99
+ ActiveRecord::Schema.define(version: 1) do
100
+ create_table :categories do |t|
101
+ t.string :title
102
+ t.timestamps
103
+ end
104
+
105
+ create_table :categories_posts, id: false do |t|
106
+ t.references :post, :category
107
+ end
108
+
109
+ create_table :authors do |t|
110
+ t.string :first_name, :last_name
111
+ t.timestamps
112
+ end
113
+
114
+ create_table :authorships do |t|
115
+ t.string :first_name, :last_name
116
+ t.references :post
117
+ t.references :author
118
+ t.timestamps
119
+ end
120
+
121
+ create_table :comments do |t|
122
+ t.string :text
123
+ t.string :author
124
+ t.references :post
125
+ t.timestamps
126
+ end and add_index(:comments, :post_id)
127
+
128
+ create_table :posts do |t|
129
+ t.string :title
130
+ t.text :text
131
+ t.boolean :published
132
+ t.timestamps
133
+ end
134
+ end
135
+
136
+ # ----- Reset the index -----------------------------------------------------------------
137
+
138
+ Post.delete_all
139
+ Post.__elasticsearch__.create_index! force: true
140
+ end
141
+
142
+ should "index and find a document" do
143
+ Post.create! title: 'Test'
144
+ Post.create! title: 'Testing Coding'
145
+ Post.create! title: 'Coding'
146
+ Post.__elasticsearch__.refresh_index!
147
+
148
+ response = Post.search('title:test')
149
+
150
+ assert_equal 2, response.results.size
151
+ assert_equal 2, response.records.size
152
+
153
+ assert_equal 'Test', response.results.first.title
154
+ assert_equal 'Test', response.records.first.title
155
+ end
156
+
157
+ should "reindex a document after categories are changed" do
158
+ # Create categories
159
+ category_a = Category.where(title: "One").first_or_create!
160
+ category_b = Category.where(title: "Two").first_or_create!
161
+
162
+ # Create post
163
+ post = Post.create! title: "First Post", text: "This is the first post..."
164
+
165
+ # Assign categories
166
+ post.categories = [category_a, category_b]
167
+
168
+ Post.__elasticsearch__.refresh_index!
169
+
170
+ query = { query: {
171
+ filtered: {
172
+ query: {
173
+ multi_match: {
174
+ fields: ['title'],
175
+ query: 'first'
176
+ }
177
+ },
178
+ filter: {
179
+ terms: {
180
+ categories: ['One']
181
+ }
182
+ }
183
+ }
184
+ }
185
+ }
186
+
187
+ response = Post.search query
188
+
189
+ assert_equal 1, response.results.size
190
+ assert_equal 1, response.records.size
191
+
192
+ # Remove category "One"
193
+ post.categories = [category_b]
194
+
195
+ Post.__elasticsearch__.refresh_index!
196
+ response = Post.search query
197
+
198
+ assert_equal 0, response.results.size
199
+ assert_equal 0, response.records.size
200
+ end
201
+
202
+ should "reindex a document after authors are changed" do
203
+ # Create authors
204
+ author_a = Author.where(first_name: "John", last_name: "Smith").first_or_create!
205
+ author_b = Author.where(first_name: "Mary", last_name: "Smith").first_or_create!
206
+ author_c = Author.where(first_name: "Kobe", last_name: "Griss").first_or_create!
207
+
208
+ # Create posts
209
+ post_1 = Post.create! title: "First Post", text: "This is the first post..."
210
+ post_2 = Post.create! title: "Second Post", text: "This is the second post..."
211
+ post_3 = Post.create! title: "Third Post", text: "This is the third post..."
212
+
213
+ # Assign authors
214
+ post_1.authors = [author_a, author_b]
215
+ post_2.authors = [author_a]
216
+ post_3.authors = [author_c]
217
+
218
+ Post.__elasticsearch__.refresh_index!
219
+
220
+ response = Post.search 'authors.full_name:john'
221
+
222
+ assert_equal 2, response.results.size
223
+ assert_equal 2, response.records.size
224
+
225
+ post_3.authors << author_a
226
+
227
+ Post.__elasticsearch__.refresh_index!
228
+
229
+ response = Post.search 'authors.full_name:john'
230
+
231
+ assert_equal 3, response.results.size
232
+ assert_equal 3, response.records.size
233
+ end if defined?(::ActiveRecord) && ::ActiveRecord::VERSION::MAJOR >= 4
234
+
235
+ should "reindex a document after comments are added" do
236
+ # Create posts
237
+ post_1 = Post.create! title: "First Post", text: "This is the first post..."
238
+ post_2 = Post.create! title: "Second Post", text: "This is the second post..."
239
+
240
+ # Add comments
241
+ post_1.comments.create! author: 'John', text: 'Excellent'
242
+ post_1.comments.create! author: 'Abby', text: 'Good'
243
+
244
+ post_2.comments.create! author: 'John', text: 'Terrible'
245
+
246
+ Post.__elasticsearch__.refresh_index!
247
+
248
+ response = Post.search 'comments.author:john AND comments.text:good'
249
+ assert_equal 0, response.results.size
250
+
251
+ # Add comment
252
+ post_1.comments.create! author: 'John', text: 'Or rather just good...'
253
+
254
+ Post.__elasticsearch__.refresh_index!
255
+
256
+ response = Post.search 'comments.author:john AND comments.text:good'
257
+ assert_equal 0, response.results.size
258
+
259
+ response = Post.search \
260
+ query: {
261
+ nested: {
262
+ path: 'comments',
263
+ query: {
264
+ bool: {
265
+ must: [
266
+ { match: { 'comments.author' => 'john' } },
267
+ { match: { 'comments.text' => 'good' } }
268
+ ]
269
+ }
270
+ }
271
+ }
272
+ }
273
+
274
+ assert_equal 1, response.results.size
275
+ end if defined?(::ActiveRecord) && ::ActiveRecord::VERSION::MAJOR >= 4
276
+
277
+ should "reindex a document after Post#touch" do
278
+ # Create categories
279
+ category_a = Category.where(title: "One").first_or_create!
280
+
281
+ # Create post
282
+ post = Post.create! title: "First Post", text: "This is the first post..."
283
+
284
+ # Assign category
285
+ post.categories << category_a
286
+
287
+ Post.__elasticsearch__.refresh_index!
288
+
289
+ assert_equal 1, Post.search('categories:One').size
290
+
291
+ # Update category
292
+ category_a.update_attribute :title, "Updated"
293
+
294
+ # Trigger touch on posts in category
295
+ category_a.posts.each { |p| p.touch }
296
+
297
+ Post.__elasticsearch__.refresh_index!
298
+
299
+ assert_equal 0, Post.search('categories:One').size
300
+ assert_equal 1, Post.search('categories:Updated').size
301
+ end
302
+ end
303
+
304
+ end
305
+ end
306
+ end