searchkick 5.0.0 → 5.0.3
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 +4 -4
- data/CHANGELOG.md +14 -1
- data/README.md +83 -73
- data/lib/searchkick/index.rb +29 -15
- data/lib/searchkick/index_options.rb +1 -1
- data/lib/searchkick/model.rb +9 -7
- data/lib/searchkick/query.rb +2 -2
- data/lib/searchkick/record_data.rb +1 -0
- data/lib/searchkick/record_indexer.rb +2 -2
- data/lib/searchkick/reindex_queue.rb +1 -1
- data/lib/searchkick/relation.rb +193 -4
- data/lib/searchkick/results.rb +5 -5
- data/lib/searchkick/version.rb +1 -1
- data/lib/searchkick/where.rb +11 -0
- data/lib/searchkick.rb +5 -4
- metadata +4 -3
    
        checksums.yaml
    CHANGED
    
    | @@ -1,7 +1,7 @@ | |
| 1 1 | 
             
            ---
         | 
| 2 2 | 
             
            SHA256:
         | 
| 3 | 
            -
              metadata.gz:  | 
| 4 | 
            -
              data.tar.gz:  | 
| 3 | 
            +
              metadata.gz: ec7741cb306a56f1a5ae5a07450c5c102236bc6103079fc34a5f602fa2853b31
         | 
| 4 | 
            +
              data.tar.gz: 2bd747ee31846c901ce2a58125b34e9c90af6937a2c7dc041f2dd9f69701f1e9
         | 
| 5 5 | 
             
            SHA512:
         | 
| 6 | 
            -
              metadata.gz:  | 
| 7 | 
            -
              data.tar.gz:  | 
| 6 | 
            +
              metadata.gz: f92f2a3c7bb27862b1768f5ecedc19ad0eab515d62515f94d72412ed7c22a20493049dd0e52cba21b3a43d8c846b0dc0c6cda40c68edc12f6af37e8487fb9403
         | 
| 7 | 
            +
              data.tar.gz: f69a1cfc401bad0f09bda3f2788b1096cc004dcb765eb20b3d8ada06202185eeae630d953005298e5944ed1b8c239fa808eeebb32e9440cc7ae9a32e771469dc
         | 
    
        data/CHANGELOG.md
    CHANGED
    
    | @@ -1,3 +1,16 @@ | |
| 1 | 
            +
            ## 5.0.3 (2022-03-13)
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            - Fixed context for index name for inherited models
         | 
| 4 | 
            +
             | 
| 5 | 
            +
            ## 5.0.2 (2022-03-03)
         | 
| 6 | 
            +
             | 
| 7 | 
            +
            - Fixed index name for inherited models
         | 
| 8 | 
            +
             | 
| 9 | 
            +
            ## 5.0.1 (2022-02-27)
         | 
| 10 | 
            +
             | 
| 11 | 
            +
            - Prefer `mode: :async` over `async: true` for full reindex
         | 
| 12 | 
            +
            - Fixed instance method overriding with concerns
         | 
| 13 | 
            +
             | 
| 1 14 | 
             
            ## 5.0.0 (2022-02-21)
         | 
| 2 15 |  | 
| 3 16 | 
             
            - Searches now use lazy loading (similar to Active Record)
         | 
| @@ -15,7 +28,7 @@ | |
| 15 28 | 
             
            - Raise error when `search` called on relations
         | 
| 16 29 | 
             
            - Raise `ArgumentError` (instead of warning) for invalid regular expression modifiers
         | 
| 17 30 | 
             
            - Raise `ArgumentError` instead of `RuntimeError` for unknown operators
         | 
| 18 | 
            -
            - Removed mapping of `id` to `_id` with `order` option
         | 
| 31 | 
            +
            - Removed mapping of `id` to `_id` with `order` option (not supported in Elasticsearch 8)
         | 
| 19 32 | 
             
            - Removed `wordnet` option (no longer worked)
         | 
| 20 33 | 
             
            - Removed dependency on `elasticsearch` gem (can use `elasticsearch` or `opensearch-ruby`)
         | 
| 21 34 | 
             
            - Dropped support for Elasticsearch 6
         | 
    
        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
         | 
| @@ -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 | 
| 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 | 
| 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 | 
| 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 | 
| 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 | 
| 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 | 
            -
             | 
| 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 | 
            -
             | 
| 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 | 
| 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 | 
| 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 | 
| 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 | 
| 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 | 
| 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 | 
| 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 | 
| 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 | 
| 502 | 
            +
            Product.search("🍨🍰", emoji: true)
         | 
| 503 503 | 
             
            ```
         | 
| 504 504 |  | 
| 505 505 | 
             
            ## Indexing
         | 
| @@ -596,7 +596,7 @@ You can also do bulk updates. | |
| 596 596 |  | 
| 597 597 | 
             
            ```ruby
         | 
| 598 598 | 
             
            Searchkick.callbacks(:bulk) do
         | 
| 599 | 
            -
               | 
| 599 | 
            +
              Product.find_each(&:update_fields)
         | 
| 600 600 | 
             
            end
         | 
| 601 601 | 
             
            ```
         | 
| 602 602 |  | 
| @@ -604,7 +604,7 @@ Or temporarily skip updates. | |
| 604 604 |  | 
| 605 605 | 
             
            ```ruby
         | 
| 606 606 | 
             
            Searchkick.callbacks(false) do
         | 
| 607 | 
            -
               | 
| 607 | 
            +
              Product.find_each(&:update_fields)
         | 
| 608 608 | 
             
            end
         | 
| 609 609 | 
             
            ```
         | 
| 610 610 |  | 
| @@ -651,7 +651,7 @@ end | |
| 651 651 | 
             
            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 652 |  | 
| 653 653 | 
             
            ```ruby
         | 
| 654 | 
            -
            Product.search | 
| 654 | 
            +
            Product.search("apple", track: {user_id: current_user.id})
         | 
| 655 655 | 
             
            ```
         | 
| 656 656 |  | 
| 657 657 | 
             
            [See the docs](https://github.com/ankane/searchjoy) for how to install and use.
         | 
| @@ -683,7 +683,7 @@ end | |
| 683 683 |  | 
| 684 684 | 
             
            Reindex and set up a cron job to add new conversions daily.
         | 
| 685 685 |  | 
| 686 | 
            -
            ``` | 
| 686 | 
            +
            ```sh
         | 
| 687 687 | 
             
            rake searchkick:reindex CLASS=Product
         | 
| 688 688 | 
             
            ```
         | 
| 689 689 |  | 
| @@ -709,7 +709,7 @@ end | |
| 709 709 | 
             
            Reindex and search with:
         | 
| 710 710 |  | 
| 711 711 | 
             
            ```ruby
         | 
| 712 | 
            -
            Product.search | 
| 712 | 
            +
            Product.search("milk", boost_where: {orderer_ids: current_user.id})
         | 
| 713 713 | 
             
            ```
         | 
| 714 714 |  | 
| 715 715 | 
             
            ## Instant Search / Autocomplete
         | 
| @@ -733,7 +733,7 @@ end | |
| 733 733 | 
             
            Reindex and search with:
         | 
| 734 734 |  | 
| 735 735 | 
             
            ```ruby
         | 
| 736 | 
            -
            Movie.search | 
| 736 | 
            +
            Movie.search("jurassic pa", fields: [:title], match: :word_start)
         | 
| 737 737 | 
             
            ```
         | 
| 738 738 |  | 
| 739 739 | 
             
            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 +793,7 @@ end | |
| 793 793 | 
             
            Reindex and search with:
         | 
| 794 794 |  | 
| 795 795 | 
             
            ```ruby
         | 
| 796 | 
            -
            products = Product.search | 
| 796 | 
            +
            products = Product.search("peantu butta", suggest: true)
         | 
| 797 797 | 
             
            products.suggestions # ["peanut butter"]
         | 
| 798 798 | 
             
            ```
         | 
| 799 799 |  | 
| @@ -804,40 +804,40 @@ products.suggestions # ["peanut butter"] | |
| 804 804 | 
             
            
         | 
| 805 805 |  | 
| 806 806 | 
             
            ```ruby
         | 
| 807 | 
            -
            products = Product.search | 
| 807 | 
            +
            products = Product.search("chuck taylor", aggs: [:product_type, :gender, :brand])
         | 
| 808 808 | 
             
            products.aggs
         | 
| 809 809 | 
             
            ```
         | 
| 810 810 |  | 
| 811 811 | 
             
            By default, `where` conditions apply to aggregations.
         | 
| 812 812 |  | 
| 813 813 | 
             
            ```ruby
         | 
| 814 | 
            -
            Product.search | 
| 814 | 
            +
            Product.search("wingtips", where: {color: "brandy"}, aggs: [:size])
         | 
| 815 815 | 
             
            # aggregations for brandy wingtips are returned
         | 
| 816 816 | 
             
            ```
         | 
| 817 817 |  | 
| 818 818 | 
             
            Change this with:
         | 
| 819 819 |  | 
| 820 820 | 
             
            ```ruby
         | 
| 821 | 
            -
            Product.search | 
| 821 | 
            +
            Product.search("wingtips", where: {color: "brandy"}, aggs: [:size], smart_aggs: false)
         | 
| 822 822 | 
             
            # aggregations for all wingtips are returned
         | 
| 823 823 | 
             
            ```
         | 
| 824 824 |  | 
| 825 825 | 
             
            Set `where` conditions for each aggregation separately with:
         | 
| 826 826 |  | 
| 827 827 | 
             
            ```ruby
         | 
| 828 | 
            -
            Product.search | 
| 828 | 
            +
            Product.search("wingtips", aggs: {size: {where: {color: "brandy"}}})
         | 
| 829 829 | 
             
            ```
         | 
| 830 830 |  | 
| 831 831 | 
             
            Limit
         | 
| 832 832 |  | 
| 833 833 | 
             
            ```ruby
         | 
| 834 | 
            -
            Product.search | 
| 834 | 
            +
            Product.search("apples", aggs: {store_id: {limit: 10}})
         | 
| 835 835 | 
             
            ```
         | 
| 836 836 |  | 
| 837 837 | 
             
            Order
         | 
| 838 838 |  | 
| 839 839 | 
             
            ```ruby
         | 
| 840 | 
            -
            Product.search | 
| 840 | 
            +
            Product.search("wingtips", aggs: {color: {order: {"_key" => "asc"}}}) # alphabetically
         | 
| 841 841 | 
             
            ```
         | 
| 842 842 |  | 
| 843 843 | 
             
            [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 +846,31 @@ Ranges | |
| 846 846 |  | 
| 847 847 | 
             
            ```ruby
         | 
| 848 848 | 
             
            price_ranges = [{to: 20}, {from: 20, to: 50}, {from: 50}]
         | 
| 849 | 
            -
            Product.search | 
| 849 | 
            +
            Product.search("*", aggs: {price: {ranges: price_ranges}})
         | 
| 850 850 | 
             
            ```
         | 
| 851 851 |  | 
| 852 852 | 
             
            Minimum document count
         | 
| 853 853 |  | 
| 854 854 | 
             
            ```ruby
         | 
| 855 | 
            -
            Product.search | 
| 855 | 
            +
            Product.search("apples", aggs: {store_id: {min_doc_count: 2}})
         | 
| 856 856 | 
             
            ```
         | 
| 857 857 |  | 
| 858 858 | 
             
            Script support
         | 
| 859 859 |  | 
| 860 860 | 
             
            ```ruby
         | 
| 861 | 
            -
            Product.search | 
| 861 | 
            +
            Product.search("*", aggs: {color: {script: {source: "'Color: ' + _value"}}})
         | 
| 862 862 | 
             
            ```
         | 
| 863 863 |  | 
| 864 864 | 
             
            Date histogram
         | 
| 865 865 |  | 
| 866 866 | 
             
            ```ruby
         | 
| 867 | 
            -
            Product.search | 
| 867 | 
            +
            Product.search("pear", aggs: {products_per_year: {date_histogram: {field: :created_at, interval: :year}}})
         | 
| 868 868 | 
             
            ```
         | 
| 869 869 |  | 
| 870 870 | 
             
            For other aggregation types, including sub-aggregations, use `body_options`:
         | 
| 871 871 |  | 
| 872 872 | 
             
            ```ruby
         | 
| 873 | 
            -
            Product.search | 
| 873 | 
            +
            Product.search("orange", body_options: {aggs: {price: {histogram: {field: :price, interval: 10}}}})
         | 
| 874 874 | 
             
            ```
         | 
| 875 875 |  | 
| 876 876 | 
             
            ## Highlight
         | 
| @@ -878,7 +878,7 @@ Product.search "orange", body_options: {aggs: {price: {histogram: {field: :price | |
| 878 878 | 
             
            Specify which fields to index with highlighting.
         | 
| 879 879 |  | 
| 880 880 | 
             
            ```ruby
         | 
| 881 | 
            -
            class  | 
| 881 | 
            +
            class Band < ApplicationRecord
         | 
| 882 882 | 
             
              searchkick highlight: [:name]
         | 
| 883 883 | 
             
            end
         | 
| 884 884 | 
             
            ```
         | 
| @@ -886,7 +886,7 @@ end | |
| 886 886 | 
             
            Highlight the search query in the results.
         | 
| 887 887 |  | 
| 888 888 | 
             
            ```ruby
         | 
| 889 | 
            -
            bands = Band.search | 
| 889 | 
            +
            bands = Band.search("cinema", highlight: true)
         | 
| 890 890 | 
             
            ```
         | 
| 891 891 |  | 
| 892 892 | 
             
            View the highlighted fields with:
         | 
| @@ -900,19 +900,19 @@ end | |
| 900 900 | 
             
            To change the tag, use:
         | 
| 901 901 |  | 
| 902 902 | 
             
            ```ruby
         | 
| 903 | 
            -
            Band.search | 
| 903 | 
            +
            Band.search("cinema", highlight: {tag: "<strong>"})
         | 
| 904 904 | 
             
            ```
         | 
| 905 905 |  | 
| 906 906 | 
             
            To highlight and search different fields, use:
         | 
| 907 907 |  | 
| 908 908 | 
             
            ```ruby
         | 
| 909 | 
            -
            Band.search | 
| 909 | 
            +
            Band.search("cinema", fields: [:name], highlight: {fields: [:description]})
         | 
| 910 910 | 
             
            ```
         | 
| 911 911 |  | 
| 912 912 | 
             
            By default, the entire field is highlighted. To get small snippets instead, use:
         | 
| 913 913 |  | 
| 914 914 | 
             
            ```ruby
         | 
| 915 | 
            -
            bands = Band.search | 
| 915 | 
            +
            bands = Band.search("cinema", highlight: {fragment_size: 20})
         | 
| 916 916 | 
             
            bands.with_highlights(multiple: true).each do |band, highlights|
         | 
| 917 917 | 
             
              highlights[:name].join(" and ")
         | 
| 918 918 | 
             
            end
         | 
| @@ -921,7 +921,7 @@ end | |
| 921 921 | 
             
            Additional options can be specified for each field:
         | 
| 922 922 |  | 
| 923 923 | 
             
            ```ruby
         | 
| 924 | 
            -
            Band.search | 
| 924 | 
            +
            Band.search("cinema", fields: [:name], highlight: {fields: {name: {fragment_size: 200}}})
         | 
| 925 925 | 
             
            ```
         | 
| 926 926 |  | 
| 927 927 | 
             
            You can find available highlight options in the [Elasticsearch reference](https://www.elastic.co/guide/en/elasticsearch/reference/current/highlighting.html).
         | 
| @@ -950,13 +950,13 @@ end | |
| 950 950 | 
             
            Reindex and search with:
         | 
| 951 951 |  | 
| 952 952 | 
             
            ```ruby
         | 
| 953 | 
            -
            Restaurant.search | 
| 953 | 
            +
            Restaurant.search("pizza", where: {location: {near: {lat: 37, lon: -114}, within: "100mi"}}) # or 160km
         | 
| 954 954 | 
             
            ```
         | 
| 955 955 |  | 
| 956 956 | 
             
            Bounded by a box
         | 
| 957 957 |  | 
| 958 958 | 
             
            ```ruby
         | 
| 959 | 
            -
            Restaurant.search | 
| 959 | 
            +
            Restaurant.search("sushi", where: {location: {top_left: {lat: 38, lon: -123}, bottom_right: {lat: 37, lon: -122}}})
         | 
| 960 960 | 
             
            ```
         | 
| 961 961 |  | 
| 962 962 | 
             
            **Note:** `top_right` and `bottom_left` also work
         | 
| @@ -964,7 +964,7 @@ Restaurant.search "sushi", where: {location: {top_left: {lat: 38, lon: -123}, bo | |
| 964 964 | 
             
            Bounded by a polygon
         | 
| 965 965 |  | 
| 966 966 | 
             
            ```ruby
         | 
| 967 | 
            -
            Restaurant.search | 
| 967 | 
            +
            Restaurant.search("dessert", where: {location: {geo_polygon: {points: [{lat: 38, lon: -123}, {lat: 39, lon: -123}, {lat: 37, lon: 122}]}}})
         | 
| 968 968 | 
             
            ```
         | 
| 969 969 |  | 
| 970 970 | 
             
            ### Boost By Distance
         | 
| @@ -972,13 +972,13 @@ Restaurant.search "dessert", where: {location: {geo_polygon: {points: [{lat: 38, | |
| 972 972 | 
             
            Boost results by distance - closer results are boosted more
         | 
| 973 973 |  | 
| 974 974 | 
             
            ```ruby
         | 
| 975 | 
            -
            Restaurant.search | 
| 975 | 
            +
            Restaurant.search("noodles", boost_by_distance: {location: {origin: {lat: 37, lon: -122}}})
         | 
| 976 976 | 
             
            ```
         | 
| 977 977 |  | 
| 978 978 | 
             
            Also supports [additional options](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-function-score-query.html#function-decay)
         | 
| 979 979 |  | 
| 980 980 | 
             
            ```ruby
         | 
| 981 | 
            -
            Restaurant.search | 
| 981 | 
            +
            Restaurant.search("wings", boost_by_distance: {location: {origin: {lat: 37, lon: -122}, function: "linear", scale: "30mi", decay: 0.5}})
         | 
| 982 982 | 
             
            ```
         | 
| 983 983 |  | 
| 984 984 | 
             
            ### Geo Shapes
         | 
| @@ -1005,19 +1005,19 @@ See the [Elasticsearch documentation](https://www.elastic.co/guide/en/elasticsea | |
| 1005 1005 | 
             
            Find shapes intersecting with the query shape
         | 
| 1006 1006 |  | 
| 1007 1007 | 
             
            ```ruby
         | 
| 1008 | 
            -
            Restaurant.search | 
| 1008 | 
            +
            Restaurant.search("soup", where: {bounds: {geo_shape: {type: "polygon", coordinates: [[{lat: 38, lon: -123}, ...]]}}})
         | 
| 1009 1009 | 
             
            ```
         | 
| 1010 1010 |  | 
| 1011 1011 | 
             
            Falling entirely within the query shape
         | 
| 1012 1012 |  | 
| 1013 1013 | 
             
            ```ruby
         | 
| 1014 | 
            -
            Restaurant.search | 
| 1014 | 
            +
            Restaurant.search("salad", where: {bounds: {geo_shape: {type: "circle", relation: "within", coordinates: [{lat: 38, lon: -123}], radius: "1km"}}})
         | 
| 1015 1015 | 
             
            ```
         | 
| 1016 1016 |  | 
| 1017 1017 | 
             
            Not touching the query shape
         | 
| 1018 1018 |  | 
| 1019 1019 | 
             
            ```ruby
         | 
| 1020 | 
            -
            Restaurant.search | 
| 1020 | 
            +
            Restaurant.search("burger", where: {bounds: {geo_shape: {type: "envelope", relation: "disjoint", coordinates: [{lat: 38, lon: -123}, {lat: 37, lon: -122}]}}})
         | 
| 1021 1021 | 
             
            ```
         | 
| 1022 1022 |  | 
| 1023 1023 | 
             
            ## Inheritance
         | 
| @@ -1047,9 +1047,9 @@ Dog.reindex # equivalent, all animals reindexed | |
| 1047 1047 | 
             
            And to search, use:
         | 
| 1048 1048 |  | 
| 1049 1049 | 
             
            ```ruby
         | 
| 1050 | 
            -
            Animal.search | 
| 1051 | 
            -
            Dog.search | 
| 1052 | 
            -
            Animal.search | 
| 1050 | 
            +
            Animal.search("*")                   # all animals
         | 
| 1051 | 
            +
            Dog.search("*")                      # just dogs
         | 
| 1052 | 
            +
            Animal.search("*", type: [Dog, Cat]) # just cats and dogs
         | 
| 1053 1053 | 
             
            ```
         | 
| 1054 1054 |  | 
| 1055 1055 | 
             
            **Notes:**
         | 
| @@ -1057,7 +1057,7 @@ Animal.search "*", type: [Dog, Cat] # just cats and dogs | |
| 1057 1057 | 
             
            1. The `suggest` option retrieves suggestions from the parent at the moment.
         | 
| 1058 1058 |  | 
| 1059 1059 | 
             
                ```ruby
         | 
| 1060 | 
            -
                Dog.search | 
| 1060 | 
            +
                Dog.search("airbudd", suggest: true) # suggestions for all animals
         | 
| 1061 1061 | 
             
                ```
         | 
| 1062 1062 | 
             
            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 1063 |  | 
| @@ -1100,7 +1100,7 @@ Product.search_index.tokens("dieg", analyzer: "searchkick_word_search") | |
| 1100 1100 | 
             
            # ["dieg"] - match!!
         | 
| 1101 1101 | 
             
            ```
         | 
| 1102 1102 |  | 
| 1103 | 
            -
            See the [complete list of analyzers]( | 
| 1103 | 
            +
            See the [complete list of analyzers](lib/searchkick/index_options.rb#L36).
         | 
| 1104 1104 |  | 
| 1105 1105 | 
             
            ## Testing
         | 
| 1106 1106 |  | 
| @@ -1376,13 +1376,15 @@ Bonsai, Elastic Cloud, and Amazon OpenSearch Service all support encryption at r | |
| 1376 1376 |  | 
| 1377 1377 | 
             
            ### Automatic Failover
         | 
| 1378 1378 |  | 
| 1379 | 
            -
            Create an initializer  | 
| 1379 | 
            +
            Create an initializer with multiple hosts:
         | 
| 1380 1380 |  | 
| 1381 1381 | 
             
            ```ruby
         | 
| 1382 1382 | 
             
            ENV["ELASTICSEARCH_URL"] = "https://user:password@host1,https://user:password@host2"
         | 
| 1383 | 
            +
            # or
         | 
| 1384 | 
            +
            ENV["OPENSEARCH_URL"] = "https://user:password@host1,https://user:password@host2"
         | 
| 1383 1385 | 
             
            ```
         | 
| 1384 1386 |  | 
| 1385 | 
            -
            See [ | 
| 1387 | 
            +
            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.
         | 
| 1386 1388 |  | 
| 1387 1389 | 
             
            ### Lograge
         | 
| 1388 1390 |  | 
| @@ -1453,7 +1455,7 @@ end | |
| 1453 1455 | 
             
            For large data sets, you can use background jobs to parallelize reindexing.
         | 
| 1454 1456 |  | 
| 1455 1457 | 
             
            ```ruby
         | 
| 1456 | 
            -
            Product.reindex( | 
| 1458 | 
            +
            Product.reindex(mode: :async)
         | 
| 1457 1459 | 
             
            # {index_name: "products_production_20170111210018065"}
         | 
| 1458 1460 | 
             
            ```
         | 
| 1459 1461 |  | 
| @@ -1478,7 +1480,7 @@ Searchkick.reindex_status(index_name) | |
| 1478 1480 | 
             
            You can also have Searchkick wait for reindexing to complete
         | 
| 1479 1481 |  | 
| 1480 1482 | 
             
            ```ruby
         | 
| 1481 | 
            -
            Product.reindex( | 
| 1483 | 
            +
            Product.reindex(mode: :async, wait: true)
         | 
| 1482 1484 | 
             
            ```
         | 
| 1483 1485 |  | 
| 1484 1486 | 
             
            You can use [ActiveJob::TrafficControl](https://github.com/nickelser/activejob-traffic_control) to control concurrency. Install the gem:
         | 
| @@ -1504,7 +1506,7 @@ This will allow only 3 jobs to run at once. | |
| 1504 1506 | 
             
            You can specify a longer refresh interval while reindexing to increase performance.
         | 
| 1505 1507 |  | 
| 1506 1508 | 
             
            ```ruby
         | 
| 1507 | 
            -
            Product.reindex( | 
| 1509 | 
            +
            Product.reindex(mode: :async, refresh_interval: "30s")
         | 
| 1508 1510 | 
             
            ```
         | 
| 1509 1511 |  | 
| 1510 1512 | 
             
            **Note:** This only makes a noticable difference with parallel reindexing.
         | 
| @@ -1562,7 +1564,7 @@ end | |
| 1562 1564 | 
             
            Reindex and search with:
         | 
| 1563 1565 |  | 
| 1564 1566 | 
             
            ```ruby
         | 
| 1565 | 
            -
            Business.search | 
| 1567 | 
            +
            Business.search("ice cream", routing: params[:city_id])
         | 
| 1566 1568 | 
             
            ```
         | 
| 1567 1569 |  | 
| 1568 1570 | 
             
            ### Partial Reindexing
         | 
| @@ -1685,7 +1687,7 @@ end | |
| 1685 1687 | 
             
            And use the `body` option to search:
         | 
| 1686 1688 |  | 
| 1687 1689 | 
             
            ```ruby
         | 
| 1688 | 
            -
            products = Product.search | 
| 1690 | 
            +
            products = Product.search(body: {query: {match: {name: "milk"}}})
         | 
| 1689 1691 | 
             
            ```
         | 
| 1690 1692 |  | 
| 1691 1693 | 
             
            View the response with:
         | 
| @@ -1697,21 +1699,21 @@ products.response | |
| 1697 1699 | 
             
            To modify the query generated by Searchkick, use:
         | 
| 1698 1700 |  | 
| 1699 1701 | 
             
            ```ruby
         | 
| 1700 | 
            -
            products = Product.search | 
| 1702 | 
            +
            products = Product.search("milk", body_options: {min_score: 1})
         | 
| 1701 1703 | 
             
            ```
         | 
| 1702 1704 |  | 
| 1703 1705 | 
             
            or
         | 
| 1704 1706 |  | 
| 1705 1707 | 
             
            ```ruby
         | 
| 1706 1708 | 
             
            products =
         | 
| 1707 | 
            -
              Product.search | 
| 1709 | 
            +
              Product.search("apples") do |body|
         | 
| 1708 1710 | 
             
                body[:min_score] = 1
         | 
| 1709 1711 | 
             
              end
         | 
| 1710 1712 | 
             
            ```
         | 
| 1711 1713 |  | 
| 1712 1714 | 
             
            ### Client
         | 
| 1713 1715 |  | 
| 1714 | 
            -
             | 
| 1716 | 
            +
            To access the `Elasticsearch::Client` or `OpenSearch::Client` directly, use:
         | 
| 1715 1717 |  | 
| 1716 1718 | 
             
            ```ruby
         | 
| 1717 1719 | 
             
            Searchkick.client
         | 
| @@ -1736,7 +1738,7 @@ Then use `products` and `coupons` as typical results. | |
| 1736 1738 | 
             
            Search across multiple models with:
         | 
| 1737 1739 |  | 
| 1738 1740 | 
             
            ```ruby
         | 
| 1739 | 
            -
            Searchkick.search | 
| 1741 | 
            +
            Searchkick.search("milk", models: [Product, Category])
         | 
| 1740 1742 | 
             
            ```
         | 
| 1741 1743 |  | 
| 1742 1744 | 
             
            Boost specific models with:
         | 
| @@ -1762,7 +1764,7 @@ end | |
| 1762 1764 | 
             
            You can also scroll batches manually.
         | 
| 1763 1765 |  | 
| 1764 1766 | 
             
            ```ruby
         | 
| 1765 | 
            -
            products = Product.search | 
| 1767 | 
            +
            products = Product.search("*", scroll: "1m")
         | 
| 1766 1768 | 
             
            while products.any?
         | 
| 1767 1769 | 
             
              # process batch ...
         | 
| 1768 1770 |  | 
| @@ -1793,7 +1795,7 @@ Product.search("pears", body_options: {track_total_hits: true}) | |
| 1793 1795 | 
             
            To query nested data, use dot notation.
         | 
| 1794 1796 |  | 
| 1795 1797 | 
             
            ```ruby
         | 
| 1796 | 
            -
             | 
| 1798 | 
            +
            Product.search("san", fields: ["store.city"], where: {"store.zip_code" => 12345})
         | 
| 1797 1799 | 
             
            ```
         | 
| 1798 1800 |  | 
| 1799 1801 | 
             
            ## Reference
         | 
| @@ -1923,7 +1925,7 @@ Searchkick.queue_name = :search_reindex | |
| 1923 1925 | 
             
            Eager load associations
         | 
| 1924 1926 |  | 
| 1925 1927 | 
             
            ```ruby
         | 
| 1926 | 
            -
            Product.search | 
| 1928 | 
            +
            Product.search("milk", includes: [:brand, :stores])
         | 
| 1927 1929 | 
             
            ```
         | 
| 1928 1930 |  | 
| 1929 1931 | 
             
            Eager load different associations by model
         | 
| @@ -1935,7 +1937,7 @@ Searchkick.search("*",  models: [Product, Store], model_includes: {Product => [: | |
| 1935 1937 | 
             
            Run additional scopes on results
         | 
| 1936 1938 |  | 
| 1937 1939 | 
             
            ```ruby
         | 
| 1938 | 
            -
            Product.search | 
| 1940 | 
            +
            Product.search("milk", scope_results: ->(r) { r.with_attached_images })
         | 
| 1939 1941 | 
             
            ```
         | 
| 1940 1942 |  | 
| 1941 1943 | 
             
            Specify default fields to search
         | 
| @@ -2031,13 +2033,13 @@ rake searchkick:reindex:all | |
| 2031 2033 | 
             
            Turn on misspellings after a certain number of characters
         | 
| 2032 2034 |  | 
| 2033 2035 | 
             
            ```ruby
         | 
| 2034 | 
            -
            Product.search | 
| 2036 | 
            +
            Product.search("api", misspellings: {prefix_length: 2}) # api, apt, no ahi
         | 
| 2035 2037 | 
             
            ```
         | 
| 2036 2038 |  | 
| 2037 2039 | 
             
            **Note:** With this option, if the query length is the same as `prefix_length`, misspellings are turned off with Elasticsearch 7 and OpenSearch
         | 
| 2038 2040 |  | 
| 2039 2041 | 
             
            ```ruby
         | 
| 2040 | 
            -
            Product.search | 
| 2042 | 
            +
            Product.search("ah", misspellings: {prefix_length: 2}) # ah, no aha
         | 
| 2041 2043 | 
             
            ```
         | 
| 2042 2044 |  | 
| 2043 2045 | 
             
            ## Gotchas
         | 
| @@ -2087,9 +2089,17 @@ Product.search("milk") | |
| 2087 2089 | 
             
            Product.search("milk").to_a
         | 
| 2088 2090 | 
             
            ```
         | 
| 2089 2091 |  | 
| 2092 | 
            +
            You can reindex relations in the background:
         | 
| 2093 | 
            +
             | 
| 2094 | 
            +
            ```ruby
         | 
| 2095 | 
            +
            store.products.reindex(mode: :async)
         | 
| 2096 | 
            +
            # or
         | 
| 2097 | 
            +
            store.products.reindex(mode: :queue)
         | 
| 2098 | 
            +
            ```
         | 
| 2099 | 
            +
             | 
| 2090 2100 | 
             
            And there’s a [new option](#default-scopes) for models with default scopes.
         | 
| 2091 2101 |  | 
| 2092 | 
            -
            Check out the [changelog](https://github.com/ankane/searchkick/blob/master/CHANGELOG.md) for the full list of changes.
         | 
| 2102 | 
            +
            Check out the [changelog](https://github.com/ankane/searchkick/blob/master/CHANGELOG.md#500-2022-02-21) for the full list of changes.
         | 
| 2093 2103 |  | 
| 2094 2104 | 
             
            ## History
         | 
| 2095 2105 |  | 
    
        data/lib/searchkick/index.rb
    CHANGED
    
    | @@ -71,12 +71,12 @@ module Searchkick | |
| 71 71 | 
             
                      }
         | 
| 72 72 | 
             
                    )
         | 
| 73 73 |  | 
| 74 | 
            -
                   | 
| 74 | 
            +
                  Results.new(nil, response).total_count
         | 
| 75 75 | 
             
                end
         | 
| 76 76 |  | 
| 77 77 | 
             
                def promote(new_name, update_refresh_interval: false)
         | 
| 78 78 | 
             
                  if update_refresh_interval
         | 
| 79 | 
            -
                    new_index =  | 
| 79 | 
            +
                    new_index = Index.new(new_name, @options)
         | 
| 80 80 | 
             
                    settings = options[:settings] || {}
         | 
| 81 81 | 
             
                    refresh_interval = (settings[:index] && settings[:index][:refresh_interval]) || "1s"
         | 
| 82 82 | 
             
                    new_index.update_settings(index: {refresh_interval: refresh_interval})
         | 
| @@ -123,7 +123,7 @@ module Searchkick | |
| 123 123 | 
             
                def clean_indices
         | 
| 124 124 | 
             
                  indices = all_indices(unaliased: true)
         | 
| 125 125 | 
             
                  indices.each do |index|
         | 
| 126 | 
            -
                     | 
| 126 | 
            +
                    Index.new(index).delete
         | 
| 127 127 | 
             
                  end
         | 
| 128 128 | 
             
                  indices
         | 
| 129 129 | 
             
                end
         | 
| @@ -204,7 +204,7 @@ module Searchkick | |
| 204 204 | 
             
                # queue
         | 
| 205 205 |  | 
| 206 206 | 
             
                def reindex_queue
         | 
| 207 | 
            -
                   | 
| 207 | 
            +
                  ReindexQueue.new(name)
         | 
| 208 208 | 
             
                end
         | 
| 209 209 |  | 
| 210 210 | 
             
                # reindex
         | 
| @@ -237,13 +237,26 @@ module Searchkick | |
| 237 237 | 
             
                    self.refresh if refresh
         | 
| 238 238 | 
             
                    true
         | 
| 239 239 | 
             
                  else
         | 
| 240 | 
            +
                    async = options.delete(:async)
         | 
| 241 | 
            +
                    if async
         | 
| 242 | 
            +
                      if async.is_a?(Hash) && async[:wait]
         | 
| 243 | 
            +
                        # TODO warn in 5.1
         | 
| 244 | 
            +
                        # Searchkick.warn "async option is deprecated - use mode: :async, wait: true instead"
         | 
| 245 | 
            +
                        options[:wait] = true unless options.key?(:wait)
         | 
| 246 | 
            +
                      else
         | 
| 247 | 
            +
                        # TODO warn in 5.1
         | 
| 248 | 
            +
                        # Searchkick.warn "async option is deprecated - use mode: :async instead"
         | 
| 249 | 
            +
                      end
         | 
| 250 | 
            +
                      options[:mode] ||= :async
         | 
| 251 | 
            +
                    end
         | 
| 252 | 
            +
             | 
| 240 253 | 
             
                    full_reindex(relation, **options)
         | 
| 241 254 | 
             
                  end
         | 
| 242 255 | 
             
                end
         | 
| 243 256 |  | 
| 244 257 | 
             
                def create_index(index_options: nil)
         | 
| 245 258 | 
             
                  index_options ||= self.index_options
         | 
| 246 | 
            -
                  index =  | 
| 259 | 
            +
                  index = Index.new("#{name}_#{Time.now.strftime('%Y%m%d%H%M%S%L')}", @options)
         | 
| 247 260 | 
             
                  index.create(index_options)
         | 
| 248 261 | 
             
                  index
         | 
| 249 262 | 
             
                end
         | 
| @@ -326,7 +339,7 @@ module Searchkick | |
| 326 339 | 
             
                end
         | 
| 327 340 |  | 
| 328 341 | 
             
                def reindex_records(object, mode: nil, refresh: false, **options)
         | 
| 329 | 
            -
                  mode ||= Searchkick.callbacks_value || @options[:callbacks] ||  | 
| 342 | 
            +
                  mode ||= Searchkick.callbacks_value || @options[:callbacks] || :inline
         | 
| 330 343 | 
             
                  mode = :inline if mode == :bulk
         | 
| 331 344 |  | 
| 332 345 | 
             
                  result = RecordIndexer.new(self).reindex(object, mode: mode, full: false, **options)
         | 
| @@ -336,12 +349,13 @@ module Searchkick | |
| 336 349 |  | 
| 337 350 | 
             
                # https://gist.github.com/jarosan/3124884
         | 
| 338 351 | 
             
                # http://www.elasticsearch.org/blog/changing-mapping-with-zero-downtime/
         | 
| 339 | 
            -
                 | 
| 340 | 
            -
             | 
| 352 | 
            +
                def full_reindex(relation, import: true, resume: false, retain: false, mode: nil, refresh_interval: nil, scope: nil, wait: nil)
         | 
| 353 | 
            +
                  raise ArgumentError, "wait only available in :async mode" if !wait.nil? && mode != :async
         | 
| 354 | 
            +
             | 
| 341 355 | 
             
                  if resume
         | 
| 342 356 | 
             
                    index_name = all_indices.sort.last
         | 
| 343 | 
            -
                    raise  | 
| 344 | 
            -
                    index =  | 
| 357 | 
            +
                    raise Error, "No index to resume" unless index_name
         | 
| 358 | 
            +
                    index = Index.new(index_name, @options)
         | 
| 345 359 | 
             
                  else
         | 
| 346 360 | 
             
                    clean_indices unless retain
         | 
| 347 361 |  | 
| @@ -351,7 +365,7 @@ module Searchkick | |
| 351 365 | 
             
                  end
         | 
| 352 366 |  | 
| 353 367 | 
             
                  import_options = {
         | 
| 354 | 
            -
                    mode: ( | 
| 368 | 
            +
                    mode: (mode || :inline),
         | 
| 355 369 | 
             
                    full: true,
         | 
| 356 370 | 
             
                    resume: resume,
         | 
| 357 371 | 
             
                    scope: scope
         | 
| @@ -365,7 +379,7 @@ module Searchkick | |
| 365 379 | 
             
                    import_before_promotion(index, relation, **import_options) if import
         | 
| 366 380 |  | 
| 367 381 | 
             
                    # get existing indices to remove
         | 
| 368 | 
            -
                    unless async
         | 
| 382 | 
            +
                    unless mode == :async
         | 
| 369 383 | 
             
                      check_uuid(uuid, index.uuid)
         | 
| 370 384 | 
             
                      promote(index.name, update_refresh_interval: !refresh_interval.nil?)
         | 
| 371 385 | 
             
                      clean_indices unless retain
         | 
| @@ -378,8 +392,8 @@ module Searchkick | |
| 378 392 | 
             
                    index.import_scope(relation, **import_options) if import
         | 
| 379 393 | 
             
                  end
         | 
| 380 394 |  | 
| 381 | 
            -
                  if async
         | 
| 382 | 
            -
                    if  | 
| 395 | 
            +
                  if mode == :async
         | 
| 396 | 
            +
                    if wait
         | 
| 383 397 | 
             
                      puts "Created index: #{index.name}"
         | 
| 384 398 | 
             
                      puts "Jobs queued. Waiting..."
         | 
| 385 399 | 
             
                      loop do
         | 
| @@ -417,7 +431,7 @@ module Searchkick | |
| 417 431 | 
             
                # https://www.elastic.co/guide/en/elasticsearch/reference/current/docs-index_.html#index-creation
         | 
| 418 432 | 
             
                def check_uuid(old_uuid, new_uuid)
         | 
| 419 433 | 
             
                  if old_uuid != new_uuid
         | 
| 420 | 
            -
                    raise  | 
| 434 | 
            +
                    raise Error, "Safety check failed - only run one Model.reindex per model at a time"
         | 
| 421 435 | 
             
                  end
         | 
| 422 436 | 
             
                end
         | 
| 423 437 |  | 
| @@ -513,7 +513,7 @@ module Searchkick | |
| 513 513 | 
             
                    else
         | 
| 514 514 | 
             
                      [:searchkick_search2, :searchkick_word_search].each do |analyzer|
         | 
| 515 515 | 
             
                        unless settings[:analysis][:analyzer][analyzer].key?(:filter)
         | 
| 516 | 
            -
                          raise  | 
| 516 | 
            +
                          raise Error, "Search synonyms are not supported yet for language"
         | 
| 517 517 | 
             
                        end
         | 
| 518 518 |  | 
| 519 519 | 
             
                        settings[:analysis][:analyzer][analyzer][:filter].insert(2, "searchkick_synonym_graph")
         | 
    
        data/lib/searchkick/model.rb
    CHANGED
    
    | @@ -22,16 +22,18 @@ module Searchkick | |
| 22 22 | 
             
                    raise ArgumentError, "Invalid value for callbacks"
         | 
| 23 23 | 
             
                  end
         | 
| 24 24 |  | 
| 25 | 
            +
                  base = self
         | 
| 26 | 
            +
             | 
| 25 27 | 
             
                  mod = Module.new
         | 
| 26 28 | 
             
                  include(mod)
         | 
| 27 29 | 
             
                  mod.module_eval do
         | 
| 28 30 | 
             
                    def reindex(method_name = nil, mode: nil, refresh: false)
         | 
| 29 31 | 
             
                      self.class.searchkick_index.reindex([self], method_name: method_name, mode: mode, refresh: refresh, single: true)
         | 
| 30 | 
            -
                    end
         | 
| 32 | 
            +
                    end unless base.method_defined?(:reindex)
         | 
| 31 33 |  | 
| 32 34 | 
             
                    def similar(**options)
         | 
| 33 35 | 
             
                      self.class.searchkick_index.similar_record(self, **options)
         | 
| 34 | 
            -
                    end
         | 
| 36 | 
            +
                    end unless base.method_defined?(:similar)
         | 
| 35 37 |  | 
| 36 38 | 
             
                    def search_data
         | 
| 37 39 | 
             
                      data = respond_to?(:to_hash) ? to_hash : serializable_hash
         | 
| @@ -39,11 +41,11 @@ module Searchkick | |
| 39 41 | 
             
                      data.delete("_id")
         | 
| 40 42 | 
             
                      data.delete("_type")
         | 
| 41 43 | 
             
                      data
         | 
| 42 | 
            -
                    end
         | 
| 44 | 
            +
                    end unless base.method_defined?(:search_data)
         | 
| 43 45 |  | 
| 44 46 | 
             
                    def should_index?
         | 
| 45 47 | 
             
                      true
         | 
| 46 | 
            -
                    end
         | 
| 48 | 
            +
                    end unless base.method_defined?(:should_index?)
         | 
| 47 49 | 
             
                  end
         | 
| 48 50 |  | 
| 49 51 | 
             
                  class_eval do
         | 
| @@ -64,10 +66,10 @@ module Searchkick | |
| 64 66 | 
             
                      alias_method Searchkick.search_method_name, :searchkick_search if Searchkick.search_method_name
         | 
| 65 67 |  | 
| 66 68 | 
             
                      def searchkick_index(name: nil)
         | 
| 67 | 
            -
                         | 
| 68 | 
            -
                         | 
| 69 | 
            +
                        index_name = name || searchkick_klass.searchkick_index_name
         | 
| 70 | 
            +
                        index_name = index_name.call if index_name.respond_to?(:call)
         | 
| 69 71 | 
             
                        index_cache = class_variable_get(:@@searchkick_index_cache)
         | 
| 70 | 
            -
                        index_cache.fetch( | 
| 72 | 
            +
                        index_cache.fetch(index_name) { Searchkick::Index.new(index_name, searchkick_options) }
         | 
| 71 73 | 
             
                      end
         | 
| 72 74 | 
             
                      alias_method :search_index, :searchkick_index unless method_defined?(:search_index)
         | 
| 73 75 |  | 
    
        data/lib/searchkick/query.rb
    CHANGED
    
    | @@ -187,11 +187,11 @@ module Searchkick | |
| 187 187 | 
             
                  end
         | 
| 188 188 |  | 
| 189 189 | 
             
                  # set execute for multi search
         | 
| 190 | 
            -
                  @execute =  | 
| 190 | 
            +
                  @execute = Results.new(searchkick_klass, response, opts)
         | 
| 191 191 | 
             
                end
         | 
| 192 192 |  | 
| 193 193 | 
             
                def retry_misspellings?(response)
         | 
| 194 | 
            -
                  @misspellings_below &&  | 
| 194 | 
            +
                  @misspellings_below && Results.new(searchkick_klass, response).total_count < @misspellings_below
         | 
| 195 195 | 
             
                end
         | 
| 196 196 |  | 
| 197 197 | 
             
                private
         | 
| @@ -14,7 +14,7 @@ module Searchkick | |
| 14 14 | 
             
                  case mode
         | 
| 15 15 | 
             
                  when :async
         | 
| 16 16 | 
             
                    unless defined?(ActiveJob)
         | 
| 17 | 
            -
                      raise  | 
| 17 | 
            +
                      raise Error, "Active Job not found"
         | 
| 18 18 | 
             
                    end
         | 
| 19 19 |  | 
| 20 20 | 
             
                    # we could likely combine ReindexV2Job, BulkReindexJob, and ProcessBatchJob
         | 
| @@ -45,7 +45,7 @@ module Searchkick | |
| 45 45 | 
             
                    end
         | 
| 46 46 | 
             
                  when :queue
         | 
| 47 47 | 
             
                    if method_name
         | 
| 48 | 
            -
                      raise  | 
| 48 | 
            +
                      raise Error, "Partial reindex not supported with queue option"
         | 
| 49 49 | 
             
                    end
         | 
| 50 50 |  | 
| 51 51 | 
             
                    index.reindex_queue.push_records(records)
         | 
    
        data/lib/searchkick/relation.rb
    CHANGED
    
    | @@ -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:  | 
| 8 | 
            +
                delegate :body, :params, to: :query
         | 
| 7 9 | 
             
                delegate_missing_to :private_execute
         | 
| 8 10 |  | 
| 9 11 | 
             
                def initialize(model, term = "*", **options)
         | 
| 10 | 
            -
                  @ | 
| 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 ||=  | 
| 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
         | 
    
        data/lib/searchkick/results.rb
    CHANGED
    
    | @@ -141,7 +141,7 @@ module Searchkick | |
| 141 141 |  | 
| 142 142 | 
             
                def hits
         | 
| 143 143 | 
             
                  if error
         | 
| 144 | 
            -
                    raise  | 
| 144 | 
            +
                    raise Error, "Query error - use the error method to view it"
         | 
| 145 145 | 
             
                  else
         | 
| 146 146 | 
             
                    @response["hits"]["hits"]
         | 
| 147 147 | 
             
                  end
         | 
| @@ -178,7 +178,7 @@ module Searchkick | |
| 178 178 | 
             
                end
         | 
| 179 179 |  | 
| 180 180 | 
             
                def scroll
         | 
| 181 | 
            -
                  raise  | 
| 181 | 
            +
                  raise Error, "Pass `scroll` option to the search method for scrolling" unless scroll_id
         | 
| 182 182 |  | 
| 183 183 | 
             
                  if block_given?
         | 
| 184 184 | 
             
                    records = self
         | 
| @@ -191,10 +191,10 @@ module Searchkick | |
| 191 191 | 
             
                  else
         | 
| 192 192 | 
             
                    begin
         | 
| 193 193 | 
             
                      # TODO Active Support notifications for this scroll call
         | 
| 194 | 
            -
                       | 
| 194 | 
            +
                      Results.new(@klass, Searchkick.client.scroll(scroll: options[:scroll], body: {scroll_id: scroll_id}), @options)
         | 
| 195 195 | 
             
                    rescue => e
         | 
| 196 196 | 
             
                      if Searchkick.not_found_error?(e) && e.message =~ /search_context_missing_exception/i
         | 
| 197 | 
            -
                        raise  | 
| 197 | 
            +
                        raise Error, "Scroll id has expired"
         | 
| 198 198 | 
             
                      else
         | 
| 199 199 | 
             
                        raise e
         | 
| 200 200 | 
             
                      end
         | 
| @@ -232,7 +232,7 @@ module Searchkick | |
| 232 232 | 
             
                            index_alias = index.split("_")[0..-2].join("_")
         | 
| 233 233 | 
             
                            Array((options[:index_mapping] || {})[index_alias])
         | 
| 234 234 | 
             
                          end
         | 
| 235 | 
            -
                        raise  | 
| 235 | 
            +
                        raise Error, "Unknown model for index: #{index}. Pass the `models` option to the search method." unless models.any?
         | 
| 236 236 | 
             
                        index_models[index] = models
         | 
| 237 237 | 
             
                      end
         | 
| 238 238 |  | 
    
        data/lib/searchkick/version.rb
    CHANGED
    
    
    
        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)
         | 
| @@ -171,7 +172,7 @@ module Searchkick | |
| 171 172 | 
             
                end
         | 
| 172 173 |  | 
| 173 174 | 
             
                options = options.merge(block: block) if block
         | 
| 174 | 
            -
                 | 
| 175 | 
            +
                Relation.new(klass, term, **options)
         | 
| 175 176 | 
             
              end
         | 
| 176 177 |  | 
| 177 178 | 
             
              def self.multi_search(queries)
         | 
| @@ -183,7 +184,7 @@ module Searchkick | |
| 183 184 | 
             
                  body: queries.flat_map { |q| [q.params.except(:body).to_json, q.body.to_json] }.map { |v| "#{v}\n" }.join,
         | 
| 184 185 | 
             
                }
         | 
| 185 186 | 
             
                ActiveSupport::Notifications.instrument("multi_search.searchkick", event) do
         | 
| 186 | 
            -
                   | 
| 187 | 
            +
                  MultiSearch.new(queries).perform
         | 
| 187 188 | 
             
                end
         | 
| 188 189 | 
             
              end
         | 
| 189 190 |  | 
| @@ -241,9 +242,9 @@ module Searchkick | |
| 241 242 | 
             
              end
         | 
| 242 243 |  | 
| 243 244 | 
             
              def self.reindex_status(index_name)
         | 
| 244 | 
            -
                raise  | 
| 245 | 
            +
                raise Error, "Redis not configured" unless redis
         | 
| 245 246 |  | 
| 246 | 
            -
                batches_left =  | 
| 247 | 
            +
                batches_left = Index.new(index_name).batches_left
         | 
| 247 248 | 
             
                {
         | 
| 248 249 | 
             
                  completed: batches_left == 0,
         | 
| 249 250 | 
             
                  batches_left: batches_left
         | 
    
        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. | 
| 4 | 
            +
              version: 5.0.3
         | 
| 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- | 
| 11 | 
            +
            date: 2022-03-13 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. | 
| 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
         |