searchkick 5.0.1 → 5.0.4

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: ab19b7696cc775bda54eef31aaf9fea04a13a3027335440023f58f0c3b761fa0
4
- data.tar.gz: 461f230a62f440e84684e9eb42cc85d307b726830eba80999c5648aef13de92e
3
+ metadata.gz: 89c6a97d4c898be7f1f494cc4bfafc8aed5acc202a855588e81f73d86ea7b123
4
+ data.tar.gz: c362bb2916ec0b1fa83d72efd2314e747077b5cd696ed2ece089204be9452010
5
5
  SHA512:
6
- metadata.gz: 5382e2de584271fc9f80802f858329c613f3b9aff7877c9bb9653dab550deb6ed1ac6971cbb2bc306f4d6e67056f16ee98eea04d69f2fd9ff15fd899e37f8962
7
- data.tar.gz: dd8121e4991c9aaad5cd331cdc3adef3c703ee47abf8238ea3cc6feb1b1e5dcd85cb59c9f7ce91c086644d5946c5ccba936f48d0bc27988a59cca81adf161bb3
6
+ metadata.gz: bf1a9191ee97a19afde4c44b84c02b34b7f6b8b3ac464cdd34671dd60cb8a191ec338a028db8221c2a502fadf0059987f957c918221175631e8aa70344371ee7
7
+ data.tar.gz: 32e7b4b9f0899088e709a77389d8fa845ef68a3fd819d3823229eb1ffd99a6121647d5ab82d209e6254e4c242530d7698441d3b97f65a1e2436acbced6b6086f
data/CHANGELOG.md CHANGED
@@ -1,3 +1,16 @@
1
+ ## 5.0.4 (2022-06-16)
2
+
3
+ - Added `max_result_window` option
4
+ - Improved error message for unsupported versions of Elasticsearch
5
+
6
+ ## 5.0.3 (2022-03-13)
7
+
8
+ - Fixed context for index name for inherited models
9
+
10
+ ## 5.0.2 (2022-03-03)
11
+
12
+ - Fixed index name for inherited models
13
+
1
14
  ## 5.0.1 (2022-02-27)
2
15
 
3
16
  - Prefer `mode: :async` over `async: true` for full reindex
data/README.md CHANGED
@@ -50,8 +50,8 @@ Searchkick 5.0 was recently released! See [how to upgrade](#upgrading)
50
50
  Install [Elasticsearch](https://www.elastic.co/downloads/elasticsearch) or [OpenSearch](https://opensearch.org/downloads.html). For Homebrew, use:
51
51
 
52
52
  ```sh
53
- brew install elasticsearch
54
- brew services start elasticsearch
53
+ brew install elastic/tap/elasticsearch-full
54
+ brew services start elasticsearch-full
55
55
  # or
56
56
  brew install opensearch
57
57
  brew services start opensearch
@@ -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
 
@@ -98,7 +98,7 @@ Searchkick supports the complete [Elasticsearch Search API](https://www.elastic.
98
98
  Query like SQL
99
99
 
100
100
  ```ruby
101
- Product.search "apples", where: {in_stock: true}, limit: 10, offset: 50
101
+ Product.search("apples", where: {in_stock: true}, limit: 10, offset: 50)
102
102
  ```
103
103
 
104
104
  Search specific fields
@@ -226,7 +226,7 @@ You can also boost by:
226
226
  Use a `*` for the query.
227
227
 
228
228
  ```ruby
229
- Product.search "*"
229
+ Product.search("*")
230
230
  ```
231
231
 
232
232
  ### Pagination
@@ -235,7 +235,7 @@ Plays nicely with kaminari and will_paginate.
235
235
 
236
236
  ```ruby
237
237
  # controller
238
- @products = Product.search "milk", page: params[:page], per_page: 20
238
+ @products = Product.search("milk", page: params[:page], per_page: 20)
239
239
  ```
240
240
 
241
241
  View with kaminari
@@ -255,13 +255,13 @@ View with will_paginate
255
255
  By default, results must match all words in the query.
256
256
 
257
257
  ```ruby
258
- Product.search "fresh honey" # fresh AND honey
258
+ Product.search("fresh honey") # fresh AND honey
259
259
  ```
260
260
 
261
261
  To change this, use:
262
262
 
263
263
  ```ruby
264
- Product.search "fresh honey", operator: "or" # fresh OR honey
264
+ Product.search("fresh honey", operator: "or") # fresh OR honey
265
265
  ```
266
266
 
267
267
  By default, results must match the entire word - `back` will not match `backpack`. You can change this behavior with:
@@ -275,7 +275,7 @@ end
275
275
  And to search (after you reindex):
276
276
 
277
277
  ```ruby
278
- Product.search "back", fields: [:name], match: :word_start
278
+ Product.search("back", fields: [:name], match: :word_start)
279
279
  ```
280
280
 
281
281
  Available options are:
@@ -297,7 +297,7 @@ The default is `:word`. The most matches will happen with `:word_middle`.
297
297
  To match a field exactly (case-sensitive), use:
298
298
 
299
299
  ```ruby
300
- User.search query, fields: [{email: :exact}, :name]
300
+ Product.search(query, fields: [{email: :exact}, :name])
301
301
  ```
302
302
 
303
303
  ### Phrase Matches
@@ -305,7 +305,7 @@ User.search query, fields: [{email: :exact}, :name]
305
305
  To only match the exact order, use:
306
306
 
307
307
  ```ruby
308
- User.search "fresh honey", match: :phrase
308
+ Product.search("fresh honey", match: :phrase)
309
309
  ```
310
310
 
311
311
  ### Stemming and Language
@@ -426,7 +426,7 @@ end
426
426
  Search with:
427
427
 
428
428
  ```ruby
429
- Product.search query, fields: [:name_tagged]
429
+ Product.search(query, fields: [:name_tagged])
430
430
  ```
431
431
 
432
432
  ### Misspellings
@@ -436,13 +436,13 @@ By default, Searchkick handles misspelled queries by returning results with an [
436
436
  You can change this with:
437
437
 
438
438
  ```ruby
439
- Product.search "zucini", misspellings: {edit_distance: 2} # zucchini
439
+ Product.search("zucini", misspellings: {edit_distance: 2}) # zucchini
440
440
  ```
441
441
 
442
442
  To prevent poor precision and improve performance for correctly spelled queries (which should be a majority for most applications), Searchkick can first perform a search without misspellings, and if there are too few results, perform another with them.
443
443
 
444
444
  ```ruby
445
- Product.search "zuchini", misspellings: {below: 5}
445
+ Product.search("zuchini", misspellings: {below: 5})
446
446
  ```
447
447
 
448
448
  If there are fewer than 5 results, a 2nd search is performed with misspellings enabled. The result of this query is returned.
@@ -450,13 +450,13 @@ If there are fewer than 5 results, a 2nd search is performed with misspellings e
450
450
  Turn off misspellings with:
451
451
 
452
452
  ```ruby
453
- Product.search "zuchini", misspellings: false # no zucchini
453
+ Product.search("zuchini", misspellings: false) # no zucchini
454
454
  ```
455
455
 
456
456
  Specify which fields can include misspellings with:
457
457
 
458
458
  ```ruby
459
- Product.search "zucini", fields: [:name, :color], misspellings: {fields: [:name]}
459
+ Product.search("zucini", fields: [:name, :color], misspellings: {fields: [:name]})
460
460
  ```
461
461
 
462
462
  > When doing this, you must also specify fields to search
@@ -466,7 +466,7 @@ Product.search "zucini", fields: [:name, :color], misspellings: {fields: [:name]
466
466
  If a user searches `butter`, they may also get results for `peanut butter`. To prevent this, use:
467
467
 
468
468
  ```ruby
469
- Product.search "butter", exclude: ["peanut butter"]
469
+ Product.search("butter", exclude: ["peanut butter"])
470
470
  ```
471
471
 
472
472
  You can map queries and terms to exclude with:
@@ -477,7 +477,7 @@ exclude_queries = {
477
477
  "cream" => ["ice cream", "whipped cream"]
478
478
  }
479
479
 
480
- Product.search query, exclude: exclude_queries[query]
480
+ Product.search(query, exclude: exclude_queries[query])
481
481
  ```
482
482
 
483
483
  You can demote results by boosting by a factor less than one:
@@ -499,7 +499,7 @@ gem "gemoji-parser"
499
499
  And use:
500
500
 
501
501
  ```ruby
502
- Product.search "🍨🍰", emoji: true
502
+ Product.search("🍨🍰", emoji: true)
503
503
  ```
504
504
 
505
505
  ## Indexing
@@ -592,6 +592,14 @@ There are four strategies for keeping the index synced with your database.
592
592
  end
593
593
  ```
594
594
 
595
+ And reindex a record or relation manually.
596
+
597
+ ```ruby
598
+ product.reindex
599
+ # or
600
+ store.products.reindex(mode: :async)
601
+ ```
602
+
595
603
  You can also do bulk updates.
596
604
 
597
605
  ```ruby
@@ -608,6 +616,12 @@ Searchkick.callbacks(false) do
608
616
  end
609
617
  ```
610
618
 
619
+ Or override the model’s strategy.
620
+
621
+ ```ruby
622
+ product.reindex(mode: :async) # :inline or :queue
623
+ ```
624
+
611
625
  ### Associations
612
626
 
613
627
  Data is **not** automatically synced when an association is updated. If this is desired, add a callback to reindex:
@@ -651,23 +665,19 @@ end
651
665
  The best starting point to improve your search **by far** is to track searches and conversions. [Searchjoy](https://github.com/ankane/searchjoy) makes it easy.
652
666
 
653
667
  ```ruby
654
- Product.search "apple", track: {user_id: current_user.id}
668
+ Product.search("apple", track: {user_id: current_user.id})
655
669
  ```
656
670
 
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
671
+ [See the docs](https://github.com/ankane/searchjoy) for how to install and use. Focus on top searches with a low conversion rate.
663
672
 
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.
673
+ 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
674
 
666
675
  Add conversion data with:
667
676
 
668
677
  ```ruby
669
678
  class Product < ApplicationRecord
670
- has_many :searches, class_name: "Searchjoy::Search", as: :convertable
679
+ has_many :conversions, class_name: "Searchjoy::Conversion", as: :convertable
680
+ has_many :searches, class_name: "Searchjoy::Search", through: :conversions
671
681
 
672
682
  searchkick conversions: [:conversions] # name of field
673
683
 
@@ -681,15 +691,100 @@ class Product < ApplicationRecord
681
691
  end
682
692
  ```
683
693
 
684
- Reindex and set up a cron job to add new conversions daily.
694
+ 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.
695
+
696
+ ### Performant Conversions
697
+
698
+ A performant way to do conversions is to cache them to prevent N+1 queries. For Postgres, create a migration with:
699
+
700
+ ```ruby
701
+ add_column :products, :search_conversions, :jsonb
702
+ ```
703
+
704
+ For MySQL, use `:json`, and for others, use `:text` with a [JSON serializer](https://api.rubyonrails.org/classes/ActiveRecord/AttributeMethods/Serialization/ClassMethods.html).
705
+
706
+ Next, update your model. Create a separate method for conversion data so you can use [partial reindexing](#partial-reindexing).
707
+
708
+ ```ruby
709
+ class Product < ApplicationRecord
710
+ searchkick conversions: [:conversions]
711
+
712
+ def search_data
713
+ {
714
+ name: name,
715
+ category: category
716
+ }.merge(conversions_data)
717
+ end
718
+
719
+ def conversions_data
720
+ {
721
+ conversions: search_conversions || {}
722
+ }
723
+ end
724
+ end
725
+ ```
726
+
727
+ Deploy and reindex your data. For zero downtime deployment, temporarily set `conversions: false` in your search calls until the data is reindexed.
728
+
729
+ ```ruby
730
+ Product.reindex
731
+ ```
732
+
733
+ Then, create a job to update the conversions column and reindex records with new conversions. Here’s one you can use for Searchjoy:
734
+
735
+ ```ruby
736
+ class UpdateConversionsJob < ApplicationJob
737
+ def perform(class_name, since: nil, update: true, reindex: true)
738
+ model = Searchkick.load_model(class_name)
739
+
740
+ # get records that have a recent conversion
741
+ recently_converted_ids =
742
+ Searchjoy::Conversion.where(convertable_type: class_name).where(created_at: since..)
743
+ .order(:convertable_id).distinct.pluck(:convertable_id)
744
+
745
+ # split into batches
746
+ recently_converted_ids.in_groups_of(1000, false) do |ids|
747
+ if update
748
+ # fetch conversions
749
+ conversions =
750
+ Searchjoy::Conversion.where(convertable_id: ids, convertable_type: class_name)
751
+ .joins(:search).where.not(searchjoy_searches: {user_id: nil})
752
+ .group(:convertable_id, :query).distinct.count(:user_id)
753
+
754
+ # group by record
755
+ conversions_by_record = {}
756
+ conversions.each do |(id, query), count|
757
+ (conversions_by_record[id] ||= {})[query] = count
758
+ end
759
+
760
+ # update conversions column
761
+ model.transaction do
762
+ conversions_by_record.each do |id, conversions|
763
+ model.where(id: id).update_all(search_conversions: conversions)
764
+ end
765
+ end
766
+ end
767
+
768
+ if reindex
769
+ # reindex conversions data
770
+ model.where(id: ids).reindex(:conversions_data)
771
+ end
772
+ end
773
+ end
774
+ end
775
+ ```
776
+
777
+ Run the job:
685
778
 
686
779
  ```ruby
687
- rake searchkick:reindex CLASS=Product
780
+ UpdateConversionsJob.perform_now("Product")
688
781
  ```
689
782
 
690
- This can make a huge difference on the quality of your search.
783
+ And set it up to run daily.
691
784
 
692
- For a more performant way to reindex conversion data, check out [performant conversions](#performant-conversions).
785
+ ```ruby
786
+ UpdateConversionsJob.perform_later("Product", since: 1.day.ago)
787
+ ```
693
788
 
694
789
  ## Personalized Results
695
790
 
@@ -709,7 +804,7 @@ end
709
804
  Reindex and search with:
710
805
 
711
806
  ```ruby
712
- Product.search "milk", boost_where: {orderer_ids: current_user.id}
807
+ Product.search("milk", boost_where: {orderer_ids: current_user.id})
713
808
  ```
714
809
 
715
810
  ## Instant Search / Autocomplete
@@ -733,7 +828,7 @@ end
733
828
  Reindex and search with:
734
829
 
735
830
  ```ruby
736
- Movie.search "jurassic pa", fields: [:title], match: :word_start
831
+ Movie.search("jurassic pa", fields: [:title], match: :word_start)
737
832
  ```
738
833
 
739
834
  Typically, you want to use a JavaScript library like [typeahead.js](https://twitter.github.io/typeahead.js/) or [jQuery UI](https://jqueryui.com/autocomplete/).
@@ -793,7 +888,7 @@ end
793
888
  Reindex and search with:
794
889
 
795
890
  ```ruby
796
- products = Product.search "peantu butta", suggest: true
891
+ products = Product.search("peantu butta", suggest: true)
797
892
  products.suggestions # ["peanut butter"]
798
893
  ```
799
894
 
@@ -804,40 +899,40 @@ products.suggestions # ["peanut butter"]
804
899
  ![Aggregations](https://gist.github.com/ankane/b6988db2802aca68a589b31e41b44195/raw/40febe948427e5bc53ec4e5dc248822855fef76f/facets.png)
805
900
 
806
901
  ```ruby
807
- products = Product.search "chuck taylor", aggs: [:product_type, :gender, :brand]
902
+ products = Product.search("chuck taylor", aggs: [:product_type, :gender, :brand])
808
903
  products.aggs
809
904
  ```
810
905
 
811
906
  By default, `where` conditions apply to aggregations.
812
907
 
813
908
  ```ruby
814
- Product.search "wingtips", where: {color: "brandy"}, aggs: [:size]
909
+ Product.search("wingtips", where: {color: "brandy"}, aggs: [:size])
815
910
  # aggregations for brandy wingtips are returned
816
911
  ```
817
912
 
818
913
  Change this with:
819
914
 
820
915
  ```ruby
821
- Product.search "wingtips", where: {color: "brandy"}, aggs: [:size], smart_aggs: false
916
+ Product.search("wingtips", where: {color: "brandy"}, aggs: [:size], smart_aggs: false)
822
917
  # aggregations for all wingtips are returned
823
918
  ```
824
919
 
825
920
  Set `where` conditions for each aggregation separately with:
826
921
 
827
922
  ```ruby
828
- Product.search "wingtips", aggs: {size: {where: {color: "brandy"}}}
923
+ Product.search("wingtips", aggs: {size: {where: {color: "brandy"}}})
829
924
  ```
830
925
 
831
926
  Limit
832
927
 
833
928
  ```ruby
834
- Product.search "apples", aggs: {store_id: {limit: 10}}
929
+ Product.search("apples", aggs: {store_id: {limit: 10}})
835
930
  ```
836
931
 
837
932
  Order
838
933
 
839
934
  ```ruby
840
- Product.search "wingtips", aggs: {color: {order: {"_key" => "asc"}}} # alphabetically
935
+ Product.search("wingtips", aggs: {color: {order: {"_key" => "asc"}}}) # alphabetically
841
936
  ```
842
937
 
843
938
  [All of these options are supported](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-bucket-terms-aggregation.html#search-aggregations-bucket-terms-aggregation-order)
@@ -846,31 +941,31 @@ Ranges
846
941
 
847
942
  ```ruby
848
943
  price_ranges = [{to: 20}, {from: 20, to: 50}, {from: 50}]
849
- Product.search "*", aggs: {price: {ranges: price_ranges}}
944
+ Product.search("*", aggs: {price: {ranges: price_ranges}})
850
945
  ```
851
946
 
852
947
  Minimum document count
853
948
 
854
949
  ```ruby
855
- Product.search "apples", aggs: {store_id: {min_doc_count: 2}}
950
+ Product.search("apples", aggs: {store_id: {min_doc_count: 2}})
856
951
  ```
857
952
 
858
953
  Script support
859
954
 
860
955
  ```ruby
861
- Product.search "*", aggs: {color: {script: {source: "'Color: ' + _value"}}}
956
+ Product.search("*", aggs: {color: {script: {source: "'Color: ' + _value"}}})
862
957
  ```
863
958
 
864
959
  Date histogram
865
960
 
866
961
  ```ruby
867
- Product.search "pear", aggs: {products_per_year: {date_histogram: {field: :created_at, interval: :year}}}
962
+ Product.search("pear", aggs: {products_per_year: {date_histogram: {field: :created_at, interval: :year}}})
868
963
  ```
869
964
 
870
965
  For other aggregation types, including sub-aggregations, use `body_options`:
871
966
 
872
967
  ```ruby
873
- Product.search "orange", body_options: {aggs: {price: {histogram: {field: :price, interval: 10}}}}
968
+ Product.search("orange", body_options: {aggs: {price: {histogram: {field: :price, interval: 10}}}})
874
969
  ```
875
970
 
876
971
  ## Highlight
@@ -878,7 +973,7 @@ Product.search "orange", body_options: {aggs: {price: {histogram: {field: :price
878
973
  Specify which fields to index with highlighting.
879
974
 
880
975
  ```ruby
881
- class Product < ApplicationRecord
976
+ class Band < ApplicationRecord
882
977
  searchkick highlight: [:name]
883
978
  end
884
979
  ```
@@ -886,7 +981,7 @@ end
886
981
  Highlight the search query in the results.
887
982
 
888
983
  ```ruby
889
- bands = Band.search "cinema", highlight: true
984
+ bands = Band.search("cinema", highlight: true)
890
985
  ```
891
986
 
892
987
  View the highlighted fields with:
@@ -900,19 +995,19 @@ end
900
995
  To change the tag, use:
901
996
 
902
997
  ```ruby
903
- Band.search "cinema", highlight: {tag: "<strong>"}
998
+ Band.search("cinema", highlight: {tag: "<strong>"})
904
999
  ```
905
1000
 
906
1001
  To highlight and search different fields, use:
907
1002
 
908
1003
  ```ruby
909
- Band.search "cinema", fields: [:name], highlight: {fields: [:description]}
1004
+ Band.search("cinema", fields: [:name], highlight: {fields: [:description]})
910
1005
  ```
911
1006
 
912
1007
  By default, the entire field is highlighted. To get small snippets instead, use:
913
1008
 
914
1009
  ```ruby
915
- bands = Band.search "cinema", highlight: {fragment_size: 20}
1010
+ bands = Band.search("cinema", highlight: {fragment_size: 20})
916
1011
  bands.with_highlights(multiple: true).each do |band, highlights|
917
1012
  highlights[:name].join(" and ")
918
1013
  end
@@ -921,7 +1016,7 @@ end
921
1016
  Additional options can be specified for each field:
922
1017
 
923
1018
  ```ruby
924
- Band.search "cinema", fields: [:name], highlight: {fields: {name: {fragment_size: 200}}}
1019
+ Band.search("cinema", fields: [:name], highlight: {fields: {name: {fragment_size: 200}}})
925
1020
  ```
926
1021
 
927
1022
  You can find available highlight options in the [Elasticsearch reference](https://www.elastic.co/guide/en/elasticsearch/reference/current/highlighting.html).
@@ -950,13 +1045,13 @@ end
950
1045
  Reindex and search with:
951
1046
 
952
1047
  ```ruby
953
- Restaurant.search "pizza", where: {location: {near: {lat: 37, lon: -114}, within: "100mi"}} # or 160km
1048
+ Restaurant.search("pizza", where: {location: {near: {lat: 37, lon: -114}, within: "100mi"}}) # or 160km
954
1049
  ```
955
1050
 
956
1051
  Bounded by a box
957
1052
 
958
1053
  ```ruby
959
- Restaurant.search "sushi", where: {location: {top_left: {lat: 38, lon: -123}, bottom_right: {lat: 37, lon: -122}}}
1054
+ Restaurant.search("sushi", where: {location: {top_left: {lat: 38, lon: -123}, bottom_right: {lat: 37, lon: -122}}})
960
1055
  ```
961
1056
 
962
1057
  **Note:** `top_right` and `bottom_left` also work
@@ -964,7 +1059,7 @@ Restaurant.search "sushi", where: {location: {top_left: {lat: 38, lon: -123}, bo
964
1059
  Bounded by a polygon
965
1060
 
966
1061
  ```ruby
967
- Restaurant.search "dessert", where: {location: {geo_polygon: {points: [{lat: 38, lon: -123}, {lat: 39, lon: -123}, {lat: 37, lon: 122}]}}}
1062
+ Restaurant.search("dessert", where: {location: {geo_polygon: {points: [{lat: 38, lon: -123}, {lat: 39, lon: -123}, {lat: 37, lon: 122}]}}})
968
1063
  ```
969
1064
 
970
1065
  ### Boost By Distance
@@ -972,13 +1067,13 @@ Restaurant.search "dessert", where: {location: {geo_polygon: {points: [{lat: 38,
972
1067
  Boost results by distance - closer results are boosted more
973
1068
 
974
1069
  ```ruby
975
- Restaurant.search "noodles", boost_by_distance: {location: {origin: {lat: 37, lon: -122}}}
1070
+ Restaurant.search("noodles", boost_by_distance: {location: {origin: {lat: 37, lon: -122}}})
976
1071
  ```
977
1072
 
978
1073
  Also supports [additional options](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-function-score-query.html#function-decay)
979
1074
 
980
1075
  ```ruby
981
- Restaurant.search "wings", boost_by_distance: {location: {origin: {lat: 37, lon: -122}, function: "linear", scale: "30mi", decay: 0.5}}
1076
+ Restaurant.search("wings", boost_by_distance: {location: {origin: {lat: 37, lon: -122}, function: "linear", scale: "30mi", decay: 0.5}})
982
1077
  ```
983
1078
 
984
1079
  ### Geo Shapes
@@ -1005,19 +1100,19 @@ See the [Elasticsearch documentation](https://www.elastic.co/guide/en/elasticsea
1005
1100
  Find shapes intersecting with the query shape
1006
1101
 
1007
1102
  ```ruby
1008
- Restaurant.search "soup", where: {bounds: {geo_shape: {type: "polygon", coordinates: [[{lat: 38, lon: -123}, ...]]}}}
1103
+ Restaurant.search("soup", where: {bounds: {geo_shape: {type: "polygon", coordinates: [[{lat: 38, lon: -123}, ...]]}}})
1009
1104
  ```
1010
1105
 
1011
1106
  Falling entirely within the query shape
1012
1107
 
1013
1108
  ```ruby
1014
- Restaurant.search "salad", where: {bounds: {geo_shape: {type: "circle", relation: "within", coordinates: [{lat: 38, lon: -123}], radius: "1km"}}}
1109
+ Restaurant.search("salad", where: {bounds: {geo_shape: {type: "circle", relation: "within", coordinates: [{lat: 38, lon: -123}], radius: "1km"}}})
1015
1110
  ```
1016
1111
 
1017
1112
  Not touching the query shape
1018
1113
 
1019
1114
  ```ruby
1020
- Restaurant.search "burger", where: {bounds: {geo_shape: {type: "envelope", relation: "disjoint", coordinates: [{lat: 38, lon: -123}, {lat: 37, lon: -122}]}}}
1115
+ Restaurant.search("burger", where: {bounds: {geo_shape: {type: "envelope", relation: "disjoint", coordinates: [{lat: 38, lon: -123}, {lat: 37, lon: -122}]}}})
1021
1116
  ```
1022
1117
 
1023
1118
  ## Inheritance
@@ -1047,9 +1142,9 @@ Dog.reindex # equivalent, all animals reindexed
1047
1142
  And to search, use:
1048
1143
 
1049
1144
  ```ruby
1050
- Animal.search "*" # all animals
1051
- Dog.search "*" # just dogs
1052
- Animal.search "*", type: [Dog, Cat] # just cats and dogs
1145
+ Animal.search("*") # all animals
1146
+ Dog.search("*") # just dogs
1147
+ Animal.search("*", type: [Dog, Cat]) # just cats and dogs
1053
1148
  ```
1054
1149
 
1055
1150
  **Notes:**
@@ -1057,7 +1152,7 @@ Animal.search "*", type: [Dog, Cat] # just cats and dogs
1057
1152
  1. The `suggest` option retrieves suggestions from the parent at the moment.
1058
1153
 
1059
1154
  ```ruby
1060
- Dog.search "airbudd", suggest: true # suggestions for all animals
1155
+ Dog.search("airbudd", suggest: true) # suggestions for all animals
1061
1156
  ```
1062
1157
  2. This relies on a `type` field that is automatically added to the indexed document. Be wary of defining your own `type` field in `search_data`, as it will take precedence.
1063
1158
 
@@ -1380,6 +1475,8 @@ Create an initializer with multiple hosts:
1380
1475
 
1381
1476
  ```ruby
1382
1477
  ENV["ELASTICSEARCH_URL"] = "https://user:password@host1,https://user:password@host2"
1478
+ # or
1479
+ ENV["OPENSEARCH_URL"] = "https://user:password@host1,https://user:password@host2"
1383
1480
  ```
1384
1481
 
1385
1482
  See [elastic-transport](https://github.com/elastic/elastic-transport-ruby) or [opensearch-transport](https://github.com/opensearch-project/opensearch-ruby/tree/main/opensearch-transport) for a complete list of options.
@@ -1562,7 +1659,7 @@ end
1562
1659
  Reindex and search with:
1563
1660
 
1564
1661
  ```ruby
1565
- Business.search "ice cream", routing: params[:city_id]
1662
+ Business.search("ice cream", routing: params[:city_id])
1566
1663
  ```
1567
1664
 
1568
1665
  ### Partial Reindexing
@@ -1573,11 +1670,12 @@ Reindex a subset of attributes to reduce time spent generating search data and c
1573
1670
  class Product < ApplicationRecord
1574
1671
  def search_data
1575
1672
  {
1576
- name: name
1577
- }.merge(search_prices)
1673
+ name: name,
1674
+ category: category
1675
+ }.merge(prices_data)
1578
1676
  end
1579
1677
 
1580
- def search_prices
1678
+ def prices_data
1581
1679
  {
1582
1680
  price: price,
1583
1681
  sale_price: sale_price
@@ -1589,68 +1687,7 @@ end
1589
1687
  And use:
1590
1688
 
1591
1689
  ```ruby
1592
- Product.reindex(:search_prices)
1593
- ```
1594
-
1595
- ### Performant Conversions
1596
-
1597
- 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.
1598
-
1599
- ```ruby
1600
- class Product < ApplicationRecord
1601
- def search_data
1602
- {
1603
- name: name
1604
- }.merge(search_conversions)
1605
- end
1606
-
1607
- def search_conversions
1608
- {
1609
- conversions: Rails.cache.read("search_conversions:#{self.class.name}:#{id}") || {}
1610
- }
1611
- end
1612
- end
1613
- ```
1614
-
1615
- Create a job to update the cache and reindex records with new conversions.
1616
-
1617
- ```ruby
1618
- class ReindexConversionsJob < ApplicationJob
1619
- def perform(class_name)
1620
- # get records that have a recent conversion
1621
- recently_converted_ids =
1622
- Searchjoy::Search.where("convertable_type = ? AND converted_at > ?", class_name, 1.day.ago)
1623
- .order(:convertable_id).distinct.pluck(:convertable_id)
1624
-
1625
- # split into groups
1626
- recently_converted_ids.in_groups_of(1000, false) do |ids|
1627
- # fetch conversions
1628
- conversions =
1629
- Searchjoy::Search.where(convertable_id: ids, convertable_type: class_name)
1630
- .group(:convertable_id, :query).distinct.count(:user_id)
1631
-
1632
- # group conversions by record
1633
- conversions_by_record = {}
1634
- conversions.each do |(id, query), count|
1635
- (conversions_by_record[id] ||= {})[query] = count
1636
- end
1637
-
1638
- # write to cache
1639
- conversions_by_record.each do |id, conversions|
1640
- Rails.cache.write("search_conversions:#{class_name}:#{id}", conversions)
1641
- end
1642
-
1643
- # partial reindex
1644
- class_name.constantize.where(id: ids).reindex(:search_conversions)
1645
- end
1646
- end
1647
- end
1648
- ```
1649
-
1650
- Run the job with:
1651
-
1652
- ```ruby
1653
- ReindexConversionsJob.perform_later("Product")
1690
+ Product.reindex(:prices_data)
1654
1691
  ```
1655
1692
 
1656
1693
  ## Advanced
@@ -1685,7 +1722,7 @@ end
1685
1722
  And use the `body` option to search:
1686
1723
 
1687
1724
  ```ruby
1688
- products = Product.search body: {query: {match: {name: "milk"}}}
1725
+ products = Product.search(body: {query: {match: {name: "milk"}}})
1689
1726
  ```
1690
1727
 
1691
1728
  View the response with:
@@ -1697,14 +1734,14 @@ products.response
1697
1734
  To modify the query generated by Searchkick, use:
1698
1735
 
1699
1736
  ```ruby
1700
- products = Product.search "milk", body_options: {min_score: 1}
1737
+ products = Product.search("milk", body_options: {min_score: 1})
1701
1738
  ```
1702
1739
 
1703
1740
  or
1704
1741
 
1705
1742
  ```ruby
1706
1743
  products =
1707
- Product.search "apples" do |body|
1744
+ Product.search("apples") do |body|
1708
1745
  body[:min_score] = 1
1709
1746
  end
1710
1747
  ```
@@ -1736,7 +1773,7 @@ Then use `products` and `coupons` as typical results.
1736
1773
  Search across multiple models with:
1737
1774
 
1738
1775
  ```ruby
1739
- Searchkick.search "milk", models: [Product, Category]
1776
+ Searchkick.search("milk", models: [Product, Category])
1740
1777
  ```
1741
1778
 
1742
1779
  Boost specific models with:
@@ -1762,7 +1799,7 @@ end
1762
1799
  You can also scroll batches manually.
1763
1800
 
1764
1801
  ```ruby
1765
- products = Product.search "*", scroll: "1m"
1802
+ products = Product.search("*", scroll: "1m")
1766
1803
  while products.any?
1767
1804
  # process batch ...
1768
1805
 
@@ -1793,7 +1830,7 @@ Product.search("pears", body_options: {track_total_hits: true})
1793
1830
  To query nested data, use dot notation.
1794
1831
 
1795
1832
  ```ruby
1796
- User.search "san", fields: ["address.city"], where: {"address.zip_code" => 12345}
1833
+ Product.search("san", fields: ["store.city"], where: {"store.zip_code" => 12345})
1797
1834
  ```
1798
1835
 
1799
1836
  ## Reference
@@ -1923,7 +1960,7 @@ Searchkick.queue_name = :search_reindex
1923
1960
  Eager load associations
1924
1961
 
1925
1962
  ```ruby
1926
- Product.search "milk", includes: [:brand, :stores]
1963
+ Product.search("milk", includes: [:brand, :stores])
1927
1964
  ```
1928
1965
 
1929
1966
  Eager load different associations by model
@@ -1935,7 +1972,7 @@ Searchkick.search("*", models: [Product, Store], model_includes: {Product => [:
1935
1972
  Run additional scopes on results
1936
1973
 
1937
1974
  ```ruby
1938
- Product.search "milk", scope_results: ->(r) { r.with_attached_images }
1975
+ Product.search("milk", scope_results: ->(r) { r.with_attached_images })
1939
1976
  ```
1940
1977
 
1941
1978
  Specify default fields to search
@@ -2031,13 +2068,25 @@ rake searchkick:reindex:all
2031
2068
  Turn on misspellings after a certain number of characters
2032
2069
 
2033
2070
  ```ruby
2034
- Product.search "api", misspellings: {prefix_length: 2} # api, apt, no ahi
2071
+ Product.search("api", misspellings: {prefix_length: 2}) # api, apt, no ahi
2072
+ ```
2073
+
2074
+ **Note:** With this option, if the query length is the same as `prefix_length`, misspellings are turned off with Elasticsearch 7 and OpenSearch 1
2075
+
2076
+ ```ruby
2077
+ Product.search("ah", misspellings: {prefix_length: 2}) # ah, no aha
2035
2078
  ```
2036
2079
 
2037
- **Note:** With this option, if the query length is the same as `prefix_length`, misspellings are turned off with Elasticsearch 7 and OpenSearch
2080
+ BigDecimal values are indexed as floats by default so they can be used for boosting. Convert them to strings to keep full precision.
2038
2081
 
2039
2082
  ```ruby
2040
- Product.search "ah", misspellings: {prefix_length: 2} # ah, no aha
2083
+ class Product < ApplicationRecord
2084
+ def search_data
2085
+ {
2086
+ units: units.to_s("F")
2087
+ }
2088
+ end
2089
+ end
2041
2090
  ```
2042
2091
 
2043
2092
  ## Gotchas
@@ -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?
@@ -66,7 +66,7 @@ module Searchkick
66
66
  alias_method Searchkick.search_method_name, :searchkick_search if Searchkick.search_method_name
67
67
 
68
68
  def searchkick_index(name: nil)
69
- index_name = name || searchkick_index_name
69
+ index_name = name || searchkick_klass.searchkick_index_name
70
70
  index_name = index_name.call if index_name.respond_to?(:call)
71
71
  index_cache = class_variable_get(:@@searchkick_index_cache)
72
72
  index_cache.fetch(index_name) { Searchkick::Index.new(index_name, searchkick_options) }
@@ -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
 
@@ -1,13 +1,20 @@
1
1
  module Searchkick
2
2
  class Relation
3
+ NO_DEFAULT_VALUE = Object.new
4
+
3
5
  # note: modifying body directly is not supported
4
6
  # and has no impact on query after being executed
5
7
  # TODO freeze body object?
6
- delegate :body, :params, to: :@query
8
+ delegate :body, :params, to: :query
7
9
  delegate_missing_to :private_execute
8
10
 
9
11
  def initialize(model, term = "*", **options)
10
- @query = Query.new(model, term, **options)
12
+ @model = model
13
+ @term = term
14
+ @options = options
15
+
16
+ # generate query to validate options
17
+ query
11
18
  end
12
19
 
13
20
  # same as Active Record
@@ -23,14 +30,196 @@ module Searchkick
23
30
  self
24
31
  end
25
32
 
33
+ # experimental
34
+ def limit(value)
35
+ clone.limit!(value)
36
+ end
37
+
38
+ # experimental
39
+ def limit!(value)
40
+ check_loaded
41
+ @options[:limit] = value
42
+ self
43
+ end
44
+
45
+ # experimental
46
+ def offset(value = NO_DEFAULT_VALUE)
47
+ # TODO remove in Searchkick 6
48
+ if value == NO_DEFAULT_VALUE
49
+ private_execute.offset
50
+ else
51
+ clone.offset!(value)
52
+ end
53
+ end
54
+
55
+ # experimental
56
+ def offset!(value)
57
+ check_loaded
58
+ @options[:offset] = value
59
+ self
60
+ end
61
+
62
+ # experimental
63
+ def page(value)
64
+ clone.page!(value)
65
+ end
66
+
67
+ # experimental
68
+ def page!(value)
69
+ check_loaded
70
+ @options[:page] = value
71
+ self
72
+ end
73
+
74
+ # experimental
75
+ def per_page(value = NO_DEFAULT_VALUE)
76
+ # TODO remove in Searchkick 6
77
+ if value == NO_DEFAULT_VALUE
78
+ private_execute.per_page
79
+ else
80
+ clone.per_page!(value)
81
+ end
82
+ end
83
+
84
+ # experimental
85
+ def per_page!(value)
86
+ check_loaded
87
+ @options[:per_page] = value
88
+ self
89
+ end
90
+
91
+ # experimental
92
+ def where(value = NO_DEFAULT_VALUE)
93
+ if value == NO_DEFAULT_VALUE
94
+ Where.new(self)
95
+ else
96
+ clone.where!(value)
97
+ end
98
+ end
99
+
100
+ # experimental
101
+ def where!(value)
102
+ check_loaded
103
+ if @options[:where]
104
+ @options[:where] = {_and: [@options[:where], ensure_permitted(value)]}
105
+ else
106
+ @options[:where] = ensure_permitted(value)
107
+ end
108
+ self
109
+ end
110
+
111
+ # experimental
112
+ def rewhere(value)
113
+ clone.rewhere!(value)
114
+ end
115
+
116
+ # experimental
117
+ def rewhere!(value)
118
+ check_loaded
119
+ @options[:where] = ensure_permitted(value)
120
+ self
121
+ end
122
+
123
+ # experimental
124
+ def order(*values)
125
+ clone.order!(*values)
126
+ end
127
+
128
+ # experimental
129
+ def order!(*values)
130
+ values = values.first if values.size == 1 && values.first.is_a?(Array)
131
+ check_loaded
132
+ (@options[:order] ||= []).concat(values)
133
+ self
134
+ end
135
+
136
+ # experimental
137
+ def reorder(*values)
138
+ clone.reorder!(*values)
139
+ end
140
+
141
+ # experimental
142
+ def reorder!(*values)
143
+ check_loaded
144
+ @options[:order] = values
145
+ self
146
+ end
147
+
148
+ # experimental
149
+ def select(*values, &block)
150
+ if block_given?
151
+ private_execute.select(*values, &block)
152
+ else
153
+ clone.select!(*values)
154
+ end
155
+ end
156
+
157
+ # experimental
158
+ def select!(*values)
159
+ check_loaded
160
+ (@options[:select] ||= []).concat(values)
161
+ self
162
+ end
163
+
164
+ # experimental
165
+ def reselect(*values)
166
+ clone.reselect!(*values)
167
+ end
168
+
169
+ # experimental
170
+ def reselect!(*values)
171
+ check_loaded
172
+ @options[:select] = values
173
+ self
174
+ end
175
+
176
+ # experimental
177
+ def includes(*values)
178
+ clone.includes!(*values)
179
+ end
180
+
181
+ # experimental
182
+ def includes!(*values)
183
+ check_loaded
184
+ (@options[:includes] ||= []).concat(values)
185
+ self
186
+ end
187
+
188
+ # experimental
189
+ def only(*keys)
190
+ Relation.new(@model, @term, **@options.slice(*keys))
191
+ end
192
+
193
+ # experimental
194
+ def except(*keys)
195
+ Relation.new(@model, @term, **@options.except(*keys))
196
+ end
197
+
198
+ def loaded?
199
+ !@execute.nil?
200
+ end
201
+
26
202
  private
27
203
 
28
204
  def private_execute
29
- @execute ||= @query.execute
205
+ @execute ||= query.execute
30
206
  end
31
207
 
32
208
  def query
33
- @query
209
+ @query ||= Query.new(@model, @term, **@options)
210
+ end
211
+
212
+ def check_loaded
213
+ raise Error, "Relation loaded" if loaded?
214
+
215
+ # reset query since options will change
216
+ @query = nil
217
+ end
218
+
219
+ # provides *very* basic protection from unfiltered parameters
220
+ # this is not meant to be comprehensive and may be expanded in the future
221
+ def ensure_permitted(obj)
222
+ obj.to_h
34
223
  end
35
224
  end
36
225
  end
@@ -1,3 +1,3 @@
1
1
  module Searchkick
2
- VERSION = "5.0.1"
2
+ VERSION = "5.0.4"
3
3
  end
@@ -0,0 +1,11 @@
1
+ module Searchkick
2
+ class Where
3
+ def initialize(relation)
4
+ @relation = relation
5
+ end
6
+
7
+ def not(value)
8
+ @relation.where(_not: value)
9
+ end
10
+ end
11
+ end
data/lib/searchkick.rb CHANGED
@@ -27,6 +27,7 @@ require "searchkick/relation"
27
27
  require "searchkick/relation_indexer"
28
28
  require "searchkick/results"
29
29
  require "searchkick/version"
30
+ require "searchkick/where"
30
31
 
31
32
  # integrations
32
33
  require "searchkick/railtie" if defined?(Rails)
@@ -134,8 +135,9 @@ module Searchkick
134
135
  @opensearch
135
136
  end
136
137
 
137
- def self.server_below?(version)
138
- 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
139
141
  Gem::Version.new(server_version.split("-")[0]) < Gem::Version.new(version.split("-")[0])
140
142
  end
141
143
 
@@ -283,7 +285,7 @@ module Searchkick
283
285
  relation
284
286
  end
285
287
 
286
- # private
288
+ # public (for reindexing conversions)
287
289
  def self.load_model(class_name, allow_child: false)
288
290
  model = class_name.safe_constantize
289
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.1
4
+ version: 5.0.4
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-02-27 00:00:00.000000000 Z
11
+ date: 2022-06-17 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activemodel
@@ -71,6 +71,7 @@ files:
71
71
  - lib/searchkick/relation_indexer.rb
72
72
  - lib/searchkick/results.rb
73
73
  - lib/searchkick/version.rb
74
+ - lib/searchkick/where.rb
74
75
  - lib/tasks/searchkick.rake
75
76
  homepage: https://github.com/ankane/searchkick
76
77
  licenses:
@@ -91,7 +92,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
91
92
  - !ruby/object:Gem::Version
92
93
  version: '0'
93
94
  requirements: []
94
- rubygems_version: 3.3.3
95
+ rubygems_version: 3.3.7
95
96
  signing_key:
96
97
  specification_version: 4
97
98
  summary: Intelligent search made easy with Rails and Elasticsearch or OpenSearch