searchkick 5.0.3 → 5.0.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.
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