elasticsearch-model-queryable 0.1.5

Sign up to get free protection for your applications and to get access to all the features.
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