elasticsearch-model-queryable 0.1.5

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 (70) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +20 -0
  3. data/CHANGELOG.md +26 -0
  4. data/Gemfile +4 -0
  5. data/LICENSE.txt +13 -0
  6. data/README.md +695 -0
  7. data/Rakefile +59 -0
  8. data/elasticsearch-model.gemspec +57 -0
  9. data/examples/activerecord_article.rb +77 -0
  10. data/examples/activerecord_associations.rb +162 -0
  11. data/examples/couchbase_article.rb +66 -0
  12. data/examples/datamapper_article.rb +71 -0
  13. data/examples/mongoid_article.rb +68 -0
  14. data/examples/ohm_article.rb +70 -0
  15. data/examples/riak_article.rb +52 -0
  16. data/gemfiles/3.0.gemfile +12 -0
  17. data/gemfiles/4.0.gemfile +11 -0
  18. data/lib/elasticsearch/model/adapter.rb +145 -0
  19. data/lib/elasticsearch/model/adapters/active_record.rb +104 -0
  20. data/lib/elasticsearch/model/adapters/default.rb +50 -0
  21. data/lib/elasticsearch/model/adapters/mongoid.rb +92 -0
  22. data/lib/elasticsearch/model/callbacks.rb +35 -0
  23. data/lib/elasticsearch/model/client.rb +61 -0
  24. data/lib/elasticsearch/model/ext/active_record.rb +14 -0
  25. data/lib/elasticsearch/model/hash_wrapper.rb +15 -0
  26. data/lib/elasticsearch/model/importing.rb +144 -0
  27. data/lib/elasticsearch/model/indexing.rb +472 -0
  28. data/lib/elasticsearch/model/naming.rb +101 -0
  29. data/lib/elasticsearch/model/proxy.rb +127 -0
  30. data/lib/elasticsearch/model/response/base.rb +44 -0
  31. data/lib/elasticsearch/model/response/pagination.rb +173 -0
  32. data/lib/elasticsearch/model/response/records.rb +69 -0
  33. data/lib/elasticsearch/model/response/result.rb +63 -0
  34. data/lib/elasticsearch/model/response/results.rb +31 -0
  35. data/lib/elasticsearch/model/response.rb +71 -0
  36. data/lib/elasticsearch/model/searching.rb +107 -0
  37. data/lib/elasticsearch/model/serializing.rb +35 -0
  38. data/lib/elasticsearch/model/version.rb +5 -0
  39. data/lib/elasticsearch/model.rb +157 -0
  40. data/test/integration/active_record_associations_parent_child.rb +139 -0
  41. data/test/integration/active_record_associations_test.rb +307 -0
  42. data/test/integration/active_record_basic_test.rb +179 -0
  43. data/test/integration/active_record_custom_serialization_test.rb +62 -0
  44. data/test/integration/active_record_import_test.rb +100 -0
  45. data/test/integration/active_record_namespaced_model_test.rb +49 -0
  46. data/test/integration/active_record_pagination_test.rb +132 -0
  47. data/test/integration/mongoid_basic_test.rb +193 -0
  48. data/test/test_helper.rb +63 -0
  49. data/test/unit/adapter_active_record_test.rb +140 -0
  50. data/test/unit/adapter_default_test.rb +41 -0
  51. data/test/unit/adapter_mongoid_test.rb +102 -0
  52. data/test/unit/adapter_test.rb +69 -0
  53. data/test/unit/callbacks_test.rb +31 -0
  54. data/test/unit/client_test.rb +27 -0
  55. data/test/unit/importing_test.rb +176 -0
  56. data/test/unit/indexing_test.rb +478 -0
  57. data/test/unit/module_test.rb +57 -0
  58. data/test/unit/naming_test.rb +76 -0
  59. data/test/unit/proxy_test.rb +89 -0
  60. data/test/unit/response_base_test.rb +40 -0
  61. data/test/unit/response_pagination_kaminari_test.rb +189 -0
  62. data/test/unit/response_pagination_will_paginate_test.rb +208 -0
  63. data/test/unit/response_records_test.rb +91 -0
  64. data/test/unit/response_result_test.rb +90 -0
  65. data/test/unit/response_results_test.rb +31 -0
  66. data/test/unit/response_test.rb +67 -0
  67. data/test/unit/searching_search_request_test.rb +78 -0
  68. data/test/unit/searching_test.rb +41 -0
  69. data/test/unit/serializing_test.rb +17 -0
  70. metadata +466 -0
@@ -0,0 +1,157 @@
1
+ require 'elasticsearch'
2
+
3
+ require 'hashie'
4
+
5
+ require 'active_support/core_ext/module/delegation'
6
+
7
+ require 'elasticsearch/model/version'
8
+
9
+ require 'elasticsearch/model/client'
10
+
11
+ require 'elasticsearch/model/adapter'
12
+ require 'elasticsearch/model/adapters/default'
13
+ require 'elasticsearch/model/adapters/active_record'
14
+ require 'elasticsearch/model/adapters/mongoid'
15
+
16
+ require 'elasticsearch/model/importing'
17
+ require 'elasticsearch/model/indexing'
18
+ require 'elasticsearch/model/naming'
19
+ require 'elasticsearch/model/serializing'
20
+ require 'elasticsearch/model/searching'
21
+ require 'elasticsearch/model/callbacks'
22
+
23
+ require 'elasticsearch/model/proxy'
24
+
25
+ require 'elasticsearch/model/response'
26
+ require 'elasticsearch/model/response/base'
27
+ require 'elasticsearch/model/response/result'
28
+ require 'elasticsearch/model/response/results'
29
+ require 'elasticsearch/model/response/records'
30
+ require 'elasticsearch/model/response/pagination'
31
+
32
+ require 'elasticsearch/model/ext/active_record'
33
+
34
+ case
35
+ when defined?(::Kaminari)
36
+ Elasticsearch::Model::Response::Response.__send__ :include, Elasticsearch::Model::Response::Pagination::Kaminari
37
+ when defined?(::WillPaginate)
38
+ Elasticsearch::Model::Response::Response.__send__ :include, Elasticsearch::Model::Response::Pagination::WillPaginate
39
+ end
40
+
41
+ module Elasticsearch
42
+
43
+ # Elasticsearch integration for Ruby models
44
+ # =========================================
45
+ #
46
+ # `Elasticsearch::Model` contains modules for integrating the Elasticsearch search and analytical engine
47
+ # with ActiveModel-based classes, or models, for the Ruby programming language.
48
+ #
49
+ # It facilitates importing your data into an index, automatically updating it when a record changes,
50
+ # searching the specific index, setting up the index mapping or the model JSON serialization.
51
+ #
52
+ # When the `Elasticsearch::Model` module is included in your class, it automatically extends it
53
+ # with the functionality; see {Elasticsearch::Model.included}. Most methods are available via
54
+ # the `__elasticsearch__` class and instance method proxies.
55
+ #
56
+ # It is possible to include/extend the model with the corresponding
57
+ # modules directly, if that is desired:
58
+ #
59
+ # MyModel.__send__ :extend, Elasticsearch::Model::Client::ClassMethods
60
+ # MyModel.__send__ :include, Elasticsearch::Model::Client::InstanceMethods
61
+ # MyModel.__send__ :extend, Elasticsearch::Model::Searching::ClassMethods
62
+ # # ...
63
+ #
64
+ module Model
65
+ METHODS = [:search, :mapping, :mappings, :settings, :index_name, :document_type, :import]
66
+
67
+ # Adds the `Elasticsearch::Model` functionality to the including class.
68
+ #
69
+ # * Creates the `__elasticsearch__` class and instance methods, pointing to the proxy object
70
+ # * Includes the necessary modules in the proxy classes
71
+ # * Sets up delegation for crucial methods such as `search`, etc.
72
+ #
73
+ # @example Include the module in the `Article` model definition
74
+ #
75
+ # class Article < ActiveRecord::Base
76
+ # include Elasticsearch::Model
77
+ # end
78
+ #
79
+ # @example Inject the module into the `Article` model during run time
80
+ #
81
+ # Article.__send__ :include, Elasticsearch::Model
82
+ #
83
+ #
84
+ def self.included(base)
85
+ base.class_eval do
86
+ include Elasticsearch::Model::Proxy
87
+
88
+ Elasticsearch::Model::Proxy::ClassMethodsProxy.class_eval do
89
+ include Elasticsearch::Model::Client::ClassMethods
90
+ include Elasticsearch::Model::Naming::ClassMethods
91
+ include Elasticsearch::Model::Indexing::ClassMethods
92
+ include Elasticsearch::Model::Searching::ClassMethods
93
+ end
94
+
95
+ Elasticsearch::Model::Proxy::InstanceMethodsProxy.class_eval do
96
+ include Elasticsearch::Model::Client::InstanceMethods
97
+ include Elasticsearch::Model::Naming::InstanceMethods
98
+ include Elasticsearch::Model::Indexing::InstanceMethods
99
+ include Elasticsearch::Model::Serializing::InstanceMethods
100
+ end
101
+
102
+ Elasticsearch::Model::Proxy::InstanceMethodsProxy.class_eval <<-CODE, __FILE__, __LINE__ + 1
103
+ def as_indexed_json(options={})
104
+ target.respond_to?(:as_indexed_json) ? target.__send__(:as_indexed_json, options) : super
105
+ end
106
+ CODE
107
+
108
+ # Delegate important methods to the `__elasticsearch__` proxy, unless they are defined already
109
+ #
110
+ class << self
111
+ METHODS.each do |method|
112
+ delegate method, to: :__elasticsearch__ unless self.public_instance_methods.include?(method)
113
+ end
114
+ end
115
+
116
+ # Mix the importing module into the proxy
117
+ #
118
+ self.__elasticsearch__.class_eval do
119
+ include Elasticsearch::Model::Importing::ClassMethods
120
+ include Adapter.from_class(base).importing_mixin
121
+ end
122
+ end
123
+ end
124
+
125
+ module ClassMethods
126
+
127
+ # Get the client common for all models
128
+ #
129
+ # @example Get the client
130
+ #
131
+ # Elasticsearch::Model.client
132
+ # => #<Elasticsearch::Transport::Client:0x007f96a7d0d000 @transport=... >
133
+ #
134
+ def client
135
+ @client ||= Elasticsearch::Client.new
136
+ end
137
+
138
+ # Set the client for all models
139
+ #
140
+ # @example Configure (set) the client for all models
141
+ #
142
+ # Elasticsearch::Model.client Elasticsearch::Client.new host: 'http://localhost:9200', tracer: true
143
+ # => #<Elasticsearch::Transport::Client:0x007f96a6dd0d80 @transport=... >
144
+ #
145
+ # @note You have to set the client before you call Elasticsearch methods on the model,
146
+ # or set it directly on the model; see {Elasticsearch::Model::Client::ClassMethods#client}
147
+ #
148
+ def client=(client)
149
+ @client = client
150
+ end
151
+
152
+ end
153
+ extend ClassMethods
154
+
155
+ class NotImplemented < NoMethodError; end
156
+ end
157
+ end
@@ -0,0 +1,139 @@
1
+ require 'test_helper'
2
+ require 'active_record'
3
+
4
+ class Question < ActiveRecord::Base
5
+ include Elasticsearch::Model
6
+
7
+ has_many :answers, dependent: :destroy
8
+
9
+ index_name 'questions_and_answers'
10
+
11
+ mapping do
12
+ indexes :title
13
+ indexes :text
14
+ indexes :author
15
+ end
16
+
17
+ after_commit lambda { __elasticsearch__.index_document }, on: :create
18
+ after_commit lambda { __elasticsearch__.update_document }, on: :update
19
+ after_commit lambda { __elasticsearch__.delete_document }, on: :destroy
20
+ end
21
+
22
+ class Answer < ActiveRecord::Base
23
+ include Elasticsearch::Model
24
+
25
+ belongs_to :question
26
+
27
+ index_name 'questions_and_answers'
28
+
29
+ mapping _parent: { type: 'question', required: true } do
30
+ indexes :text
31
+ indexes :author
32
+ end
33
+
34
+ after_commit lambda { __elasticsearch__.index_document(parent: question_id) }, on: :create
35
+ after_commit lambda { __elasticsearch__.update_document(parent: question_id) }, on: :update
36
+ after_commit lambda { __elasticsearch__.delete_document(parent: question_id) }, on: :destroy
37
+ end
38
+
39
+ module ParentChildSearchable
40
+ INDEX_NAME = 'questions_and_answers'
41
+
42
+ def create_index!(options={})
43
+ client = Question.__elasticsearch__.client
44
+ client.indices.delete index: INDEX_NAME rescue nil if options[:force]
45
+
46
+ settings = Question.settings.to_hash.merge Answer.settings.to_hash
47
+ mappings = Question.mappings.to_hash.merge Answer.mappings.to_hash
48
+
49
+ client.indices.create index: INDEX_NAME,
50
+ body: {
51
+ settings: settings.to_hash,
52
+ mappings: mappings.to_hash }
53
+ end
54
+
55
+ extend self
56
+ end
57
+
58
+ module Elasticsearch
59
+ module Model
60
+ class ActiveRecordAssociationsParentChildIntegrationTest < Elasticsearch::Test::IntegrationTestCase
61
+
62
+ context "ActiveRecord associations with parent/child modelling" do
63
+ setup do
64
+ ActiveRecord::Schema.define(version: 1) do
65
+ create_table :questions do |t|
66
+ t.string :title
67
+ t.text :text
68
+ t.string :author
69
+ t.timestamps
70
+ end
71
+ create_table :answers do |t|
72
+ t.text :text
73
+ t.string :author
74
+ t.references :question
75
+ t.timestamps
76
+ end and add_index(:answers, :question_id)
77
+ end
78
+
79
+ Question.delete_all
80
+ ParentChildSearchable.create_index! force: true
81
+
82
+ q_1 = Question.create! title: 'First Question', author: 'John'
83
+ q_2 = Question.create! title: 'Second Question', author: 'Jody'
84
+
85
+ q_1.answers.create! text: 'Lorem Ipsum', author: 'Adam'
86
+ q_1.answers.create! text: 'Dolor Sit', author: 'Ryan'
87
+
88
+ q_2.answers.create! text: 'Amet Et', author: 'John'
89
+
90
+ Question.__elasticsearch__.refresh_index!
91
+ end
92
+
93
+ should "find questions by matching answers" do
94
+ response = Question.search(
95
+ { query: {
96
+ has_child: {
97
+ type: 'answer',
98
+ query: {
99
+ match: {
100
+ author: 'john'
101
+ }
102
+ }
103
+ }
104
+ }
105
+ })
106
+
107
+ assert_equal 'Second Question', response.records.first.title
108
+ end
109
+
110
+ should "find answers for matching questions" do
111
+ response = Answer.search(
112
+ { query: {
113
+ has_parent: {
114
+ parent_type: 'question',
115
+ query: {
116
+ match: {
117
+ author: 'john'
118
+ }
119
+ }
120
+ }
121
+ }
122
+ })
123
+
124
+ assert_same_elements ['Adam', 'Ryan'], response.records.map(&:author)
125
+ end
126
+
127
+ should "delete answers when the question is deleted" do
128
+ Question.where(title: 'First Question').each(&:destroy)
129
+ Question.__elasticsearch__.refresh_index!
130
+
131
+ response = Answer.search query: { match_all: {} }
132
+
133
+ assert_equal 1, response.results.total
134
+ end
135
+ end
136
+
137
+ end
138
+ end
139
+ end
@@ -0,0 +1,307 @@
1
+ require 'test_helper'
2
+ require 'active_record'
3
+
4
+ module Elasticsearch
5
+ module Model
6
+ class ActiveRecordAssociationsIntegrationTest < Elasticsearch::Test::IntegrationTestCase
7
+
8
+ context "ActiveRecord associations" do
9
+ setup do
10
+
11
+ # ----- Schema definition ---------------------------------------------------------------
12
+
13
+ ActiveRecord::Schema.define(version: 1) do
14
+ create_table :categories do |t|
15
+ t.string :title
16
+ t.timestamps
17
+ end
18
+
19
+ create_table :categories_posts, id: false do |t|
20
+ t.references :post, :category
21
+ end
22
+
23
+ create_table :authors do |t|
24
+ t.string :first_name, :last_name
25
+ t.timestamps
26
+ end
27
+
28
+ create_table :authorships do |t|
29
+ t.string :first_name, :last_name
30
+ t.references :post
31
+ t.references :author
32
+ t.timestamps
33
+ end
34
+
35
+ create_table :comments do |t|
36
+ t.string :text
37
+ t.string :author
38
+ t.references :post
39
+ t.timestamps
40
+ end and add_index(:comments, :post_id)
41
+
42
+ create_table :posts do |t|
43
+ t.string :title
44
+ t.text :text
45
+ t.boolean :published
46
+ t.timestamps
47
+ end
48
+ end
49
+
50
+ # ----- Models definition -------------------------------------------------------------------------
51
+
52
+ class Category < ActiveRecord::Base
53
+ has_and_belongs_to_many :posts
54
+ end
55
+
56
+ class Author < ActiveRecord::Base
57
+ has_many :authorships
58
+
59
+ def full_name
60
+ [first_name, last_name].compact.join(' ')
61
+ end
62
+ end
63
+
64
+ class Authorship < ActiveRecord::Base
65
+ belongs_to :author
66
+ belongs_to :post, touch: true
67
+ end
68
+
69
+ class Comment < ActiveRecord::Base
70
+ belongs_to :post, touch: true
71
+ end
72
+
73
+ class Post < ActiveRecord::Base
74
+ has_and_belongs_to_many :categories, after_add: [ lambda { |a,c| a.__elasticsearch__.index_document } ],
75
+ after_remove: [ lambda { |a,c| a.__elasticsearch__.index_document } ]
76
+ has_many :authorships
77
+ has_many :authors, through: :authorships
78
+ has_many :comments
79
+ end
80
+
81
+ # ----- Search integration via Concern module -----------------------------------------------------
82
+
83
+ module Searchable
84
+ extend ActiveSupport::Concern
85
+
86
+ included do
87
+ include Elasticsearch::Model
88
+ include Elasticsearch::Model::Callbacks
89
+
90
+ # Set up the mapping
91
+ #
92
+ settings index: { number_of_shards: 1, number_of_replicas: 0 } do
93
+ mapping do
94
+ indexes :title, analyzer: 'snowball'
95
+ indexes :created_at, type: 'date'
96
+
97
+ indexes :authors do
98
+ indexes :first_name
99
+ indexes :last_name
100
+ indexes :full_name, type: 'multi_field' do
101
+ indexes :full_name
102
+ indexes :raw, analyzer: 'keyword'
103
+ end
104
+ end
105
+
106
+ indexes :categories, analyzer: 'keyword'
107
+
108
+ indexes :comments, type: 'nested' do
109
+ indexes :text
110
+ indexes :author
111
+ end
112
+ end
113
+ end
114
+
115
+ # Customize the JSON serialization for Elasticsearch
116
+ #
117
+ def as_indexed_json(options={})
118
+ {
119
+ title: title,
120
+ text: text,
121
+ categories: categories.map(&:title),
122
+ authors: authors.as_json(methods: [:full_name], only: [:full_name, :first_name, :last_name]),
123
+ comments: comments.as_json(only: [:text, :author])
124
+ }
125
+ end
126
+
127
+ # Update document in the index after touch
128
+ #
129
+ after_touch() { __elasticsearch__.index_document }
130
+ end
131
+ end
132
+
133
+ # Include the search integration
134
+ #
135
+ Post.__send__ :include, Searchable
136
+
137
+ # ----- Reset the index -----------------------------------------------------------------
138
+
139
+ Post.delete_all
140
+ Post.__elasticsearch__.create_index! force: true
141
+ end
142
+
143
+ should "index and find a document" do
144
+ Post.create! title: 'Test'
145
+ Post.create! title: 'Testing Coding'
146
+ Post.create! title: 'Coding'
147
+ Post.__elasticsearch__.refresh_index!
148
+
149
+ response = Post.search('title:test')
150
+
151
+ assert_equal 2, response.results.size
152
+ assert_equal 2, response.records.size
153
+
154
+ assert_equal 'Test', response.results.first.title
155
+ assert_equal 'Test', response.records.first.title
156
+ end
157
+
158
+ should "reindex a document after categories are changed" do
159
+ # Create categories
160
+ category_a = Category.where(title: "One").first_or_create!
161
+ category_b = Category.where(title: "Two").first_or_create!
162
+
163
+ # Create post
164
+ post = Post.create! title: "First Post", text: "This is the first post..."
165
+
166
+ # Assign categories
167
+ post.categories = [category_a, category_b]
168
+
169
+ Post.__elasticsearch__.refresh_index!
170
+
171
+ query = { query: {
172
+ filtered: {
173
+ query: {
174
+ multi_match: {
175
+ fields: ['title'],
176
+ query: 'first'
177
+ }
178
+ },
179
+ filter: {
180
+ terms: {
181
+ categories: ['One']
182
+ }
183
+ }
184
+ }
185
+ }
186
+ }
187
+
188
+ response = Post.search query
189
+
190
+ assert_equal 1, response.results.size
191
+ assert_equal 1, response.records.size
192
+
193
+ # Remove category "One"
194
+ post.categories = [category_b]
195
+
196
+ Post.__elasticsearch__.refresh_index!
197
+ response = Post.search query
198
+
199
+ assert_equal 0, response.results.size
200
+ assert_equal 0, response.records.size
201
+ end
202
+
203
+ should "reindex a document after authors are changed" do
204
+ # Create authors
205
+ author_a = Author.where(first_name: "John", last_name: "Smith").first_or_create!
206
+ author_b = Author.where(first_name: "Mary", last_name: "Smith").first_or_create!
207
+ author_c = Author.where(first_name: "Kobe", last_name: "Griss").first_or_create!
208
+
209
+ # Create posts
210
+ post_1 = Post.create! title: "First Post", text: "This is the first post..."
211
+ post_2 = Post.create! title: "Second Post", text: "This is the second post..."
212
+ post_3 = Post.create! title: "Third Post", text: "This is the third post..."
213
+
214
+ # Assign authors
215
+ post_1.authors = [author_a, author_b]
216
+ post_2.authors = [author_a]
217
+ post_3.authors = [author_c]
218
+
219
+ Post.__elasticsearch__.refresh_index!
220
+
221
+ response = Post.search 'authors.full_name:john'
222
+
223
+ assert_equal 2, response.results.size
224
+ assert_equal 2, response.records.size
225
+
226
+ post_3.authors << author_a
227
+
228
+ Post.__elasticsearch__.refresh_index!
229
+
230
+ response = Post.search 'authors.full_name:john'
231
+
232
+ assert_equal 3, response.results.size
233
+ assert_equal 3, response.records.size
234
+ end if defined?(::ActiveRecord) && ::ActiveRecord::VERSION::MAJOR >= 4
235
+
236
+ should "reindex a document after comments are added" do
237
+ # Create posts
238
+ post_1 = Post.create! title: "First Post", text: "This is the first post..."
239
+ post_2 = Post.create! title: "Second Post", text: "This is the second post..."
240
+
241
+ # Add comments
242
+ post_1.comments.create! author: 'John', text: 'Excellent'
243
+ post_1.comments.create! author: 'Abby', text: 'Good'
244
+
245
+ post_2.comments.create! author: 'John', text: 'Terrible'
246
+
247
+ Post.__elasticsearch__.refresh_index!
248
+
249
+ response = Post.search 'comments.author:john AND comments.text:good'
250
+ assert_equal 0, response.results.size
251
+
252
+ # Add comment
253
+ post_1.comments.create! author: 'John', text: 'Or rather just good...'
254
+
255
+ Post.__elasticsearch__.refresh_index!
256
+
257
+ response = Post.search 'comments.author:john AND comments.text:good'
258
+ assert_equal 0, response.results.size
259
+
260
+ response = Post.search \
261
+ query: {
262
+ nested: {
263
+ path: 'comments',
264
+ query: {
265
+ bool: {
266
+ must: [
267
+ { match: { 'comments.author' => 'john' } },
268
+ { match: { 'comments.text' => 'good' } }
269
+ ]
270
+ }
271
+ }
272
+ }
273
+ }
274
+
275
+ assert_equal 1, response.results.size
276
+ end if defined?(::ActiveRecord) && ::ActiveRecord::VERSION::MAJOR >= 4
277
+
278
+ should "reindex a document after Post#touch" do
279
+ # Create categories
280
+ category_a = Category.where(title: "One").first_or_create!
281
+
282
+ # Create post
283
+ post = Post.create! title: "First Post", text: "This is the first post..."
284
+
285
+ # Assign category
286
+ post.categories << category_a
287
+
288
+ Post.__elasticsearch__.refresh_index!
289
+
290
+ assert_equal 1, Post.search('categories:One').size
291
+
292
+ # Update category
293
+ category_a.update_attribute :title, "Updated"
294
+
295
+ # Trigger touch on posts in category
296
+ category_a.posts.each { |p| p.touch }
297
+
298
+ Post.__elasticsearch__.refresh_index!
299
+
300
+ assert_equal 0, Post.search('categories:One').size
301
+ assert_equal 1, Post.search('categories:Updated').size
302
+ end
303
+ end
304
+
305
+ end
306
+ end
307
+ end