searchkick 5.0.3 → 5.0.5

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ec7741cb306a56f1a5ae5a07450c5c102236bc6103079fc34a5f602fa2853b31
4
- data.tar.gz: 2bd747ee31846c901ce2a58125b34e9c90af6937a2c7dc041f2dd9f69701f1e9
3
+ metadata.gz: 8a93f95464f2b83d27f621d925326ea3b7728b2d71fff9831e20e07658ff0466
4
+ data.tar.gz: acf15b38474dea853f3e3574ea5fac21d4129920b6365e2c92ca58cfaf518cc0
5
5
  SHA512:
6
- metadata.gz: f92f2a3c7bb27862b1768f5ecedc19ad0eab515d62515f94d72412ed7c22a20493049dd0e52cba21b3a43d8c846b0dc0c6cda40c68edc12f6af37e8487fb9403
7
- data.tar.gz: f69a1cfc401bad0f09bda3f2788b1096cc004dcb765eb20b3d8ada06202185eeae630d953005298e5944ed1b8c239fa808eeebb32e9440cc7ae9a32e771469dc
6
+ metadata.gz: 51f9d3b6c110a7ec24988518b7e154fef368c2d027c00337e54096c3214871d29da9bdc9e549d4b9967742636bb8ae60d5a288ca066ff992691692a017b9bdda
7
+ data.tar.gz: 811825c1043231828c2d04a3a2081e453440fd470563680c8e470fea00569ffe57109bca64e07978ba8944d53a03f95c9fd4768872d6bf2ae6c4e87ede961e7a
data/CHANGELOG.md CHANGED
@@ -1,3 +1,15 @@
1
+ ## 5.0.5 (2022-10-09)
2
+
3
+ - Added `model` method to `Searchkick::Relation`
4
+ - Fixed deprecation warning with `redis` gem
5
+ - Fixed `respond_to?` method on relation loading relation
6
+ - Fixed `Relation loaded` error for non-mutating methods on relation
7
+
8
+ ## 5.0.4 (2022-06-16)
9
+
10
+ - Added `max_result_window` option
11
+ - Improved error message for unsupported versions of Elasticsearch
12
+
1
13
  ## 5.0.3 (2022-03-13)
2
14
 
3
15
  - Fixed context for index name for inherited models
data/README.md CHANGED
@@ -66,7 +66,7 @@ gem "elasticsearch" # select one
66
66
  gem "opensearch-ruby" # select one
67
67
  ```
68
68
 
69
- The latest version works with Elasticsearch 7 and 8 and OpenSearch 1. For Elasticsearch 6, use version 4.6.3 and [this readme](https://github.com/ankane/searchkick/blob/v4.6.3/README.md).
69
+ The latest version works with Elasticsearch 7 and 8 and OpenSearch 1 and 2. For Elasticsearch 6, use version 4.6.3 and [this readme](https://github.com/ankane/searchkick/blob/v4.6.3/README.md).
70
70
 
71
71
  Add searchkick to models you want to search.
72
72
 
@@ -292,12 +292,18 @@ Option | Matches | Example
292
292
 
293
293
  The default is `:word`. The most matches will happen with `:word_middle`.
294
294
 
295
+ To specify different matching for different fields, use:
296
+
297
+ ```ruby
298
+ Product.search(query, fields: [{name: :word_start}, {brand: :word_middle}])
299
+ ```
300
+
295
301
  ### Exact Matches
296
302
 
297
303
  To match a field exactly (case-sensitive), use:
298
304
 
299
305
  ```ruby
300
- Product.search(query, fields: [{email: :exact}, :name])
306
+ Product.search(query, fields: [{name: :exact}])
301
307
  ```
302
308
 
303
309
  ### Phrase Matches
@@ -323,11 +329,11 @@ end
323
329
  See the [list of languages](https://www.elastic.co/guide/en/elasticsearch/reference/current/analysis-stemmer-tokenfilter.html#analysis-stemmer-tokenfilter-configure-parms). A few languages require plugins:
324
330
 
325
331
  - `chinese` - [analysis-ik plugin](https://github.com/medcl/elasticsearch-analysis-ik)
326
- - `chinese2` - [analysis-smartcn plugin](https://www.elastic.co/guide/en/elasticsearch/plugins/7.4/analysis-smartcn.html)
327
- - `japanese` - [analysis-kuromoji plugin](https://www.elastic.co/guide/en/elasticsearch/plugins/7.4/analysis-kuromoji.html)
332
+ - `chinese2` - [analysis-smartcn plugin](https://www.elastic.co/guide/en/elasticsearch/plugins/current/analysis-smartcn.html)
333
+ - `japanese` - [analysis-kuromoji plugin](https://www.elastic.co/guide/en/elasticsearch/plugins/current/analysis-kuromoji.html)
328
334
  - `korean` - [analysis-openkoreantext plugin](https://github.com/open-korean-text/elasticsearch-analysis-openkoreantext)
329
- - `korean2` - [analysis-nori plugin](https://www.elastic.co/guide/en/elasticsearch/plugins/7.4/analysis-nori.html)
330
- - `polish` - [analysis-stempel plugin](https://www.elastic.co/guide/en/elasticsearch/plugins/7.4/analysis-stempel.html)
335
+ - `korean2` - [analysis-nori plugin](https://www.elastic.co/guide/en/elasticsearch/plugins/current/analysis-nori.html)
336
+ - `polish` - [analysis-stempel plugin](https://www.elastic.co/guide/en/elasticsearch/plugins/current/analysis-stempel.html)
331
337
  - `ukrainian` - [analysis-ukrainian plugin](https://www.elastic.co/guide/en/elasticsearch/plugins/7.4/analysis-ukrainian.html)
332
338
  - `vietnamese` - [analysis-vietnamese plugin](https://github.com/duydo/elasticsearch-analysis-vietnamese)
333
339
 
@@ -592,6 +598,14 @@ There are four strategies for keeping the index synced with your database.
592
598
  end
593
599
  ```
594
600
 
601
+ And reindex a record or relation manually.
602
+
603
+ ```ruby
604
+ product.reindex
605
+ # or
606
+ store.products.reindex(mode: :async)
607
+ ```
608
+
595
609
  You can also do bulk updates.
596
610
 
597
611
  ```ruby
@@ -608,6 +622,12 @@ Searchkick.callbacks(false) do
608
622
  end
609
623
  ```
610
624
 
625
+ Or override the model’s strategy.
626
+
627
+ ```ruby
628
+ product.reindex(mode: :async) # :inline or :queue
629
+ ```
630
+
611
631
  ### Associations
612
632
 
613
633
  Data is **not** automatically synced when an association is updated. If this is desired, add a callback to reindex:
@@ -654,20 +674,16 @@ The best starting point to improve your search **by far** is to track searches a
654
674
  Product.search("apple", track: {user_id: current_user.id})
655
675
  ```
656
676
 
657
- [See the docs](https://github.com/ankane/searchjoy) for how to install and use.
658
-
659
- Focus on:
660
-
661
- - top searches with low conversions
662
- - top searches with no results
677
+ [See the docs](https://github.com/ankane/searchjoy) for how to install and use. Focus on top searches with a low conversion rate.
663
678
 
664
- Searchkick can then use the conversion data to learn what users are looking for. If a user searches for “ice cream” and adds Ben & Jerry’s Chunky Monkey to the cart (our conversion metric at Instacart), that item gets a little more weight for similar searches.
679
+ Searchkick can then use the conversion data to learn what users are looking for. If a user searches for “ice cream” and adds Ben & Jerry’s Chunky Monkey to the cart (our conversion metric at Instacart), that item gets a little more weight for similar searches. This can make a huge difference on the quality of your search.
665
680
 
666
681
  Add conversion data with:
667
682
 
668
683
  ```ruby
669
684
  class Product < ApplicationRecord
670
- has_many :searches, class_name: "Searchjoy::Search", as: :convertable
685
+ has_many :conversions, class_name: "Searchjoy::Conversion", as: :convertable
686
+ has_many :searches, class_name: "Searchjoy::Search", through: :conversions
671
687
 
672
688
  searchkick conversions: [:conversions] # name of field
673
689
 
@@ -681,15 +697,100 @@ class Product < ApplicationRecord
681
697
  end
682
698
  ```
683
699
 
684
- Reindex and set up a cron job to add new conversions daily.
700
+ Reindex and set up a cron job to add new conversions daily. For zero downtime deployment, temporarily set `conversions: false` in your search calls until the data is reindexed.
685
701
 
686
- ```sh
687
- rake searchkick:reindex CLASS=Product
702
+ ### Performant Conversions
703
+
704
+ A performant way to do conversions is to cache them to prevent N+1 queries. For Postgres, create a migration with:
705
+
706
+ ```ruby
707
+ add_column :products, :search_conversions, :jsonb
708
+ ```
709
+
710
+ For MySQL, use `:json`, and for others, use `:text` with a [JSON serializer](https://api.rubyonrails.org/classes/ActiveRecord/AttributeMethods/Serialization/ClassMethods.html).
711
+
712
+ Next, update your model. Create a separate method for conversion data so you can use [partial reindexing](#partial-reindexing).
713
+
714
+ ```ruby
715
+ class Product < ApplicationRecord
716
+ searchkick conversions: [:conversions]
717
+
718
+ def search_data
719
+ {
720
+ name: name,
721
+ category: category
722
+ }.merge(conversions_data)
723
+ end
724
+
725
+ def conversions_data
726
+ {
727
+ conversions: search_conversions || {}
728
+ }
729
+ end
730
+ end
731
+ ```
732
+
733
+ Deploy and reindex your data. For zero downtime deployment, temporarily set `conversions: false` in your search calls until the data is reindexed.
734
+
735
+ ```ruby
736
+ Product.reindex
737
+ ```
738
+
739
+ Then, create a job to update the conversions column and reindex records with new conversions. Here’s one you can use for Searchjoy:
740
+
741
+ ```ruby
742
+ class UpdateConversionsJob < ApplicationJob
743
+ def perform(class_name, since: nil, update: true, reindex: true)
744
+ model = Searchkick.load_model(class_name)
745
+
746
+ # get records that have a recent conversion
747
+ recently_converted_ids =
748
+ Searchjoy::Conversion.where(convertable_type: class_name).where(created_at: since..)
749
+ .order(:convertable_id).distinct.pluck(:convertable_id)
750
+
751
+ # split into batches
752
+ recently_converted_ids.in_groups_of(1000, false) do |ids|
753
+ if update
754
+ # fetch conversions
755
+ conversions =
756
+ Searchjoy::Conversion.where(convertable_id: ids, convertable_type: class_name)
757
+ .joins(:search).where.not(searchjoy_searches: {user_id: nil})
758
+ .group(:convertable_id, :query).distinct.count(:user_id)
759
+
760
+ # group by record
761
+ conversions_by_record = {}
762
+ conversions.each do |(id, query), count|
763
+ (conversions_by_record[id] ||= {})[query] = count
764
+ end
765
+
766
+ # update conversions column
767
+ model.transaction do
768
+ conversions_by_record.each do |id, conversions|
769
+ model.where(id: id).update_all(search_conversions: conversions)
770
+ end
771
+ end
772
+ end
773
+
774
+ if reindex
775
+ # reindex conversions data
776
+ model.where(id: ids).reindex(:conversions_data)
777
+ end
778
+ end
779
+ end
780
+ end
688
781
  ```
689
782
 
690
- This can make a huge difference on the quality of your search.
783
+ Run the job:
691
784
 
692
- For a more performant way to reindex conversion data, check out [performant conversions](#performant-conversions).
785
+ ```ruby
786
+ UpdateConversionsJob.perform_now("Product")
787
+ ```
788
+
789
+ And set it up to run daily.
790
+
791
+ ```ruby
792
+ UpdateConversionsJob.perform_later("Product", since: 1.day.ago)
793
+ ```
693
794
 
694
795
  ## Personalized Results
695
796
 
@@ -1575,11 +1676,12 @@ Reindex a subset of attributes to reduce time spent generating search data and c
1575
1676
  class Product < ApplicationRecord
1576
1677
  def search_data
1577
1678
  {
1578
- name: name
1579
- }.merge(search_prices)
1679
+ name: name,
1680
+ category: category
1681
+ }.merge(prices_data)
1580
1682
  end
1581
1683
 
1582
- def search_prices
1684
+ def prices_data
1583
1685
  {
1584
1686
  price: price,
1585
1687
  sale_price: sale_price
@@ -1591,68 +1693,7 @@ end
1591
1693
  And use:
1592
1694
 
1593
1695
  ```ruby
1594
- Product.reindex(:search_prices)
1595
- ```
1596
-
1597
- ### Performant Conversions
1598
-
1599
- Split out conversions into a separate method so you can use partial reindexing, and cache conversions to prevent N+1 queries. Be sure to use a centralized cache store like Memcached or Redis.
1600
-
1601
- ```ruby
1602
- class Product < ApplicationRecord
1603
- def search_data
1604
- {
1605
- name: name
1606
- }.merge(search_conversions)
1607
- end
1608
-
1609
- def search_conversions
1610
- {
1611
- conversions: Rails.cache.read("search_conversions:#{self.class.name}:#{id}") || {}
1612
- }
1613
- end
1614
- end
1615
- ```
1616
-
1617
- Create a job to update the cache and reindex records with new conversions.
1618
-
1619
- ```ruby
1620
- class ReindexConversionsJob < ApplicationJob
1621
- def perform(class_name)
1622
- # get records that have a recent conversion
1623
- recently_converted_ids =
1624
- Searchjoy::Search.where("convertable_type = ? AND converted_at > ?", class_name, 1.day.ago)
1625
- .order(:convertable_id).distinct.pluck(:convertable_id)
1626
-
1627
- # split into groups
1628
- recently_converted_ids.in_groups_of(1000, false) do |ids|
1629
- # fetch conversions
1630
- conversions =
1631
- Searchjoy::Search.where(convertable_id: ids, convertable_type: class_name)
1632
- .group(:convertable_id, :query).distinct.count(:user_id)
1633
-
1634
- # group conversions by record
1635
- conversions_by_record = {}
1636
- conversions.each do |(id, query), count|
1637
- (conversions_by_record[id] ||= {})[query] = count
1638
- end
1639
-
1640
- # write to cache
1641
- conversions_by_record.each do |id, conversions|
1642
- Rails.cache.write("search_conversions:#{class_name}:#{id}", conversions)
1643
- end
1644
-
1645
- # partial reindex
1646
- class_name.constantize.where(id: ids).reindex(:search_conversions)
1647
- end
1648
- end
1649
- end
1650
- ```
1651
-
1652
- Run the job with:
1653
-
1654
- ```ruby
1655
- ReindexConversionsJob.perform_later("Product")
1696
+ Product.reindex(:prices_data)
1656
1697
  ```
1657
1698
 
1658
1699
  ## Advanced
@@ -2036,12 +2077,24 @@ Turn on misspellings after a certain number of characters
2036
2077
  Product.search("api", misspellings: {prefix_length: 2}) # api, apt, no ahi
2037
2078
  ```
2038
2079
 
2039
- **Note:** With this option, if the query length is the same as `prefix_length`, misspellings are turned off with Elasticsearch 7 and OpenSearch
2080
+ **Note:** With this option, if the query length is the same as `prefix_length`, misspellings are turned off with Elasticsearch 7 and OpenSearch 1
2040
2081
 
2041
2082
  ```ruby
2042
2083
  Product.search("ah", misspellings: {prefix_length: 2}) # ah, no aha
2043
2084
  ```
2044
2085
 
2086
+ BigDecimal values are indexed as floats by default so they can be used for boosting. Convert them to strings to keep full precision.
2087
+
2088
+ ```ruby
2089
+ class Product < ApplicationRecord
2090
+ def search_data
2091
+ {
2092
+ units: units.to_s("F")
2093
+ }
2094
+ end
2095
+ end
2096
+ ```
2097
+
2045
2098
  ## Gotchas
2046
2099
 
2047
2100
  ### Consistency
@@ -418,7 +418,7 @@ module Searchkick
418
418
  true
419
419
  end
420
420
  rescue => e
421
- if Searchkick.transport_error?(e) && e.message.include?("No handler for type [text]")
421
+ if Searchkick.transport_error?(e) && (e.message.include?("No handler for type [text]") || e.message.include?("class java.util.ArrayList cannot be cast to class java.util.Map"))
422
422
  raise UnsupportedVersionError
423
423
  end
424
424
 
@@ -19,7 +19,7 @@ module Searchkick
19
19
  mappings = generate_mappings.deep_symbolize_keys.deep_merge(custom_mappings)
20
20
  end
21
21
 
22
- set_deep_paging(settings) if options[:deep_paging]
22
+ set_deep_paging(settings) if options[:deep_paging] || options[:max_result_window]
23
23
 
24
24
  {
25
25
  settings: settings,
@@ -525,7 +525,7 @@ module Searchkick
525
525
  def set_deep_paging(settings)
526
526
  if !settings.dig(:index, :max_result_window) && !settings[:"index.max_result_window"]
527
527
  settings[:index] ||= {}
528
- settings[:index][:max_result_window] = 1_000_000_000
528
+ settings[:index][:max_result_window] = options[:max_result_window] || 1_000_000_000
529
529
  end
530
530
  end
531
531
 
@@ -5,7 +5,7 @@ module Searchkick
5
5
 
6
6
  unknown_keywords = options.keys - [:_all, :_type, :batch_size, :callbacks, :case_sensitive, :conversions, :deep_paging, :default_fields,
7
7
  :filterable, :geo_shape, :highlight, :ignore_above, :index_name, :index_prefix, :inheritance, :language,
8
- :locations, :mappings, :match, :merge_mappings, :routing, :searchable, :search_synonyms, :settings, :similarity,
8
+ :locations, :mappings, :match, :max_result_window, :merge_mappings, :routing, :searchable, :search_synonyms, :settings, :similarity,
9
9
  :special_characters, :stem, :stemmer, :stem_conversions, :stem_exclusion, :stemmer_override, :suggest, :synonyms, :text_end,
10
10
  :text_middle, :text_start, :unscope, :word, :word_end, :word_middle, :word_start]
11
11
  raise ArgumentError, "unknown keywords: #{unknown_keywords.join(", ")}" if unknown_keywords.any?
@@ -254,6 +254,12 @@ module Searchkick
254
254
  offset = options[:offset] || (page - 1) * per_page + padding
255
255
  scroll = options[:scroll]
256
256
 
257
+ max_result_window = searchkick_options[:max_result_window]
258
+ if max_result_window
259
+ offset = max_result_window if offset > max_result_window
260
+ per_page = max_result_window - offset if offset + per_page > max_result_window
261
+ end
262
+
257
263
  # model and eager loading
258
264
  load = options[:load].nil? ? true : options[:load]
259
265
 
@@ -8,6 +8,9 @@ module Searchkick
8
8
  delegate :body, :params, to: :query
9
9
  delegate_missing_to :private_execute
10
10
 
11
+ attr_reader :model
12
+ alias_method :klass, :model
13
+
11
14
  def initialize(model, term = "*", **options)
12
15
  @model = model
13
16
  @term = term
@@ -26,8 +29,7 @@ module Searchkick
26
29
 
27
30
  def execute
28
31
  Searchkick.warn("The execute method is no longer needed")
29
- private_execute
30
- self
32
+ load
31
33
  end
32
34
 
33
35
  # experimental
@@ -195,10 +197,25 @@ module Searchkick
195
197
  Relation.new(@model, @term, **@options.except(*keys))
196
198
  end
197
199
 
200
+ # experimental
201
+ def load
202
+ private_execute
203
+ self
204
+ end
205
+
198
206
  def loaded?
199
207
  !@execute.nil?
200
208
  end
201
209
 
210
+ def respond_to_missing?(method_name, include_all)
211
+ Results.new(nil, nil, nil).respond_to?(method_name, include_all) || super
212
+ end
213
+
214
+ # TODO uncomment in 6.0
215
+ # def to_yaml
216
+ # private_execute.to_a.to_yaml
217
+ # end
218
+
202
219
  private
203
220
 
204
221
  def private_execute
@@ -221,5 +238,10 @@ module Searchkick
221
238
  def ensure_permitted(obj)
222
239
  obj.to_h
223
240
  end
241
+
242
+ def initialize_copy(other)
243
+ super
244
+ @execute = nil
245
+ end
224
246
  end
225
247
  end
@@ -46,7 +46,7 @@ module Searchkick
46
46
  end
47
47
 
48
48
  def batch_completed(batch_id)
49
- Searchkick.with_redis { |r| r.srem(batches_key, batch_id) }
49
+ Searchkick.with_redis { |r| r.srem(batches_key, [batch_id]) }
50
50
  end
51
51
 
52
52
  private
@@ -134,7 +134,7 @@ module Searchkick
134
134
  end
135
135
 
136
136
  def batch_job(class_name, batch_id, record_ids)
137
- Searchkick.with_redis { |r| r.sadd(batches_key, batch_id) }
137
+ Searchkick.with_redis { |r| r.sadd(batches_key, [batch_id]) }
138
138
  Searchkick::BulkReindexJob.perform_later(
139
139
  class_name: class_name,
140
140
  index_name: index.name,
@@ -3,6 +3,7 @@ module Searchkick
3
3
  include Enumerable
4
4
  extend Forwardable
5
5
 
6
+ # TODO remove klass and options in 6.0
6
7
  attr_reader :klass, :response, :options
7
8
 
8
9
  def_delegators :results, :each, :any?, :empty?, :size, :length, :slice, :[], :to_ary
@@ -13,6 +14,7 @@ module Searchkick
13
14
  @options = options
14
15
  end
15
16
 
17
+ # TODO make private in 6.0
16
18
  def results
17
19
  @results ||= with_hit.map(&:first)
18
20
  end
@@ -1,3 +1,3 @@
1
1
  module Searchkick
2
- VERSION = "5.0.3"
2
+ VERSION = "5.0.5"
3
3
  end
data/lib/searchkick.rb CHANGED
@@ -135,8 +135,9 @@ module Searchkick
135
135
  @opensearch
136
136
  end
137
137
 
138
- def self.server_below?(version)
139
- server_version = opensearch? ? "7.10.2" : self.server_version
138
+ # TODO always check true version in Searchkick 6
139
+ def self.server_below?(version, true_version = false)
140
+ server_version = !true_version && opensearch? ? "7.10.2" : self.server_version
140
141
  Gem::Version.new(server_version.split("-")[0]) < Gem::Version.new(version.split("-")[0])
141
142
  end
142
143
 
@@ -284,7 +285,7 @@ module Searchkick
284
285
  relation
285
286
  end
286
287
 
287
- # private
288
+ # public (for reindexing conversions)
288
289
  def self.load_model(class_name, allow_child: false)
289
290
  model = class_name.safe_constantize
290
291
  raise Error, "Could not find class: #{class_name}" unless model
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: searchkick
3
3
  version: !ruby/object:Gem::Version
4
- version: 5.0.3
4
+ version: 5.0.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andrew Kane
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2022-03-13 00:00:00.000000000 Z
11
+ date: 2022-10-10 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activemodel