spree_core 5.4.0.beta9 → 5.4.0.beta10

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 (32) hide show
  1. checksums.yaml +4 -4
  2. data/app/finders/spree/products/find.rb +1 -1
  3. data/app/jobs/spree/search_provider/index_job.rb +22 -0
  4. data/app/jobs/spree/search_provider/remove_job.rb +19 -0
  5. data/app/models/concerns/spree/product_scopes.rb +95 -135
  6. data/app/models/concerns/spree/search_indexable.rb +83 -0
  7. data/app/models/concerns/spree/{multi_searchable.rb → searchable.rb} +10 -3
  8. data/app/models/concerns/spree/user_methods.rb +11 -8
  9. data/app/models/spree/address.rb +3 -15
  10. data/app/models/spree/credit_card.rb +1 -0
  11. data/app/models/spree/line_item.rb +4 -0
  12. data/app/models/spree/metafield_definition.rb +7 -4
  13. data/app/models/spree/option_type.rb +3 -0
  14. data/app/models/spree/option_value.rb +3 -0
  15. data/app/models/spree/order.rb +27 -9
  16. data/app/models/spree/order_promotion.rb +1 -1
  17. data/app/models/spree/product.rb +9 -31
  18. data/app/models/spree/search_provider/base.rb +81 -0
  19. data/app/models/spree/search_provider/database.rb +95 -0
  20. data/app/models/spree/search_provider/meilisearch.rb +302 -0
  21. data/app/models/spree/search_provider/search_result.rb +5 -0
  22. data/app/models/spree/taxon.rb +2 -7
  23. data/app/models/spree/variant.rb +6 -25
  24. data/app/models/spree/wishlist.rb +1 -0
  25. data/app/presenters/spree/search_provider/product_presenter.rb +131 -0
  26. data/app/services/spree/carts/update.rb +5 -4
  27. data/db/sample_data/payment_methods.rb +2 -0
  28. data/lib/spree/core/version.rb +1 -1
  29. data/lib/spree/core.rb +16 -3
  30. data/lib/tasks/search.rake +15 -0
  31. metadata +28 -6
  32. data/app/models/spree/cart_promotion.rb +0 -7
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 47046110413017b3697dc5b8937c77dc5cb1577035ca12ed2e5c828c826457ba
4
- data.tar.gz: c0318740d1a1eb2a71a4bd4a99c88d6ea7100f69d6746a3084a1c54b4e7a74d8
3
+ metadata.gz: 73ed6afd1feda3d20e24a8910273785420d08dfadebf8af34aa90c20b017a0b8
4
+ data.tar.gz: 323a92ea512ad2d37bac8be8b8f878f60dbbc4058ed020875c5740ef02a7481b
5
5
  SHA512:
6
- metadata.gz: f306e9d832c4dcedb23306edd9a3d32b8459e04125c50139c7f77b6bc8726c2363facd9652d6b0132aef92a63b21228873349bb9a5ba812e5ab7e63f3b460240
7
- data.tar.gz: 946536ad1ed9dccbb48ad8882811381449656391c3d0f05205a8bc0165380624400e9b5c58973daa71467f4e3b6384b74f4fb358bf9657bfff3de634b93cddfb
6
+ metadata.gz: 711d0f622de8f398bd3f0e8696c5e12e476d3671a6330781fd5a34a0ecb15cc32f17fcf9d8e1e3028584108a354465ba7721ca990e723077c89dddb9c428fcbb
7
+ data.tar.gz: 3634efec05dc63e7c44a27bcfe0265331224592ee8d88bc1b417446967af051ccd450723344bc287977de3793b95d17ca0fef1235e905aa25e7a787f65325a81
@@ -112,7 +112,7 @@ module Spree
112
112
  def by_query(products)
113
113
  return products unless query?
114
114
 
115
- products.multi_search(query)
115
+ products.search(query)
116
116
  end
117
117
 
118
118
  def by_ids(products)
@@ -0,0 +1,22 @@
1
+ module Spree
2
+ module SearchProvider
3
+ class IndexJob < Spree::BaseJob
4
+ queue_as Spree.queues.search
5
+
6
+ retry_on StandardError, wait: :polynomially_longer, attempts: 5
7
+ discard_on ActiveRecord::RecordNotFound
8
+
9
+ # @param resource_class [String] e.g. 'Spree::Product'
10
+ # @param resource_id [String] always pass as string for UUID support
11
+ # @param store_id [String] always pass as string for UUID support
12
+ def perform(resource_class, resource_id, store_id)
13
+ resource = resource_class.constantize.find_by(id: resource_id)
14
+ store = Spree::Store.find_by(id: store_id)
15
+ return unless resource && store
16
+
17
+ provider = Spree.search_provider.constantize.new(store)
18
+ provider.index(resource)
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,19 @@
1
+ module Spree
2
+ module SearchProvider
3
+ class RemoveJob < Spree::BaseJob
4
+ queue_as Spree.queues.search
5
+
6
+ retry_on StandardError, wait: :polynomially_longer, attempts: 5
7
+
8
+ # @param prefixed_id [String] prefixed ID of the document to remove (e.g. 'prod_abc')
9
+ # @param store_id [String] always pass as string for UUID support
10
+ def perform(prefixed_id, store_id)
11
+ store = Spree::Store.find_by(id: store_id)
12
+ return unless store
13
+
14
+ provider = Spree.search_provider.constantize.new(store)
15
+ provider.remove_by_id(prefixed_id)
16
+ end
17
+ end
18
+ end
19
+ end
@@ -3,41 +3,20 @@ module Spree
3
3
  extend ActiveSupport::Concern
4
4
 
5
5
  included do
6
- cattr_accessor :search_scopes do
7
- []
8
- end
9
-
10
- def self.add_search_scope(name, &block)
11
- singleton_class.send(:define_method, name.to_sym, &block)
12
- search_scopes << name.to_sym
13
- end
14
-
15
- def self.simple_scopes
16
- [
17
- :ascend_by_updated_at,
18
- :descend_by_updated_at,
19
- :ascend_by_name,
20
- :descend_by_name
21
- ]
22
- end
23
-
24
- def self.add_simple_scopes(scopes)
25
- scopes.each do |name|
26
- # We should not define price scopes here, as they require something slightly different
27
- next if name.to_s.include?('master_price')
28
-
29
- parts = name.to_s.match(/(.*)_by_(.*)/)
30
- scope(name.to_s, -> { order(Arel.sql(sanitize_sql("#{Product.quoted_table_name}.#{parts[2]} #{parts[1] == 'ascend' ? 'ASC' : 'DESC'}"))) })
31
- end
32
- end
33
-
34
- add_simple_scopes simple_scopes
35
-
36
- add_search_scope :ascend_by_master_price do
6
+ scope :ascend_by_updated_at, -> { order("#{Product.quoted_table_name}.updated_at ASC") }
7
+ scope :descend_by_updated_at, -> { order("#{Product.quoted_table_name}.updated_at DESC") }
8
+ scope :ascend_by_name, -> { order("#{Product.quoted_table_name}.name ASC") }
9
+ scope :descend_by_name, -> { order("#{Product.quoted_table_name}.name DESC") }
10
+
11
+ # Deprecated — master variant being removed in 6.0. Use ascend_by_price / descend_by_price instead.
12
+ def self.ascend_by_master_price
13
+ Spree::Deprecation.warn('ascend_by_master_price is deprecated and will be removed in Spree 6.0. Use ascend_by_price instead.')
37
14
  order(price_table_name => { amount: :asc })
38
15
  end
39
16
 
40
- add_search_scope :descend_by_master_price do
17
+ # Deprecated — master variant being removed in 6.0. Use ascend_by_price / descend_by_price instead.
18
+ def self.descend_by_master_price
19
+ Spree::Deprecation.warn('descend_by_master_price is deprecated and will be removed in Spree 6.0. Use descend_by_price instead.')
41
20
  order(price_table_name => { amount: :desc })
42
21
  end
43
22
 
@@ -47,7 +26,7 @@ module Spree
47
26
  # Uses Arel::Nodes::As for select expressions so that:
48
27
  # - PG allows ORDER BY with DISTINCT (expressions must appear in SELECT list)
49
28
  # - Mobility's select_for_count can safely call .right on all select_values
50
- add_search_scope :ascend_by_price do
29
+ scope :ascend_by_price, -> {
51
30
  price_agg_sql = Price.non_zero.joins(:variant)
52
31
  .select("#{Variant.table_name}.product_id AS product_id, MIN(#{Price.table_name}.amount) AS agg_price")
53
32
  .group("#{Variant.table_name}.product_id")
@@ -59,9 +38,9 @@ module Spree
59
38
  select("#{Product.table_name}.*").
60
39
  select(Arel::Nodes::As.new(price_expr, Arel.sql('min_price'))).
61
40
  order(price_expr.asc)
62
- end
41
+ }
63
42
 
64
- add_search_scope :descend_by_price do
43
+ scope :descend_by_price, -> {
65
44
  price_agg_sql = Price.non_zero.joins(:variant)
66
45
  .select("#{Variant.table_name}.product_id AS product_id, MAX(#{Price.table_name}.amount) AS agg_price")
67
46
  .group("#{Variant.table_name}.product_id")
@@ -73,17 +52,21 @@ module Spree
73
52
  select("#{Product.table_name}.*").
74
53
  select(Arel::Nodes::As.new(price_expr, Arel.sql('max_price'))).
75
54
  order(price_expr.desc)
76
- end
55
+ }
77
56
 
78
- add_search_scope :price_between do |low, high|
57
+ scope :price_between, ->(low, high) {
79
58
  where(Price.table_name => { amount: low..high })
80
- end
59
+ }
81
60
 
82
- add_search_scope :master_price_lte do |price|
61
+ # Deprecated master variant being removed in 6.0. Use price_lte / price_gte instead.
62
+ def self.master_price_lte(price)
63
+ Spree::Deprecation.warn('master_price_lte is deprecated and will be removed in Spree 6.0. Use price_lte instead.')
83
64
  where(Price.table_name => { amount: ..price })
84
65
  end
85
66
 
86
- add_search_scope :master_price_gte do |price|
67
+ # Deprecated master variant being removed in 6.0. Use price_lte / price_gte instead.
68
+ def self.master_price_gte(price)
69
+ Spree::Deprecation.warn('master_price_gte is deprecated and will be removed in Spree 6.0. Use price_gte instead.')
87
70
  where(Price.table_name => { amount: price.. })
88
71
  end
89
72
 
@@ -106,7 +89,6 @@ module Spree
106
89
  )
107
90
  }
108
91
 
109
- # Can't use add_search_scope for this as it needs a default argument
110
92
  # Ransack calls with '1' to activate, '0' or nil to skip
111
93
  # In Ruby code: in_stock(true) for in-stock, in_stock(false) for out-of-stock
112
94
  def self.in_stock(in_stock = true)
@@ -117,14 +99,13 @@ module Spree
117
99
  end
118
100
  end
119
101
 
120
- add_search_scope :price_lte do |price|
102
+ scope :price_lte, ->(price) {
121
103
  where(Price.table_name => { amount: ..price })
122
- end
104
+ }
123
105
 
124
- add_search_scope :price_gte do |price|
106
+ scope :price_gte, ->(price) {
125
107
  where(Price.table_name => { amount: price.. })
126
- end
127
- search_scopes << :in_stock
108
+ }
128
109
 
129
110
  def self.out_of_stock(out_of_stock = true)
130
111
  if out_of_stock == '0' || !out_of_stock
@@ -133,13 +114,12 @@ module Spree
133
114
  where.not(id: join_variants_and_stock_items.in_stock_or_backorderable_condition)
134
115
  end
135
116
  end
136
- search_scopes << :out_of_stock
137
117
 
138
- add_search_scope :backorderable do
118
+ def self.backorderable
139
119
  join_variants_and_stock_items.where(StockItem.table_name => { backorderable: true })
140
120
  end
141
121
 
142
- add_search_scope :in_stock_or_backorderable do
122
+ def self.in_stock_or_backorderable
143
123
  join_variants_and_stock_items.in_stock_or_backorderable_condition
144
124
  end
145
125
 
@@ -152,34 +132,22 @@ module Spree
152
132
  # `:distinct` option as well:
153
133
  #
154
134
  # Spree::Product.in_taxon(taxon).count(distinct: true)
155
- #
156
- # This is so that the count query is distinct'd:
157
- #
158
- # SELECT COUNT(DISTINCT "spree_products"."id") ...
159
- #
160
- # vs.
161
- #
162
- # SELECT COUNT(*) ...
163
- add_search_scope :in_taxon do |taxon|
135
+ scope :in_taxon, ->(taxon) {
164
136
  joins(:classifications).
165
137
  where("#{Classification.table_name}.taxon_id" => taxon.cached_self_and_descendants_ids).distinct
166
- end
138
+ }
167
139
 
168
140
  # Alias for in_taxon — public API name
169
- add_search_scope :in_category do |category|
170
- in_taxon(category)
171
- end
141
+ scope :in_category, ->(category) { in_taxon(category) }
172
142
 
173
- # This scope selects products in all taxons AND all its descendants
174
- # If you need products only within one taxon use
175
- #
176
- # Spree::Product.taxons_id_eq([x,y])
177
- add_search_scope :in_taxons do |*taxons|
143
+ # Deprecated remove in 6.0. Use in_taxon instead.
144
+ def self.in_taxons(*taxons)
145
+ Spree::Deprecation.warn('in_taxons is deprecated and will be removed in Spree 6.0. Use in_taxon instead.')
178
146
  taxons = get_taxons(taxons)
179
147
  taxons.first ? prepare_taxon_conditions(taxons) : where(nil)
180
148
  end
181
149
 
182
- add_search_scope :ascend_by_taxons_min_position do |taxon_ids|
150
+ scope :ascend_by_taxons_min_position, ->(taxon_ids) {
183
151
  min_position_sql = "MIN(#{Classification.table_name}.position)"
184
152
 
185
153
  joins(:classifications).
@@ -187,9 +155,11 @@ module Spree
187
155
  select("#{Product.table_name}.*", "#{min_position_sql} AS min_taxon_position").
188
156
  group("#{Product.table_name}.id").
189
157
  order(Arel.sql("#{min_position_sql} ASC"))
190
- end
158
+ }
191
159
 
192
- add_search_scope :with_option do |option|
160
+ # Deprecated remove in 6.0. Not used internally.
161
+ def self.with_option(option)
162
+ Spree::Deprecation.warn('with_option is deprecated and will be removed in Spree 6.0.')
193
163
  if option.is_a?(OptionType)
194
164
  joins(:option_types).where(spree_option_types: { id: option.id })
195
165
  elsif option.is_a?(Integer)
@@ -201,7 +171,7 @@ module Spree
201
171
  end
202
172
  end
203
173
 
204
- add_search_scope :with_option_value do |option, value|
174
+ scope :with_option_value, ->(option, value) {
205
175
  option_type_id = case option
206
176
  when OptionType then option.id
207
177
  when Integer then option
@@ -210,89 +180,66 @@ module Spree
210
180
  OptionType.where(id: option).or(OptionType.where(name: option))&.first&.id
211
181
  else
212
182
  OptionType.where(name: option)&.first&.id
213
- OptionType.where(name: option)&.first&.id
214
183
  end
215
184
  end
216
185
 
217
- return Product.group("#{Spree::Product.table_name}.id").none if option_type_id.blank?
186
+ next Product.group("#{Spree::Product.table_name}.id").none if option_type_id.blank?
218
187
 
219
188
  group("#{Spree::Product.table_name}.id").
220
189
  joins(variants: :option_values).
221
190
  where(Spree::OptionValue.table_name => { name: value, option_type_id: option_type_id })
222
- end
191
+ }
223
192
 
224
193
  # Filters products by option value IDs (prefix IDs like 'optval_xxx')
225
194
  # Accepts an array of option value IDs
226
- add_search_scope :with_option_value_ids do |*ids|
195
+ scope :with_option_value_ids, ->(*ids) {
227
196
  ids = ids.flatten.compact
228
- return none if ids.empty?
197
+ next none if ids.empty?
229
198
 
230
199
  # Handle prefixed IDs (optval_xxx) by decoding to actual IDs
231
200
  actual_ids = ids.map do |id|
232
201
  id.to_s.include?('_') ? OptionValue.decode_prefixed_id(id) : id
233
202
  end.compact
234
203
 
235
- return none if actual_ids.empty?
204
+ next none if actual_ids.empty?
236
205
 
237
206
  joins(variants: :option_values).where(Spree::OptionValue.table_name => { id: actual_ids })
238
- end
207
+ }
239
208
 
240
- # Finds all products which have an option value with the name matching the one given
241
- add_search_scope :with do |value|
209
+ # Deprecated remove in 6.0. Not used internally.
210
+ def self.with(value)
211
+ Spree::Deprecation.warn('Product.with is deprecated and will be removed in Spree 6.0.')
242
212
  includes(variants: :option_values).
243
213
  where("#{OptionValue.table_name}.name = ?", value)
244
214
  end
245
215
 
246
- # Finds all products that have a name containing the given words.
247
- add_search_scope :in_name do |words|
216
+ # Deprecated remove in 6.0. Use .search scope instead.
217
+ def self.in_name(words)
218
+ Spree::Deprecation.warn('in_name is deprecated and will be removed in Spree 6.0. Use .search instead.')
248
219
  like_any([:name], prepare_words(words))
249
220
  end
250
221
 
251
- # Finds all products that have a name or meta_keywords containing the given words.
252
- add_search_scope :in_name_or_keywords do |words|
222
+ # Deprecated remove in 6.0. Use .search scope instead.
223
+ def self.in_name_or_keywords(words)
224
+ Spree::Deprecation.warn('in_name_or_keywords is deprecated and will be removed in Spree 6.0. Use .search instead.')
253
225
  like_any([:name, :meta_keywords], prepare_words(words))
254
226
  end
255
227
 
256
- # Finds all products that have a name, description, meta_description or meta_keywords containing the given keywords.
257
- add_search_scope :in_name_or_description do |words|
228
+ # Deprecated remove in 6.0. Use .search scope instead.
229
+ def self.in_name_or_description(words)
230
+ Spree::Deprecation.warn('in_name_or_description is deprecated and will be removed in Spree 6.0. Use .search instead.')
258
231
  like_any([:name, :description, :meta_description, :meta_keywords], prepare_words(words))
259
232
  end
260
233
 
261
- # Finds all products that have the ids matching the given collection of ids.
262
- # Alternatively, you could use find(collection_of_ids), but that would raise an exception if one product couldn't be found
263
- add_search_scope :with_ids do |*ids|
234
+ # Deprecated remove in 6.0. Use where(id: ids) directly.
235
+ def self.with_ids(*ids)
236
+ Spree::Deprecation.warn('with_ids is deprecated and will be removed in Spree 6.0. Use where(id: ids) instead.')
264
237
  where(id: ids)
265
238
  end
266
239
 
267
- # Sorts products from most popular (popularity is extracted from how many
268
- # times use has put product in cart, not completed orders)
269
- #
270
- # there is alternative faster and more elegant solution, it has small drawback though,
271
- # it doesn stack with other scopes :/
272
- #
273
- # joins: "LEFT OUTER JOIN (SELECT line_items.variant_id as vid, COUNT(*) as cnt FROM line_items GROUP BY line_items.variant_id) AS popularity_count ON variants.id = vid",
274
- # order: 'COALESCE(cnt, 0) DESC'
275
- add_search_scope :descend_by_popularity do
276
- joins(:master).
277
- order(%Q{
278
- COALESCE((
279
- SELECT
280
- COUNT(#{LineItem.quoted_table_name}.id)
281
- FROM
282
- #{LineItem.quoted_table_name}
283
- JOIN
284
- #{Variant.quoted_table_name} AS popular_variants
285
- ON
286
- popular_variants.id = #{LineItem.quoted_table_name}.variant_id
287
- WHERE
288
- popular_variants.product_id = #{Product.quoted_table_name}.id
289
- ), 0) DESC
290
- })
291
- end
292
-
293
- add_search_scope :not_deleted do
240
+ scope :not_deleted, -> {
294
241
  where("#{Product.quoted_table_name}.deleted_at IS NULL or #{Product.quoted_table_name}.deleted_at >= ?", Time.zone.now)
295
- end
242
+ }
296
243
 
297
244
  def self.not_discontinued(only_not_discontinued = true)
298
245
  if only_not_discontinued != '0' && only_not_discontinued
@@ -301,7 +248,6 @@ module Spree
301
248
  all
302
249
  end
303
250
  end
304
- search_scopes << :not_discontinued
305
251
 
306
252
  def self.with_currency(currency)
307
253
  joins(variants_including_master: :prices).
@@ -309,9 +255,7 @@ module Spree
309
255
  where.not(Price.table_name => { amount: nil }).
310
256
  distinct
311
257
  end
312
- search_scopes << :with_currency
313
258
 
314
- # Can't use add_search_scope for this as it needs a default argument
315
259
  def self.available(available_on = nil, currency = nil)
316
260
  scope = not_discontinued.where(status: 'active')
317
261
  if available_on
@@ -326,21 +270,22 @@ module Spree
326
270
 
327
271
  scope
328
272
  end
329
- search_scopes << :available
330
273
 
331
274
  def self.active(currency = nil)
332
275
  available(nil, currency)
333
276
  end
334
- search_scopes << :active
335
277
 
278
+ # Deprecated — remove in 6.0. Use active(currency).in_taxon(taxon) directly.
336
279
  def self.for_filters(currency, taxon: nil)
280
+ Spree::Deprecation.warn('for_filters is deprecated and will be removed in Spree 6.0. Use active(currency).in_taxon(taxon) instead.')
337
281
  scope = active(currency)
338
282
  scope = scope.in_taxon(taxon) if taxon.present?
339
283
  scope
340
284
  end
341
- search_scopes << :for_filters
342
285
 
286
+ # Deprecated — remove in 6.0. Not used internally.
343
287
  def self.for_user(user = nil)
288
+ Spree::Deprecation.warn('for_user is deprecated and will be removed in Spree 6.0.')
344
289
  if user.try(:has_spree_role?, 'admin')
345
290
  with_deleted
346
291
  else
@@ -348,7 +293,9 @@ module Spree
348
293
  end
349
294
  end
350
295
 
351
- add_search_scope :taxons_name_eq do |name|
296
+ # Deprecated remove in 6.0. Not used internally.
297
+ def self.taxons_name_eq(name)
298
+ Spree::Deprecation.warn('taxons_name_eq is deprecated and will be removed in Spree 6.0.')
352
299
  group('spree_products.id').joins(:taxons).where(Taxon.arel_table[:name].eq(name))
353
300
  end
354
301
 
@@ -357,7 +304,7 @@ module Spree
357
304
  #
358
305
  # Uses Arel::Nodes::As so that ORDER BY expressions appear in SELECT
359
306
  # and work with DISTINCT (same pattern as the price sorting scopes).
360
- add_search_scope :by_best_selling do |order_direction = :desc|
307
+ scope :by_best_selling, ->(order_direction = :desc) {
361
308
  sp_table = StoreProduct.table_name
362
309
  units_expr = Arel.sql("COALESCE(#{sp_table}.units_sold_count, 0)")
363
310
  revenue_expr = Arel.sql("COALESCE(#{sp_table}.revenue, 0)")
@@ -369,19 +316,32 @@ module Spree
369
316
  select(Arel::Nodes::As.new(revenue_expr, Arel.sql('best_selling_revenue'))).
370
317
  order(units_expr.send(order_dir)).
371
318
  order(revenue_expr.send(order_dir))
372
- end
319
+ }
373
320
 
374
- # .search_by_name
375
- if defined?(PgSearch)
376
- include PgSearch::Model
321
+ # Deprecated — remove in 6.0. Use by_best_selling instead.
322
+ def self.descend_by_popularity
323
+ Spree::Deprecation.warn('descend_by_popularity is deprecated and will be removed in Spree 6.0. Use by_best_selling instead.')
324
+ joins(:master).
325
+ order(%Q{
326
+ COALESCE((
327
+ SELECT
328
+ COUNT(#{LineItem.quoted_table_name}.id)
329
+ FROM
330
+ #{LineItem.quoted_table_name}
331
+ JOIN
332
+ #{Variant.quoted_table_name} AS popular_variants
333
+ ON
334
+ popular_variants.id = #{LineItem.quoted_table_name}.variant_id
335
+ WHERE
336
+ popular_variants.product_id = #{Product.quoted_table_name}.id
337
+ ), 0) DESC
338
+ })
339
+ end
377
340
 
378
- pg_search_scope :search_by_name, against: { name: 'A', meta_title: 'B' }, using: { trigram: { threshold: 0.3, word_similarity: true } }
379
- else
380
- def self.search_by_name(query)
381
- i18n { name.lower.matches("%#{query.downcase}%") }
382
- end
341
+ # .search_by_name simple ILIKE on product name
342
+ def self.search_by_name(query)
343
+ i18n { name.lower.matches("%#{query.downcase}%") }
383
344
  end
384
- search_scopes << :search_by_name
385
345
 
386
346
  def self.price_table_name
387
347
  Price.quoted_table_name
@@ -0,0 +1,83 @@
1
+ module Spree
2
+ module SearchIndexable
3
+ extend ActiveSupport::Concern
4
+
5
+ included do
6
+ after_commit :enqueue_search_index, on: [:create, :update]
7
+ after_commit :enqueue_search_removal, on: :destroy
8
+ end
9
+
10
+ # Index this record synchronously (inline, no job).
11
+ # Useful for bulk imports, rake tasks, or when you need immediate indexing.
12
+ def add_to_search_index
13
+ return unless search_indexing_enabled?
14
+
15
+ store_ids_for_indexing.each do |store_id|
16
+ store = Spree::Store.find_by(id: store_id)
17
+ next unless store
18
+
19
+ provider = Spree.search_provider.constantize.new(store)
20
+ provider.index(self)
21
+ end
22
+ end
23
+
24
+ # Returns the hash that would be sent to the search provider for indexing.
25
+ # Useful for debugging and previewing what gets indexed.
26
+ #
27
+ # product.search_presentation # uses Spree::Current.store
28
+ # product.search_presentation(store) # explicit store
29
+ # => { id: 1, name: "Shirt", price_USD: 19.99, ... }
30
+ def search_presentation(store = nil)
31
+ store ||= Spree::Current.store
32
+ Spree::SearchProvider::ProductPresenter.new(self, store).call
33
+ end
34
+
35
+ # Remove this record from search index synchronously (inline, no job).
36
+ def remove_from_search_index
37
+ return unless search_indexing_enabled?
38
+
39
+ store_ids_for_indexing.each do |store_id|
40
+ store = Spree::Store.find_by(id: store_id)
41
+ next unless store
42
+
43
+ provider = Spree.search_provider.constantize.new(store)
44
+ provider.remove(self)
45
+ end
46
+ end
47
+
48
+ private
49
+
50
+ def enqueue_search_index
51
+ return unless search_indexing_enabled?
52
+
53
+ store_ids_for_indexing.each do |store_id|
54
+ Spree::SearchProvider::IndexJob.perform_later(self.class.name, id.to_s, store_id.to_s)
55
+ end
56
+ end
57
+
58
+ def enqueue_search_removal
59
+ return unless search_indexing_enabled?
60
+
61
+ pid = prefixed_id
62
+ store_ids_for_indexing.each do |store_id|
63
+ Spree::SearchProvider::RemoveJob.perform_later(pid, store_id.to_s)
64
+ end
65
+ end
66
+
67
+ def search_indexing_enabled?
68
+ Spree.search_provider.constantize.indexing_required?
69
+ rescue NameError
70
+ false
71
+ end
72
+
73
+ def store_ids_for_indexing
74
+ if respond_to?(:store_ids)
75
+ store_ids
76
+ elsif respond_to?(:store_id)
77
+ [store_id].compact
78
+ else
79
+ Spree::Store.pluck(:id)
80
+ end
81
+ end
82
+ end
83
+ end
@@ -1,13 +1,13 @@
1
1
  module Spree
2
- module MultiSearchable
2
+ module Searchable
3
3
  extend ActiveSupport::Concern
4
4
 
5
5
  included do
6
- def self.sanitize_query_for_multi_search(query)
6
+ def self.sanitize_query_for_search(query)
7
7
  ActiveRecord::Base.sanitize_sql_like(query.to_s.downcase.strip)
8
8
  end
9
9
 
10
- def self.multi_search_condition(model_class, attribute, query)
10
+ def self.search_condition(model_class, attribute, query)
11
11
  encrypted_attributes = model_class.encrypted_attributes.presence || []
12
12
 
13
13
  if encrypted_attributes.include?(attribute.to_sym)
@@ -16,6 +16,13 @@ module Spree
16
16
  model_class.arel_table[attribute.to_sym].lower.matches("%#{query}%")
17
17
  end
18
18
  end
19
+
20
+ # Backward compatibility aliases — remove in Spree 6.0
21
+ def self.sanitize_query_for_multi_search(query) = sanitize_query_for_search(query)
22
+ def self.multi_search_condition(model_class, attribute, query) = search_condition(model_class, attribute, query)
19
23
  end
20
24
  end
25
+
26
+ # Backward compatibility alias — remove in Spree 6.0
27
+ MultiSearchable = Searchable
21
28
  end
@@ -9,7 +9,7 @@ module Spree
9
9
  include Spree::UserReporting
10
10
  include Spree::UserRoles
11
11
  include Spree::RansackableAttributes
12
- include Spree::MultiSearchable
12
+ include Spree::Searchable
13
13
  include Spree::Publishable
14
14
 
15
15
  included do
@@ -74,28 +74,31 @@ module Spree
74
74
  find_by_token_for!(:password_reset, token)
75
75
  end
76
76
 
77
- def self.multi_search(query)
78
- sanitized_query = sanitize_query_for_multi_search(query)
77
+ def self.search(query)
78
+ sanitized_query = sanitize_query_for_search(query)
79
79
  return none if query.blank?
80
80
 
81
81
  name_conditions = []
82
82
 
83
- name_conditions << multi_search_condition(self, :first_name, sanitized_query)
84
- name_conditions << multi_search_condition(self, :last_name, sanitized_query)
83
+ name_conditions << search_condition(self, :first_name, sanitized_query)
84
+ name_conditions << search_condition(self, :last_name, sanitized_query)
85
85
 
86
86
  full_name = NameOfPerson::PersonName.full(sanitized_query)
87
87
 
88
88
  if full_name.first.present? && full_name.last.present?
89
- name_conditions << multi_search_condition(self, :first_name, full_name.first)
90
- name_conditions << multi_search_condition(self, :last_name, full_name.last)
89
+ name_conditions << search_condition(self, :first_name, full_name.first)
90
+ name_conditions << search_condition(self, :last_name, full_name.last)
91
91
  end
92
92
 
93
93
  where(arel_table[:email].lower.eq(query.downcase)).or(where(name_conditions.reduce(:or)))
94
94
  end
95
95
 
96
+ # Backward compatibility alias — remove in Spree 6.0
97
+ def self.multi_search(query) = search(query)
98
+
96
99
  self.whitelisted_ransackable_associations = %w[bill_address ship_address addresses tags spree_roles]
97
100
  self.whitelisted_ransackable_attributes = %w[id email first_name last_name accepts_email_marketing]
98
- self.whitelisted_ransackable_scopes = %w[multi_search]
101
+ self.whitelisted_ransackable_scopes = %w[search multi_search]
99
102
 
100
103
  def self.with_email(query)
101
104
  where("#{table_name}.email LIKE ?", "%#{query}%")