├── .github ├── CODEOWNERS ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── PULL_REQUEST_TEMPLATE.md ├── dependabot.yml └── workflows │ └── ruby.yml ├── .gitignore ├── .rspec ├── .rubocop.yml ├── .rubocop_todo.yml ├── .yardopts ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── Gemfile ├── Guardfile ├── LICENSE.txt ├── README.md ├── Rakefile ├── chewy.gemspec ├── docker-compose.yml ├── filters ├── gemfiles ├── base.gemfile ├── rails.6.1.activerecord.gemfile ├── rails.7.0.activerecord.gemfile ├── rails.7.1.activerecord.gemfile └── rails.7.2.activerecord.gemfile ├── lib ├── chewy.rb ├── chewy │ ├── config.rb │ ├── elastic_client.rb │ ├── errors.rb │ ├── fields │ │ ├── base.rb │ │ └── root.rb │ ├── index.rb │ ├── index │ │ ├── actions.rb │ │ ├── adapter │ │ │ ├── active_record.rb │ │ │ ├── base.rb │ │ │ ├── object.rb │ │ │ └── orm.rb │ │ ├── aliases.rb │ │ ├── crutch.rb │ │ ├── import.rb │ │ ├── import │ │ │ ├── bulk_builder.rb │ │ │ ├── bulk_request.rb │ │ │ ├── journal_builder.rb │ │ │ └── routine.rb │ │ ├── mapping.rb │ │ ├── observe.rb │ │ ├── observe │ │ │ ├── active_record_methods.rb │ │ │ └── callback.rb │ │ ├── settings.rb │ │ ├── specification.rb │ │ ├── syncer.rb │ │ ├── witchcraft.rb │ │ └── wrapper.rb │ ├── journal.rb │ ├── log_subscriber.rb │ ├── minitest.rb │ ├── minitest │ │ ├── helpers.rb │ │ └── search_index_receiver.rb │ ├── multi_search.rb │ ├── railtie.rb │ ├── rake_helper.rb │ ├── repository.rb │ ├── rspec.rb │ ├── rspec │ │ ├── build_query.rb │ │ ├── helpers.rb │ │ └── update_index.rb │ ├── runtime.rb │ ├── runtime │ │ └── version.rb │ ├── search.rb │ ├── search │ │ ├── loader.rb │ │ ├── pagination │ │ │ └── kaminari.rb │ │ ├── parameters.rb │ │ ├── parameters │ │ │ ├── aggs.rb │ │ │ ├── allow_partial_search_results.rb │ │ │ ├── collapse.rb │ │ │ ├── concerns │ │ │ │ ├── bool_storage.rb │ │ │ │ ├── hash_storage.rb │ │ │ │ ├── integer_storage.rb │ │ │ │ ├── query_storage.rb │ │ │ │ ├── string_array_storage.rb │ │ │ │ └── string_storage.rb │ │ │ ├── docvalue_fields.rb │ │ │ ├── explain.rb │ │ │ ├── filter.rb │ │ │ ├── highlight.rb │ │ │ ├── ignore_unavailable.rb │ │ │ ├── indices.rb │ │ │ ├── indices_boost.rb │ │ │ ├── knn.rb │ │ │ ├── limit.rb │ │ │ ├── load.rb │ │ │ ├── min_score.rb │ │ │ ├── none.rb │ │ │ ├── offset.rb │ │ │ ├── order.rb │ │ │ ├── post_filter.rb │ │ │ ├── preference.rb │ │ │ ├── profile.rb │ │ │ ├── query.rb │ │ │ ├── request_cache.rb │ │ │ ├── rescore.rb │ │ │ ├── script_fields.rb │ │ │ ├── search_after.rb │ │ │ ├── search_type.rb │ │ │ ├── source.rb │ │ │ ├── storage.rb │ │ │ ├── stored_fields.rb │ │ │ ├── suggest.rb │ │ │ ├── terminate_after.rb │ │ │ ├── timeout.rb │ │ │ ├── track_scores.rb │ │ │ ├── track_total_hits.rb │ │ │ └── version.rb │ │ ├── query_proxy.rb │ │ ├── request.rb │ │ ├── response.rb │ │ ├── scoping.rb │ │ └── scrolling.rb │ ├── stash.rb │ ├── strategy.rb │ ├── strategy │ │ ├── active_job.rb │ │ ├── atomic.rb │ │ ├── atomic_no_refresh.rb │ │ ├── base.rb │ │ ├── bypass.rb │ │ ├── delayed_sidekiq.rb │ │ ├── delayed_sidekiq │ │ │ ├── scheduler.rb │ │ │ └── worker.rb │ │ ├── lazy_sidekiq.rb │ │ ├── sidekiq.rb │ │ └── urgent.rb │ └── version.rb ├── generators │ ├── chewy │ │ └── install_generator.rb │ └── templates │ │ └── chewy.yml └── tasks │ └── chewy.rake ├── migration_guide.md └── spec ├── chewy ├── config_spec.rb ├── elastic_client_spec.rb ├── fields │ ├── base_spec.rb │ ├── root_spec.rb │ └── time_fields_spec.rb ├── index │ ├── actions_spec.rb │ ├── adapter │ │ ├── active_record_spec.rb │ │ └── object_spec.rb │ ├── aliases_spec.rb │ ├── import │ │ ├── bulk_builder_spec.rb │ │ ├── bulk_request_spec.rb │ │ ├── journal_builder_spec.rb │ │ └── routine_spec.rb │ ├── import_spec.rb │ ├── mapping_spec.rb │ ├── observe │ │ ├── active_record_methods_spec.rb │ │ └── callback_spec.rb │ ├── observe_spec.rb │ ├── settings_spec.rb │ ├── specification_spec.rb │ ├── syncer_spec.rb │ ├── witchcraft_spec.rb │ └── wrapper_spec.rb ├── index_spec.rb ├── journal_spec.rb ├── minitest │ ├── helpers_spec.rb │ └── search_index_receiver_spec.rb ├── multi_search_spec.rb ├── rake_helper_spec.rb ├── repository_spec.rb ├── rspec │ ├── build_query_spec.rb │ ├── helpers_spec.rb │ └── update_index_spec.rb ├── runtime │ └── version_spec.rb ├── runtime_spec.rb ├── search │ ├── loader_spec.rb │ ├── pagination │ │ ├── kaminari_examples.rb │ │ └── kaminari_spec.rb │ ├── parameters │ │ ├── aggs_spec.rb │ │ ├── bool_storage_examples.rb │ │ ├── collapse_spec.rb │ │ ├── docvalue_fields_spec.rb │ │ ├── explain_spec.rb │ │ ├── filter_spec.rb │ │ ├── hash_storage_examples.rb │ │ ├── highlight_spec.rb │ │ ├── ignore_unavailable_spec.rb │ │ ├── indices_spec.rb │ │ ├── integer_storage_examples.rb │ │ ├── knn_spec.rb │ │ ├── limit_spec.rb │ │ ├── load_spec.rb │ │ ├── min_score_spec.rb │ │ ├── none_spec.rb │ │ ├── offset_spec.rb │ │ ├── order_spec.rb │ │ ├── post_filter_spec.rb │ │ ├── preference_spec.rb │ │ ├── profile_spec.rb │ │ ├── query_spec.rb │ │ ├── query_storage_examples.rb │ │ ├── request_cache_spec.rb │ │ ├── rescore_spec.rb │ │ ├── script_fields_spec.rb │ │ ├── search_after_spec.rb │ │ ├── search_type_spec.rb │ │ ├── source_spec.rb │ │ ├── storage_spec.rb │ │ ├── stored_fields_spec.rb │ │ ├── string_array_storage_examples.rb │ │ ├── string_storage_examples.rb │ │ ├── suggest_spec.rb │ │ ├── terminate_after_spec.rb │ │ ├── timeout_spec.rb │ │ ├── track_scores_spec.rb │ │ ├── track_total_hits_spec.rb │ │ └── version_spec.rb │ ├── parameters_spec.rb │ ├── query_proxy_spec.rb │ ├── request_spec.rb │ ├── response_spec.rb │ └── scrolling_spec.rb ├── search_spec.rb ├── stash_spec.rb ├── strategy │ ├── active_job_spec.rb │ ├── atomic_no_refresh_spec.rb │ ├── atomic_spec.rb │ ├── delayed_sidekiq_spec.rb │ ├── lazy_sidekiq_spec.rb │ └── sidekiq_spec.rb └── strategy_spec.rb ├── chewy_spec.rb ├── spec_helper.rb └── support ├── active_record.rb ├── class_helpers.rb └── fail_helpers.rb /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @toptal/sre 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug Report 3 | about: Report an issue with Chewy you've discovered. 4 | --- 5 | 6 | *Be clear, concise and precise in your description of the problem. 7 | Open an issue with a descriptive title and a summary in grammatically correct, 8 | complete sentences.* 9 | 10 | *Use the template below when reporting bugs. Please, make sure that 11 | you're running the latest stable Chewy and that the problem you're reporting 12 | hasn't been reported (and potentially fixed) already.* 13 | 14 | *Before filing the ticket you should replace all text above the horizontal 15 | rule with your own words.* 16 | 17 | -------- 18 | 19 | ## Expected behavior 20 | 21 | Describe here how you expected Chewy to behave in this particular situation. 22 | 23 | ## Actual behavior 24 | 25 | Describe here what actually happened. 26 | 27 | ## Steps to reproduce the problem 28 | 29 | This is extremely important! Providing us with a reliable way to reproduce 30 | a problem will expedite its solution. 31 | 32 | ## Version Information 33 | 34 | Share here essential version information such as: 35 | 36 | * Chewy version 37 | * Elasticsearch version 38 | * Ruby version 39 | * Rails version 40 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature Request 3 | about: Suggest new Chewy features or improvements to existing features. 4 | --- 5 | 6 | ## Is your feature request related to a problem? Please describe. 7 | 8 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 9 | 10 | ## Describe the solution you'd like 11 | 12 | A clear and concise description of what you want to happen. 13 | 14 | ## Describe alternatives you've considered 15 | 16 | A clear and concise description of any alternative solutions or features you've considered. 17 | 18 | ## Additional context 19 | 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | **Replace this text with a summary of the changes in your PR. 2 | The more detailed you are, the better.** 3 | 4 | ----------------- 5 | 6 | Before submitting the PR make sure the following are checked: 7 | 8 | * [ ] The PR relates to *only* one subject with a clear title and description in grammatically correct, complete sentences. 9 | * [ ] Wrote [good commit messages][1]. 10 | * [ ] Commit message starts with `[Fix #issue-number]` (if the related issue exists). 11 | * [ ] Feature branch is up-to-date with `master` (if not - rebase it). 12 | * [ ] Squashed related commits together. 13 | * [ ] Added tests. 14 | * [ ] Added an entry to the changelog if the new code introduces user-observable changes. See [changelog entry format](https://github.com/toptal/chewy/blob/master/CONTRIBUTING.md#changelog-entry-format) for details. 15 | 16 | [1]: https://chris.beams.io/posts/git-commit/ 17 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | registries: 3 | toptal-github: 4 | type: "git" 5 | url: "https://github.com" 6 | username: "x-access-token" 7 | password: "${{secrets.DEPENDABOT_GITHUB_TOKEN}}" 8 | 9 | updates: 10 | - package-ecosystem: bundler 11 | directory: "/" 12 | schedule: 13 | interval: "weekly" 14 | day: "wednesday" 15 | time: "07:00" 16 | pull-request-branch-name: 17 | separator: "-" 18 | labels: 19 | - "no-jira" 20 | - "ruby" 21 | - "dependencies" 22 | reviewers: 23 | - "toptal/sre" 24 | registries: 25 | - toptal-github 26 | insecure-external-code-execution: allow 27 | open-pull-requests-limit: 3 28 | - package-ecosystem: "github-actions" 29 | directory: "/" 30 | schedule: 31 | interval: "weekly" 32 | day: "wednesday" 33 | time: "07:00" 34 | pull-request-branch-name: 35 | separator: "-" 36 | labels: 37 | - "no-jira" 38 | - "dependencies" 39 | - "gha" 40 | reviewers: 41 | - "toptal/sre" 42 | open-pull-requests-limit: 3 43 | -------------------------------------------------------------------------------- /.github/workflows/ruby.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | pull_request: 7 | types: [ 8 | synchronize, # PR was updated 9 | opened, # PR was open 10 | reopened # PR was reopened 11 | ] 12 | 13 | jobs: 14 | ruby-3: 15 | runs-on: ubuntu-latest 16 | strategy: 17 | fail-fast: false 18 | matrix: 19 | ruby: [ '3.0', '3.1', '3.2', '3.3' ] 20 | gemfile: [rails.6.1.activerecord, rails.7.0.activerecord, rails.7.1.activerecord, rails.7.2.activerecord] 21 | exclude: 22 | - ruby: '3.0' 23 | gemfile: rails.7.2.activerecord 24 | name: ${{ matrix.ruby }}-${{ matrix.gemfile }} 25 | 26 | env: 27 | BUNDLE_GEMFILE: gemfiles/${{ matrix.gemfile }}.gemfile 28 | 29 | services: 30 | redis: 31 | # Docker Hub image 32 | image: redis 33 | ports: 34 | - '6379:6379' 35 | # Set health checks to wait until redis has started 36 | options: >- 37 | --health-cmd "redis-cli ping" 38 | --health-interval 10s 39 | --health-timeout 5s 40 | --health-retries 5 41 | steps: 42 | - uses: actions/checkout@v4 43 | - uses: ruby/setup-ruby@v1 44 | with: 45 | ruby-version: ${{ matrix.ruby }} 46 | bundler-cache: true 47 | - name: Start containers 48 | run: | 49 | docker compose up elasticsearch_test -d 50 | sleep 15 51 | 52 | - name: Tests 53 | run: bundle exec rspec 54 | 55 | rubocop: 56 | runs-on: ubuntu-latest 57 | steps: 58 | - uses: actions/checkout@v4 59 | - uses: ruby/setup-ruby@v1 60 | with: 61 | ruby-version: 3.0 62 | bundler-cache: true 63 | - run: bundle exec rubocop --format simple 64 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | *.rbc 3 | .bundle 4 | .config 5 | .yardoc 6 | Gemfile.lock 7 | gemfiles/*.lock 8 | InstalledFiles 9 | _yardoc 10 | coverage 11 | doc/ 12 | lib/bundler/man 13 | pkg 14 | rdoc 15 | spec/reports 16 | test/tmp 17 | test/version_tmp 18 | tmp 19 | .rvmrc 20 | _site 21 | .sass-cache 22 | file::memory:?cache=shared 23 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --color 2 | --backtrace 3 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | inherit_from: .rubocop_todo.yml 2 | 3 | AllCops: 4 | NewCops: enable 5 | TargetRubyVersion: 3.0 6 | 7 | Layout/AccessModifierIndentation: 8 | EnforcedStyle: outdent 9 | 10 | Layout/HashAlignment: 11 | EnforcedLastArgumentHashStyle: always_ignore 12 | 13 | Layout/ParameterAlignment: 14 | EnforcedStyle: with_fixed_indentation 15 | 16 | Layout/CaseIndentation: 17 | EnforcedStyle: end 18 | 19 | Layout/EndAlignment: 20 | EnforcedStyleAlignWith: variable 21 | 22 | Layout/FirstArrayElementIndentation: 23 | EnforcedStyle: consistent 24 | 25 | Layout/FirstHashElementIndentation: 26 | EnforcedStyle: consistent 27 | 28 | Layout/HeredocIndentation: 29 | Enabled: false 30 | 31 | Layout/MultilineMethodCallIndentation: 32 | EnforcedStyle: indented 33 | 34 | Layout/MultilineOperationIndentation: 35 | EnforcedStyle: indented 36 | 37 | Layout/SpaceInsideHashLiteralBraces: 38 | EnforcedStyle: no_space 39 | 40 | Lint/AmbiguousBlockAssociation: 41 | Enabled: false 42 | 43 | Style/Alias: 44 | EnforcedStyle: prefer_alias_method 45 | 46 | Style/AndOr: 47 | EnforcedStyle: conditionals 48 | 49 | Style/DoubleNegation: 50 | Enabled: false 51 | 52 | Metrics/BlockLength: 53 | Exclude: 54 | - '**/*_spec.rb' 55 | - '**/*_examples.rb' 56 | - '**/*.rake' 57 | 58 | Metrics/ModuleLength: 59 | Exclude: 60 | - 'lib/chewy/rake_helper.rb' 61 | - '**/*_spec.rb' 62 | 63 | Style/ArgumentsForwarding: 64 | Enabled: false 65 | -------------------------------------------------------------------------------- /.yardopts: -------------------------------------------------------------------------------- 1 | --markup-provider=redcarpet 2 | --markup=markdown 3 | - 4 | README.md 5 | CHANGELOG.md 6 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # The Chewy Community Code of Conduct 2 | 3 | **Note:** We have picked the following code of conduct based on [Ruby's own code of conduct](https://www.ruby-lang.org/en/conduct/). 4 | 5 | This document provides a few simple community guidelines for a safe, respectful, 6 | productive, and collaborative place for any person who is willing to contribute 7 | to the Chewy community. It applies to all "collaborative spaces", which are 8 | defined as community communications channels (such as mailing lists, submitted 9 | patches, commit comments, etc.). 10 | 11 | * Participants will be tolerant of opposing views. 12 | * Participants must ensure that their language and actions are free of personal attacks and disparaging personal remarks. 13 | * When interpreting the words and actions of others, participants should always assume good intentions. 14 | * Behaviour which can be reasonably considered harassment will not be tolerated. 15 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | If you discover issues, have ideas for improvements or new features, 4 | please report them to the [issue tracker][1] of the repository or 5 | submit a pull request. Please, try to follow these guidelines when you 6 | do so. 7 | 8 | ## Issue reporting 9 | 10 | * Check that the issue has not already been reported. 11 | * Check that the issue has not already been fixed in the latest code 12 | (a.k.a. `master`). 13 | * Be clear, concise and precise in your description of the problem. 14 | * Open an issue with a descriptive title and a summary in grammatically correct, 15 | complete sentences. 16 | * Include the versions of Chewy, Elasticsearch, Ruby, Rails, etc. 17 | * Include any relevant code to the issue summary. 18 | 19 | ## Pull requests 20 | 21 | * Read [how to properly contribute to open source projects on GitHub][2]. 22 | * Fork the project. 23 | * Use a topic/feature branch to easily amend a pull request later, if necessary. 24 | * Write [good commit messages][3]. 25 | * Use the same coding conventions as the rest of the project. 26 | * Commit and push until you are happy with your contribution. 27 | * If your change has a corresponding open GitHub issue, prefix the commit message with `[Fix #github-issue-number]`. 28 | * Make sure to add tests for it. This is important so I don't break it 29 | in a future version unintentionally. 30 | * Add an entry to the [Changelog](CHANGELOG.md). 31 | * Please try not to mess with the Rakefile, version, or history. If 32 | you want to have your own version, or is otherwise necessary, that 33 | is fine, but please isolate to its own commit so I can cherry-pick 34 | around it. 35 | * Make sure the test suite is passing and the code you wrote doesn't produce 36 | RuboCop offenses. 37 | * [Squash related commits together][5]. 38 | * Open a [pull request][4] that relates to *only* one subject with a clear title 39 | and description in grammatically correct, complete sentences. 40 | 41 | ## Changelog entry format 42 | 43 | Here are a few examples: 44 | 45 | ``` 46 | * [#753](https://github.com/toptal/chewy/pull/753): Add support for direct_import parameter to skip objects reloading. ([@TikiTDO][], [@dalthon][]) 47 | * [#739](https://github.com/toptal/chewy/pull/739): Remove explicit `main` branch dependencies on `rspec-*` gems after `rspec-mocks` 3.10.2 is released. ([@rabotyaga][]) 48 | ``` 49 | 50 | * Mark it up in [Markdown syntax][6]. 51 | * The entry line should start with `* ` (an asterisk and a space). 52 | * If the change has a related GitHub issue (e.g. a bug fix for a reported issue), put a link to the issue as `[#123](https://github.com/toptal/chewy/issues/123): `. 53 | * Describe the brief of the change. The sentence should end with a punctuation. 54 | * If this is a breaking change, mark it with `**(Breaking)**`. 55 | * At the end of the entry, add an implicit link to your GitHub user page as `([@username][])`. 56 | * If this is your first contribution to the project, add a link definition for the implicit link to the bottom of the changelog as `[@username]: https://github.com/username`. 57 | 58 | [1]: https://github.com/toptal/chewy/issues 59 | [2]: https://www.gun.io/blog/how-to-github-fork-branch-and-pull-request 60 | [3]: https://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html 61 | [4]: https://help.github.com/articles/about-pull-requests 62 | [5]: http://gitready.com/advanced/2009/02/10/squashing-commits-with-rebase.html 63 | [6]: https://daringfireball.net/projects/markdown/syntax 64 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gem 'activerecord' 4 | 5 | gem 'activejob', require: false 6 | gem 'sidekiq', require: false 7 | 8 | gem 'kaminari-core', require: false 9 | 10 | gem 'parallel', require: false 11 | gem 'ruby-progressbar', require: false 12 | 13 | gem 'guard' 14 | gem 'guard-rspec' 15 | 16 | gem 'redcarpet' 17 | gem 'yard' 18 | 19 | gem 'rexml' 20 | 21 | eval_gemfile 'gemfiles/base.gemfile' 22 | gemspec 23 | -------------------------------------------------------------------------------- /Guardfile: -------------------------------------------------------------------------------- 1 | # A sample Guardfile 2 | # More info at https://github.com/guard/guard#readme 3 | 4 | guard :rspec, cmd: 'rspec' do 5 | watch(%r{^spec/.+_spec\.rb$}) 6 | watch(%r{^lib/(.+)\.rb$}) { |m| "spec/#{m[1]}_spec.rb" } 7 | watch('spec/spec_helper.rb') { 'spec' } 8 | 9 | # Rails example 10 | watch(%r{^app/(.+)\.rb$}) { |m| "spec/#{m[1]}_spec.rb" } 11 | watch(%r{^app/(.*)(\.erb|\.haml|\.slim)$}) { |m| "spec/#{m[1]}#{m[2]}_spec.rb" } 12 | watch(%r{^app/controllers/(.+)_(controller)\.rb$}) do |m| 13 | ["spec/routing/#{m[1]}_routing_spec.rb", "spec/#{m[2]}s/#{m[1]}_#{m[2]}_spec.rb", "spec/acceptance/#{m[1]}_spec.rb"] 14 | end 15 | watch(%r{^spec/support/(.+)\.rb$}) { 'spec' } 16 | watch('config/routes.rb') { 'spec/routing' } 17 | watch('app/controllers/application_controller.rb') { 'spec/controllers' } 18 | 19 | # Capybara features specs 20 | watch(%r{^app/views/(.+)/.*\.(erb|haml|slim)$}) { |m| "spec/features/#{m[1]}_spec.rb" } 21 | 22 | # Turnip features and steps 23 | watch(%r{^spec/acceptance/(.+)\.feature$}) 24 | watch(%r{^spec/acceptance/steps/(.+)_steps\.rb$}) { |m| Dir[File.join("**/#{m[1]}.feature")][0] || 'spec/acceptance' } 25 | end 26 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013-2021 Toptal, LLC 2 | 3 | MIT License 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'bundler/gem_tasks' 2 | require 'rspec/core/rake_task' 3 | require 'elasticsearch/extensions/test/cluster/tasks' 4 | 5 | RSpec::Core::RakeTask.new(:spec) 6 | 7 | task default: :spec 8 | 9 | namespace :es do 10 | task :start do 11 | Rake.application['elasticsearch:start'].invoke 12 | end 13 | 14 | task :stop do 15 | Rake.application['elasticsearch:stop'].invoke 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /chewy.gemspec: -------------------------------------------------------------------------------- 1 | lib = File.expand_path('lib', __dir__) 2 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 3 | require 'chewy/version' 4 | 5 | Gem::Specification.new do |spec| 6 | spec.name = 'chewy' 7 | spec.version = Chewy::VERSION 8 | spec.authors = ['Toptal, LLC', 'pyromaniac'] 9 | spec.email = ['open-source@toptal.com', 'kinwizard@gmail.com'] 10 | spec.summary = 'Elasticsearch ODM client wrapper' 11 | spec.description = 'Chewy provides functionality for Elasticsearch index handling, documents import mappings and chainable query DSL' 12 | spec.homepage = 'https://github.com/toptal/chewy' 13 | spec.license = 'MIT' 14 | spec.required_ruby_version = '~> 3.0' 15 | 16 | spec.files = `git ls-files`.split($RS) 17 | spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) } 18 | spec.require_paths = ['lib'] 19 | 20 | spec.add_dependency 'activesupport', '>= 6.1' 21 | spec.add_dependency 'elasticsearch', '>= 8.14', '< 9.0' 22 | spec.add_dependency 'elasticsearch-dsl' 23 | spec.metadata['rubygems_mfa_required'] = 'true' 24 | end 25 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.4" 2 | services: 3 | elasticsearch_test: 4 | image: "elasticsearch:8.15.0" 5 | environment: 6 | - bootstrap.memory_lock=${ES_MEMORY_LOCK:-false} 7 | - "ES_JAVA_OPTS=-Xms${TEST_ES_HEAP_SIZE:-500m} -Xmx${TEST_ES_HEAP_SIZE:-500m}" 8 | - discovery.type=single-node 9 | - xpack.security.enabled=false 10 | ports: 11 | - "127.0.0.1:9250:9200" 12 | ulimits: 13 | nofile: 14 | soft: 65536 15 | hard: 65536 16 | -------------------------------------------------------------------------------- /filters: -------------------------------------------------------------------------------- 1 | term 2 | name == 'value' 3 | name != 'value' 4 | terms 5 | name == ['value1', 'value2'] plain 6 | name != ['value1', 'value2'] 7 | 8 | name(:&) == ['value1', 'value2'] or 9 | name(:|) == ['value1', 'value2'] and 10 | name(:b) == ['value1', 'value2'] bool 11 | name(:f) == ['value1', 'value2'] fielddata 12 | regexp 13 | name == /regexp/ 14 | name =~ /regexp/ 15 | name != /regexp/ 16 | name !~ /regexp/ 17 | name(:anystring, :intersection) == /regexp/ 18 | prefix 19 | name =~ 'pref' 20 | name !~ 'pref' 21 | 22 | exists 23 | name? 24 | missing 25 | !name 26 | !name? 27 | name == nil 28 | 29 | numeric_range Numeric 30 | range Other 31 | date >= Date.today 32 | date > Date.today 33 | date <= Date.today 34 | date < Date.today 35 | 36 | date == (2.days.ago..3.days.since) () 37 | date == [2.days.ago..3.days.since] [] 38 | 39 | bool 40 | must(name == 'name', email == 'email') 41 | .should(name == 'name', email == 'email') 42 | .must_not(name == 'name', email == 'email') 43 | and 44 | (name == 'name') & (email == 'email') 45 | or 46 | (name == 'name') | (email == 'email') 47 | not 48 | !(name == 'name') 49 | email != 'email' 50 | 51 | script s() 52 | match all 53 | match_all 54 | 55 | has child 56 | has_child('type').query() 57 | has_child('type').filter() 58 | has parent 59 | has_parent('type').query() 60 | has_parent('type').filter() 61 | nested 62 | name.nested() 63 | 64 | query q() 65 | type 66 | .types() 67 | 68 | geo bounding box 69 | geo distance 70 | geo distance range 71 | geo polygon 72 | geoshape 73 | geohash cell 74 | 75 | indices 76 | ids 77 | 78 | limit 79 | -------------------------------------------------------------------------------- /gemfiles/base.gemfile: -------------------------------------------------------------------------------- 1 | gem 'database_cleaner' 2 | gem 'elasticsearch-extensions' 3 | gem 'method_source' 4 | gem 'rake' 5 | gem 'redis', require: false 6 | gem 'rspec', '>= 3.7.0' 7 | gem 'rspec-collection_matchers' 8 | gem 'rspec-its' 9 | gem 'rubocop', '1.65.1' 10 | gem 'sqlite3', '~> 1.4' 11 | gem 'timecop' 12 | gem 'unparser' 13 | -------------------------------------------------------------------------------- /gemfiles/rails.6.1.activerecord.gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gem 'activejob', '~> 6.1.0' 4 | gem 'activerecord', '~> 6.1.0' 5 | gem 'activesupport', '~> 6.1.0' 6 | gem 'kaminari-core', '~> 1.1.0', require: false 7 | gem 'parallel', require: false 8 | gem 'rspec_junit_formatter', '~> 0.4.1' 9 | gem 'sidekiq', require: false 10 | 11 | gem 'rexml' 12 | 13 | gemspec path: '../' 14 | eval_gemfile 'base.gemfile' 15 | -------------------------------------------------------------------------------- /gemfiles/rails.7.0.activerecord.gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gem 'activejob', '~> 7.0.0' 4 | gem 'activerecord', '~> 7.0.0' 5 | gem 'activesupport', '~> 7.0.0' 6 | gem 'kaminari-core', '~> 1.1.0', require: false 7 | gem 'parallel', require: false 8 | gem 'rspec_junit_formatter', '~> 0.4.1' 9 | gem 'sidekiq', require: false 10 | 11 | gem 'rexml' 12 | 13 | gemspec path: '../' 14 | eval_gemfile 'base.gemfile' 15 | -------------------------------------------------------------------------------- /gemfiles/rails.7.1.activerecord.gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gem 'activejob', '~> 7.1.0' 4 | gem 'activerecord', '~> 7.1.0' 5 | gem 'activesupport', '~> 7.1.0' 6 | gem 'kaminari-core', '~> 1.1.0', require: false 7 | gem 'parallel', require: false 8 | gem 'rspec_junit_formatter', '~> 0.4.1' 9 | gem 'sidekiq', require: false 10 | 11 | gem 'rexml' 12 | 13 | gemspec path: '../' 14 | eval_gemfile 'base.gemfile' 15 | -------------------------------------------------------------------------------- /gemfiles/rails.7.2.activerecord.gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gem 'activejob', '~> 7.2.0' 4 | gem 'activerecord', '~> 7.2.0' 5 | gem 'activesupport', '~> 7.2.0' 6 | gem 'kaminari-core', '~> 1.1.0', require: false 7 | gem 'parallel', require: false 8 | gem 'rspec_junit_formatter', '~> 0.4.1' 9 | gem 'sidekiq', require: false 10 | 11 | gem 'rexml' 12 | 13 | gemspec path: '../' 14 | eval_gemfile 'base.gemfile' 15 | -------------------------------------------------------------------------------- /lib/chewy/elastic_client.rb: -------------------------------------------------------------------------------- 1 | module Chewy 2 | # Replacement for Chewy.client 3 | class ElasticClient 4 | def self.build_es_client(configuration = Chewy.configuration) 5 | client_configuration = configuration.deep_dup 6 | client_configuration.delete(:prefix) # used by Chewy, not relevant to Elasticsearch::Client 7 | block = client_configuration[:transport_options].try(:delete, :proc) 8 | ::Elasticsearch::Client.new(client_configuration, &block) 9 | end 10 | 11 | def initialize(elastic_client = self.class.build_es_client) 12 | @elastic_client = elastic_client 13 | end 14 | 15 | private 16 | 17 | def method_missing(name, *args, **kwargs, &block) 18 | inspect_payload(name, args, kwargs) 19 | 20 | @elastic_client.__send__(name, *args, **kwargs, &block) 21 | end 22 | 23 | def respond_to_missing?(name, _include_private = false) 24 | @elastic_client.respond_to?(name) || super 25 | end 26 | 27 | def inspect_payload(name, args, kwargs) 28 | Chewy.config.before_es_request_filter&.call(name, args, kwargs) 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /lib/chewy/errors.rb: -------------------------------------------------------------------------------- 1 | module Chewy 2 | class Error < StandardError 3 | end 4 | 5 | class UndefinedIndex < Error 6 | end 7 | 8 | class UndefinedUpdateStrategy < Error 9 | def initialize(_type) 10 | super(<<-MESSAGE) 11 | Index update strategy is undefined for current context. 12 | Please wrap your code with `Chewy.strategy(:strategy_name) block.` 13 | MESSAGE 14 | end 15 | end 16 | 17 | class DocumentNotFound < Error 18 | end 19 | 20 | class ImportFailed < Error 21 | def initialize(type, import_errors) 22 | message = "Import failed for `#{type}` with:\n" 23 | import_errors.each do |action, action_errors| 24 | message << " #{action.to_s.humanize} errors:\n" 25 | action_errors.each do |error, documents| 26 | message << " `#{error}`\n" 27 | message << " on #{documents.count} documents: #{documents}\n" 28 | end 29 | end 30 | super(message) 31 | end 32 | end 33 | 34 | class InvalidJoinFieldType < Error 35 | def initialize(join_field_type, join_field_name, relations) 36 | super("`#{join_field_type}` set for the join field `#{join_field_name}` is not on the :relations list (#{relations})") 37 | end 38 | end 39 | 40 | class ImportScopeCleanupError < Error 41 | end 42 | 43 | class FeatureDisabled < Error 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /lib/chewy/fields/root.rb: -------------------------------------------------------------------------------- 1 | module Chewy 2 | module Fields 3 | class Root < Chewy::Fields::Base 4 | attr_reader :dynamic_templates, :id 5 | 6 | def initialize(name, **options) 7 | super 8 | 9 | @value ||= -> { self } 10 | @dynamic_templates = [] 11 | end 12 | 13 | def update_options!(**options) 14 | @id = options.fetch(:id, options.fetch(:_id, @id)) 15 | @options.merge!(options.except(:id, :_id, :type)) 16 | end 17 | 18 | def mappings_hash 19 | mappings = super 20 | mappings[name].delete(:type) 21 | 22 | if dynamic_templates.present? 23 | mappings[name][:dynamic_templates] ||= [] 24 | mappings[name][:dynamic_templates].concat dynamic_templates 25 | end 26 | 27 | mappings[name] 28 | end 29 | 30 | def dynamic_template(*args) 31 | options = args.extract_options!.deep_symbolize_keys 32 | if args.first 33 | template_name = :"template_#{dynamic_templates.count.next}" 34 | template = {template_name => {mapping: options}} 35 | 36 | template[template_name][:match_mapping_type] = args.second.to_s if args.second.present? 37 | 38 | regexp = args.first.is_a?(Regexp) 39 | template[template_name][:match_pattern] = 'regexp' if regexp 40 | 41 | match = regexp ? args.first.source : args.first 42 | path = match.include?(regexp ? '\.' : '.') 43 | 44 | template[template_name][path ? :path_match : :match] = match 45 | @dynamic_templates.push(template) 46 | else 47 | @dynamic_templates.push(options) 48 | end 49 | end 50 | 51 | def compose_id(object) 52 | return unless id 53 | 54 | id.arity.zero? ? object.instance_exec(&id) : id.call(object) 55 | end 56 | 57 | # Converts passed object to JSON-ready hash. Used for objects import. 58 | # 59 | # @param object [Object] a base object for composition 60 | # @param crutches [Object] any object that will be passed to every field value proc as a last argument 61 | # @param fields [Array] a list of fields to compose, every field will be composed if empty 62 | # @return [Hash] JSON-ready hash with stringified keys 63 | # 64 | def compose(object, crutches = nil, fields: []) 65 | result = evaluate([object, crutches]) 66 | 67 | if children.present? 68 | child_fields = if fields.present? 69 | child_hash.slice(*fields).values 70 | else 71 | children 72 | end 73 | 74 | child_fields.each_with_object({}) do |field, memo| 75 | memo.merge!(field.compose(result, crutches) || {}) 76 | end.as_json 77 | elsif fields.present? 78 | result.as_json(only: fields, root: false) 79 | else 80 | result.as_json(root: false) 81 | end 82 | end 83 | 84 | # Children indexed by name as a hash. 85 | # 86 | # @return [Hash{Symbol => Chewy::Fields::Base}] 87 | def child_hash 88 | @child_hash ||= children.index_by(&:name) 89 | end 90 | end 91 | end 92 | end 93 | -------------------------------------------------------------------------------- /lib/chewy/index/aliases.rb: -------------------------------------------------------------------------------- 1 | module Chewy 2 | class Index 3 | module Aliases 4 | extend ActiveSupport::Concern 5 | 6 | module ClassMethods 7 | def indexes 8 | indexes = empty_if_not_found { client.indices.get(index: index_name).keys } 9 | indexes += empty_if_not_found { client.indices.get_alias(name: index_name).keys } 10 | indexes.compact.uniq 11 | end 12 | 13 | def aliases 14 | empty_if_not_found do 15 | client.indices.get_alias(index: index_name, name: '*').values.flat_map do |aliases| 16 | aliases['aliases'].keys 17 | end 18 | end.compact.uniq 19 | end 20 | 21 | private 22 | 23 | def empty_if_not_found 24 | yield 25 | rescue Elastic::Transport::Transport::Errors::NotFound 26 | [] 27 | end 28 | end 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /lib/chewy/index/crutch.rb: -------------------------------------------------------------------------------- 1 | module Chewy 2 | class Index 3 | module Crutch 4 | extend ActiveSupport::Concern 5 | 6 | included do 7 | class_attribute :_crutches 8 | self._crutches = {} 9 | end 10 | 11 | class Crutches 12 | def initialize(index, collection) 13 | @index = index 14 | @collection = collection 15 | @crutches_instances = {} 16 | end 17 | 18 | def method_missing(name, *, **) 19 | return self[name] if @index._crutches.key?(name) 20 | 21 | super 22 | end 23 | 24 | def respond_to_missing?(name, include_private = false) 25 | @index._crutches.key?(name) || super 26 | end 27 | 28 | def [](name) 29 | @crutches_instances[name] ||= @index._crutches[:"#{name}"].call(@collection) 30 | end 31 | end 32 | 33 | module ClassMethods 34 | def crutch(name, &block) 35 | self._crutches = _crutches.merge(name.to_sym => block) 36 | end 37 | end 38 | end 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /lib/chewy/index/import/bulk_request.rb: -------------------------------------------------------------------------------- 1 | module Chewy 2 | class Index 3 | module Import 4 | # Adds additional features to elasticsearch-api bulk method: 5 | # * supports Chewy index suffix if necessary; 6 | # * supports bulk_size, devides the passed body in chunks 7 | # and peforms a separate request for each chunk; 8 | # * returns only errored document entries from the response 9 | # if any present. 10 | # 11 | # @see https://github.com/elastic/elasticsearch-ruby/blob/master/elasticsearch-api/lib/elasticsearch/api/actions/bulk.rb 12 | class BulkRequest 13 | # @param index [Chewy::Index] an index for the request 14 | # @param suffix [String] an index name optional suffix 15 | # @param bulk_size [Integer] bulk size in bytes 16 | # @param bulk_options [Hash] options passed to the elasticsearch-api bulk method 17 | def initialize(index, suffix: nil, bulk_size: nil, **bulk_options) 18 | @index = index 19 | @suffix = suffix 20 | @bulk_size = bulk_size - 1.kilobyte if bulk_size # 1 kilobyte for request header and newlines 21 | @bulk_options = bulk_options 22 | 23 | raise ArgumentError, '`bulk_size` can\'t be less than 1 kilobyte' if @bulk_size && @bulk_size <= 0 24 | end 25 | 26 | # Performs a bulk request with the passed body, returns empty 27 | # array if everything is fine and array filled with errored 28 | # document entries if something went wrong. 29 | # 30 | # @param body [Array] a standard bulk request body 31 | # @return [Array] an array of bulk errors 32 | def perform(body) 33 | return [] if body.blank? 34 | 35 | request_bodies(body).each_with_object([]) do |request_body, results| 36 | response = @index.client.bulk(**request_base.merge(body: request_body)) if request_body.present? 37 | 38 | next unless response.try(:[], 'errors') 39 | 40 | response_items = (response.try(:[], 'items') || []) 41 | .select { |item| item.values.first['error'] } 42 | results.concat(response_items) 43 | end 44 | end 45 | 46 | private 47 | 48 | def request_base 49 | @request_base ||= { 50 | index: @index.index_name(suffix: @suffix) 51 | }.merge!(@bulk_options) 52 | end 53 | 54 | def request_bodies(body) 55 | if @bulk_size 56 | serializer = ::Elasticsearch::API.serializer 57 | pieces = body.each_with_object(['']) do |piece, result| 58 | operation, meta = piece.to_a.first 59 | data = meta.delete(:data) 60 | piece = serializer.dump(operation => meta) 61 | piece << "\n" << serializer.dump(data) if data.present? 62 | 63 | if result.last.bytesize + piece.bytesize > @bulk_size 64 | result.push(piece) 65 | else 66 | result[-1].blank? ? (result[-1] = piece) : (result[-1] << "\n" << piece) 67 | end 68 | end 69 | pieces.each { |piece| piece << "\n" } 70 | else 71 | [body] 72 | end 73 | end 74 | end 75 | end 76 | end 77 | end 78 | -------------------------------------------------------------------------------- /lib/chewy/index/import/journal_builder.rb: -------------------------------------------------------------------------------- 1 | module Chewy 2 | class Index 3 | module Import 4 | class JournalBuilder 5 | def initialize(index, to_index: [], delete: []) 6 | @index = index 7 | @to_index = to_index 8 | @delete = delete 9 | end 10 | 11 | def bulk_body 12 | Chewy::Index::Import::BulkBuilder.new( 13 | Chewy::Stash::Journal, 14 | to_index: [ 15 | entries(:index, @to_index), 16 | entries(:delete, @delete) 17 | ].compact 18 | ).bulk_body.each do |item| 19 | item.values.first.merge!( 20 | _index: Chewy::Stash::Journal.index_name 21 | ) 22 | end 23 | end 24 | 25 | private 26 | 27 | def entries(action, objects) 28 | return unless objects.present? 29 | 30 | { 31 | index_name: @index.derivable_name, 32 | action: action, 33 | references: identify(objects).map { |item| Base64.encode64(::Elasticsearch::API.serializer.dump(item)) }, 34 | created_at: Time.now.utc 35 | } 36 | end 37 | 38 | def identify(objects) 39 | @index.adapter.identify(objects) 40 | end 41 | end 42 | end 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /lib/chewy/index/observe.rb: -------------------------------------------------------------------------------- 1 | require 'chewy/index/observe/callback' 2 | require 'chewy/index/observe/active_record_methods' 3 | 4 | module Chewy 5 | class Index 6 | module Observe 7 | extend ActiveSupport::Concern 8 | 9 | module ClassMethods 10 | def update_index(objects, options = {}) 11 | Chewy.strategy.current.update(self, objects, options) 12 | true 13 | end 14 | end 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /lib/chewy/index/observe/active_record_methods.rb: -------------------------------------------------------------------------------- 1 | module Chewy 2 | class Index 3 | module Observe 4 | module Helpers 5 | def update_proc(index_name, *args, &block) 6 | options = args.extract_options! 7 | method = args.first 8 | 9 | proc do 10 | reference = if index_name.is_a?(Proc) 11 | if index_name.arity.zero? 12 | instance_exec(&index_name) 13 | else 14 | index_name.call(self) 15 | end 16 | else 17 | index_name 18 | end 19 | 20 | index = Chewy.derive_name(reference) 21 | 22 | next if Chewy.strategy.current.name == :bypass 23 | 24 | backreference = if method && method.to_s == 'self' 25 | self 26 | elsif method 27 | send(method) 28 | else 29 | instance_eval(&block) 30 | end 31 | 32 | index.update_index(backreference, options) 33 | end 34 | end 35 | 36 | def extract_callback_options!(args) 37 | options = args.extract_options! 38 | result = options.each_key.with_object({}) do |key, hash| 39 | hash[key] = options.delete(key) if %i[if unless].include?(key) 40 | end 41 | args.push(options) unless options.empty? 42 | result 43 | end 44 | end 45 | 46 | extend Helpers 47 | 48 | module ActiveRecordMethods 49 | extend ActiveSupport::Concern 50 | 51 | def run_chewy_callbacks 52 | chewy_callbacks.each { |callback| callback.call(self) } 53 | end 54 | 55 | def update_chewy_indices 56 | Chewy.strategy.current.update_chewy_indices(self) 57 | end 58 | 59 | included do 60 | class_attribute :chewy_callbacks, default: [] 61 | end 62 | 63 | class_methods do 64 | def initialize_chewy_callbacks 65 | if Chewy.use_after_commit_callbacks 66 | after_commit :update_chewy_indices, on: %i[create update] 67 | after_commit :run_chewy_callbacks, on: :destroy 68 | else 69 | after_save :update_chewy_indices 70 | after_destroy :run_chewy_callbacks 71 | end 72 | end 73 | 74 | def update_index(type_name, *args, &block) 75 | callback_options = Observe.extract_callback_options!(args) 76 | update_proc = Observe.update_proc(type_name, *args, &block) 77 | callback = Chewy::Index::Observe::Callback.new(update_proc, callback_options) 78 | 79 | initialize_chewy_callbacks if chewy_callbacks.empty? 80 | 81 | self.chewy_callbacks = chewy_callbacks.dup << callback 82 | end 83 | end 84 | end 85 | end 86 | end 87 | end 88 | -------------------------------------------------------------------------------- /lib/chewy/index/observe/callback.rb: -------------------------------------------------------------------------------- 1 | module Chewy 2 | class Index 3 | module Observe 4 | class Callback 5 | def initialize(executable, filters = {}) 6 | @executable = executable 7 | @if_filter = filters[:if] 8 | @unless_filter = filters[:unless] 9 | end 10 | 11 | def call(context) 12 | return if !@if_filter.nil? && !eval_filter(@if_filter, context) 13 | return if !@unless_filter.nil? && eval_filter(@unless_filter, context) 14 | 15 | eval_proc(@executable, context) 16 | end 17 | 18 | private 19 | 20 | def eval_filter(filter, context) 21 | case filter 22 | when Symbol then context.send(filter) 23 | when Proc then eval_proc(filter, context) 24 | else filter 25 | end 26 | end 27 | 28 | def eval_proc(executable, context) 29 | executable.arity.zero? ? context.instance_exec(&executable) : executable.call(context) 30 | end 31 | end 32 | end 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /lib/chewy/index/settings.rb: -------------------------------------------------------------------------------- 1 | module Chewy 2 | class Index 3 | # Stores ElasticSearch index settings and resolves `analysis` 4 | # hash. At first, you need to store some analyzers or other 5 | # analysis options to the corresponding repository: 6 | # 7 | # @example 8 | # Chewy.analyzer :title_analyzer, type: 'custom', filter: %w(lowercase icu_folding title_nysiis) 9 | # Chewy.filter :title_nysiis, type: 'phonetic', encoder: 'nysiis', replace: false 10 | # 11 | # `title_nysiis` filter here will be expanded automatically when 12 | # `title_analyzer` analyser will be used in index settings: 13 | # 14 | # @example 15 | # class ProductsIndex < Chewy::Index 16 | # settings analysis: { 17 | # analyzer: [ 18 | # 'title_analyzer', 19 | # {one_more_analyzer: {type: 'custom', tokenizer: 'lowercase'}} 20 | # ] 21 | # } 22 | # end 23 | # 24 | # Additional analysing options, which wasn't stored in repositories, 25 | # might be used as well. 26 | # 27 | class Settings 28 | def initialize(params = {}, &block) 29 | @params = params 30 | @proc_params = block 31 | end 32 | 33 | def to_hash 34 | settings = @params.deep_symbolize_keys 35 | settings.merge!((@proc_params.call || {}).deep_symbolize_keys) if @proc_params 36 | 37 | settings[:analysis] = resolve_analysis(settings[:analysis]) if settings[:analysis] 38 | 39 | if settings[:index] || Chewy.configuration[:index] 40 | settings[:index] = (Chewy.configuration[:index] || {}) 41 | .deep_merge((settings[:index] || {}).deep_symbolize_keys) 42 | end 43 | 44 | settings.present? ? {settings: settings} : {} 45 | end 46 | 47 | private 48 | 49 | def resolve_analysis(analysis) 50 | analyzer = resolve(analysis[:analyzer], Chewy.analyzers) 51 | 52 | options = %i[tokenizer filter char_filter].each.with_object({}) do |type, result| 53 | dependencies = collect_dependencies(type, analyzer) 54 | resolved = resolve(dependencies.push(analysis[type]), Chewy.send(type.to_s.pluralize)) 55 | result.merge!(type => resolved) if resolved.present? 56 | end 57 | 58 | options[:analyzer] = analyzer if analyzer.present? 59 | analysis = analysis.except(:analyzer, :tokenizer, :filter, :char_filter) 60 | analysis.merge(options) 61 | end 62 | 63 | def collect_dependencies(type, analyzer) 64 | analyzer.map { |_, options| options[type] }.compact.flatten.uniq 65 | end 66 | 67 | def resolve(params, repository) 68 | if params.is_a?(Array) 69 | params.flatten.reject(&:blank?).each.with_object({}) do |name_or_hash, result| 70 | options = if name_or_hash.is_a?(Hash) 71 | name_or_hash 72 | else 73 | name_or_hash = name_or_hash.to_sym 74 | resolved = repository[name_or_hash] 75 | resolved ? {name_or_hash => resolved} : {} 76 | end 77 | result.merge!(options) 78 | end 79 | else 80 | params || {} 81 | end 82 | end 83 | end 84 | end 85 | end 86 | -------------------------------------------------------------------------------- /lib/chewy/index/specification.rb: -------------------------------------------------------------------------------- 1 | module Chewy 2 | class Index 3 | # Index specification is a combination of index settings and 4 | # mappings. The idea behind this class is that specification 5 | # can be locked in the `Chewy::Stash::Specification` between 6 | # resets, so it is possible to track changes. In the future 7 | # it is planned to be way smarter but right now `rake chewy:deploy` 8 | # checks if there were changes and resets the index only if 9 | # anything was changed. Otherwise, the index reset is skipped. 10 | # 11 | # @see Chewy::Stash::Specification 12 | class Specification 13 | # @see Chewy::Index::Specification 14 | # @param index [Chewy::Index] Just a chewy index 15 | def initialize(index) 16 | @index = index 17 | end 18 | 19 | # Stores the current index specification to the `Chewy::Stash::Specification` 20 | # as json. 21 | # 22 | # @raise [Chewy::ImportFailed] if something went wrong 23 | # @return [true] if everything is fine 24 | def lock! 25 | Chewy::Stash::Specification.import!([ 26 | id: @index.derivable_name, 27 | specification: Base64.encode64(current.to_json) 28 | ], journal: false) 29 | end 30 | 31 | # Returns the last locked specification as ruby hash. Returns 32 | # empty hash if nothing is stored yet. 33 | # 34 | # @return [Hash] hash produced with JSON parser 35 | def locked 36 | filter = {ids: {values: [@index.derivable_name]}} 37 | document = Chewy::Stash::Specification.filter(filter).first 38 | return {} unless document 39 | 40 | JSON.load(Base64.decode64(document.specification)) # rubocop:disable Security/JSONLoad 41 | end 42 | 43 | # Simply returns `Chewy::Index.specification_hash`, but 44 | # prepared for JSON with `as_json` method. This means all the 45 | # keys are strings and there are only values of types handled in JSON. 46 | # 47 | # @see Chewy::Index.specification_hash 48 | # @return [Hash] a JSON-ready hash 49 | def current 50 | @index.specification_hash.as_json 51 | end 52 | 53 | # Compares previously locked and current specifications. 54 | # 55 | # @return [true, false] the result of comparison 56 | def changed? 57 | current != locked 58 | end 59 | end 60 | end 61 | end 62 | -------------------------------------------------------------------------------- /lib/chewy/index/wrapper.rb: -------------------------------------------------------------------------------- 1 | module Chewy 2 | class Index 3 | module Wrapper 4 | extend ActiveSupport::Concern 5 | 6 | included do 7 | attr_accessor :_data, :_object 8 | attr_reader :attributes 9 | end 10 | 11 | module ClassMethods 12 | def build(hit) 13 | attributes = (hit['_source'] || {}) 14 | .reverse_merge(id: hit['_id']) 15 | .merge!(_score: hit['_score']) 16 | .merge!(_explanation: hit['_explanation']) 17 | 18 | wrapper = new(attributes) 19 | wrapper._data = hit 20 | wrapper 21 | end 22 | end 23 | 24 | def initialize(attributes = {}) 25 | @attributes = attributes.stringify_keys 26 | end 27 | 28 | def ==(other) 29 | return true if super 30 | 31 | if other.is_a?(Chewy::Index) 32 | self.class == other.class && (respond_to?(:id) ? id == other.id : attributes == other.attributes) 33 | elsif other.respond_to?(:id) 34 | self.class.adapter.target.is_a?(Class) && 35 | other.is_a?(self.class.adapter.target) && 36 | id.to_s == other.id.to_s 37 | else 38 | false 39 | end 40 | end 41 | 42 | %w[_id _type _index].each do |name| 43 | define_method name do 44 | _data[name] 45 | end 46 | end 47 | 48 | def method_missing(method, *args, &block) 49 | m = method.to_s 50 | if (name = highlight_name(m)) 51 | highlight(name) 52 | elsif (name = highlight_names(m)) 53 | highlights(name) 54 | elsif @attributes.key?(m) 55 | @attributes[m] 56 | elsif attribute_defined?(m) 57 | nil 58 | else 59 | super 60 | end 61 | end 62 | 63 | def respond_to_missing?(method, include_private = false) 64 | m = method.to_s 65 | highlight_name(m) || highlight_names(m) || @attributes.key?(m) || attribute_defined?(m) || super 66 | end 67 | 68 | private 69 | 70 | def highlight_name(method) 71 | method.sub(/_highlight\z/, '') if method.end_with?('_highlight') 72 | end 73 | 74 | def highlight_names(method) 75 | method.sub(/_highlights\z/, '') if method.end_with?('_highlights') 76 | end 77 | 78 | def attribute_defined?(attribute) 79 | self.class.root && self.class.root.children.find { |a| a.name.to_s == attribute }.present? 80 | end 81 | 82 | def highlight(attribute) 83 | _data['highlight'][attribute].first if highlight?(attribute) 84 | end 85 | 86 | def highlights(attribute) 87 | _data['highlight'][attribute] if highlight?(attribute) 88 | end 89 | 90 | def highlight?(attribute) 91 | _data.key?('highlight') && _data['highlight'].key?(attribute) 92 | end 93 | end 94 | end 95 | end 96 | -------------------------------------------------------------------------------- /lib/chewy/journal.rb: -------------------------------------------------------------------------------- 1 | module Chewy 2 | # A class to perform journal-related actions for the specified indexes/types. 3 | # 4 | # @example 5 | # journal = Chewy::Journal.new('places', UsersIndex) 6 | # journal.apply(20.minutes.ago) 7 | # journal.clean 8 | # 9 | class Journal 10 | # @param only [Array] indexes or string references to perform actions on 11 | def initialize(*only) 12 | @only = only 13 | end 14 | 15 | # Applies all changes that were done since the specified time to the 16 | # specified indexes. 17 | # 18 | # @param since_time [Time, DateTime] timestamp from which changes will be applied 19 | # @param fetch_limit [Int] amount of entries to be fetched on each cycle 20 | # @return [Integer] the amount of journal entries found 21 | def apply(since_time, fetch_limit: 10, **import_options) 22 | stage = 1 23 | since_time -= 1 24 | count = 0 25 | 26 | total_count = entries(since_time, fetch_limit).total_count 27 | 28 | while count < total_count 29 | entries = entries(since_time, fetch_limit).to_a.presence or break 30 | count += entries.size 31 | groups = reference_groups(entries) 32 | ActiveSupport::Notifications.instrument 'apply_journal.chewy', stage: stage, groups: groups 33 | groups.each do |index, references| 34 | index.import(references, import_options.merge(journal: false)) 35 | end 36 | stage += 1 37 | since_time = entries.map(&:created_at).max 38 | end 39 | count 40 | end 41 | 42 | # Cleans journal for the specified indexes/types. 43 | # 44 | # @param until_time [Time, DateTime] time to clean up until it 45 | # @return [Hash] delete_by_query ES API call result 46 | def clean(until_time = nil, delete_by_query_options: {}) 47 | Chewy::Stash::Journal.clean( 48 | until_time, 49 | only: @only, 50 | delete_by_query_options: delete_by_query_options.merge(refresh: false) 51 | ) 52 | end 53 | 54 | private 55 | 56 | def entries(since_time, fetch_limit) 57 | Chewy::Stash::Journal.entries(since_time, only: @only).order(:created_at).limit(fetch_limit) 58 | end 59 | 60 | def reference_groups(entries) 61 | entries.group_by(&:index_name) 62 | .transform_keys { |index_name| Chewy.derive_name(index_name) } 63 | .transform_values { |grouped_entries| grouped_entries.map(&:references).inject(:|) } 64 | end 65 | end 66 | end 67 | -------------------------------------------------------------------------------- /lib/chewy/log_subscriber.rb: -------------------------------------------------------------------------------- 1 | module Chewy 2 | class LogSubscriber < ActiveSupport::LogSubscriber 3 | def logger 4 | Chewy.logger 5 | end 6 | 7 | def import_objects(event) 8 | render_action('Import', event) { |payload| payload[:import] } 9 | end 10 | 11 | def search_query(event) 12 | render_action('Search', event) { |payload| payload[:request] } 13 | end 14 | 15 | def delete_query(event) 16 | render_action('Delete by Query', event) { |payload| payload[:request] } 17 | end 18 | 19 | def render_action(action, event) 20 | payload = event.payload 21 | description = yield(payload) 22 | 23 | return if description.blank? 24 | 25 | subject = payload[:type].presence || payload[:index] 26 | action = "#{subject} #{action} (#{event.duration.round(1)}ms)" 27 | action = if ActiveSupport.version >= Gem::Version.new('7.1') 28 | color(action, GREEN, bold: true) 29 | else 30 | color(action, GREEN, true) 31 | end 32 | 33 | debug(" #{action} #{description}") 34 | end 35 | end 36 | end 37 | 38 | Chewy::LogSubscriber.attach_to :chewy 39 | -------------------------------------------------------------------------------- /lib/chewy/minitest.rb: -------------------------------------------------------------------------------- 1 | require 'chewy/minitest/helpers' 2 | -------------------------------------------------------------------------------- /lib/chewy/minitest/search_index_receiver.rb: -------------------------------------------------------------------------------- 1 | # Test helper class to provide minitest hooks for Chewy::Index testing. 2 | # 3 | # @note Intended to be used in conjunction with a test helper which mocks over the #bulk 4 | # method on a {Chewy::Index} class. (See {Chewy::Minitest::Helpers}) 5 | # 6 | # The class will capture the data from the *param on the Chewy::Index.bulk method and 7 | # aggregate the data for test analysis. 8 | class SearchIndexReceiver 9 | MUTATION_FOR_CLASS = Struct.new(:indexes, :deletes, keyword_init: true) 10 | 11 | def initialize 12 | @mutations = {} 13 | end 14 | 15 | # @param bulk_params [Hash] the bulk_params that should be sent to the Chewy::Index.bulk method. 16 | # @param index [Chewy::Index] the index executing this query. 17 | def catch(bulk_params, index) 18 | Array.wrap(bulk_params).map { |y| y[:body] }.flatten.each do |update| 19 | if update[:delete] 20 | mutation_for(index).deletes << update[:delete][:_id] 21 | elsif update[:index] 22 | mutation_for(index).indexes << update[:index] 23 | end 24 | end 25 | end 26 | 27 | # @param index [Chewy::Index] return only index requests to the specified {Chewy::Index} index. 28 | # @return [Hash] the index changes captured by the mock. 29 | def indexes_for(index = nil) 30 | if index 31 | mutation_for(index).indexes 32 | else 33 | @mutations.transform_values(&:indexes) 34 | end 35 | end 36 | alias_method :indexes, :indexes_for 37 | 38 | # @param index [Chewy::Index] return only delete requests to the specified {Chewy::Index} index. 39 | # @return [Hash] the index deletes captured by the mock. 40 | def deletes_for(index = nil) 41 | if index 42 | mutation_for(index).deletes 43 | else 44 | @mutations.transform_values(&:deletes) 45 | end 46 | end 47 | alias_method :deletes, :deletes_for 48 | 49 | # Check to see if a given object has been indexed. 50 | # @param obj [#id] obj the object to look for. 51 | # @param index [Chewy::Index] what index the object should be indexed in. 52 | # @return [true, false] if the object was indexed. 53 | def indexed?(obj, index) 54 | indexes_for(index).map { |i| i[:_id] }.include? obj.id 55 | end 56 | 57 | # Check to see if a given object has been deleted. 58 | # @param obj [#id] obj the object to look for. 59 | # @param index [Chewy::Index] what index the object should have been deleted from. 60 | # @return [true, false] if the object was deleted. 61 | def deleted?(obj, index) 62 | deletes_for(index).include? obj.id 63 | end 64 | 65 | # @return [Array] a list of indexes changed. 66 | def updated_indexes 67 | @mutations.keys 68 | end 69 | 70 | private 71 | 72 | # Get the mutation object for a given index. 73 | # @param index [Chewy::Index] the index to fetch. 74 | # @return [#indexes, #deletes] an object with a list of indexes and a list of deletes. 75 | def mutation_for(index) 76 | @mutations[index] ||= MUTATION_FOR_CLASS.new(indexes: [], deletes: []) 77 | end 78 | end 79 | -------------------------------------------------------------------------------- /lib/chewy/multi_search.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Chewy 4 | # `Chewy::MultiSearch` provides an interface for executing multiple 5 | # queries via the Elasticsearch Multi Search API. When a MultiSearch 6 | # is performed it wraps the responses from Elasticsearch and assigns 7 | # them to the appropriate queries. 8 | class MultiSearch 9 | attr_reader :queries 10 | 11 | # Instantiate a new MultiSearch instance. 12 | # 13 | # @param queries [Array] 14 | # @option [Elasticsearch::Transport::Client] :client (Chewy.client) 15 | # The Elasticsearch client that should be used for issuing requests. 16 | def initialize(queries, client: Chewy.client) 17 | @client = client 18 | @queries = Array(queries) 19 | end 20 | 21 | # Adds a query to be performed by the MultiSearch 22 | # 23 | # @param query [Chewy::Search::Request] 24 | def add_query(query) 25 | @queries << query 26 | end 27 | 28 | # Performs any unperformed queries and returns the responses for all queries. 29 | # 30 | # @return [Array] 31 | def responses 32 | perform 33 | queries.map(&:response) 34 | end 35 | 36 | # Performs any unperformed queries. 37 | def perform 38 | unperformed_queries = queries.reject(&:performed?) 39 | return if unperformed_queries.empty? 40 | 41 | responses = msearch(unperformed_queries)['responses'] 42 | unperformed_queries.zip(responses).map { |query, response| query.response = response } 43 | end 44 | 45 | private 46 | 47 | attr_reader :client 48 | 49 | def msearch(queries_to_search) 50 | body = queries_to_search.flat_map do |query| 51 | rendered = query.render 52 | [rendered.except(:body), rendered[:body]] 53 | end 54 | 55 | client.msearch(body: body) 56 | end 57 | end 58 | 59 | def self.msearch(queries) 60 | Chewy::MultiSearch.new(queries) 61 | end 62 | end 63 | -------------------------------------------------------------------------------- /lib/chewy/railtie.rb: -------------------------------------------------------------------------------- 1 | module Chewy 2 | class Railtie < Rails::Railtie 3 | def self.all_engines 4 | Rails::Engine.subclasses.map(&:instance) + [Rails.application] 5 | end 6 | 7 | class RequestStrategy 8 | def initialize(app) 9 | @app = app 10 | end 11 | 12 | def call(env) 13 | # For Rails applications in `api_only` mode, the `assets` config isn't present 14 | if Rails.application.config.respond_to?(:assets) && env['PATH_INFO'].start_with?(Rails.application.config.assets.prefix) 15 | @app.call(env) 16 | else 17 | if Chewy.logger && @request_strategy != Chewy.request_strategy 18 | Chewy.logger.info("Chewy request strategy is `#{Chewy.request_strategy}`") 19 | end 20 | @request_strategy = Chewy.request_strategy 21 | Chewy.strategy(Chewy.request_strategy) { @app.call(env) } 22 | end 23 | end 24 | end 25 | 26 | module MigrationStrategy 27 | def migrate(*args) 28 | Chewy.strategy(:bypass) { super } 29 | end 30 | end 31 | 32 | rake_tasks do 33 | load 'tasks/chewy.rake' 34 | end 35 | 36 | console do |app| 37 | if app.sandbox? 38 | Chewy.strategy(:bypass) 39 | else 40 | Chewy.strategy(Chewy.console_strategy) 41 | end 42 | puts "Chewy console strategy is `#{Chewy.strategy.current.name}`" 43 | end 44 | 45 | initializer 'chewy.logger', after: 'active_record.logger' do 46 | ActiveSupport.on_load(:active_record) { Chewy.logger ||= ActiveRecord::Base.logger } 47 | end 48 | 49 | initializer 'chewy.migration_strategy' do 50 | ActiveSupport.on_load(:active_record) do 51 | ActiveRecord::Migration.prepend(MigrationStrategy) 52 | ActiveRecord::Migrator.prepend(MigrationStrategy) if defined? ActiveRecord::Migrator 53 | end 54 | end 55 | 56 | initializer 'chewy.request_strategy' do |app| 57 | app.config.middleware.insert_before(ActionDispatch::ShowExceptions, RequestStrategy) 58 | end 59 | 60 | initializer 'chewy.add_indices_path' do |_app| 61 | Chewy::Railtie.all_engines.each do |engine| 62 | engine.paths.add Chewy.configuration[:indices_path] 63 | end 64 | end 65 | end 66 | end 67 | -------------------------------------------------------------------------------- /lib/chewy/repository.rb: -------------------------------------------------------------------------------- 1 | module Chewy 2 | class Repository 3 | include Singleton 4 | 5 | attr_reader :analyzers, :tokenizers, :filters, :char_filters 6 | 7 | def self.delegated 8 | public_instance_methods - superclass.public_instance_methods - Singleton.public_instance_methods 9 | end 10 | 11 | def self.repository(name) 12 | plural_name = name.to_s.pluralize 13 | 14 | class_eval <<-METHOD, __FILE__, __LINE__ + 1 15 | def #{name}(name, options = nil) 16 | options ? #{plural_name}[name.to_sym] = options : #{plural_name}[name.to_sym] 17 | end 18 | METHOD 19 | end 20 | 21 | # Analysers repository: 22 | # 23 | # Chewy.analyzer :my_analyzer2, { 24 | # type: custom, 25 | # tokenizer: 'my_tokenizer1', 26 | # filter : ['my_token_filter1', 'my_token_filter2'] 27 | # char_filter : ['my_html'] 28 | # } 29 | # Chewy.analyzer(:my_analyzer2) # => {type: 'custom', tokenizer: ...} 30 | # 31 | repository :analyzer 32 | 33 | # Tokenizers repository: 34 | # 35 | # Chewy.tokenizer :my_tokenizer1, {type: standard, max_token_length: 900} 36 | # Chewy.tokenizer(:my_tokenizer1) # => {type: standard, max_token_length: 900} 37 | # 38 | repository :tokenizer 39 | 40 | # Token filters repository: 41 | # 42 | # Chewy.filter :my_token_filter1, {type: stop, stopwords: [stop1, stop2, stop3, stop4]} 43 | # Chewy.filter(:my_token_filter1) # => {type: stop, stopwords: [stop1, stop2, stop3, stop4]} 44 | # 45 | repository :filter 46 | 47 | # Char filters repository: 48 | # 49 | # Chewy.char_filter :my_html, {type: html_strip, escaped_tags: [xxx, yyy], read_ahead: 1024} 50 | # Chewy.char_filter(:my_html) # => {type: html_strip, escaped_tags: [xxx, yyy], read_ahead: 1024} 51 | # 52 | repository :char_filter 53 | 54 | def initialize 55 | @analyzers = {} 56 | @tokenizers = {} 57 | @filters = {} 58 | @char_filters = {} 59 | end 60 | end 61 | end 62 | -------------------------------------------------------------------------------- /lib/chewy/rspec.rb: -------------------------------------------------------------------------------- 1 | require 'chewy/rspec/build_query' 2 | require 'chewy/rspec/helpers' 3 | require 'chewy/rspec/update_index' 4 | -------------------------------------------------------------------------------- /lib/chewy/rspec/build_query.rb: -------------------------------------------------------------------------------- 1 | # Rspec helper to compare request and expected query 2 | # To use it - add `require 'chewy/rspec/build_query'` to the `spec_helper.rb` 3 | # Simple usage - just pass expected response as argument 4 | # and then call needed query. 5 | # 6 | # expect { method1.method2...methodN }.to build_query(expected_query) 7 | # 8 | RSpec::Matchers.define :build_query do |expected_query = {}| 9 | match do |request| 10 | request.render == expected_query 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /lib/chewy/rspec/helpers.rb: -------------------------------------------------------------------------------- 1 | module Chewy 2 | module Rspec 3 | module Helpers 4 | extend ActiveSupport::Concern 5 | # Rspec helper to mock elasticsearch response 6 | # To use it - add `require 'chewy/rspec'` to the `spec_helper.rb` 7 | # 8 | # mock_elasticsearch_response(CitiesIndex, raw_response) 9 | # expect(CitiesIndex.query({}).hits).to eq(hits) 10 | # 11 | def mock_elasticsearch_response(index, raw_response) 12 | mocked_request = Chewy::Search::Request.new(index) 13 | allow(Chewy::Search::Request).to receive(:new).and_return(mocked_request) 14 | allow(mocked_request).to receive(:perform).and_return(raw_response) 15 | end 16 | 17 | # Rspec helper to mock Elasticsearch response source 18 | # To use it - add `require 'chewy/rspec'` to the `spec_helper.rb` 19 | # 20 | # mock_elasticsearch_response_sources(CitiesIndex, sources) 21 | # expect(CitiesIndex.query({}).hits).to eq(hits) 22 | # 23 | def mock_elasticsearch_response_sources(index, hits) 24 | raw_response = { 25 | 'took' => 4, 26 | 'timed_out' => false, 27 | '_shards' => { 28 | 'total' => 1, 29 | 'successful' => 1, 30 | 'skipped' => 0, 31 | 'failed' => 0 32 | }, 33 | 'hits' => { 34 | 'total' => { 35 | 'value' => hits.count, 36 | 'relation' => 'eq' 37 | }, 38 | 'max_score' => 1.0, 39 | 'hits' => hits.each_with_index.map do |hit, i| 40 | { 41 | '_index' => index.index_name, 42 | '_type' => '_doc', 43 | '_id' => (i + 1).to_s, 44 | '_score' => 3.14, 45 | '_source' => hit 46 | } 47 | end 48 | } 49 | } 50 | 51 | mock_elasticsearch_response(index, raw_response) 52 | end 53 | end 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /lib/chewy/runtime.rb: -------------------------------------------------------------------------------- 1 | require 'chewy/runtime/version' 2 | 3 | module Chewy 4 | module Runtime 5 | def self.version 6 | Chewy.current[:chewy_runtime_version] ||= Version.new(Chewy.client.info['version']['number']) 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /lib/chewy/runtime/version.rb: -------------------------------------------------------------------------------- 1 | module Chewy 2 | module Runtime 3 | class Version 4 | include Comparable 5 | attr_reader :major, :minor, :patch 6 | 7 | def initialize(version) 8 | @major, @minor, @patch = *(version.to_s.split('.', 3) + ([0] * 3)).first(3).map(&:to_i) 9 | end 10 | 11 | def to_s 12 | [major, minor, patch].join('.') 13 | end 14 | 15 | def <=>(other) 16 | other = self.class.new(other) unless other.is_a?(self.class) 17 | [ 18 | major <=> other.major, 19 | minor <=> other.minor, 20 | patch <=> other.patch 21 | ].detect(&:nonzero?) || 0 22 | end 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /lib/chewy/search.rb: -------------------------------------------------------------------------------- 1 | require 'chewy/search/scoping' 2 | require 'chewy/search/scrolling' 3 | require 'chewy/search/query_proxy' 4 | require 'chewy/search/parameters' 5 | require 'chewy/search/response' 6 | require 'chewy/search/loader' 7 | require 'chewy/search/request' 8 | require 'chewy/search/pagination/kaminari' 9 | 10 | module Chewy 11 | # This module being included to any provides an interface to the 12 | # request DSL. By default it is included to {Chewy::Index}. 13 | # 14 | # The class used as a request DSL provider is 15 | # inherited from {Chewy::Search::Request} 16 | # 17 | # Also, the search class is refined with the pagination module {Chewy::Search::Pagination::Kaminari}. 18 | # 19 | # @example 20 | # PlacesIndex.query(match: {name: 'Moscow'}) 21 | # @see Chewy::Index 22 | # @see Chewy::Search::Request 23 | # @see Chewy::Search::ClassMethods 24 | # @see Chewy::Search::Pagination::Kaminari 25 | module Search 26 | extend ActiveSupport::Concern 27 | 28 | module ClassMethods 29 | # This is the entry point for the request composition, however, 30 | # most of the {Chewy::Search::Request} methods are delegated 31 | # directly as well. 32 | # 33 | # This method also provides an ability to use names scopes. 34 | # 35 | # @example 36 | # PlacesIndex.all.limit(10) 37 | # # is basically the same as: 38 | # PlacesIndex.limit(10) 39 | # @see Chewy::Search::Request 40 | # @see Chewy::Search::Scoping 41 | # @return [Chewy::Search::Request] request instance 42 | def all 43 | search_class.scopes.last || search_class.new(self) 44 | end 45 | 46 | # A simple way to execute search string query. 47 | # 48 | # @see https://www.elastic.co/guide/en/elasticsearch/reference/current/search-uri-request.html 49 | # @return [Hash] the request result 50 | def search_string(query, options = {}) 51 | options = options.merge(all.render.slice(:index).merge(q: query)) 52 | Chewy.client.search(options) 53 | end 54 | 55 | # Delegates methods from the request class to the index class 56 | # 57 | # @example 58 | # PlacesIndex.query(match: {name: 'Moscow'}) 59 | def method_missing(name, *args, &block) 60 | if search_class::DELEGATED_METHODS.include?(name) 61 | all.send(name, *args, &block) 62 | else 63 | super 64 | end 65 | end 66 | 67 | def respond_to_missing?(name, _) 68 | search_class::DELEGATED_METHODS.include?(name) || super 69 | end 70 | 71 | private 72 | 73 | def search_class 74 | @search_class ||= build_search_class(Chewy.search_class) 75 | end 76 | 77 | def build_search_class(base) 78 | search_class = Class.new(base) 79 | 80 | delegate_scoped self, search_class, scopes 81 | const_set('Query', search_class) 82 | end 83 | 84 | def delegate_scoped(source, destination, methods) 85 | methods.each do |method| 86 | destination.class_eval do 87 | define_method method do |*args, **kwargs, &block| 88 | scoping do 89 | source.public_send(method, *args, **kwargs, &block) 90 | end 91 | end 92 | method 93 | end 94 | end 95 | end 96 | end 97 | end 98 | end 99 | -------------------------------------------------------------------------------- /lib/chewy/search/loader.rb: -------------------------------------------------------------------------------- 1 | module Chewy 2 | module Search 3 | # This class is used for two different purposes: load ORM/ODM 4 | # source objects. 5 | # 6 | # @see Chewy::Index::Import 7 | # @see Chewy::Search::Request#load 8 | # @see Chewy::Search::Response#objects 9 | # @see Chewy::Search::Scrolling#scroll_objects 10 | class Loader 11 | # @param indexes [Array] list of indexes to lookup 12 | # @param options [Hash] adapter-specific load options 13 | # @see Chewy::Index::Adapter::Base#load 14 | def initialize(indexes: [], **options) 15 | @indexes = indexes 16 | @options = options 17 | end 18 | 19 | def derive_index(index_name) 20 | index = (@derive_index ||= {})[index_name] ||= indexes_hash[index_name] || 21 | indexes_hash[indexes_hash.keys.sort_by(&:length) 22 | .reverse.detect do |name| 23 | index_name.match(/#{name}(_.+|\z)/) 24 | end] 25 | raise Chewy::UndefinedIndex, "Can not find index named `#{index}`" unless index 26 | 27 | index 28 | end 29 | 30 | # For each passed hit this method loads an ORM/ORD source object 31 | # using `hit['_id']`. The returned array is exactly in the same order 32 | # as hits were. If source object was not found for some hit, `nil` 33 | # will be returned at the corresponding position in array. 34 | # 35 | # Records/documents are loaded in an efficient manner, performing 36 | # a single query for each index present. 37 | # 38 | # @param hits [Array] ES hits array 39 | # @return [Array] the array of corresponding ORM/ODM objects 40 | def load(hits) 41 | hit_groups = hits.group_by { |hit| hit['_index'] } 42 | loaded_objects = hit_groups.each_with_object({}) do |(index_name, hit_group), result| 43 | index = derive_index(index_name) 44 | ids = hit_group.map { |hit| hit['_id'] } 45 | loaded = index.adapter.load(ids, **@options.merge(_index: index.base_name)) 46 | loaded ||= hit_group.map { |hit| index.build(hit) } 47 | 48 | result.merge!(hit_group.zip(loaded).to_h) 49 | end 50 | 51 | hits.map { |hit| loaded_objects[hit] } 52 | end 53 | 54 | private 55 | 56 | def indexes_hash 57 | @indexes_hash ||= @indexes.index_by(&:index_name) 58 | end 59 | end 60 | end 61 | end 62 | -------------------------------------------------------------------------------- /lib/chewy/search/pagination/kaminari.rb: -------------------------------------------------------------------------------- 1 | module Chewy 2 | module Search 3 | module Pagination 4 | # This module provides `Kaminari` support for {Chewy::Search::Request} 5 | # It is included automatically if `Kaminari` is available. 6 | # 7 | # @example 8 | # PlacesIndex.all.page(3).per(10).order(:name) 9 | # # => {:size=>10, :from=>20, :sort=>["name"]}}> 10 | module Kaminari 11 | extend ActiveSupport::Concern 12 | 13 | included do 14 | include ::Kaminari::PageScopeMethods 15 | 16 | delegate :default_per_page, :max_per_page, :max_pages, to: :_kaminari_config 17 | 18 | class_eval <<-METHOD, __FILE__, __LINE__ + 1 19 | def #{::Kaminari.config.page_method_name}(num = 1) 20 | limit(limit_value).offset(limit_value * ([num.to_i, 1].max - 1)) 21 | end 22 | METHOD 23 | end 24 | 25 | def limit_value 26 | (raw_limit_value || default_per_page).to_i 27 | end 28 | 29 | def offset_value 30 | raw_offset_value.to_i 31 | end 32 | 33 | private 34 | 35 | def _kaminari_config 36 | ::Kaminari.config 37 | end 38 | 39 | def paginated_collection(collection) 40 | ::Kaminari.paginate_array(collection, limit: limit_value, offset: offset_value, total_count: total_count) 41 | end 42 | end 43 | end 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /lib/chewy/search/parameters/aggs.rb: -------------------------------------------------------------------------------- 1 | require 'chewy/search/parameters/storage' 2 | 3 | module Chewy 4 | module Search 5 | class Parameters 6 | # Just a standard hash storage. Nothing to see here. 7 | # 8 | # @see Chewy::Search::Parameters::HashStorage 9 | # @see Chewy::Search::Request#aggregations 10 | # @see https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations.html 11 | class Aggs < Storage 12 | include HashStorage 13 | end 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /lib/chewy/search/parameters/allow_partial_search_results.rb: -------------------------------------------------------------------------------- 1 | require 'chewy/search/parameters/storage' 2 | 3 | module Chewy 4 | module Search 5 | class Parameters 6 | # Stores boolean value, but has 3 states: `true`, `false` and `nil`. 7 | # 8 | # @see Chewy::Search::Request#allow_partial_search_results 9 | # @see https://www.elastic.co/guide/en/elasticsearch/reference/6.4/search-request-body.html#_parameters_4 10 | class AllowPartialSearchResults < Storage 11 | # We don't want to render `nil`, but render `true` and `false` values. 12 | # 13 | # @see Chewy::Search::Parameters::Storage#render 14 | # @return [{Symbol => Object}, nil] 15 | def render 16 | {self.class.param_name => value} unless value.nil? 17 | end 18 | 19 | private 20 | 21 | def normalize(value) 22 | !!value unless value.nil? 23 | end 24 | end 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /lib/chewy/search/parameters/collapse.rb: -------------------------------------------------------------------------------- 1 | require 'chewy/search/parameters/storage' 2 | 3 | module Chewy 4 | module Search 5 | class Parameters 6 | # Just a standard hash storage. Nothing to see here. 7 | # 8 | # @see Chewy::Search::Parameters::HashStorage 9 | # @see Chewy::Search::Request#collapse 10 | # @see https://www.elastic.co/guide/en/elasticsearch/reference/current/collapse-search-results.html 11 | class Collapse < Storage 12 | include HashStorage 13 | end 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /lib/chewy/search/parameters/concerns/bool_storage.rb: -------------------------------------------------------------------------------- 1 | module Chewy 2 | module Search 3 | class Parameters 4 | # Stores a boolean value. Any passed value is coerced to 5 | # a boolean value. 6 | module BoolStorage 7 | # Performs values disjunction on update. 8 | # 9 | # @see Chewy::Search::Parameters::Storage#update! 10 | # @param other_value [true, false, Object] any acceptable storage value 11 | # @return [true, false] updated value 12 | def update!(other_value) 13 | replace!(value || normalize(other_value)) 14 | end 15 | 16 | private 17 | 18 | def normalize(value) 19 | !!value 20 | end 21 | end 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /lib/chewy/search/parameters/concerns/hash_storage.rb: -------------------------------------------------------------------------------- 1 | module Chewy 2 | module Search 3 | class Parameters 4 | # Stores hashes with stringified keys. 5 | module HashStorage 6 | # Simply merges two value hashes on update 7 | # 8 | # @see Chewy::Search::Parameters::Storage#update! 9 | # @param other_value [{String, Symbol => Object}] any acceptable storage value 10 | # @return [{String => Object}] updated value 11 | def update!(other_value) 12 | value.merge!(normalize(other_value)) 13 | end 14 | 15 | private 16 | 17 | def normalize(value) 18 | (value || {}).stringify_keys 19 | end 20 | end 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /lib/chewy/search/parameters/concerns/integer_storage.rb: -------------------------------------------------------------------------------- 1 | module Chewy 2 | module Search 3 | class Parameters 4 | # Just a simple value storage, all the values are coerced to integer. 5 | module IntegerStorage 6 | private 7 | 8 | def normalize(value) 9 | Integer(value) if value 10 | end 11 | end 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /lib/chewy/search/parameters/concerns/string_array_storage.rb: -------------------------------------------------------------------------------- 1 | module Chewy 2 | module Search 3 | class Parameters 4 | # Stores value as an array of strings. 5 | module StringArrayStorage 6 | # Unions two arrays. 7 | # 8 | # @see Chewy::Search::Parameters::Storage#update! 9 | # @param other_value [String, Symbol, Array] any acceptable storage value 10 | # @return [Array] updated value 11 | def update!(other_value) 12 | @value = value | normalize(other_value) 13 | end 14 | 15 | private 16 | 17 | def normalize(value) 18 | Array.wrap(value).flatten(1).map(&:to_s).reject(&:blank?) 19 | end 20 | end 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /lib/chewy/search/parameters/concerns/string_storage.rb: -------------------------------------------------------------------------------- 1 | module Chewy 2 | module Search 3 | class Parameters 4 | # Just a simple value storage, all the values are coerced to string. 5 | module StringStorage 6 | private 7 | 8 | def normalize(value) 9 | value.to_s if value.present? 10 | end 11 | end 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /lib/chewy/search/parameters/docvalue_fields.rb: -------------------------------------------------------------------------------- 1 | require 'chewy/search/parameters/storage' 2 | 3 | module Chewy 4 | module Search 5 | class Parameters 6 | # @see Chewy::Search::Parameters::StringArrayStorage 7 | class DocvalueFields < Storage 8 | include StringArrayStorage 9 | end 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /lib/chewy/search/parameters/explain.rb: -------------------------------------------------------------------------------- 1 | require 'chewy/search/parameters/storage' 2 | 3 | module Chewy 4 | module Search 5 | class Parameters 6 | # Just a standard boolean storage, nothing to see here. 7 | # 8 | # @see Chewy::Search::Parameters::BoolStorage 9 | # @see Chewy::Search::Request#explain 10 | # @see https://www.elastic.co/guide/en/elasticsearch/reference/5.4/search-request-explain.html 11 | class Explain < Storage 12 | include BoolStorage 13 | end 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /lib/chewy/search/parameters/filter.rb: -------------------------------------------------------------------------------- 1 | require 'chewy/search/parameters/storage' 2 | 3 | module Chewy 4 | module Search 5 | class Parameters 6 | # This parameter storage doesn't have its own parameter at the 7 | # ES request body. Instead, it is embedded to the root `bool` 8 | # query of the `query` request parameter. Some additional query 9 | # reduction is performed in case of only several `must` filters 10 | # presence. 11 | # 12 | # @example 13 | # scope = PlacesIndex.filter(term: {name: 'Moscow'}) 14 | # # => {:query=>{:bool=>{:filter=>{:term=>{:name=>"Moscow"}}}}}}> 15 | # scope.query(match: {name: 'London'}) 16 | # # => {:query=>{:bool=>{ 17 | # # :must=>{:match=>{:name=>"London"}}, 18 | # # :filter=>{:term=>{:name=>"Moscow"}}}}}}> 19 | # @see https://www.elastic.co/guide/en/elasticsearch/reference/current/query-filter-context.html 20 | # @see Chewy::Search::Parameters::QueryStorage 21 | class Filter < Storage 22 | include QueryStorage 23 | 24 | # Even more reduction added here, we don't need to wrap with 25 | # `bool` query consists on `must` only. 26 | # 27 | # @see Chewy::Search::Parameters::Storage#render 28 | # @return [{Symbol => Hash}] 29 | def render 30 | rendered_bool = filter_query(value.query) 31 | {self.class.param_name => rendered_bool} if rendered_bool.present? 32 | end 33 | 34 | private 35 | 36 | def filter_query(value) 37 | bool = value[:bool] if value 38 | if bool && bool[:must].present? && bool[:should].blank? && bool[:must_not].blank? 39 | bool[:must] 40 | else 41 | value 42 | end 43 | end 44 | end 45 | end 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /lib/chewy/search/parameters/highlight.rb: -------------------------------------------------------------------------------- 1 | require 'chewy/search/parameters/storage' 2 | 3 | module Chewy 4 | module Search 5 | class Parameters 6 | # Just a standard hash storage. Nothing to see here. 7 | # 8 | # @see Chewy::Search::Parameters::HashStorage 9 | # @see Chewy::Search::Request#highlight 10 | # @see https://www.elastic.co/guide/en/elasticsearch/reference/5.4/search-request-highlighting.html 11 | class Highlight < Storage 12 | include HashStorage 13 | end 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /lib/chewy/search/parameters/ignore_unavailable.rb: -------------------------------------------------------------------------------- 1 | require 'chewy/search/parameters/storage' 2 | 3 | module Chewy 4 | module Search 5 | class Parameters 6 | # Stores boolean value, but has 3 states: `true`, `false` and `nil`. 7 | # 8 | # @see Chewy::Search::Request#ignore_unavailable 9 | # @see https://www.elastic.co/guide/en/elasticsearch/reference/current/multi-index.html#multi-index 10 | class IgnoreUnavailable < Storage 11 | # We don't want to render `nil`, but render `true` and `false` values. 12 | # 13 | # @see Chewy::Search::Parameters::Storage#render 14 | # @return [{Symbol => Object}, nil] 15 | def render 16 | {self.class.param_name => value} unless value.nil? 17 | end 18 | 19 | private 20 | 21 | def normalize(value) 22 | !!value unless value.nil? 23 | end 24 | end 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /lib/chewy/search/parameters/indices.rb: -------------------------------------------------------------------------------- 1 | require 'chewy/search/parameters/storage' 2 | 3 | module Chewy 4 | module Search 5 | class Parameters 6 | # Stores indices to query. 7 | # Renders it to lists of string accepted by ElasticSearch 8 | # API. 9 | # 10 | # If index is added to the storage, no matter, a class 11 | # or a string/symbol, it gets appended to the list. 12 | class Indices < Storage 13 | # Two index storages are equal if they produce the 14 | # same output on render. 15 | # 16 | # @see Chewy::Search::Parameters::Storage#== 17 | # @param other [Chewy::Search::Parameters::Storage] any storage instance 18 | # @return [true, false] the result of comparison 19 | def ==(other) 20 | super || (other.class == self.class && other.render == render) 21 | end 22 | 23 | # Just adds indices to indices. 24 | # 25 | # @see Chewy::Search::Parameters::Storage#update! 26 | # @param other_value [{Symbol => Array}] any acceptable storage value 27 | # @return [{Symbol => Array}] updated value 28 | def update!(other_value) 29 | new_value = normalize(other_value) 30 | 31 | @value = {indices: value[:indices] | new_value[:indices]} 32 | end 33 | 34 | # Returns desired index names. 35 | # 36 | # @see Chewy::Search::Parameters::Storage#render 37 | # @return [{Symbol => Array}] rendered value with the parameter name 38 | def render 39 | {index: index_names.uniq.sort}.reject { |_, v| v.blank? } 40 | end 41 | 42 | # Returns index classes used for the request. 43 | # No strings/symbols included. 44 | # 45 | # @return [Array] a list of index classes 46 | def indices 47 | index_classes 48 | end 49 | 50 | private 51 | 52 | def initialize_clone(origin) 53 | @value = origin.value.dup 54 | end 55 | 56 | def normalize(value) 57 | value ||= {} 58 | 59 | {indices: Array.wrap(value[:indices]).flatten.compact} 60 | end 61 | 62 | def index_classes 63 | value[:indices].select do |klass| 64 | klass.is_a?(Class) && klass < Chewy::Index 65 | end 66 | end 67 | 68 | def index_identifiers 69 | value[:indices] - index_classes 70 | end 71 | 72 | def index_names 73 | indices.map(&:index_name) | index_identifiers.map(&:to_s) 74 | end 75 | end 76 | end 77 | end 78 | end 79 | -------------------------------------------------------------------------------- /lib/chewy/search/parameters/indices_boost.rb: -------------------------------------------------------------------------------- 1 | require 'chewy/search/parameters/storage' 2 | 3 | module Chewy 4 | module Search 5 | class Parameters 6 | # Stores provided values as a string-float hash, but also takes 7 | # keys order into account. 8 | # 9 | # @see Chewy::Search::Request#indices_boost 10 | # @see https://www.elastic.co/guide/en/elasticsearch/reference/5.4/search-request-index-boost.html 11 | class IndicesBoost < Storage 12 | # Merges two hashes, but puts keys from the second hash 13 | # at the end of the result hash. 14 | # 15 | # @see Chewy::Search::Parameters::Storage#update! 16 | # @param other_value [{String, Symbol => String, Integer, Float}] any acceptable storage value 17 | # @return [{String => Float}] updated value 18 | def update!(other_value) 19 | new_value = normalize(other_value) 20 | value.except!(*new_value.keys).merge!(new_value) 21 | end 22 | 23 | # Renders the value hash as an array of hashes for 24 | # each key-value pair. 25 | # 26 | # @see Chewy::Search::Parameters::Storage#render 27 | # @return [Array<{String => Float}>] updated value 28 | def render 29 | {self.class.param_name => value.map { |k, v| {k => v} }} if value.present? 30 | end 31 | 32 | # Comparison also reqires additional logic. Since indexes boost 33 | # is sensitive to the order index templates are provided, we have 34 | # to compare stored hashes keys as well. 35 | # 36 | # @see Chewy::Search::Parameters::Storage#== 37 | # @return [true, false] 38 | def ==(other) 39 | super && value.keys == other.value.keys 40 | end 41 | 42 | private 43 | 44 | def normalize(value) 45 | value = (value || {}).stringify_keys 46 | value.each { |k, v| value[k] = Float(v) } 47 | value 48 | end 49 | end 50 | end 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /lib/chewy/search/parameters/knn.rb: -------------------------------------------------------------------------------- 1 | require 'chewy/search/parameters/storage' 2 | 3 | module Chewy 4 | module Search 5 | class Parameters 6 | # Just a standard hash storage. Nothing to see here. 7 | # 8 | # @see Chewy::Search::Parameters::HashStorage 9 | # @see Chewy::Search::Request#knn 10 | # @see https://www.elastic.co/guide/en/elasticsearch/reference/current/knn-search.html 11 | class Knn < Storage 12 | include HashStorage 13 | end 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /lib/chewy/search/parameters/limit.rb: -------------------------------------------------------------------------------- 1 | require 'chewy/search/parameters/storage' 2 | 3 | module Chewy 4 | module Search 5 | class Parameters 6 | # Just a standard integer value storage, nothing to see here. 7 | # 8 | # @see Chewy::Search::Parameters::IntegerStorage 9 | # @see Chewy::Search::Request#limit 10 | # @see https://www.elastic.co/guide/en/elasticsearch/reference/5.4/search-request-from-size.html 11 | class Limit < Storage 12 | include IntegerStorage 13 | self.param_name = :size 14 | end 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /lib/chewy/search/parameters/load.rb: -------------------------------------------------------------------------------- 1 | require 'chewy/search/parameters/storage' 2 | 3 | module Chewy 4 | module Search 5 | class Parameters 6 | # Acts like standard hash storage, with one exception: 7 | # all the keys are deeply symbolized for convenience. 8 | # 9 | # @see Chewy::Search::Request#load 10 | # @see Chewy::Search::Loader 11 | class Load < Storage 12 | # Simply merges two value hashes on update 13 | # 14 | # @see Chewy::Search::Parameters::Storage#update! 15 | # @param other_value [{String, Symbol => Object}] any acceptable storage value 16 | # @return [{Symbol => Object}] updated value 17 | def update!(other_value) 18 | value.merge!(normalize(other_value)) 19 | end 20 | 21 | # Doesn't render anythig, has specific handling logic. 22 | def render; end 23 | 24 | private 25 | 26 | def normalize(value) 27 | (value || {}).deep_symbolize_keys 28 | end 29 | end 30 | end 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /lib/chewy/search/parameters/min_score.rb: -------------------------------------------------------------------------------- 1 | require 'chewy/search/parameters/storage' 2 | 3 | module Chewy 4 | module Search 5 | class Parameters 6 | # Just a simple value storage, all the values are coerced to float. 7 | class MinScore < Storage 8 | private 9 | 10 | def normalize(value) 11 | Float(value) if value 12 | end 13 | end 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /lib/chewy/search/parameters/none.rb: -------------------------------------------------------------------------------- 1 | require 'chewy/search/parameters/storage' 2 | 3 | module Chewy 4 | module Search 5 | class Parameters 6 | # Just a standard boolean storage, except the rendering logic. 7 | # 8 | # @see Chewy::Search::Parameters::BoolStorage 9 | # @see Chewy::Search::Request#none 10 | # @see https://en.wikipedia.org/wiki/Null_Object_pattern 11 | class None < Storage 12 | include BoolStorage 13 | 14 | # Renders `match_none` query if the values is set to true. 15 | # 16 | # @see https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-match-all-query.html#query-dsl-match-none-query 17 | # @see Chewy::Search::Request 18 | # @see Chewy::Search::Request#response 19 | def render 20 | {query: {match_none: {}}} if value.present? 21 | end 22 | end 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /lib/chewy/search/parameters/offset.rb: -------------------------------------------------------------------------------- 1 | require 'chewy/search/parameters/storage' 2 | 3 | module Chewy 4 | module Search 5 | class Parameters 6 | # Just a standard integer value storage, nothing to see here. 7 | # 8 | # @see Chewy::Search::Parameters::IntegerStorage 9 | # @see Chewy::Search::Request#offset 10 | # @see https://www.elastic.co/guide/en/elasticsearch/reference/5.4/search-request-from-size.html 11 | class Offset < Storage 12 | include IntegerStorage 13 | self.param_name = :from 14 | end 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /lib/chewy/search/parameters/order.rb: -------------------------------------------------------------------------------- 1 | require 'chewy/search/parameters/storage' 2 | 3 | module Chewy 4 | module Search 5 | class Parameters 6 | # Sort parameter storage. Stores a hash of fields with the `nil` 7 | # key if no options for the field were specified. Normalizer 8 | # accepts an array of any hash-string-symbols combinations, or a hash. 9 | # 10 | # @see Chewy::Search::Request#order 11 | # @see Chewy::Search::Request#reorder 12 | # @see https://www.elastic.co/guide/en/elasticsearch/reference/current/search-request-sort.html 13 | class Order < Storage 14 | # Merges two hashes. 15 | # 16 | # @see Chewy::Search::Parameters::Storage#update! 17 | # @param other_value [Object] any acceptable storage value 18 | # @return [Object] updated value 19 | def update!(other_value) 20 | value.concat(normalize(other_value)) 21 | end 22 | 23 | # Size requires specialized rendering logic, it should return 24 | # an array to satisfy ES. 25 | # 26 | # @see Chewy::Search::Parameters::Storage#render 27 | # @return [{Symbol => Array}] 28 | def render 29 | return if value.blank? 30 | 31 | {sort: value} 32 | end 33 | 34 | private 35 | 36 | def normalize(value) 37 | case value 38 | when Array 39 | value.each_with_object([]) do |sv, res| 40 | res.concat(normalize(sv)) 41 | end 42 | when Hash 43 | [value.stringify_keys] 44 | else 45 | value.present? ? [value.to_s] : [] 46 | end 47 | end 48 | end 49 | end 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /lib/chewy/search/parameters/post_filter.rb: -------------------------------------------------------------------------------- 1 | require 'chewy/search/parameters/storage' 2 | 3 | module Chewy 4 | module Search 5 | class Parameters 6 | # A standard parameter storage, which updates `post_filter` parameter 7 | # of the ES request. 8 | # 9 | # @example 10 | # PlacesIndex.post_filter(match: {name: 'Moscow'}) 11 | # # => {:post_filter=>{:match=>{:name=>"Moscow"}}}}> 12 | # @see https://www.elastic.co/guide/en/elasticsearch/reference/current/search-request-post-filter.html 13 | # @see Chewy::Search::Parameters::QueryStorage 14 | class PostFilter < Storage 15 | include QueryStorage 16 | end 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /lib/chewy/search/parameters/preference.rb: -------------------------------------------------------------------------------- 1 | require 'chewy/search/parameters/storage' 2 | 3 | module Chewy 4 | module Search 5 | class Parameters 6 | # Just a standard string value storage, nothing to see here. 7 | # 8 | # @see Chewy::Search::Parameters::StringStorage 9 | # @see Chewy::Search::Request#preference 10 | # @see https://www.elastic.co/guide/en/elasticsearch/reference/5.4/search-request-preference.html 11 | class Preference < Storage 12 | include StringStorage 13 | end 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /lib/chewy/search/parameters/profile.rb: -------------------------------------------------------------------------------- 1 | require 'chewy/search/parameters/storage' 2 | 3 | module Chewy 4 | module Search 5 | class Parameters 6 | # Just a standard boolean storage, nothing to see here. 7 | # 8 | # @see Chewy::Search::Parameters::BoolStorage 9 | # @see Chewy::Search::Request#profile 10 | # @see https://www.elastic.co/guide/en/elasticsearch/reference/5.4/search-profile.html 11 | class Profile < Storage 12 | include BoolStorage 13 | end 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /lib/chewy/search/parameters/query.rb: -------------------------------------------------------------------------------- 1 | require 'chewy/search/parameters/storage' 2 | 3 | module Chewy 4 | module Search 5 | class Parameters 6 | # A standard parameter storage, which updates `query` parameter 7 | # of the ES request. 8 | # 9 | # @example 10 | # PlacesIndex.query(match: {name: 'Moscow'}) 11 | # # => {:query=>{:match=>{:name=>"Moscow"}}}}> 12 | # @see https://www.elastic.co/guide/en/elasticsearch/reference/current/search-request-query.html 13 | # @see Chewy::Search::Parameters::QueryStorage 14 | class Query < Storage 15 | include QueryStorage 16 | end 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /lib/chewy/search/parameters/request_cache.rb: -------------------------------------------------------------------------------- 1 | require 'chewy/search/parameters/storage' 2 | 3 | module Chewy 4 | module Search 5 | class Parameters 6 | # Stores boolean value, but has 3 states: `true`, `false` and `nil`. 7 | # 8 | # @see Chewy::Search::Request#request_cache 9 | # @see https://www.elastic.co/guide/en/elasticsearch/reference/5.4/shard-request-cache.html#_enabling_and_disabling_caching_per_request 10 | class RequestCache < Storage 11 | # We don't want to render `nil`, but render `true` and `false` values. 12 | # 13 | # @see Chewy::Search::Parameters::Storage#render 14 | # @return [{Symbol => Object}, nil] 15 | def render 16 | {self.class.param_name => value} unless value.nil? 17 | end 18 | 19 | private 20 | 21 | def normalize(value) 22 | !!value unless value.nil? 23 | end 24 | end 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /lib/chewy/search/parameters/rescore.rb: -------------------------------------------------------------------------------- 1 | require 'chewy/search/parameters/storage' 2 | 3 | module Chewy 4 | module Search 5 | class Parameters 6 | # Stores data as an array of hashes, exactly the same way 7 | # ES requires `rescore` to be provided. 8 | # 9 | # @see Chewy::Search::Request#rescore 10 | # @see https://www.elastic.co/guide/en/elasticsearch/reference/5.4/search-request-rescore.html 11 | class Rescore < Storage 12 | # Adds new data to the existing data array. 13 | # 14 | # @see Chewy::Search::Parameters::Storage#update! 15 | # @param other_value [Hash, Array] any acceptable storage value 16 | # @return [Array] updated value 17 | def update!(other_value) 18 | @value = value | normalize(other_value) 19 | end 20 | 21 | private 22 | 23 | def normalize(value) 24 | Array.wrap(value).flatten(1).reject(&:blank?) 25 | end 26 | end 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /lib/chewy/search/parameters/script_fields.rb: -------------------------------------------------------------------------------- 1 | require 'chewy/search/parameters/storage' 2 | 3 | module Chewy 4 | module Search 5 | class Parameters 6 | # Just a standard hash storage. Nothing to see here. 7 | # 8 | # @see Chewy::Search::Parameters::HashStorage 9 | # @see Chewy::Search::Request#script_fields 10 | # @see https://www.elastic.co/guide/en/elasticsearch/reference/5.4/search-request-script-fields.html 11 | class ScriptFields < Storage 12 | include HashStorage 13 | end 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /lib/chewy/search/parameters/search_after.rb: -------------------------------------------------------------------------------- 1 | require 'chewy/search/parameters/storage' 2 | 3 | module Chewy 4 | module Search 5 | class Parameters 6 | # Almost standard array storage without any typecasting. 7 | # The value is simply replaced on update. 8 | # 9 | # @see Chewy::Search::Request#search_after 10 | # @see https://www.elastic.co/guide/en/elasticsearch/reference/5.4/search-request-search-after.html 11 | class SearchAfter < Storage 12 | private 13 | 14 | def normalize(value) 15 | Array.wrap(value) if value 16 | end 17 | end 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /lib/chewy/search/parameters/search_type.rb: -------------------------------------------------------------------------------- 1 | require 'chewy/search/parameters/storage' 2 | 3 | module Chewy 4 | module Search 5 | class Parameters 6 | # Just a standard string value storage, nothing to see here. 7 | # 8 | # @see Chewy::Search::Parameters::StringStorage 9 | # @see Chewy::Search::Request#search_type 10 | # @see https://www.elastic.co/guide/en/elasticsearch/reference/5.4/search-request-search-type.html 11 | class SearchType < Storage 12 | include StringStorage 13 | end 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /lib/chewy/search/parameters/source.rb: -------------------------------------------------------------------------------- 1 | require 'chewy/search/parameters/storage' 2 | 3 | module Chewy 4 | module Search 5 | class Parameters 6 | # This storage handles either an array of strings/symbols 7 | # or a hash with `includes` and `excludes` keys and 8 | # arrays of strings/symbols as values. Any other key is ignored. 9 | # 10 | # @see Chewy::Search::Request#source 11 | # @see https://www.elastic.co/guide/en/elasticsearch/reference/5.4/search-request-source-filtering.html 12 | class Source < Storage 13 | self.param_name = :_source 14 | 15 | # If array or simple string/symbol is passed, it is treated 16 | # as a part of `includes` array and gets concatenated with it. 17 | # In case of hash, respective values are concatenated as well. 18 | # 19 | # @see Chewy::Search::Parameters::Storage#update! 20 | # @param other_value 21 | # [true, false, { 22 | # Symbol => Array, String, Symbol}, 23 | # Array, String, Symbol 24 | # ] any acceptable storage value 25 | # @return [{Symbol => Array, true, false}] updated value 26 | def update!(other_value) 27 | new_value = normalize(other_value) 28 | new_value[:includes] = value[:includes] | new_value[:includes] 29 | new_value[:excludes] = value[:excludes] | new_value[:excludes] 30 | @value = new_value 31 | end 32 | 33 | # Requires an additional logic to merge `enabled` value. 34 | # 35 | # @see Chewy::Search::Parameters::Storage#merge! 36 | # @param other [Chewy::Search::Parameters::Storage] other storage 37 | # @return [{Symbol => Array, true, false}] updated value 38 | def merge!(other) 39 | super 40 | update!(other.value[:enabled]) 41 | end 42 | 43 | # Renders `false` if `source` is disabled, otherwise renders the 44 | # contents of `includes` value or even the entire hash if `excludes` 45 | # also specified. 46 | # 47 | # @see Chewy::Search::Parameters::Storage#render 48 | # @return [{Symbol => Object}, nil] rendered value with the parameter name 49 | def render 50 | if !value[:enabled] 51 | {self.class.param_name => false} 52 | elsif value[:excludes].present? 53 | {self.class.param_name => value.slice(:includes, :excludes).reject { |_, v| v.blank? }} 54 | elsif value[:includes].present? 55 | {self.class.param_name => value[:includes]} 56 | end 57 | end 58 | 59 | private 60 | 61 | def normalize(value) 62 | includes, excludes, enabled = case value 63 | when TrueClass, FalseClass 64 | [[], [], value] 65 | when Hash 66 | [*value.values_at(:includes, :excludes), true] 67 | else 68 | [value, [], true] 69 | end 70 | {includes: Array.wrap(includes).reject(&:blank?).map(&:to_s), 71 | excludes: Array.wrap(excludes).reject(&:blank?).map(&:to_s), 72 | enabled: enabled} 73 | end 74 | end 75 | end 76 | end 77 | end 78 | -------------------------------------------------------------------------------- /lib/chewy/search/parameters/storage.rb: -------------------------------------------------------------------------------- 1 | module Chewy 2 | module Search 3 | class Parameters 4 | # Base parameter storage, defines a conventional API and 5 | # its default behavior. 6 | class Storage 7 | class << self 8 | attr_writer :param_name 9 | 10 | # @!attribute [rw] param_name 11 | # The parameter name is used on rendering, derived from the class 12 | # name by default, but can be easily redefined for child classes. 13 | # 14 | # @example 15 | # class Limit < Storage 16 | # self.param_name = :size 17 | # end 18 | # @return [Symbol] parameter name 19 | def param_name 20 | @param_name ||= name.demodulize.underscore.to_sym 21 | end 22 | end 23 | 24 | # Returns normalized storage value. 25 | attr_reader :value 26 | 27 | # @param value [Object] any acceptable storage value 28 | def initialize(value = nil) 29 | replace!(value) 30 | end 31 | 32 | # Compares two storages, basically, classes and values should 33 | # be identical. 34 | # 35 | # @param other [Chewy::Search::Parameters::Storage] any storage instance 36 | # @return [true, false] the result of comparision 37 | def ==(other) 38 | super || (other.class == self.class && other.value == value) 39 | end 40 | 41 | # Replaces current value with normalized provided one. Doesn't 42 | # make sense to redefine it in child classes, the replacement 43 | # logic should be kept as is. 44 | # 45 | # @see Chewy::Search::Request 46 | # @param new_value [Object] any acceptable storage value 47 | # @return [Object] new normalized value 48 | def replace!(new_value) 49 | @value = normalize(new_value) 50 | end 51 | 52 | # Implements the storage update logic, picks the first present 53 | # value by default, but can be redefined if necessary. 54 | # 55 | # @see Chewy::Search::Request 56 | # @param other_value [Object] any acceptable storage value 57 | # @return [Object] updated value 58 | def update!(other_value) 59 | replace!([value, normalize(other_value)].compact.last) 60 | end 61 | 62 | # Merges one storage with another one using update by default. 63 | # Requires redefinition sometimes. 64 | # 65 | # @see Chewy::Search::Parameters#merge! 66 | # @see Chewy::Search::Request#merge 67 | # @param other [Chewy::Search::Parameters::Storage] other storage 68 | # @return [Object] updated value 69 | def merge!(other) 70 | update!(other.value) 71 | end 72 | 73 | # Basic parameter rendering logic, don't need to return anything 74 | # if parameter doesn't require rendering for the current value. 75 | # 76 | # @see Chewy::Search::Parameters#render 77 | # @see Chewy::Search::Request#render 78 | # @return [{Symbol => Object}, nil] rendered value with the parameter name 79 | def render 80 | {self.class.param_name => value} if value.present? 81 | end 82 | 83 | private 84 | 85 | def initialize_clone(origin) 86 | @value = origin.value.deep_dup 87 | end 88 | 89 | def normalize(value) 90 | value 91 | end 92 | end 93 | end 94 | end 95 | end 96 | -------------------------------------------------------------------------------- /lib/chewy/search/parameters/stored_fields.rb: -------------------------------------------------------------------------------- 1 | require 'chewy/search/parameters/storage' 2 | 3 | module Chewy 4 | module Search 5 | class Parameters 6 | # This storage is basically an array storage, but with an 7 | # additional ability to pass `enabled` option. 8 | # 9 | # @see Chewy::Search::Request#stored_fields 10 | # @see https://www.elastic.co/guide/en/elasticsearch/reference/5.4/search-request-stored-fields.html 11 | class StoredFields < Storage 12 | # If array or just a field name is passed - it gets concatenated 13 | # to the storage array. `true` or `false` values are modifying 14 | # `enabled` parameter. 15 | # 16 | # @see Chewy::Search::Parameters::Storage#update! 17 | # @param other_value [true, false, String, Symbol, Array] any acceptable storage value 18 | # @return [{Symbol => Array, true, false}] updated value 19 | def update!(other_value) 20 | new_value = normalize(other_value) 21 | new_value[:stored_fields] = value[:stored_fields] | new_value[:stored_fields] 22 | @value = new_value 23 | end 24 | 25 | # Requires an additional logic to merge `enabled` value. 26 | # 27 | # @see Chewy::Search::Parameters::Storage#merge! 28 | # @param other [Chewy::Search::Parameters::Storage] other storage 29 | # @return [{Symbol => Array, true, false}] updated value 30 | def merge!(other) 31 | update!(other.value[:stored_fields]) 32 | update!(other.value[:enabled]) 33 | end 34 | 35 | # Renders `_none_` if `stored_fields` are disabled, otherwise renders the 36 | # array of stored field names. 37 | # 38 | # @see Chewy::Search::Parameters::Storage#render 39 | # @return [{Symbol => Object}, nil] rendered value with the parameter name 40 | def render 41 | if !value[:enabled] 42 | {self.class.param_name => '_none_'} 43 | elsif value[:stored_fields].present? 44 | {self.class.param_name => value[:stored_fields]} 45 | end 46 | end 47 | 48 | private 49 | 50 | def normalize(raw_value) 51 | stored_fields, enabled = case raw_value 52 | when TrueClass, FalseClass 53 | [[], raw_value] 54 | else 55 | [raw_value, true] 56 | end 57 | {stored_fields: Array.wrap(stored_fields).reject(&:blank?).map(&:to_s), 58 | enabled: enabled} 59 | end 60 | end 61 | end 62 | end 63 | end 64 | -------------------------------------------------------------------------------- /lib/chewy/search/parameters/suggest.rb: -------------------------------------------------------------------------------- 1 | require 'chewy/search/parameters/storage' 2 | 3 | module Chewy 4 | module Search 5 | class Parameters 6 | # Just a standard hash storage. Nothing to see here. 7 | # 8 | # @see Chewy::Search::Parameters::HashStorage 9 | # @see Chewy::Search::Request#suggest 10 | # @see https://www.elastic.co/guide/en/elasticsearch/reference/5.4/search-suggesters.html 11 | class Suggest < Storage 12 | include HashStorage 13 | end 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /lib/chewy/search/parameters/terminate_after.rb: -------------------------------------------------------------------------------- 1 | require 'chewy/search/parameters/storage' 2 | 3 | module Chewy 4 | module Search 5 | class Parameters 6 | # Just a standard integer value storage, nothing to see here. 7 | # 8 | # @see Chewy::Search::Parameters::IntegerStorage 9 | # @see Chewy::Search::Request#terminate_after 10 | # @see https://www.elastic.co/guide/en/elasticsearch/reference/5.4/search-request-body.html 11 | class TerminateAfter < Storage 12 | include IntegerStorage 13 | end 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /lib/chewy/search/parameters/timeout.rb: -------------------------------------------------------------------------------- 1 | require 'chewy/search/parameters/storage' 2 | 3 | module Chewy 4 | module Search 5 | class Parameters 6 | # Just a standard string value storage, nothing to see here. 7 | # 8 | # @see Chewy::Search::Parameters::StringStorage 9 | # @see Chewy::Search::Request#timeout 10 | # @see https://www.elastic.co/guide/en/elasticsearch/reference/5.4/common-options.html#time-units 11 | class Timeout < Storage 12 | include StringStorage 13 | end 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /lib/chewy/search/parameters/track_scores.rb: -------------------------------------------------------------------------------- 1 | require 'chewy/search/parameters/storage' 2 | 3 | module Chewy 4 | module Search 5 | class Parameters 6 | # Just a standard boolean storage, nothing to see here. 7 | # 8 | # @see Chewy::Search::Parameters::BoolStorage 9 | # @see Chewy::Search::Request#track_scores 10 | # @see https://www.elastic.co/guide/en/elasticsearch/reference/5.4/search-request-sort.html#_track_scores 11 | class TrackScores < Storage 12 | include BoolStorage 13 | end 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /lib/chewy/search/parameters/track_total_hits.rb: -------------------------------------------------------------------------------- 1 | require 'chewy/search/parameters/storage' 2 | 3 | module Chewy 4 | module Search 5 | class Parameters 6 | # Just a standard boolean storage, nothing to see here. 7 | # 8 | # @see Chewy::Search::Parameters::BoolStorage 9 | # @see Chewy::Search::Request#track_total_hits 10 | # @see https://www.elastic.co/guide/en/elasticsearch/reference/current/search-your-data.html#track-total-hits 11 | class TrackTotalHits < Storage 12 | include BoolStorage 13 | end 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /lib/chewy/search/parameters/version.rb: -------------------------------------------------------------------------------- 1 | require 'chewy/search/parameters/storage' 2 | 3 | module Chewy 4 | module Search 5 | class Parameters 6 | # Just a standard boolean storage, nothing to see here. 7 | # 8 | # @see Chewy::Search::Parameters::BoolStorage 9 | # @see Chewy::Search::Request#version 10 | # @see https://www.elastic.co/guide/en/elasticsearch/reference/5.4/search-request-version.html 11 | class Version < Storage 12 | include BoolStorage 13 | end 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /lib/chewy/search/scoping.rb: -------------------------------------------------------------------------------- 1 | module Chewy 2 | module Search 3 | # This module along with {Chewy::Search} provides an ability to 4 | # use names scopes. 5 | # 6 | # @example 7 | # class UsersIndex < Chewy::Index 8 | # def self.by_name(name) 9 | # query(match: {name: name}) 10 | # end 11 | # 12 | # 13 | # def self.by_age(age) 14 | # filter(term: {age: age}) 15 | # end 16 | # end 17 | # 18 | # UsersIndex.limit(10).by_name('Martin') 19 | # # => {:size=>10, :query=>{:match=>{:name=>"Martin"}}}}> 20 | # UsersIndex.limit(10).by_name('Martin').by_age(42) 21 | # # => {:size=>10, :query=>{:bool=>{ 22 | # # :must=>{:match=>{:name=>"Martin"}}, 23 | # # :filter=>{:term=>{:age=>42}}}}}}> 24 | module Scoping 25 | extend ActiveSupport::Concern 26 | 27 | module ClassMethods 28 | # The scopes stack. 29 | # 30 | # @return [Array] array of scopes 31 | def scopes 32 | Chewy.current[:chewy_scopes] ||= [] 33 | end 34 | end 35 | 36 | # Wraps any method to make it contents be executed inside the 37 | # current request scope. 38 | # 39 | # @see Chewy::Search::ClassMethods#all 40 | # @yield executes the block after the current context is put at the top of the scope stack 41 | def scoping 42 | self.class.scopes.push(self) 43 | yield 44 | ensure 45 | self.class.scopes.pop 46 | end 47 | end 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /lib/chewy/stash.rb: -------------------------------------------------------------------------------- 1 | module Chewy 2 | # This class is the main storage for Chewy service data, 3 | # Now index raw specifications are stored in the `chewy_specifications` 4 | # index. 5 | # Journal entries are stored in `chewy_journal` 6 | # 7 | # @see Chewy::Index::Specification 8 | module Stash 9 | class Specification < Chewy::Index 10 | index_name 'chewy_specifications' 11 | 12 | default_import_options journal: false 13 | 14 | field :specification, type: 'binary' 15 | end 16 | 17 | class Journal < Chewy::Index 18 | index_name 'chewy_journal' 19 | 20 | # Loads all entries since the specified time. 21 | # 22 | # @param since_time [Time, DateTime] a timestamp from which we load a journal 23 | # @param only [Chewy::Index, Array] journal entries related to these indices will be loaded only 24 | def self.entries(since_time, only: []) 25 | self.for(only).filter(range: {created_at: {gt: since_time}}).filter.minimum_should_match(1) 26 | end 27 | 28 | # Cleans up all the journal entries until the specified time. If nothing is 29 | # specified - cleans up everything. 30 | # 31 | # @param until_time [Time, DateTime] Clean everything before that date 32 | # @param only [Chewy::Index, Array] indexes to clean up journal entries for 33 | def self.clean(until_time = nil, only: [], delete_by_query_options: {}) 34 | scope = self.for(only) 35 | scope = scope.filter(range: {created_at: {lte: until_time}}) if until_time 36 | scope.delete_all(**delete_by_query_options) 37 | end 38 | 39 | # Selects all the journal entries for the specified indices. 40 | # 41 | # @param indices [Chewy::Index, Array] 42 | def self.for(*something) 43 | something = something.flatten.compact 44 | indexes = something.flat_map { |s| Chewy.derive_name(s) } 45 | return none if something.present? && indexes.blank? 46 | 47 | scope = all 48 | indexes.each do |index| 49 | scope = scope.or(filter(term: {index_name: index.derivable_name})) 50 | end 51 | scope 52 | end 53 | 54 | default_import_options journal: false 55 | 56 | field :index_name, type: 'keyword' 57 | field :action, type: 'keyword' 58 | field :references, type: 'binary' 59 | field :created_at, type: 'date' 60 | 61 | def references 62 | @references ||= Array.wrap(@attributes['references']).map do |item| 63 | JSON.load(Base64.decode64(item)) # rubocop:disable Security/JSONLoad 64 | end 65 | end 66 | end 67 | end 68 | end 69 | -------------------------------------------------------------------------------- /lib/chewy/strategy.rb: -------------------------------------------------------------------------------- 1 | require 'chewy/strategy/base' 2 | require 'chewy/strategy/bypass' 3 | require 'chewy/strategy/urgent' 4 | require 'chewy/strategy/atomic' 5 | require 'chewy/strategy/atomic_no_refresh' 6 | 7 | begin 8 | require 'sidekiq' 9 | require 'chewy/strategy/sidekiq' 10 | require 'chewy/strategy/lazy_sidekiq' 11 | require 'chewy/strategy/delayed_sidekiq' 12 | rescue LoadError 13 | nil 14 | end 15 | 16 | begin 17 | require 'active_job' 18 | require 'chewy/strategy/active_job' 19 | rescue LoadError 20 | nil 21 | end 22 | 23 | module Chewy 24 | # This class represents strategies stack with `:base` 25 | # Strategy on top of it. This causes raising exceptions 26 | # on every index update attempt, so other strategy must 27 | # be choosen. 28 | # 29 | # User.first.save # Raises UndefinedUpdateStrategy exception 30 | # 31 | # Chewy.strategy(:atomic) do 32 | # User.last.save # Save user according to the `:atomic` strategy rules 33 | # end 34 | # 35 | class Strategy 36 | def initialize 37 | @stack = [resolve(Chewy.root_strategy).new] 38 | end 39 | 40 | def current 41 | @stack.last 42 | end 43 | 44 | def push(name) 45 | result = @stack.push resolve(name).new 46 | debug "[#{@stack.size - 1}] <- #{current.name}" if @stack.size > 2 47 | result 48 | end 49 | 50 | def pop 51 | raise "Can't pop root strategy" if @stack.one? 52 | 53 | result = @stack.pop.tap(&:leave) 54 | debug "[#{@stack.size}] -> #{result.name}, now #{current.name}" if @stack.size > 1 55 | result 56 | end 57 | 58 | def wrap(name) 59 | stack = push(name) 60 | yield 61 | ensure 62 | pop if stack 63 | end 64 | 65 | private 66 | 67 | def debug(string) 68 | return unless Chewy.logger&.debug? 69 | 70 | line = caller.detect { |l| l !~ %r{lib/chewy/strategy.rb:|lib/chewy.rb:} } 71 | Chewy.logger.debug(["Chewy strategies stack: #{string}", line.sub(/:in\s.+$/, '')].join(' @ ')) 72 | end 73 | 74 | def resolve(name) 75 | "Chewy::Strategy::#{name.to_s.camelize}".safe_constantize or raise "Can't find update strategy `#{name}`" 76 | end 77 | end 78 | end 79 | -------------------------------------------------------------------------------- /lib/chewy/strategy/active_job.rb: -------------------------------------------------------------------------------- 1 | module Chewy 2 | class Strategy 3 | # The strategy works the same way as atomic, but performs 4 | # async index update driven by active_job 5 | # 6 | # Chewy.strategy(:active_job) do 7 | # User.all.map(&:save) # Does nothing here 8 | # Post.all.map(&:save) # And here 9 | # # It imports all the changed users and posts right here 10 | # end 11 | # 12 | class ActiveJob < Atomic 13 | class Worker < ::ActiveJob::Base 14 | queue_as { Chewy.settings.dig(:active_job, :queue) || 'chewy' } 15 | 16 | def perform(type, ids, options = {}) 17 | options[:refresh] = !Chewy.disable_refresh_async if Chewy.disable_refresh_async 18 | type.constantize.import!(ids, **options) 19 | end 20 | end 21 | 22 | def leave 23 | @stash.each do |type, ids| 24 | Chewy::Strategy::ActiveJob::Worker.perform_later(type.name, ids) unless ids.empty? 25 | end 26 | end 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /lib/chewy/strategy/atomic.rb: -------------------------------------------------------------------------------- 1 | module Chewy 2 | class Strategy 3 | # This strategy accumulates all the objects prepared for 4 | # indexing and fires index process when strategy is popped 5 | # from the strategies stack. 6 | # 7 | # Chewy.strategy(:atomic) do 8 | # User.all.map(&:save) # Does nothing here 9 | # Post.all.map(&:save) # And here 10 | # # It imports all the changed users and posts right here 11 | # # before block leaving with bulk ES API, kinda optimization 12 | # end 13 | # 14 | class Atomic < Base 15 | def initialize 16 | @stash = {} 17 | end 18 | 19 | def update(type, objects, _options = {}) 20 | @stash[type] ||= [] 21 | @stash[type] |= type.root.id ? Array.wrap(objects) : type.adapter.identify(objects) 22 | end 23 | 24 | def leave 25 | @stash.all? { |type, ids| type.import!(ids) } 26 | end 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /lib/chewy/strategy/atomic_no_refresh.rb: -------------------------------------------------------------------------------- 1 | module Chewy 2 | class Strategy 3 | # This strategy works like atomic but import objects with `refresh=false` parameter. 4 | # 5 | # Chewy.strategy(:atomic_no_refresh) do 6 | # User.all.map(&:save) # Does nothing here 7 | # Post.all.map(&:save) # And here 8 | # # It imports all the changed users and posts right here 9 | # # before block leaving with bulk ES API, kinda optimization 10 | # end 11 | # 12 | class AtomicNoRefresh < Atomic 13 | def leave 14 | @stash.all? { |type, ids| type.import!(ids, refresh: false) } 15 | end 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /lib/chewy/strategy/base.rb: -------------------------------------------------------------------------------- 1 | module Chewy 2 | class Strategy 3 | # This strategy raises exception on every index update 4 | # asking to choose some other strategy. 5 | # 6 | # Chewy.strategy(:base) do 7 | # User.all.map(&:save) # Raises UndefinedUpdateStrategy exception 8 | # end 9 | # 10 | class Base 11 | def name 12 | self.class.name.demodulize.underscore.to_sym 13 | end 14 | 15 | # This method called when some model tries to update index 16 | # 17 | def update(type, _objects, _options = {}) 18 | raise UndefinedUpdateStrategy, type 19 | end 20 | 21 | # This method called when strategy pops from the 22 | # strategies stack 23 | # 24 | def leave; end 25 | 26 | # This method called when some model record is created or updated. 27 | # Normally it will just evaluate all the Chewy callbacks and pass results 28 | # to current strategy's update method. 29 | # However it's possible to override it to achieve delayed evaluation of 30 | # callbacks, e.g. using sidekiq. 31 | # 32 | def update_chewy_indices(object) 33 | object.run_chewy_callbacks 34 | end 35 | end 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /lib/chewy/strategy/bypass.rb: -------------------------------------------------------------------------------- 1 | module Chewy 2 | class Strategy 3 | # This strategy basically does nothing. 4 | # 5 | # Chewy.strategy(:bypass) do 6 | # User.all.map(&:save) # Does nothing here 7 | # # Does not update index all over the block. 8 | # end 9 | # 10 | class Bypass < Base 11 | def update(type, objects, _options = {}); end 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /lib/chewy/strategy/delayed_sidekiq.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Chewy 4 | class Strategy 5 | class DelayedSidekiq < Sidekiq 6 | require_relative 'delayed_sidekiq/scheduler' 7 | 8 | # cleanup the redis sets used internally. Useful mainly in tests to avoid 9 | # leak and potential flaky tests. 10 | def self.clear_timechunks! 11 | ::Sidekiq.redis do |redis| 12 | keys_to_delete = redis.keys("#{Scheduler::KEY_PREFIX}*") 13 | 14 | # Delete keys one by one 15 | keys_to_delete.each do |key| 16 | redis.del(key) 17 | end 18 | end 19 | end 20 | 21 | def leave 22 | @stash.each do |type, ids| 23 | next if ids.empty? 24 | 25 | DelayedSidekiq::Scheduler.new(type, ids).postpone 26 | end 27 | end 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /lib/chewy/strategy/delayed_sidekiq/worker.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Chewy 4 | class Strategy 5 | class DelayedSidekiq 6 | class Worker 7 | include ::Sidekiq::Worker 8 | 9 | LUA_SCRIPT = <<~LUA 10 | local type = ARGV[1] 11 | local score = tonumber(ARGV[2]) 12 | local prefix = ARGV[3] 13 | local timechunks_key = prefix .. ":" .. type .. ":timechunks" 14 | 15 | -- Get timechunk_keys with scores less than or equal to the specified score 16 | local timechunk_keys = redis.call('zrangebyscore', timechunks_key, '-inf', score) 17 | 18 | -- Get all members from the sets associated with the timechunk_keys 19 | local members = {} 20 | for _, timechunk_key in ipairs(timechunk_keys) do 21 | local set_members = redis.call('smembers', timechunk_key) 22 | for _, member in ipairs(set_members) do 23 | table.insert(members, member) 24 | end 25 | end 26 | 27 | -- Remove timechunk_keys and their associated sets 28 | for _, timechunk_key in ipairs(timechunk_keys) do 29 | redis.call('del', timechunk_key) 30 | end 31 | 32 | -- Remove timechunks with scores less than or equal to the specified score 33 | redis.call('zremrangebyscore', timechunks_key, '-inf', score) 34 | 35 | return members 36 | LUA 37 | 38 | def perform(type, score, options = {}) 39 | options[:refresh] = !Chewy.disable_refresh_async if Chewy.disable_refresh_async 40 | 41 | ::Sidekiq.redis do |redis| 42 | members = redis.eval(LUA_SCRIPT, keys: [], argv: [type, score, Scheduler::KEY_PREFIX]) 43 | 44 | # extract ids and fields & do the reset of records 45 | ids, fields = extract_ids_and_fields(members) 46 | options[:update_fields] = fields if fields 47 | 48 | index = type.constantize 49 | index.strategy_config.delayed_sidekiq.reindex_wrapper.call do 50 | options.any? ? index.import!(ids, **options) : index.import!(ids) 51 | end 52 | end 53 | end 54 | 55 | private 56 | 57 | def extract_ids_and_fields(members) 58 | ids = [] 59 | fields = [] 60 | 61 | members.each do |member| 62 | member_ids, member_fields = member.split(Scheduler::FIELDS_IDS_SEPARATOR).map do |v| 63 | v.split(Scheduler::IDS_SEPARATOR) 64 | end 65 | ids |= member_ids 66 | fields |= member_fields 67 | end 68 | 69 | fields = nil if fields.include?(Scheduler::FALLBACK_FIELDS) 70 | 71 | [ids, fields] 72 | end 73 | end 74 | end 75 | end 76 | end 77 | -------------------------------------------------------------------------------- /lib/chewy/strategy/lazy_sidekiq.rb: -------------------------------------------------------------------------------- 1 | module Chewy 2 | class Strategy 3 | # The strategy works the same way as sidekiq, but performs 4 | # async evaluation of all index callbacks on model create and update 5 | # driven by sidekiq 6 | # 7 | # Chewy.strategy(:lazy_sidekiq) do 8 | # User.all.map(&:save) # Does nothing here 9 | # Post.all.map(&:save) # And here 10 | # # It schedules import of all the changed users and posts right here 11 | # end 12 | # 13 | class LazySidekiq < Sidekiq 14 | class IndicesUpdateWorker 15 | include ::Sidekiq::Worker 16 | 17 | def perform(models) 18 | Chewy.strategy(strategy) do 19 | models.each do |model_type, model_ids| 20 | model_type.constantize.where(id: model_ids).each(&:run_chewy_callbacks) 21 | end 22 | end 23 | end 24 | 25 | private 26 | 27 | def strategy 28 | Chewy.disable_refresh_async ? :atomic_no_refresh : :atomic 29 | end 30 | end 31 | 32 | def initialize 33 | # Use parent's @stash to store destroyed records, since callbacks for them have to 34 | # be run immediately on the strategy block end because we won't be able to fetch 35 | # records further in IndicesUpdateWorker. This will be done by avoiding of 36 | # LazySidekiq#update_chewy_indices call and calling LazySidekiq#update instead. 37 | super 38 | 39 | # @lazy_stash is used to store all the lazy evaluated callbacks with call of 40 | # strategy's #update_chewy_indices. 41 | @lazy_stash = {} 42 | end 43 | 44 | def leave 45 | # Fallback to Sidekiq#leave implementation for destroyed records stored in @stash. 46 | super 47 | 48 | # Proceed with other records stored in @lazy_stash 49 | return if @lazy_stash.empty? 50 | 51 | ::Sidekiq::Client.push( 52 | 'queue' => sidekiq_queue, 53 | 'class' => Chewy::Strategy::LazySidekiq::IndicesUpdateWorker, 54 | 'args' => [@lazy_stash] 55 | ) 56 | end 57 | 58 | def update_chewy_indices(object) 59 | @lazy_stash[object.class.name] ||= [] 60 | @lazy_stash[object.class.name] |= Array.wrap(object.id) 61 | end 62 | end 63 | end 64 | end 65 | -------------------------------------------------------------------------------- /lib/chewy/strategy/sidekiq.rb: -------------------------------------------------------------------------------- 1 | module Chewy 2 | class Strategy 3 | # The strategy works the same way as atomic, but performs 4 | # async index update driven by sidekiq 5 | # 6 | # Chewy.strategy(:sidekiq) do 7 | # User.all.map(&:save) # Does nothing here 8 | # Post.all.map(&:save) # And here 9 | # # It imports all the changed users and posts right here 10 | # end 11 | # 12 | class Sidekiq < Atomic 13 | class Worker 14 | include ::Sidekiq::Worker 15 | 16 | def perform(type, ids, options = {}) 17 | options[:refresh] = !Chewy.disable_refresh_async if Chewy.disable_refresh_async 18 | type.constantize.import!(ids, **options) 19 | end 20 | end 21 | 22 | def leave 23 | @stash.each do |type, ids| 24 | next if ids.empty? 25 | 26 | ::Sidekiq::Client.push( 27 | 'queue' => sidekiq_queue, 28 | 'class' => Chewy::Strategy::Sidekiq::Worker, 29 | 'args' => [type.name, ids] 30 | ) 31 | end 32 | end 33 | 34 | private 35 | 36 | def sidekiq_queue 37 | Chewy.settings.dig(:sidekiq, :queue) || 'chewy' 38 | end 39 | end 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /lib/chewy/strategy/urgent.rb: -------------------------------------------------------------------------------- 1 | module Chewy 2 | class Strategy 3 | # This strategy updates index on demand. Not the best 4 | # strategy in case of optimization. If you need to update 5 | # indexes with bulk API calls - use :atomic instead. 6 | # 7 | # Chewy.strategy(:urgent) do 8 | # User.all.map(&:save) # Updates index on every `save` call 9 | # end 10 | # 11 | class Urgent < Base 12 | def update(type, objects, _options = {}) 13 | type.import!(Array.wrap(objects)) 14 | end 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /lib/chewy/version.rb: -------------------------------------------------------------------------------- 1 | module Chewy 2 | VERSION = '8.0.0-beta'.freeze 3 | end 4 | -------------------------------------------------------------------------------- /lib/generators/chewy/install_generator.rb: -------------------------------------------------------------------------------- 1 | module Chewy 2 | module Generators 3 | class InstallGenerator < Rails::Generators::Base 4 | source_root File.expand_path('../templates', __dir__) 5 | 6 | def copy_configuration 7 | template 'chewy.yml', 'config/chewy.yml' 8 | end 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /lib/generators/templates/chewy.yml: -------------------------------------------------------------------------------- 1 | # config/chewy.yml 2 | # separate environment configs 3 | test: 4 | host: 'localhost:9250' 5 | prefix: 'test' 6 | development: 7 | host: 'localhost:9200' -------------------------------------------------------------------------------- /spec/chewy/config_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Chewy::Config do 4 | subject { described_class.send(:new) } 5 | 6 | its(:logger) { should be_nil } 7 | its(:transport_logger) { should be_nil } 8 | its(:transport_logger) { should be_nil } 9 | its(:root_strategy) { should == :base } 10 | its(:request_strategy) { should == :atomic } 11 | its(:console_strategy) { should == :urgent } 12 | its(:use_after_commit_callbacks) { should == true } 13 | its(:indices_path) { should == 'app/chewy' } 14 | its(:reset_disable_refresh_interval) { should == false } 15 | its(:reset_no_replicas) { should == false } 16 | its(:disable_refresh_async) { should == false } 17 | its(:search_class) { should be < Chewy::Search::Request } 18 | 19 | describe '#transport_logger=' do 20 | let(:logger) { Logger.new('/dev/null') } 21 | after { subject.transport_logger = nil } 22 | 23 | specify do 24 | expect { subject.transport_logger = logger } 25 | .to change { Chewy.client.transport.logger }.to(logger) 26 | end 27 | specify do 28 | expect { subject.transport_logger = logger } 29 | .to change { subject.transport_logger }.to(logger) 30 | end 31 | specify do 32 | expect { subject.transport_logger = logger } 33 | .to change { subject.configuration[:logger] }.from(nil).to(logger) 34 | end 35 | end 36 | 37 | describe '#transport_tracer=' do 38 | let(:tracer) { Logger.new('/dev/null') } 39 | after { subject.transport_tracer = nil } 40 | 41 | specify do 42 | expect { subject.transport_tracer = tracer } 43 | .to change { Chewy.client.transport.tracer }.to(tracer) 44 | end 45 | specify do 46 | expect { subject.transport_tracer = tracer } 47 | .to change { subject.transport_tracer }.to(tracer) 48 | end 49 | specify do 50 | expect { subject.transport_tracer = tracer } 51 | .to change { subject.configuration[:tracer] }.from(nil).to(tracer) 52 | end 53 | end 54 | 55 | describe '#search_class' do 56 | context 'nothing is defined' do 57 | before do 58 | hide_const('Kaminari') 59 | end 60 | 61 | specify do 62 | expect(subject.search_class.included_modules) 63 | .not_to include(Chewy::Search::Pagination::Kaminari) 64 | end 65 | end 66 | 67 | context 'kaminari' do 68 | specify do 69 | expect(subject.search_class.included_modules) 70 | .to include(Chewy::Search::Pagination::Kaminari) 71 | end 72 | end 73 | end 74 | 75 | describe '#configuration' do 76 | before { subject.settings = {indices_path: 'app/custom_indices_path'} } 77 | 78 | specify do 79 | expect(subject.configuration).to include(indices_path: 'app/custom_indices_path') 80 | end 81 | 82 | context 'when Rails::VERSION constant is defined' do 83 | it 'looks for configuration in "config/chewy.yml"' do 84 | module Rails 85 | VERSION = '5.1.1'.freeze 86 | 87 | def self.root 88 | Pathname.new(__dir__) 89 | end 90 | end 91 | 92 | expect(File).to receive(:exist?) 93 | .with(Pathname.new(__dir__).join('config', 'chewy.yml')) 94 | subject.configuration 95 | end 96 | end 97 | end 98 | 99 | describe '.console_strategy' do 100 | context 'sets .console_strategy' do 101 | let(:default_strategy) { subject.console_strategy } 102 | let(:new_strategy) { :atomic } 103 | after { subject.console_strategy = default_strategy } 104 | 105 | specify do 106 | expect { subject.console_strategy = new_strategy } 107 | .to change { subject.console_strategy }.to(new_strategy) 108 | end 109 | end 110 | end 111 | end 112 | -------------------------------------------------------------------------------- /spec/chewy/elastic_client_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Chewy::ElasticClient do 4 | describe 'payload inspection' do 5 | let(:filter) { instance_double('Proc') } 6 | let!(:filter_previous_value) { Chewy.before_es_request_filter } 7 | 8 | before do 9 | drop_indices 10 | stub_index(:products) do 11 | field :id, type: :integer 12 | end 13 | ProductsIndex.create 14 | Chewy.before_es_request_filter = filter 15 | end 16 | 17 | after do 18 | Chewy.before_es_request_filter = filter_previous_value 19 | end 20 | 21 | it 'call filter with the request body' do 22 | expect(filter).to receive(:call).with(:search, [{body: {size: 0}, index: ['products']}], {}) 23 | Chewy.client.search({index: ['products'], body: {size: 0}}).to_a 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /spec/chewy/fields/time_fields_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe 'Time fields' do 4 | before { drop_indices } 5 | 6 | before do 7 | stub_index(:posts) do 8 | field :published_at, type: 'date' 9 | end 10 | end 11 | 12 | before do 13 | PostsIndex.import( 14 | double(published_at: ActiveSupport::TimeZone[-28_800].parse('2014/12/18 19:00')), 15 | double(published_at: ActiveSupport::TimeZone[-21_600].parse('2014/12/18 20:00')), 16 | double(published_at: ActiveSupport::TimeZone[-21_600].parse('2014/12/17 20:00')) 17 | ) 18 | end 19 | 20 | let(:time) { ActiveSupport::TimeZone[-14_400].parse('2014/12/18 22:00') } 21 | let(:range) { (time - 1.minute)..(time + 1.minute) } 22 | 23 | specify { expect(PostsIndex.total).to eq(3) } 24 | specify { expect(PostsIndex.filter(range: {published_at: {gte: range.min, lte: range.max}}).size).to eq(1) } 25 | specify do 26 | expect(PostsIndex.filter(range: {published_at: {gt: range.min.utc, lt: (range.max + 1.hour).utc}}).size).to eq(2) 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /spec/chewy/index/aliases_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Chewy::Index::Aliases do 4 | before { drop_indices } 5 | 6 | before { stub_index :dummies } 7 | 8 | describe '.indexes' do 9 | specify { expect(DummiesIndex.indexes).to eq([]) } 10 | 11 | context do 12 | before { DummiesIndex.create! } 13 | specify { expect(DummiesIndex.indexes).to eq(['dummies']) } 14 | end 15 | 16 | context do 17 | before { DummiesIndex.create! } 18 | before { Chewy.client.indices.put_alias index: 'dummies', name: 'dummies_2013' } 19 | specify { expect(DummiesIndex.indexes).to eq(['dummies']) } 20 | end 21 | 22 | context do 23 | before { DummiesIndex.create! '2013' } 24 | before { DummiesIndex.create! '2014' } 25 | specify { expect(DummiesIndex.indexes).to match_array(%w[dummies_2013 dummies_2014]) } 26 | end 27 | end 28 | 29 | describe '.aliases' do 30 | specify { expect(DummiesIndex.aliases).to eq([]) } 31 | 32 | context do 33 | before { DummiesIndex.create! } 34 | specify { expect(DummiesIndex.aliases).to eq([]) } 35 | end 36 | 37 | context do 38 | before { DummiesIndex.create! } 39 | before { Chewy.client.indices.put_alias index: 'dummies', name: 'dummies_2013' } 40 | before { Chewy.client.indices.put_alias index: 'dummies', name: 'dummies_2014' } 41 | specify { expect(DummiesIndex.aliases).to match_array(%w[dummies_2013 dummies_2014]) } 42 | end 43 | 44 | context do 45 | before { DummiesIndex.create! '2013' } 46 | specify { expect(DummiesIndex.aliases).to eq(['dummies']) } 47 | end 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /spec/chewy/index/import/bulk_request_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Chewy::Index::Import::BulkRequest do 4 | before { drop_indices } 5 | 6 | subject { described_class.new(index, suffix: suffix, bulk_size: bulk_size, **bulk_options) } 7 | let(:suffix) {} 8 | let(:bulk_size) {} 9 | let(:bulk_options) { {} } 10 | let(:index) { PlacesIndex } 11 | 12 | describe '#initialize' do 13 | specify { expect { described_class.new(nil, bulk_size: 100) }.to raise_error(ArgumentError) } 14 | specify { expect { described_class.new(nil, bulk_size: 100.kilobytes) }.not_to raise_error } 15 | end 16 | 17 | describe '#perform' do 18 | before do 19 | stub_model(:city) 20 | stub_index(:places) do 21 | index_scope City 22 | field :name 23 | end 24 | end 25 | 26 | specify do 27 | expect(Chewy.client).not_to receive(:bulk) 28 | subject.perform([]) 29 | end 30 | 31 | specify do 32 | expect(Chewy.client).to receive(:bulk).with( 33 | index: 'places', 34 | body: [{index: {id: 42, data: {name: 'Name'}}}] 35 | ) 36 | subject.perform([{index: {id: 42, data: {name: 'Name'}}}]) 37 | end 38 | 39 | context ':suffix' do 40 | let(:suffix) { 'suffix' } 41 | 42 | specify do 43 | expect(Chewy.client).to receive(:bulk).with( 44 | index: 'places_suffix', 45 | body: [{index: {id: 42, data: {name: 'Name'}}}] 46 | ) 47 | subject.perform([{index: {id: 42, data: {name: 'Name'}}}]) 48 | end 49 | end 50 | 51 | context ':bulk_size' do 52 | let(:bulk_size) { 1.2.kilobyte } 53 | 54 | specify do 55 | expect(Chewy.client).to receive(:bulk).with( 56 | index: 'places', 57 | body: "{\"index\":{\"id\":42}}\n{\"name\":\"#{'Name' * 10}\"}\n{\"index\":{\"id\":43}}\n{\"name\":\"#{'Shame' * 10}\"}\n" 58 | ) 59 | subject.perform([ 60 | {index: {id: 42, data: {name: 'Name' * 10}}}, 61 | {index: {id: 43, data: {name: 'Shame' * 10}}} 62 | ]) 63 | end 64 | 65 | specify do 66 | expect(Chewy.client).to receive(:bulk).with( 67 | index: 'places', 68 | body: "{\"index\":{\"id\":42}}\n{\"name\":\"#{'Name' * 30}\"}\n" 69 | ) 70 | expect(Chewy.client).to receive(:bulk).with( 71 | index: 'places', 72 | body: "{\"index\":{\"id\":43}}\n{\"name\":\"#{'Shame' * 100}\"}\n" 73 | ) 74 | expect(Chewy.client).to receive(:bulk).with( 75 | index: 'places', 76 | body: "{\"index\":{\"id\":44}}\n{\"name\":\"#{'Blame' * 30}\"}\n" 77 | ) 78 | subject.perform([ 79 | {index: {id: 42, data: {name: 'Name' * 30}}}, 80 | {index: {id: 43, data: {name: 'Shame' * 100}}}, 81 | {index: {id: 44, data: {name: 'Blame' * 30}}} 82 | ]) 83 | end 84 | end 85 | 86 | context 'bulk_options' do 87 | let(:bulk_options) { {refresh: true} } 88 | 89 | specify do 90 | expect(Chewy.client).to receive(:bulk).with(hash_including(bulk_options)) 91 | subject.perform([{index: {id: 42, data: {name: 'Name'}}}]) 92 | end 93 | end 94 | end 95 | end 96 | -------------------------------------------------------------------------------- /spec/chewy/index/import/journal_builder_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Chewy::Index::Import::JournalBuilder, :orm do 4 | before do 5 | stub_model(:country) 6 | stub_index 'namespace/cities' 7 | stub_index 'namespace/countries' do 8 | index_scope Country 9 | end 10 | Timecop.freeze(time) 11 | end 12 | after { Timecop.return } 13 | 14 | let(:time) { Time.parse('2017-07-14 12:00Z') } 15 | 16 | let(:index) { Namespace::CitiesIndex } 17 | let(:to_index) { [] } 18 | let(:delete) { [] } 19 | subject { described_class.new(index, to_index: to_index, delete: delete) } 20 | 21 | describe '#bulk_body' do 22 | specify { expect(subject.bulk_body).to eq([]) } 23 | 24 | context do 25 | let(:to_index) { [{id: 1, name: 'City'}] } 26 | specify do 27 | expect(subject.bulk_body).to eq([{ 28 | index: { 29 | _index: 'chewy_journal', 30 | data: { 31 | 'index_name' => 'namespace/cities', 32 | 'action' => 'index', 33 | 'references' => [Base64.encode64('{"id":1,"name":"City"}')], 34 | 'created_at' => time.as_json 35 | } 36 | } 37 | }]) 38 | end 39 | end 40 | 41 | context do 42 | let(:delete) { [{id: 1, name: 'City'}] } 43 | specify do 44 | expect(subject.bulk_body).to eq([{ 45 | index: { 46 | _index: 'chewy_journal', 47 | data: { 48 | 'index_name' => 'namespace/cities', 49 | 'action' => 'delete', 50 | 'references' => [Base64.encode64('{"id":1,"name":"City"}')], 51 | 'created_at' => time.as_json 52 | } 53 | } 54 | }]) 55 | end 56 | end 57 | 58 | context do 59 | let(:index) { Namespace::CountriesIndex } 60 | let(:to_index) { [Country.new(id: 1, name: 'City')] } 61 | let(:delete) { [Country.new(id: 2, name: 'City')] } 62 | specify do 63 | expect(subject.bulk_body).to eq([{ 64 | index: { 65 | _index: 'chewy_journal', 66 | data: { 67 | 'index_name' => 'namespace/countries', 68 | 'action' => 'index', 69 | 'references' => [Base64.encode64('1')], 70 | 'created_at' => time.as_json 71 | } 72 | } 73 | }, { 74 | index: { 75 | _index: 'chewy_journal', 76 | data: { 77 | 'index_name' => 'namespace/countries', 78 | 'action' => 'delete', 79 | 'references' => [Base64.encode64('2')], 80 | 'created_at' => time.as_json 81 | } 82 | } 83 | }]) 84 | end 85 | end 86 | end 87 | end 88 | -------------------------------------------------------------------------------- /spec/chewy/index/observe/active_record_methods_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Chewy::Index::Observe::ActiveRecordMethods do 4 | describe '.update_index' do 5 | before { stub_model(:city) } 6 | 7 | it 'initializes chewy callbacks when first update_index is evaluated' do 8 | expect(City).to receive(:initialize_chewy_callbacks).once 9 | City.update_index 'cities', :self 10 | City.update_index 'countries', -> {} 11 | end 12 | 13 | it 'adds chewy callbacks to model' do 14 | expect(City.chewy_callbacks.count).to eq(0) 15 | 16 | City.update_index 'cities', :self 17 | City.update_index 'countries', -> {} 18 | 19 | expect(City.chewy_callbacks.count).to eq(2) 20 | end 21 | end 22 | 23 | describe 'callbacks' do 24 | before { stub_model(:city) { update_index 'cities', :self } } 25 | before { stub_index(:cities) { index_scope City } } 26 | before { allow(Chewy).to receive(:use_after_commit_callbacks).and_return(use_after_commit_callbacks) } 27 | 28 | let(:city) do 29 | Chewy.strategy(:bypass) do 30 | City.create! 31 | end 32 | end 33 | 34 | shared_examples 'handles callbacks correctly' do 35 | it 'handles callbacks with strategy for possible lazy evaluation on save!' do 36 | Chewy.strategy(:urgent) do 37 | expect(city).to receive(:update_chewy_indices).and_call_original 38 | expect(Chewy.strategy.current).to receive(:update_chewy_indices).with(city) 39 | expect(city).not_to receive(:run_chewy_callbacks) 40 | 41 | city.save! 42 | end 43 | end 44 | 45 | it 'runs callbacks at the moment on destroy' do 46 | Chewy.strategy(:urgent) do 47 | expect(city).not_to receive(:update_chewy_indices) 48 | expect(Chewy.strategy.current).not_to receive(:update_chewy_indices) 49 | expect(city).to receive(:run_chewy_callbacks) 50 | 51 | city.destroy 52 | end 53 | end 54 | end 55 | 56 | context 'when Chewy.use_after_commit_callbacks is true' do 57 | let(:use_after_commit_callbacks) { true } 58 | 59 | include_examples 'handles callbacks correctly' 60 | end 61 | 62 | context 'when Chewy.use_after_commit_callbacks is false' do 63 | let(:use_after_commit_callbacks) { false } 64 | 65 | include_examples 'handles callbacks correctly' 66 | end 67 | end 68 | end 69 | -------------------------------------------------------------------------------- /spec/chewy/multi_search_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'chewy/multi_search' 3 | 4 | describe Chewy::MultiSearch do 5 | before { drop_indices } 6 | 7 | before do 8 | stub_model(:city) 9 | stub_model(:country) 10 | 11 | stub_index(:cities) do 12 | def self.aggregate_by_country 13 | aggs(country: {terms: {field: :country_id}}) 14 | end 15 | 16 | index_scope City 17 | field :name, type: 'keyword' 18 | field :country_id, type: 'keyword' 19 | end 20 | end 21 | 22 | let(:places_query) { CitiesIndex.all } 23 | 24 | describe '#queries' do 25 | specify 'returns the queries that are a part of the multi search' do 26 | multi_search = described_class.new([places_query]) 27 | expect(multi_search.queries).to contain_exactly(places_query) 28 | end 29 | end 30 | 31 | describe '#add_query' do 32 | specify 'adds a query to the multi search' do 33 | multi_search = described_class.new([]) 34 | expect do 35 | multi_search.add_query(places_query) 36 | end.to change { 37 | multi_search.queries 38 | }.from([]).to([places_query]) 39 | end 40 | end 41 | 42 | context 'when given two queries' do 43 | let(:queries) { [aggregates, results] } 44 | let(:aggregates) { CitiesIndex.aggregate_by_country.limit(0) } 45 | let(:results) { CitiesIndex.limit(10) } 46 | let(:multi_search) { described_class.new(queries) } 47 | let(:cities) { Array.new(3) { |i| City.create! name: "Name#{i + 2}", country_id: i + 1 } } 48 | before { CitiesIndex.import! city: cities } 49 | 50 | describe '#perform' do 51 | specify 'performs each query' do 52 | expect { multi_search.perform } 53 | .to change(aggregates, :performed?).from(false).to(true) 54 | .and change(results, :performed?).from(false).to(true) 55 | end 56 | 57 | specify 'issues a single request using the msearch endpoint', :aggregate_failures do 58 | expect(Chewy.client).to receive(:msearch).once.and_return('responses' => []) 59 | expect(Chewy.client).to_not receive(:search) 60 | multi_search.perform 61 | end 62 | end 63 | 64 | describe '#responses' do 65 | subject(:responses) { multi_search.responses } 66 | 67 | context 'on a previously performed multi search' do 68 | before { multi_search.perform } 69 | 70 | it 'does not perform the query again' do 71 | expect(Chewy.client).to_not receive(:msearch) 72 | multi_search.responses 73 | end 74 | end 75 | 76 | specify 'returns the results of each query', :aggregate_failures do 77 | is_expected.to have(2).responses 78 | expect(responses[0]).to be_a(Chewy::Search::Response) 79 | expect(responses[1]).to be_a(Chewy::Search::Response) 80 | expect(responses[1].wrappers).to all(be_a CitiesIndex) 81 | end 82 | end 83 | end 84 | end 85 | -------------------------------------------------------------------------------- /spec/chewy/repository_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Chewy::Repository do 4 | subject { described_class.send(:new) } 5 | 6 | its(:analyzers) { should == {} } 7 | its(:tokenizers) { should == {} } 8 | its(:filters) { should == {} } 9 | its(:char_filters) { should == {} } 10 | 11 | describe '#analyzer' do 12 | specify { expect(subject.analyzer(:name)).to be_nil } 13 | 14 | context do 15 | before { subject.analyzer(:name, option: :foo) } 16 | specify { expect(subject.analyzer(:name)).to eq(option: :foo) } 17 | specify { expect(subject.analyzers).to eq(name: {option: :foo}) } 18 | end 19 | end 20 | 21 | describe '#tokenizer' do 22 | specify { expect(subject.tokenizer(:name)).to be_nil } 23 | 24 | context do 25 | before { subject.tokenizer(:name, option: :foo) } 26 | specify { expect(subject.tokenizer(:name)).to eq(option: :foo) } 27 | specify { expect(subject.tokenizers).to eq(name: {option: :foo}) } 28 | end 29 | end 30 | 31 | describe '#filter' do 32 | specify { expect(subject.filter(:name)).to be_nil } 33 | 34 | context do 35 | before { subject.filter(:name, option: :foo) } 36 | specify { expect(subject.filter(:name)).to eq(option: :foo) } 37 | specify { expect(subject.filters).to eq(name: {option: :foo}) } 38 | end 39 | end 40 | 41 | describe '#char_filter' do 42 | specify { expect(subject.char_filter(:name)).to be_nil } 43 | 44 | context do 45 | before { subject.char_filter(:name, option: :foo) } 46 | specify { expect(subject.char_filter(:name)).to eq(option: :foo) } 47 | specify { expect(subject.char_filters).to eq(name: {option: :foo}) } 48 | end 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /spec/chewy/rspec/build_query_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe :build_query do 4 | before do 5 | stub_model(:city) 6 | stub_index(:cities) { index_scope City } 7 | CitiesIndex.create 8 | end 9 | 10 | let(:expected_query) do 11 | { 12 | index: ['cities'], 13 | body: { 14 | query: { 15 | match: {name: 'name'} 16 | } 17 | } 18 | } 19 | end 20 | let(:dummy_query) { {match: {name: 'name'}} } 21 | let(:unexpected_query) { {match: {name: 'name'}} } 22 | 23 | context 'build expected query' do 24 | specify do 25 | expect(CitiesIndex.query(dummy_query)).to build_query(expected_query) 26 | end 27 | end 28 | 29 | context 'not to build unexpected query' do 30 | specify do 31 | expect(CitiesIndex.query(dummy_query)).not_to build_query(unexpected_query) 32 | end 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /spec/chewy/rspec/helpers_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe :rspec_helper do 4 | include Chewy::Rspec::Helpers 5 | 6 | before do 7 | stub_model(:city) 8 | stub_index(:cities) { index_scope City } 9 | CitiesIndex.create 10 | end 11 | 12 | let(:hits) do 13 | [ 14 | { 15 | '_index' => 'cities', 16 | '_type' => '_doc', 17 | '_id' => '1', 18 | '_score' => 3.14, 19 | '_source' => source 20 | } 21 | ] 22 | end 23 | 24 | let(:source) { {'name' => 'some_name'} } 25 | let(:sources) { [source] } 26 | 27 | context :mock_elasticsearch_response do 28 | let(:raw_response) do 29 | { 30 | 'took' => 4, 31 | 'timed_out' => false, 32 | '_shards' => { 33 | 'total' => 1, 34 | 'successful' => 1, 35 | 'skipped' => 0, 36 | 'failed' => 0 37 | }, 38 | 'hits' => { 39 | 'total' => { 40 | 'value' => 1, 41 | 'relation' => 'eq' 42 | }, 43 | 'max_score' => 1.0, 44 | 'hits' => hits 45 | } 46 | } 47 | end 48 | 49 | specify do 50 | mock_elasticsearch_response(CitiesIndex, raw_response) 51 | expect(CitiesIndex.query({}).hits).to eq(hits) 52 | end 53 | end 54 | 55 | context :mock_elasticsearch_response_sources do 56 | specify do 57 | mock_elasticsearch_response_sources(CitiesIndex, sources) 58 | expect(CitiesIndex.query({}).hits).to eq(hits) 59 | end 60 | end 61 | end 62 | -------------------------------------------------------------------------------- /spec/chewy/runtime/version_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Chewy::Runtime::Version do 4 | describe '#major' do 5 | specify { expect(described_class.new('1.2.3').major).to eq(1) } 6 | specify { expect(described_class.new('1.2').major).to eq(1) } 7 | specify { expect(described_class.new(1.2).major).to eq(1) } 8 | specify { expect(described_class.new('1').major).to eq(1) } 9 | specify { expect(described_class.new('').major).to eq(0) } 10 | end 11 | 12 | describe '#minor' do 13 | specify { expect(described_class.new('1.2.3').minor).to eq(2) } 14 | specify { expect(described_class.new('1.2').minor).to eq(2) } 15 | specify { expect(described_class.new(1.2).minor).to eq(2) } 16 | specify { expect(described_class.new('1').minor).to eq(0) } 17 | end 18 | 19 | describe '#patch' do 20 | specify { expect(described_class.new('1.2.3').patch).to eq(3) } 21 | specify { expect(described_class.new('1.2.3.pre1').patch).to eq(3) } 22 | specify { expect(described_class.new('1.2').patch).to eq(0) } 23 | specify { expect(described_class.new(1.2).patch).to eq(0) } 24 | end 25 | 26 | describe '#to_s' do 27 | specify { expect(described_class.new('1.2.3').to_s).to eq('1.2.3') } 28 | specify { expect(described_class.new('1.2.3.pre1').to_s).to eq('1.2.3') } 29 | specify { expect(described_class.new('1.2').to_s).to eq('1.2.0') } 30 | specify { expect(described_class.new(1.2).to_s).to eq('1.2.0') } 31 | specify { expect(described_class.new('1').to_s).to eq('1.0.0') } 32 | specify { expect(described_class.new('').to_s).to eq('0.0.0') } 33 | end 34 | 35 | describe '#<=>' do 36 | specify { expect(described_class.new('1.2.3')).to eq('1.2.3') } 37 | specify { expect(described_class.new('1.2.3')).to be < '1.2.4' } 38 | specify { expect(described_class.new('1.2.3')).to be < '1.2.10' } 39 | specify { expect(described_class.new('1.10.2')).to eq('1.10.2') } 40 | specify { expect(described_class.new('1.10.2')).to be > '1.7.2' } 41 | specify { expect(described_class.new('2.10.2')).to be > '1.7.2' } 42 | specify { expect(described_class.new('1.10.2')).to be < '2.7.2' } 43 | specify { expect(described_class.new('1.10.2')).to be < described_class.new('2.7.2') } 44 | specify { expect(described_class.new('1.10.2')).to be < 2.7 } 45 | specify { expect(described_class.new('1.10.2')).to be < 1.11 } 46 | specify { expect(described_class.new('1.2.0')).to eq('1.2') } 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /spec/chewy/runtime_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Chewy::Runtime do 4 | describe '.version' do 5 | specify { expect(described_class.version).to be_a(described_class::Version) } 6 | specify { expect(described_class.version).to be >= '8.0' } 7 | specify { expect(described_class.version).to be < '9.0' } 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /spec/chewy/search/loader_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Chewy::Search::Loader do 4 | before { drop_indices } 5 | 6 | before do 7 | stub_model(:city) 8 | stub_model(:country) 9 | 10 | stub_index(:cities) do 11 | index_scope City 12 | field :name 13 | field :rating, type: 'integer' 14 | end 15 | 16 | stub_index(:countries) do 17 | index_scope Country 18 | field :name 19 | field :rating, type: 'integer' 20 | end 21 | end 22 | 23 | before do 24 | CitiesIndex.import!(cities: cities) 25 | CountriesIndex.import!(countries: countries) 26 | end 27 | 28 | let(:cities) { Array.new(2) { |i| City.create!(rating: i, name: "city #{i}") } } 29 | let(:countries) { Array.new(2) { |i| Country.create!(rating: i + 2, name: "country #{i}") } } 30 | 31 | let(:options) { {} } 32 | subject { described_class.new(indexes: [CitiesIndex, CountriesIndex], **options) } 33 | 34 | describe '#derive_index' do 35 | specify { expect(subject.derive_index('cities')).to eq(CitiesIndex) } 36 | specify { expect(subject.derive_index('cities_suffix')).to eq(CitiesIndex) } 37 | 38 | specify { expect { subject.derive_index('whatever') }.to raise_error(Chewy::UndefinedIndex) } 39 | specify { expect { subject.derive_index('citiessuffix') }.to raise_error(Chewy::UndefinedIndex) } 40 | 41 | context do 42 | before { CitiesIndex.index_name :boro_goves } 43 | 44 | specify { expect(subject.derive_index('boro_goves')).to eq(CitiesIndex) } 45 | specify { expect(subject.derive_index('boro_goves_suffix')).to eq(CitiesIndex) } 46 | end 47 | end 48 | 49 | describe '#load' do 50 | let(:hits) { Chewy::Search::Request.new(CitiesIndex, CountriesIndex).order(:rating).hits } 51 | 52 | specify { expect(subject.load(hits)).to eq([*cities, *countries]) } 53 | 54 | context 'scopes', :active_record do 55 | context do 56 | let(:options) { {scope: -> { where('rating > 2') }} } 57 | specify { expect(subject.load(hits)).to eq([nil, nil, nil, countries.last]) } 58 | end 59 | 60 | context do 61 | let(:options) { {countries: {scope: -> { where('rating > 2') }}} } 62 | specify { expect(subject.load(hits)).to eq([*cities, nil, countries.last]) } 63 | end 64 | end 65 | 66 | context 'objects' do 67 | before do 68 | stub_index(:cities) do 69 | field :name 70 | field :rating, type: 'integer' 71 | end 72 | 73 | stub_index(:countries) do 74 | field :name 75 | field :rating, type: 'integer' 76 | end 77 | end 78 | 79 | specify { expect(subject.load(hits).map(&:class).uniq).to eq([CitiesIndex, CountriesIndex]) } 80 | specify { expect(subject.load(hits).map(&:rating)).to eq([*cities, *countries].map(&:rating)) } 81 | end 82 | end 83 | end 84 | -------------------------------------------------------------------------------- /spec/chewy/search/pagination/kaminari_examples.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | shared_examples :kaminari do |request_base_class| 4 | before { drop_indices } 5 | 6 | before do 7 | stub_index(:products) do 8 | field :name 9 | field :age, type: 'integer' 10 | end 11 | end 12 | 13 | let(:except_fields) { %w[_score _explanation] } 14 | let(:request_class) do 15 | Class.new(request_base_class).tap do |k| 16 | k.include Chewy::Search::Pagination::Kaminari 17 | end 18 | end 19 | let(:search) { request_class.new(ProductsIndex).order(:age) } 20 | 21 | specify { expect(search.total_pages).to eq(0) } 22 | 23 | context do 24 | let(:data) { Array.new(10) { |i| {id: i.next.to_s, name: "Name#{i.next}", age: 10 * i.next}.stringify_keys! } } 25 | 26 | before { ProductsIndex.import!(data.map { |h| double(h) }) } 27 | before { allow(Kaminari.config).to receive_messages(default_per_page: 3) } 28 | 29 | describe '#per, #page' do 30 | specify { expect(search.map { |e| e.attributes.except(*except_fields) }).to match_array(data) } 31 | specify { expect(search.page(1).map { |e| e.attributes.except(*except_fields) }).to eq(data[0..2]) } 32 | specify { expect(search.page(2).map { |e| e.attributes.except(*except_fields) }).to eq(data[3..5]) } 33 | specify { expect(search.page(2).per(4).map { |e| e.attributes.except(*except_fields) }).to eq(data[4..7]) } 34 | specify { expect(search.per(2).page(3).map { |e| e.attributes.except(*except_fields) }).to eq(data[4..5]) } 35 | specify { expect(search.per(5).page.map { |e| e.attributes.except(*except_fields) }).to eq(data[0..4]) } 36 | specify { expect(search.page.per(4).map { |e| e.attributes.except(*except_fields) }).to eq(data[0..3]) } 37 | end 38 | 39 | describe '#total_pages' do 40 | specify { expect(search.total_pages).to eq(4) } 41 | specify { expect(search.per(5).page(2).total_pages).to eq(2) } 42 | specify { expect(search.per(2).page(3).total_pages).to eq(5) } 43 | end 44 | 45 | describe '#total_count' do 46 | specify { expect(search.per(4).page(1).total_count).to eq(10) } 47 | specify { expect(search.query(range: {age: {gt: 20}}).limit(3).total_count).to eq(8) } 48 | end 49 | 50 | describe '#load' do 51 | specify { expect(search.per(2).page(1).load.first.age).to eq(10) } 52 | specify { expect(search.per(2).page(3).load.first.age).to eq(50) } 53 | specify { expect(search.per(2).page(3).load.page(2).load.first.age).to eq(30) } 54 | 55 | specify { expect(search.per(4).page(1).load.total_count).to eq(10) } 56 | specify { expect(search.per(2).page(3).load.total_pages).to eq(5) } 57 | end 58 | 59 | describe '#limit_value' do 60 | specify { expect(search.limit_value).to eq(3) } 61 | specify { expect(search.per(15).limit_value).to eq(15) } 62 | end 63 | 64 | describe '#offset_value' do 65 | specify { expect(search.offset_value).to eq(0) } 66 | specify { expect(search.page(3).offset_value).to eq(6) } 67 | end 68 | end 69 | end 70 | -------------------------------------------------------------------------------- /spec/chewy/search/pagination/kaminari_spec.rb: -------------------------------------------------------------------------------- 1 | require 'chewy/search/pagination/kaminari_examples' 2 | 3 | describe Chewy::Search::Pagination::Kaminari do 4 | it_behaves_like :kaminari, Chewy::Search::Request do 5 | describe '#objects' do 6 | let(:data) { Array.new(12) { |i| {id: i.next.to_s, name: "Name#{i.next}", age: 10 * i.next}.stringify_keys! } } 7 | 8 | before { ProductsIndex.import!(data.map { |h| double(h) }) } 9 | before { allow(Kaminari.config).to receive_messages(default_per_page: 17) } 10 | 11 | specify { expect(search.objects.class).to eq(Kaminari::PaginatableArray) } 12 | specify { expect(search.objects.total_count).to eq(12) } 13 | specify { expect(search.objects.limit_value).to eq(17) } 14 | specify { expect(search.objects.offset_value).to eq(0) } 15 | specify { expect(search.per(2).page(3).objects.class).to eq(Kaminari::PaginatableArray) } 16 | specify { expect(search.per(2).page(3).objects.total_count).to eq(12) } 17 | specify { expect(search.per(2).page(3).objects.limit_value).to eq(2) } 18 | specify { expect(search.per(2).page(3).objects.offset_value).to eq(4) } 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /spec/chewy/search/parameters/aggs_spec.rb: -------------------------------------------------------------------------------- 1 | require 'chewy/search/parameters/hash_storage_examples' 2 | 3 | describe Chewy::Search::Parameters::Aggs do 4 | it_behaves_like :hash_storage, :aggs 5 | end 6 | -------------------------------------------------------------------------------- /spec/chewy/search/parameters/bool_storage_examples.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | shared_examples :bool_storage do |param_name| 4 | subject { described_class.new(true) } 5 | 6 | describe '#initialize' do 7 | specify { expect(subject.value).to eq(true) } 8 | specify { expect(described_class.new.value).to eq(false) } 9 | specify { expect(described_class.new(42).value).to eq(true) } 10 | specify { expect(described_class.new(nil).value).to eq(false) } 11 | end 12 | 13 | describe '#replace!' do 14 | specify { expect { subject.replace!(false) }.to change { subject.value }.from(true).to(false) } 15 | specify { expect { subject.replace!(nil) }.to change { subject.value }.from(true).to(false) } 16 | end 17 | 18 | describe '#update!' do 19 | specify { expect { subject.update!(nil) }.not_to change { subject.value }.from(true) } 20 | specify { expect { subject.update!(false) }.not_to change { subject.value }.from(true) } 21 | specify { expect { subject.update!(true) }.not_to change { subject.value }.from(true) } 22 | 23 | context do 24 | subject { described_class.new } 25 | 26 | specify { expect { subject.update!(nil) }.not_to change { subject.value }.from(false) } 27 | specify { expect { subject.update!(false) }.not_to change { subject.value }.from(false) } 28 | specify { expect { subject.update!(true) }.to change { subject.value }.from(false).to(true) } 29 | end 30 | end 31 | 32 | describe '#merge!' do 33 | specify { expect { subject.merge!(described_class.new) }.not_to change { subject.value }.from(true) } 34 | specify { expect { subject.merge!(described_class.new(true)) }.not_to change { subject.value }.from(true) } 35 | 36 | context do 37 | subject { described_class.new } 38 | 39 | specify { expect { subject.merge!(described_class.new) }.not_to change { subject.value }.from(false) } 40 | specify { expect { subject.merge!(described_class.new(true)) }.to change { subject.value }.from(false).to(true) } 41 | end 42 | end 43 | 44 | describe '#render' do 45 | specify { expect(described_class.new.render).to be_nil } 46 | 47 | if param_name.is_a?(Symbol) 48 | specify { expect(subject.render).to eq(param_name => true) } 49 | else 50 | specify { expect(subject.render).to eq(param_name) } 51 | end 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /spec/chewy/search/parameters/collapse_spec.rb: -------------------------------------------------------------------------------- 1 | require 'chewy/search/parameters/hash_storage_examples' 2 | 3 | describe Chewy::Search::Parameters::Collapse do 4 | it_behaves_like :hash_storage, :collapse 5 | end 6 | -------------------------------------------------------------------------------- /spec/chewy/search/parameters/docvalue_fields_spec.rb: -------------------------------------------------------------------------------- 1 | require 'chewy/search/parameters/string_array_storage_examples' 2 | 3 | describe Chewy::Search::Parameters::DocvalueFields do 4 | it_behaves_like :string_array_storage, :docvalue_fields 5 | end 6 | -------------------------------------------------------------------------------- /spec/chewy/search/parameters/explain_spec.rb: -------------------------------------------------------------------------------- 1 | require 'chewy/search/parameters/bool_storage_examples' 2 | 3 | describe Chewy::Search::Parameters::Explain do 4 | it_behaves_like :bool_storage, :explain 5 | end 6 | -------------------------------------------------------------------------------- /spec/chewy/search/parameters/filter_spec.rb: -------------------------------------------------------------------------------- 1 | require 'chewy/search/parameters/query_storage_examples' 2 | 3 | describe Chewy::Search::Parameters::Filter do 4 | it_behaves_like :query_storage, :filter 5 | end 6 | -------------------------------------------------------------------------------- /spec/chewy/search/parameters/hash_storage_examples.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | shared_examples :hash_storage do |param_name| 4 | subject { described_class.new(field: {foo: 'bar'}) } 5 | 6 | describe '#initialize' do 7 | specify { expect(described_class.new.value).to eq({}) } 8 | specify { expect(described_class.new(nil).value).to eq({}) } 9 | specify { expect(subject.value).to eq('field' => {foo: 'bar'}) } 10 | end 11 | 12 | describe '#replace!' do 13 | specify do 14 | expect { subject.replace!(nil) } 15 | .to change { subject.value } 16 | .from('field' => {foo: 'bar'}).to({}) 17 | end 18 | 19 | specify do 20 | expect { subject.replace!(other: {moo: 'baz'}) } 21 | .to change { subject.value } 22 | .from('field' => {foo: 'bar'}) 23 | .to('other' => {moo: 'baz'}) 24 | end 25 | end 26 | 27 | describe '#update!' do 28 | specify do 29 | expect { subject.update!(nil) } 30 | .not_to change { subject.value } 31 | end 32 | 33 | specify do 34 | expect { subject.update!(other: {moo: 'baz'}) } 35 | .to change { subject.value } 36 | .from('field' => {foo: 'bar'}) 37 | .to('field' => {foo: 'bar'}, 'other' => {moo: 'baz'}) 38 | end 39 | end 40 | 41 | describe '#merge!' do 42 | specify do 43 | expect { subject.merge!(described_class.new) } 44 | .not_to change { subject.value } 45 | end 46 | 47 | specify do 48 | expect { subject.merge!(described_class.new(other: {moo: 'baz'})) } 49 | .to change { subject.value } 50 | .from('field' => {foo: 'bar'}) 51 | .to('field' => {foo: 'bar'}, 'other' => {moo: 'baz'}) 52 | end 53 | end 54 | 55 | describe '#render' do 56 | specify { expect(described_class.new.render).to be_nil } 57 | specify { expect(described_class.new(field: {foo: 'bar'}).render).to eq(param_name => {'field' => {foo: 'bar'}}) } 58 | end 59 | end 60 | -------------------------------------------------------------------------------- /spec/chewy/search/parameters/highlight_spec.rb: -------------------------------------------------------------------------------- 1 | require 'chewy/search/parameters/hash_storage_examples' 2 | 3 | describe Chewy::Search::Parameters::Highlight do 4 | it_behaves_like :hash_storage, :highlight 5 | end 6 | -------------------------------------------------------------------------------- /spec/chewy/search/parameters/ignore_unavailable_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Chewy::Search::Parameters::IgnoreUnavailable do 4 | subject { described_class.new(true) } 5 | 6 | describe '#initialize' do 7 | specify { expect(subject.value).to eq(true) } 8 | specify { expect(described_class.new.value).to eq(nil) } 9 | specify { expect(described_class.new(42).value).to eq(true) } 10 | specify { expect(described_class.new(false).value).to eq(false) } 11 | end 12 | 13 | describe '#replace!' do 14 | specify { expect { subject.replace!(false) }.to change { subject.value }.from(true).to(false) } 15 | specify { expect { subject.replace!(nil) }.to change { subject.value }.from(true).to(nil) } 16 | end 17 | 18 | describe '#update!' do 19 | specify { expect { subject.update!(nil) }.not_to change { subject.value }.from(true) } 20 | specify { expect { subject.update!(false) }.to change { subject.value }.from(true).to(false) } 21 | specify { expect { subject.update!(true) }.not_to change { subject.value }.from(true) } 22 | 23 | context do 24 | subject { described_class.new(false) } 25 | 26 | specify { expect { subject.update!(nil) }.not_to change { subject.value }.from(false) } 27 | specify { expect { subject.update!(false) }.not_to change { subject.value }.from(false) } 28 | specify { expect { subject.update!(true) }.to change { subject.value }.from(false).to(true) } 29 | end 30 | 31 | context do 32 | subject { described_class.new } 33 | 34 | specify { expect { subject.update!(nil) }.not_to change { subject.value }.from(nil) } 35 | specify { expect { subject.update!(false) }.to change { subject.value }.from(nil).to(false) } 36 | specify { expect { subject.update!(true) }.to change { subject.value }.from(nil).to(true) } 37 | end 38 | end 39 | 40 | describe '#merge!' do 41 | specify { expect { subject.merge!(described_class.new) }.not_to change { subject.value }.from(true) } 42 | specify { expect { subject.merge!(described_class.new(false)) }.to change { subject.value }.from(true).to(false) } 43 | specify { expect { subject.merge!(described_class.new(true)) }.not_to change { subject.value }.from(true) } 44 | 45 | context do 46 | subject { described_class.new(false) } 47 | 48 | specify { expect { subject.merge!(described_class.new) }.not_to change { subject.value }.from(false) } 49 | specify { expect { subject.merge!(described_class.new(false)) }.not_to change { subject.value }.from(false) } 50 | specify { expect { subject.merge!(described_class.new(true)) }.to change { subject.value }.from(false).to(true) } 51 | end 52 | 53 | context do 54 | subject { described_class.new } 55 | 56 | specify { expect { subject.merge!(described_class.new) }.not_to change { subject.value }.from(nil) } 57 | specify { expect { subject.merge!(described_class.new(false)) }.to change { subject.value }.from(nil).to(false) } 58 | specify { expect { subject.merge!(described_class.new(true)) }.to change { subject.value }.from(nil).to(true) } 59 | end 60 | end 61 | 62 | describe '#render' do 63 | specify { expect(described_class.new.render).to be_nil } 64 | specify { expect(described_class.new(false).render).to eq(ignore_unavailable: false) } 65 | specify { expect(subject.render).to eq(ignore_unavailable: true) } 66 | end 67 | end 68 | -------------------------------------------------------------------------------- /spec/chewy/search/parameters/indices_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Chewy::Search::Parameters::Indices do 4 | before do 5 | stub_index(:first) 6 | stub_index(:second) 7 | stub_index(:third) 8 | end 9 | 10 | subject { described_class.new(indices: [FirstIndex, SecondIndex]) } 11 | 12 | describe '#initialize' do 13 | specify { expect(described_class.new.value).to eq(indices: []) } 14 | specify { expect(described_class.new(nil).value).to eq(indices: []) } 15 | specify { expect(described_class.new(foo: :whatever).value).to eq(indices: []) } 16 | specify { expect(subject.value).to eq(indices: [FirstIndex, SecondIndex]) } 17 | end 18 | 19 | describe '#replace!' do 20 | specify do 21 | expect { subject.replace!(nil) } 22 | .to change { subject.value } 23 | .from(indices: [FirstIndex, SecondIndex]) 24 | .to(indices: []) 25 | end 26 | 27 | specify do 28 | expect { subject.replace!(indices: SecondIndex) } 29 | .to change { subject.value } 30 | .from(indices: [FirstIndex, SecondIndex]) 31 | .to(indices: [SecondIndex]) 32 | end 33 | end 34 | 35 | describe '#update!' do 36 | specify do 37 | expect { subject.update!(nil) } 38 | .not_to change { subject.value } 39 | end 40 | 41 | specify do 42 | expect { subject.update!(indices: ThirdIndex) } 43 | .to change { subject.value } 44 | .from(indices: [FirstIndex, SecondIndex]) 45 | .to(indices: [FirstIndex, SecondIndex, ThirdIndex]) 46 | end 47 | end 48 | 49 | describe '#merge!' do 50 | specify do 51 | expect { subject.merge!(described_class.new) } 52 | .not_to change { subject.value } 53 | end 54 | 55 | specify do 56 | expect { subject.merge!(described_class.new(indices: SecondIndex)) } 57 | .not_to change { subject.value } 58 | end 59 | end 60 | 61 | describe '#render' do 62 | specify { expect(described_class.new.render).to eq({}) } 63 | specify do 64 | expect(described_class.new( 65 | indices: FirstIndex 66 | ).render).to eq(index: %w[first]) 67 | end 68 | specify do 69 | expect(described_class.new( 70 | indices: :whatever 71 | ).render).to eq(index: %w[whatever]) 72 | end 73 | specify do 74 | expect(described_class.new( 75 | indices: [FirstIndex, :whatever] 76 | ).render).to eq(index: %w[first whatever]) 77 | end 78 | end 79 | 80 | describe '#==' do 81 | specify { expect(described_class.new).to eq(described_class.new) } 82 | specify do 83 | expect(described_class.new(indices: :first)) 84 | .to eq(described_class.new(indices: FirstIndex)) 85 | end 86 | specify do 87 | expect(described_class.new(indices: FirstIndex)) 88 | .to eq(described_class.new(indices: FirstIndex)) 89 | end 90 | end 91 | 92 | describe '#indices' do 93 | specify do 94 | expect(described_class.new( 95 | indices: [FirstIndex, :whatever] 96 | ).indices).to contain_exactly(FirstIndex) 97 | end 98 | end 99 | end 100 | -------------------------------------------------------------------------------- /spec/chewy/search/parameters/integer_storage_examples.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | shared_examples :integer_storage do |param_name| 4 | subject { described_class.new(10) } 5 | 6 | describe '#initialize' do 7 | specify { expect(described_class.new.value).to be_nil } 8 | specify { expect(described_class.new('42').value).to eq(42) } 9 | specify { expect(described_class.new(33.3).value).to eq(33) } 10 | specify { expect(described_class.new(nil).value).to be_nil } 11 | end 12 | 13 | describe '#replace!' do 14 | specify { expect { subject.replace!(42) }.to change { subject.value }.from(10).to(42) } 15 | specify { expect { subject.replace!(nil) }.to change { subject.value }.from(10).to(nil) } 16 | end 17 | 18 | describe '#update!' do 19 | specify { expect { subject.update!('42') }.to change { subject.value }.from(10).to(42) } 20 | specify { expect { subject.update!(nil) }.not_to change { subject.value }.from(10) } 21 | end 22 | 23 | describe '#merge!' do 24 | specify { expect { subject.merge!(described_class.new('33')) }.to change { subject.value }.from(10).to(33) } 25 | specify { expect { subject.merge!(described_class.new) }.not_to change { subject.value }.from(10) } 26 | end 27 | 28 | describe '#render' do 29 | specify { expect(described_class.new.render).to be_nil } 30 | specify { expect(described_class.new('42').render).to eq(param_name => 42) } 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /spec/chewy/search/parameters/knn_spec.rb: -------------------------------------------------------------------------------- 1 | require 'chewy/search/parameters/hash_storage_examples' 2 | 3 | describe Chewy::Search::Parameters::Knn do 4 | it_behaves_like :hash_storage, :knn 5 | end 6 | -------------------------------------------------------------------------------- /spec/chewy/search/parameters/limit_spec.rb: -------------------------------------------------------------------------------- 1 | require 'chewy/search/parameters/integer_storage_examples' 2 | 3 | describe Chewy::Search::Parameters::Limit do 4 | it_behaves_like :integer_storage, :size 5 | end 6 | -------------------------------------------------------------------------------- /spec/chewy/search/parameters/load_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Chewy::Search::Parameters::Load do 4 | subject { described_class.new(only: :foo) } 5 | 6 | describe '#initialize' do 7 | specify { expect(described_class.new.value).to eq({}) } 8 | specify { expect(described_class.new(nil).value).to eq({}) } 9 | specify { expect(described_class.new(scope: {'type' => :foo}).value).to eq(scope: {type: :foo}) } 10 | specify { expect(subject.value).to eq(only: :foo) } 11 | end 12 | 13 | describe '#replace!' do 14 | specify do 15 | expect { subject.replace!(nil) } 16 | .to change { subject.value } 17 | .from(only: :foo).to({}) 18 | end 19 | 20 | specify do 21 | expect { subject.replace!('except' => :bar) } 22 | .to change { subject.value } 23 | .from(only: :foo) 24 | .to(except: :bar) 25 | end 26 | end 27 | 28 | describe '#update!' do 29 | specify do 30 | expect { subject.update!(nil) } 31 | .not_to change { subject.value } 32 | end 33 | 34 | specify do 35 | expect { subject.update!('except' => :bar) } 36 | .to change { subject.value } 37 | .from(only: :foo) 38 | .to(only: :foo, except: :bar) 39 | end 40 | end 41 | 42 | describe '#merge!' do 43 | specify do 44 | expect { subject.merge!(described_class.new) } 45 | .not_to change { subject.value } 46 | end 47 | 48 | specify do 49 | expect { subject.merge!(described_class.new('except' => :bar)) } 50 | .to change { subject.value } 51 | .from(only: :foo) 52 | .to(only: :foo, except: :bar) 53 | end 54 | end 55 | 56 | describe '#render' do 57 | specify { expect(described_class.new.render).to be_nil } 58 | specify { expect(described_class.new(only: :foo).render).to be_nil } 59 | end 60 | end 61 | -------------------------------------------------------------------------------- /spec/chewy/search/parameters/min_score_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Chewy::Search::Parameters::MinScore do 4 | subject { described_class.new(0.5) } 5 | 6 | describe '#initialize' do 7 | specify { expect(described_class.new.value).to be_nil } 8 | specify { expect(described_class.new('1.4').value).to eq(1.4) } 9 | specify { expect(described_class.new(2).value).to eq(2.0) } 10 | specify { expect(described_class.new(nil).value).to be_nil } 11 | end 12 | 13 | describe '#replace!' do 14 | specify { expect { subject.replace!(1.4) }.to change { subject.value }.from(0.5).to(1.4) } 15 | specify { expect { subject.replace!(nil) }.to change { subject.value }.from(0.5).to(nil) } 16 | end 17 | 18 | describe '#update!' do 19 | specify { expect { subject.update!('1.4') }.to change { subject.value }.from(0.5).to(1.4) } 20 | specify { expect { subject.update!(nil) }.not_to change { subject.value }.from(0.5) } 21 | end 22 | 23 | describe '#merge!' do 24 | specify { expect { subject.merge!(described_class.new('2')) }.to change { subject.value }.from(0.5).to(2.0) } 25 | specify { expect { subject.merge!(described_class.new) }.not_to change { subject.value }.from(0.5) } 26 | end 27 | 28 | describe '#render' do 29 | specify { expect(described_class.new.render).to be_nil } 30 | specify { expect(described_class.new('1.4').render).to eq(min_score: 1.4) } 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /spec/chewy/search/parameters/none_spec.rb: -------------------------------------------------------------------------------- 1 | require 'chewy/search/parameters/bool_storage_examples' 2 | 3 | describe Chewy::Search::Parameters::None do 4 | it_behaves_like :bool_storage, query: {match_none: {}} 5 | end 6 | -------------------------------------------------------------------------------- /spec/chewy/search/parameters/offset_spec.rb: -------------------------------------------------------------------------------- 1 | require 'chewy/search/parameters/integer_storage_examples' 2 | 3 | describe Chewy::Search::Parameters::Offset do 4 | it_behaves_like :integer_storage, :from 5 | end 6 | -------------------------------------------------------------------------------- /spec/chewy/search/parameters/order_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Chewy::Search::Parameters::Order do 4 | subject { described_class.new(%i[foo bar]) } 5 | 6 | describe '#initialize' do 7 | specify { expect(described_class.new.value).to eq([]) } 8 | specify { expect(described_class.new(nil).value).to eq([]) } 9 | specify { expect(described_class.new('').value).to eq([]) } 10 | specify { expect(described_class.new(42).value).to eq(['42']) } 11 | specify { expect(described_class.new([42, 43]).value).to eq(%w[42 43]) } 12 | specify { expect(described_class.new([42, 42]).value).to eq(%w[42 42]) } 13 | specify { expect(described_class.new([42, [43, 44]]).value).to eq(%w[42 43 44]) } 14 | specify { expect(described_class.new(a: 1).value).to eq([{'a' => 1}]) } 15 | specify { expect(described_class.new(['a', {a: 1}, {a: 2}]).value).to eq(['a', {'a' => 1}, {'a' => 2}]) } 16 | specify { expect(described_class.new(['', 43, {a: 1}]).value).to eq(['43', {'a' => 1}]) } 17 | end 18 | 19 | describe '#replace!' do 20 | specify do 21 | expect { subject.replace!(foo: {}) } 22 | .to change { subject.value } 23 | .from(%w[foo bar]).to([{'foo' => {}}]) 24 | end 25 | 26 | specify do 27 | expect { subject.replace!(nil) } 28 | .to change { subject.value } 29 | .from(%w[foo bar]).to([]) 30 | end 31 | end 32 | 33 | describe '#update!' do 34 | specify do 35 | expect { subject.update!(foo: {}) } 36 | .to change { subject.value } 37 | .from(%w[foo bar]).to(['foo', 'bar', {'foo' => {}}]) 38 | end 39 | 40 | specify { expect { subject.update!(nil) }.not_to change { subject.value } } 41 | end 42 | 43 | describe '#merge!' do 44 | specify do 45 | expect { subject.merge!(described_class.new(foo: {})) } 46 | .to change { subject.value } 47 | .from(%w[foo bar]).to(['foo', 'bar', {'foo' => {}}]) 48 | end 49 | 50 | specify { expect { subject.merge!(described_class.new) }.not_to change { subject.value } } 51 | end 52 | 53 | describe '#render' do 54 | specify { expect(described_class.new.render).to be_nil } 55 | specify { expect(described_class.new(:foo).render).to eq(sort: ['foo']) } 56 | specify { expect(described_class.new([:foo, {bar: 42}, :baz]).render).to eq(sort: ['foo', {'bar' => 42}, 'baz']) } 57 | specify { expect(described_class.new([:foo, {bar: 42}, {bar: 43}, :baz]).render).to eq(sort: ['foo', {'bar' => 42}, {'bar' => 43}, 'baz']) } 58 | end 59 | 60 | describe '#==' do 61 | specify { expect(described_class.new).to eq(described_class.new) } 62 | specify { expect(described_class.new(:foo)).to eq(described_class.new(:foo)) } 63 | specify { expect(described_class.new(:foo)).not_to eq(described_class.new(:bar)) } 64 | specify { expect(described_class.new(%i[foo bar])).to eq(described_class.new(%i[foo bar])) } 65 | specify { expect(described_class.new(%i[foo bar])).not_to eq(described_class.new(%i[bar foo])) } 66 | specify { expect(described_class.new(%i[foo foo])).not_to eq(described_class.new(%i[foo])) } 67 | specify { expect(described_class.new(foo: {a: 42})).to eq(described_class.new(foo: {a: 42})) } 68 | specify { expect(described_class.new(foo: {a: 42})).not_to eq(described_class.new(foo: {b: 42})) } 69 | specify { expect(described_class.new(['foo', {'foo' => 42}])).not_to eq(described_class.new([{'foo' => 42}, 'foo'])) } 70 | specify { expect(described_class.new([{'foo' => 42}, {'foo' => 43}])).not_to eq(described_class.new([{'foo' => 43}, {'foo' => 42}])) } 71 | end 72 | end 73 | -------------------------------------------------------------------------------- /spec/chewy/search/parameters/post_filter_spec.rb: -------------------------------------------------------------------------------- 1 | require 'chewy/search/parameters/query_storage_examples' 2 | 3 | describe Chewy::Search::Parameters::PostFilter do 4 | it_behaves_like :query_storage, :post_filter 5 | end 6 | -------------------------------------------------------------------------------- /spec/chewy/search/parameters/preference_spec.rb: -------------------------------------------------------------------------------- 1 | require 'chewy/search/parameters/string_storage_examples' 2 | 3 | describe Chewy::Search::Parameters::Preference do 4 | it_behaves_like :string_storage, :preference 5 | end 6 | -------------------------------------------------------------------------------- /spec/chewy/search/parameters/profile_spec.rb: -------------------------------------------------------------------------------- 1 | require 'chewy/search/parameters/bool_storage_examples' 2 | 3 | describe Chewy::Search::Parameters::Profile do 4 | it_behaves_like :bool_storage, :profile 5 | end 6 | -------------------------------------------------------------------------------- /spec/chewy/search/parameters/query_spec.rb: -------------------------------------------------------------------------------- 1 | require 'chewy/search/parameters/query_storage_examples' 2 | 3 | describe Chewy::Search::Parameters::Query do 4 | it_behaves_like :query_storage, :query 5 | end 6 | -------------------------------------------------------------------------------- /spec/chewy/search/parameters/request_cache_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Chewy::Search::Parameters::RequestCache do 4 | subject { described_class.new(true) } 5 | 6 | describe '#initialize' do 7 | specify { expect(subject.value).to eq(true) } 8 | specify { expect(described_class.new.value).to eq(nil) } 9 | specify { expect(described_class.new(42).value).to eq(true) } 10 | specify { expect(described_class.new(false).value).to eq(false) } 11 | end 12 | 13 | describe '#replace!' do 14 | specify { expect { subject.replace!(false) }.to change { subject.value }.from(true).to(false) } 15 | specify { expect { subject.replace!(nil) }.to change { subject.value }.from(true).to(nil) } 16 | end 17 | 18 | describe '#update!' do 19 | specify { expect { subject.update!(nil) }.not_to change { subject.value }.from(true) } 20 | specify { expect { subject.update!(false) }.to change { subject.value }.from(true).to(false) } 21 | specify { expect { subject.update!(true) }.not_to change { subject.value }.from(true) } 22 | 23 | context do 24 | subject { described_class.new(false) } 25 | 26 | specify { expect { subject.update!(nil) }.not_to change { subject.value }.from(false) } 27 | specify { expect { subject.update!(false) }.not_to change { subject.value }.from(false) } 28 | specify { expect { subject.update!(true) }.to change { subject.value }.from(false).to(true) } 29 | end 30 | 31 | context do 32 | subject { described_class.new } 33 | 34 | specify { expect { subject.update!(nil) }.not_to change { subject.value }.from(nil) } 35 | specify { expect { subject.update!(false) }.to change { subject.value }.from(nil).to(false) } 36 | specify { expect { subject.update!(true) }.to change { subject.value }.from(nil).to(true) } 37 | end 38 | end 39 | 40 | describe '#merge!' do 41 | specify { expect { subject.merge!(described_class.new) }.not_to change { subject.value }.from(true) } 42 | specify { expect { subject.merge!(described_class.new(false)) }.to change { subject.value }.from(true).to(false) } 43 | specify { expect { subject.merge!(described_class.new(true)) }.not_to change { subject.value }.from(true) } 44 | 45 | context do 46 | subject { described_class.new(false) } 47 | 48 | specify { expect { subject.merge!(described_class.new) }.not_to change { subject.value }.from(false) } 49 | specify { expect { subject.merge!(described_class.new(false)) }.not_to change { subject.value }.from(false) } 50 | specify { expect { subject.merge!(described_class.new(true)) }.to change { subject.value }.from(false).to(true) } 51 | end 52 | 53 | context do 54 | subject { described_class.new } 55 | 56 | specify { expect { subject.merge!(described_class.new) }.not_to change { subject.value }.from(nil) } 57 | specify { expect { subject.merge!(described_class.new(false)) }.to change { subject.value }.from(nil).to(false) } 58 | specify { expect { subject.merge!(described_class.new(true)) }.to change { subject.value }.from(nil).to(true) } 59 | end 60 | end 61 | 62 | describe '#render' do 63 | specify { expect(described_class.new.render).to be_nil } 64 | specify { expect(described_class.new(false).render).to eq(request_cache: false) } 65 | specify { expect(subject.render).to eq(request_cache: true) } 66 | end 67 | end 68 | -------------------------------------------------------------------------------- /spec/chewy/search/parameters/rescore_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Chewy::Search::Parameters::Rescore do 4 | subject { described_class.new([{foo: 42}, {bar: 43}]) } 5 | 6 | describe '#initialize' do 7 | specify { expect(described_class.new.value).to eq([]) } 8 | specify { expect(described_class.new(nil).value).to eq([]) } 9 | specify { expect(described_class.new(foo: 42).value).to eq([{foo: 42}]) } 10 | specify { expect(described_class.new([{foo: 42}, nil]).value).to eq([{foo: 42}]) } 11 | specify { expect(subject.value).to eq([{foo: 42}, {bar: 43}]) } 12 | end 13 | 14 | describe '#replace!' do 15 | specify do 16 | expect { subject.replace!(nil) } 17 | .to change { subject.value } 18 | .from([{foo: 42}, {bar: 43}]).to([]) 19 | end 20 | 21 | specify do 22 | expect { subject.replace!(baz: 44) } 23 | .to change { subject.value } 24 | .from([{foo: 42}, {bar: 43}]) 25 | .to([{baz: 44}]) 26 | end 27 | end 28 | 29 | describe '#update!' do 30 | specify do 31 | expect { subject.update!(nil) } 32 | .not_to change { subject.value } 33 | end 34 | 35 | specify do 36 | expect { subject.update!(baz: 44) } 37 | .to change { subject.value } 38 | .from([{foo: 42}, {bar: 43}]) 39 | .to([{foo: 42}, {bar: 43}, {baz: 44}]) 40 | end 41 | end 42 | 43 | describe '#merge!' do 44 | specify do 45 | expect { subject.merge!(described_class.new) } 46 | .not_to change { subject.value } 47 | end 48 | 49 | specify do 50 | expect { subject.merge!(described_class.new(baz: 44)) } 51 | .to change { subject.value } 52 | .from([{foo: 42}, {bar: 43}]) 53 | .to([{foo: 42}, {bar: 43}, {baz: 44}]) 54 | end 55 | end 56 | 57 | describe '#render' do 58 | specify { expect(described_class.new.render).to be_nil } 59 | specify { expect(described_class.new(foo: 42).render).to eq(rescore: [{foo: 42}]) } 60 | specify { expect(subject.render).to eq(rescore: [{foo: 42}, {bar: 43}]) } 61 | end 62 | end 63 | -------------------------------------------------------------------------------- /spec/chewy/search/parameters/script_fields_spec.rb: -------------------------------------------------------------------------------- 1 | require 'chewy/search/parameters/hash_storage_examples' 2 | 3 | describe Chewy::Search::Parameters::ScriptFields do 4 | it_behaves_like :hash_storage, :script_fields 5 | end 6 | -------------------------------------------------------------------------------- /spec/chewy/search/parameters/search_after_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Chewy::Search::Parameters::SearchAfter do 4 | subject { described_class.new([:foo, 42]) } 5 | 6 | describe '#initialize' do 7 | specify { expect(described_class.new.value).to be_nil } 8 | specify { expect(described_class.new(:foo).value).to eq([:foo]) } 9 | specify { expect(described_class.new(nil).value).to be_nil } 10 | end 11 | 12 | describe '#replace!' do 13 | specify { expect { subject.replace!(:baz) }.to change { subject.value }.from([:foo, 42]).to([:baz]) } 14 | specify { expect { subject.replace!(nil) }.to change { subject.value }.from([:foo, 42]).to(nil) } 15 | end 16 | 17 | describe '#update!' do 18 | specify { expect { subject.update!(:baz) }.to change { subject.value }.from([:foo, 42]).to([:baz]) } 19 | specify { expect { subject.update!(nil) }.not_to change { subject.value }.from([:foo, 42]) } 20 | end 21 | 22 | describe '#merge!' do 23 | specify do 24 | expect { subject.merge!(described_class.new(:baz)) } 25 | .to change { subject.value }.from([:foo, 42]).to([:baz]) 26 | end 27 | specify { expect { subject.merge!(described_class.new) }.not_to change { subject.value }.from([:foo, 42]) } 28 | end 29 | 30 | describe '#render' do 31 | specify { expect(described_class.new.render).to be_nil } 32 | specify { expect(described_class.new(:baz).render).to eq(search_after: [:baz]) } 33 | specify { expect(subject.render).to eq(search_after: [:foo, 42]) } 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /spec/chewy/search/parameters/search_type_spec.rb: -------------------------------------------------------------------------------- 1 | require 'chewy/search/parameters/string_storage_examples' 2 | 3 | describe Chewy::Search::Parameters::SearchType do 4 | it_behaves_like :string_storage, :search_type 5 | end 6 | -------------------------------------------------------------------------------- /spec/chewy/search/parameters/storage_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Chewy::Search::Parameters::Storage do 4 | subject { described_class.new } 5 | 6 | describe '.param_name' do 7 | specify { expect(described_class.param_name).to eq(:storage) } 8 | 9 | context do 10 | before { stub_class('Namespace::CustomParamName', Class.new(described_class)) } 11 | specify { expect(Namespace::CustomParamName.param_name).to eq(:custom_param_name) } 12 | end 13 | end 14 | 15 | describe '.param_name=' do 16 | before { stub_class('Namespace::Whatever', Class.new(described_class)) } 17 | specify do 18 | expect { Namespace::Whatever.param_name = :custom } 19 | .to change { Namespace::Whatever.param_name }.from(:whatever).to(:custom) 20 | end 21 | end 22 | 23 | describe '#initialize' do 24 | specify { expect(subject.value).to be_nil } 25 | specify { expect(described_class.new(a: 1).value).to eq(a: 1) } 26 | end 27 | 28 | describe '#==' do 29 | specify { expect(subject).to eq(described_class.new) } 30 | specify { expect(described_class.new(:foo)).to eq(described_class.new(:foo)) } 31 | specify { expect(described_class.new(:foo)).not_to eq(described_class.new(:bar)) } 32 | 33 | context do 34 | let(:other_value) { Class.new(described_class) } 35 | specify { expect(other_value.new(:foo)).not_to eq(described_class.new(:foo)) } 36 | specify { expect(other_value.new(:foo)).to eq(other_value.new(:foo)) } 37 | end 38 | end 39 | 40 | describe '#replace!' do 41 | specify { expect { subject.replace!(42) }.to change { subject.value }.from(nil).to(42) } 42 | specify { expect { subject.replace!('42') }.to change { subject.value }.from(nil).to('42') } 43 | end 44 | 45 | describe '#update!' do 46 | specify { expect { subject.update!(true) }.to change { subject.value }.from(nil).to(true) } 47 | specify { expect { subject.update!(:symbol) }.to change { subject.value }.from(nil).to(:symbol) } 48 | end 49 | 50 | describe '#merge!' do 51 | let(:other) { described_class.new(['something']) } 52 | specify { expect { subject.merge!(other) }.to change { subject.value }.from(nil).to(['something']) } 53 | end 54 | 55 | describe '#render' do 56 | specify { expect(subject.render).to be_nil } 57 | specify { expect(described_class.new(false).render).to be_nil } 58 | specify { expect(described_class.new('42').render).to eq(storage: '42') } 59 | end 60 | end 61 | -------------------------------------------------------------------------------- /spec/chewy/search/parameters/string_array_storage_examples.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | shared_examples :string_array_storage do |param_name| 4 | subject { described_class.new(%i[foo bar]) } 5 | 6 | describe '#initialize' do 7 | specify { expect(described_class.new.value).to eq([]) } 8 | specify { expect(described_class.new(nil).value).to eq([]) } 9 | specify { expect(described_class.new(:foo).value).to eq(%w[foo]) } 10 | specify { expect(described_class.new([:foo, nil]).value).to eq(%w[foo]) } 11 | specify { expect(subject.value).to eq(%w[foo bar]) } 12 | end 13 | 14 | describe '#replace!' do 15 | specify do 16 | expect { subject.replace!(nil) } 17 | .to change { subject.value } 18 | .from(%w[foo bar]).to([]) 19 | end 20 | 21 | specify do 22 | expect { subject.replace!(%i[foo baz]) } 23 | .to change { subject.value } 24 | .from(%w[foo bar]).to(%w[foo baz]) 25 | end 26 | end 27 | 28 | describe '#update!' do 29 | specify do 30 | expect { subject.update!(nil) } 31 | .not_to change { subject.value } 32 | end 33 | 34 | specify do 35 | expect { subject.update!(%i[foo baz]) } 36 | .to change { subject.value } 37 | .from(%w[foo bar]).to(%w[foo bar baz]) 38 | end 39 | end 40 | 41 | describe '#merge!' do 42 | specify do 43 | expect { subject.merge!(described_class.new) } 44 | .not_to change { subject.value } 45 | end 46 | 47 | specify do 48 | expect { subject.merge!(described_class.new(%i[foo baz])) } 49 | .to change { subject.value } 50 | .from(%w[foo bar]).to(%w[foo bar baz]) 51 | end 52 | end 53 | 54 | describe '#render' do 55 | specify { expect(described_class.new.render).to be_nil } 56 | 57 | if param_name 58 | specify { expect(subject.render).to eq(param_name => %w[foo bar]) } 59 | else 60 | specify { expect(subject.render).to be_nil } 61 | end 62 | end 63 | end 64 | -------------------------------------------------------------------------------- /spec/chewy/search/parameters/string_storage_examples.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | shared_examples :string_storage do |param_name| 4 | subject { described_class.new(:foo) } 5 | 6 | describe '#initialize' do 7 | specify { expect(subject.value).to eq('foo') } 8 | specify { expect(described_class.new(42).value).to eq('42') } 9 | specify { expect(described_class.new('').value).to be_nil } 10 | end 11 | 12 | describe '#replace!' do 13 | specify { expect { subject.replace!('bar') }.to change { subject.value }.from('foo').to('bar') } 14 | specify { expect { subject.replace!('') }.to change { subject.value }.from('foo').to(nil) } 15 | end 16 | 17 | describe '#update!' do 18 | specify { expect { subject.update!('bar') }.to change { subject.value }.from('foo').to('bar') } 19 | specify { expect { subject.update!('') }.not_to change { subject.value }.from('foo') } 20 | specify { expect { subject.update!(nil) }.not_to change { subject.value }.from('foo') } 21 | end 22 | 23 | describe '#merge!' do 24 | specify { expect { subject.merge!(described_class.new('bar')) }.to change { subject.value }.from('foo').to('bar') } 25 | specify { expect { subject.merge!(described_class.new) }.not_to change { subject.value }.from('foo') } 26 | end 27 | 28 | describe '#render' do 29 | specify { expect(described_class.new.render).to be_nil } 30 | specify { expect(subject.render).to eq(param_name => 'foo') } 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /spec/chewy/search/parameters/suggest_spec.rb: -------------------------------------------------------------------------------- 1 | require 'chewy/search/parameters/hash_storage_examples' 2 | 3 | describe Chewy::Search::Parameters::Suggest do 4 | it_behaves_like :hash_storage, :suggest 5 | end 6 | -------------------------------------------------------------------------------- /spec/chewy/search/parameters/terminate_after_spec.rb: -------------------------------------------------------------------------------- 1 | require 'chewy/search/parameters/integer_storage_examples' 2 | 3 | describe Chewy::Search::Parameters::TerminateAfter do 4 | it_behaves_like :integer_storage, :terminate_after 5 | end 6 | -------------------------------------------------------------------------------- /spec/chewy/search/parameters/timeout_spec.rb: -------------------------------------------------------------------------------- 1 | require 'chewy/search/parameters/string_storage_examples' 2 | 3 | describe Chewy::Search::Parameters::Timeout do 4 | it_behaves_like :string_storage, :timeout 5 | end 6 | -------------------------------------------------------------------------------- /spec/chewy/search/parameters/track_scores_spec.rb: -------------------------------------------------------------------------------- 1 | require 'chewy/search/parameters/bool_storage_examples' 2 | 3 | describe Chewy::Search::Parameters::TrackScores do 4 | it_behaves_like :bool_storage, :track_scores 5 | end 6 | -------------------------------------------------------------------------------- /spec/chewy/search/parameters/track_total_hits_spec.rb: -------------------------------------------------------------------------------- 1 | require 'chewy/search/parameters/bool_storage_examples' 2 | 3 | describe Chewy::Search::Parameters::TrackTotalHits do 4 | it_behaves_like :bool_storage, :track_total_hits 5 | end 6 | -------------------------------------------------------------------------------- /spec/chewy/search/parameters/version_spec.rb: -------------------------------------------------------------------------------- 1 | require 'chewy/search/parameters/bool_storage_examples' 2 | 3 | describe Chewy::Search::Parameters::Version do 4 | it_behaves_like :bool_storage, :version 5 | end 6 | -------------------------------------------------------------------------------- /spec/chewy/stash_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Chewy::Stash::Journal, :orm do 4 | def fetch_deleted_number(response) 5 | response['deleted'] || response['_indices']['_all']['deleted'] 6 | end 7 | 8 | before { drop_indices } 9 | 10 | before do 11 | stub_model(:city) 12 | stub_index(:cities) do 13 | index_scope City 14 | end 15 | stub_index(:countries) 16 | stub_index(:users) 17 | stub_index(:borogoves) 18 | end 19 | 20 | before { Timecop.freeze } 21 | after { Timecop.return } 22 | 23 | before do 24 | CitiesIndex.import!(City.new(id: 1, name: 'City'), journal: true) 25 | Timecop.travel(Time.now + 1.minute) do 26 | CountriesIndex.import!([id: 2, name: 'Country'], journal: true) 27 | end 28 | Timecop.travel(Time.now + 2.minutes) do 29 | UsersIndex.import!([id: 3, name: 'User'], journal: true) 30 | end 31 | end 32 | 33 | describe '.entries' do 34 | specify do 35 | expect(described_class.entries(Time.now - 30.seconds).map(&:references)) 36 | .to contain_exactly([1], [{'id' => 2, 'name' => 'Country'}], [{'id' => 3, 'name' => 'User'}]) 37 | end 38 | specify do 39 | expect(described_class.entries(Time.now + 30.seconds).map(&:references)) 40 | .to contain_exactly([{'id' => 2, 'name' => 'Country'}], [{'id' => 3, 'name' => 'User'}]) 41 | end 42 | specify do 43 | expect(described_class.entries(Time.now + 90.seconds).map(&:references)) 44 | .to contain_exactly([{'id' => 3, 'name' => 'User'}]) 45 | end 46 | 47 | specify do 48 | expect(described_class.entries(Time.now - 30.seconds, only: UsersIndex).map(&:references)) 49 | .to contain_exactly([{'id' => 3, 'name' => 'User'}]) 50 | end 51 | specify do 52 | expect(described_class.entries(Time.now - 30.seconds, only: [CitiesIndex, UsersIndex]).map(&:references)) 53 | .to contain_exactly([1], [{'id' => 3, 'name' => 'User'}]) 54 | end 55 | specify do 56 | expect(described_class.entries(Time.now + 30.seconds, only: [CitiesIndex, UsersIndex]).map(&:references)) 57 | .to contain_exactly([{'id' => 3, 'name' => 'User'}]) 58 | end 59 | specify do 60 | expect(described_class.entries(Time.now + 30.seconds, only: [BorogovesIndex])).to eq([]) 61 | end 62 | end 63 | 64 | describe '.clean' do 65 | specify { expect(fetch_deleted_number(described_class.clean)).to eq(3) } 66 | specify { expect(fetch_deleted_number(described_class.clean(Time.now - 30.seconds))).to eq(0) } 67 | specify { expect(fetch_deleted_number(described_class.clean(Time.now + 30.seconds))).to eq(1) } 68 | specify { expect(fetch_deleted_number(described_class.clean(Time.now + 90.seconds))).to eq(2) } 69 | specify { expect(fetch_deleted_number(described_class.clean(only: BorogovesIndex))).to eq(0) } 70 | specify { expect(fetch_deleted_number(described_class.clean(only: UsersIndex))).to eq(1) } 71 | specify { expect(fetch_deleted_number(described_class.clean(only: [CitiesIndex, UsersIndex]))).to eq(2) } 72 | 73 | specify do 74 | expect(fetch_deleted_number(described_class.clean(Time.now + 30.seconds, only: CountriesIndex))).to eq(0) 75 | end 76 | specify { expect(fetch_deleted_number(described_class.clean(Time.now + 30.seconds, only: CitiesIndex))).to eq(1) } 77 | end 78 | 79 | describe '.for' do 80 | specify { expect(described_class.for(UsersIndex).map(&:index_name)).to eq(['users']) } 81 | specify do 82 | expect(described_class.for(CitiesIndex, UsersIndex).map(&:index_name)).to contain_exactly('cities', 'users') 83 | end 84 | end 85 | end 86 | -------------------------------------------------------------------------------- /spec/chewy/strategy/active_job_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | if defined?(ActiveJob) 4 | describe Chewy::Strategy::ActiveJob do 5 | around do |example| 6 | active_job_settings = Chewy.settings[:active_job] 7 | Chewy.settings[:active_job] = {queue: 'low'} 8 | Chewy.strategy(:bypass) { example.run } 9 | Chewy.settings[:active_job] = active_job_settings 10 | end 11 | before(:all) do 12 | ActiveJob::Base.logger = Chewy.logger 13 | end 14 | before do 15 | ActiveJob::Base.queue_adapter = :test 16 | ActiveJob::Base.queue_adapter.enqueued_jobs.clear 17 | ActiveJob::Base.queue_adapter.performed_jobs.clear 18 | end 19 | 20 | before do 21 | stub_model(:city) do 22 | update_index('cities') { self } 23 | end 24 | 25 | stub_index(:cities) do 26 | index_scope City 27 | end 28 | end 29 | 30 | let(:city) { City.create!(name: 'hello') } 31 | let(:other_city) { City.create!(name: 'world') } 32 | 33 | specify do 34 | expect { [city, other_city].map(&:save!) } 35 | .not_to update_index(CitiesIndex, strategy: :active_job) 36 | end 37 | 38 | specify do 39 | Chewy.strategy(:active_job) do 40 | [city, other_city].map(&:save!) 41 | end 42 | enqueued_job = ActiveJob::Base.queue_adapter.enqueued_jobs.first 43 | expect(enqueued_job[:job]).to eq(Chewy::Strategy::ActiveJob::Worker) 44 | expect(enqueued_job[:queue]).to eq('low') 45 | end 46 | 47 | specify do 48 | Chewy.strategy(:active_job) do 49 | [city, other_city].map(&:save!) 50 | end 51 | enqueued_job = ActiveJob::Base.queue_adapter.enqueued_jobs.first 52 | expect(enqueued_job[:queue]).to eq('low') 53 | end 54 | 55 | specify do 56 | ActiveJob::Base.queue_adapter = :inline 57 | expect { [city, other_city].map(&:save!) } 58 | .to update_index(CitiesIndex, strategy: :active_job) 59 | .and_reindex(city, other_city).only 60 | end 61 | 62 | specify do 63 | expect(CitiesIndex).to receive(:import!).with([city.id, other_city.id], suffix: '201601') 64 | Chewy::Strategy::ActiveJob::Worker.new.perform('CitiesIndex', [city.id, other_city.id], suffix: '201601') 65 | end 66 | 67 | specify do 68 | allow(Chewy).to receive(:disable_refresh_async).and_return(true) 69 | expect(CitiesIndex).to receive(:import!).with([city.id, other_city.id], suffix: '201601', refresh: false) 70 | Chewy::Strategy::ActiveJob::Worker.new.perform('CitiesIndex', [city.id, other_city.id], suffix: '201601') 71 | end 72 | end 73 | end 74 | -------------------------------------------------------------------------------- /spec/chewy/strategy/atomic_no_refresh_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Chewy::Strategy::AtomicNoRefresh, :orm do 4 | around { |example| Chewy.strategy(:bypass) { example.run } } 5 | 6 | before do 7 | stub_model(:country) do 8 | update_index('countries') { self } 9 | end 10 | 11 | stub_index(:countries) do 12 | index_scope Country 13 | end 14 | end 15 | 16 | let(:country) { Country.create!(name: 'hello', country_code: 'HL') } 17 | let(:other_country) { Country.create!(name: 'world', country_code: 'WD') } 18 | 19 | specify do 20 | expect { [country, other_country].map(&:save!) } 21 | .to update_index(CountriesIndex, strategy: :atomic_no_refresh) 22 | .and_reindex(country, other_country).only.no_refresh 23 | end 24 | 25 | specify do 26 | expect { [country, other_country].map(&:destroy) } 27 | .to update_index(CountriesIndex, strategy: :atomic_no_refresh) 28 | .and_delete(country, other_country).only.no_refresh 29 | end 30 | 31 | context do 32 | before do 33 | stub_index(:countries) do 34 | index_scope Country 35 | root id: -> { country_code } 36 | end 37 | end 38 | 39 | specify do 40 | expect { [country, other_country].map(&:save!) } 41 | .to update_index(CountriesIndex, strategy: :atomic_no_refresh) 42 | .and_reindex('HL', 'WD').only.no_refresh 43 | end 44 | 45 | specify do 46 | expect { [country, other_country].map(&:destroy) } 47 | .to update_index(CountriesIndex, strategy: :atomic_no_refresh) 48 | .and_delete('HL', 'WD').only.no_refresh 49 | end 50 | 51 | specify do 52 | expect do 53 | country.save! 54 | other_country.destroy 55 | end 56 | .to update_index(CountriesIndex, strategy: :atomic_no_refresh) 57 | .and_reindex('HL').and_delete('WD').only.no_refresh 58 | end 59 | end 60 | end 61 | -------------------------------------------------------------------------------- /spec/chewy/strategy/atomic_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Chewy::Strategy::Atomic, :orm do 4 | around { |example| Chewy.strategy(:bypass) { example.run } } 5 | 6 | before do 7 | stub_model(:country) do 8 | update_index('countries') { self } 9 | end 10 | 11 | stub_index(:countries) do 12 | index_scope Country 13 | end 14 | end 15 | 16 | let(:country) { Country.create!(name: 'hello', country_code: 'HL') } 17 | let(:other_country) { Country.create!(name: 'world', country_code: 'WD') } 18 | 19 | specify do 20 | expect { [country, other_country].map(&:save!) } 21 | .to update_index(CountriesIndex, strategy: :atomic) 22 | .and_reindex(country, other_country).only 23 | end 24 | 25 | specify do 26 | expect { [country, other_country].map(&:destroy) } 27 | .to update_index(CountriesIndex, strategy: :atomic) 28 | .and_delete(country, other_country).only 29 | end 30 | 31 | context do 32 | before do 33 | stub_index(:countries) do 34 | index_scope Country 35 | root id: -> { country_code } do 36 | end 37 | end 38 | end 39 | 40 | specify do 41 | expect { [country, other_country].map(&:save!) } 42 | .to update_index(CountriesIndex, strategy: :atomic) 43 | .and_reindex('HL', 'WD').only 44 | end 45 | 46 | specify do 47 | expect { [country, other_country].map(&:destroy) } 48 | .to update_index(CountriesIndex, strategy: :atomic) 49 | .and_delete('HL', 'WD').only 50 | end 51 | 52 | specify do 53 | expect do 54 | country.save! 55 | other_country.destroy 56 | end 57 | .to update_index(CountriesIndex, strategy: :atomic) 58 | .and_reindex('HL').and_delete('WD').only 59 | end 60 | end 61 | end 62 | -------------------------------------------------------------------------------- /spec/chewy/strategy/sidekiq_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | if defined?(Sidekiq) 4 | require 'sidekiq/testing' 5 | 6 | describe Chewy::Strategy::Sidekiq do 7 | around do |example| 8 | sidekiq_settings = Chewy.settings[:sidekiq] 9 | Chewy.settings[:sidekiq] = {queue: 'low'} 10 | Chewy.strategy(:bypass) { example.run } 11 | Chewy.settings[:sidekiq] = sidekiq_settings 12 | end 13 | before { Sidekiq::Worker.clear_all } 14 | before do 15 | stub_model(:city) do 16 | update_index('cities') { self } 17 | end 18 | 19 | stub_index(:cities) do 20 | index_scope City 21 | end 22 | end 23 | 24 | let(:city) { City.create!(name: 'hello') } 25 | let(:other_city) { City.create!(name: 'world') } 26 | 27 | specify do 28 | expect { [city, other_city].map(&:save!) } 29 | .not_to update_index(CitiesIndex, strategy: :sidekiq) 30 | end 31 | 32 | specify do 33 | expect(Sidekiq::Client).to receive(:push).with(hash_including('queue' => 'low')).and_call_original 34 | Sidekiq::Testing.inline! do 35 | expect { [city, other_city].map(&:save!) } 36 | .to update_index(CitiesIndex, strategy: :sidekiq) 37 | .and_reindex(city, other_city).only 38 | end 39 | end 40 | 41 | specify do 42 | expect(CitiesIndex).to receive(:import!).with([city.id, other_city.id], suffix: '201601') 43 | Chewy::Strategy::Sidekiq::Worker.new.perform('CitiesIndex', [city.id, other_city.id], suffix: '201601') 44 | end 45 | 46 | specify do 47 | allow(Chewy).to receive(:disable_refresh_async).and_return(true) 48 | expect(CitiesIndex).to receive(:import!).with([city.id, other_city.id], suffix: '201601', refresh: false) 49 | Chewy::Strategy::Sidekiq::Worker.new.perform('CitiesIndex', [city.id, other_city.id], suffix: '201601') 50 | end 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require 'bundler' 2 | 3 | Bundler.require 4 | 5 | require 'active_record' 6 | 7 | require 'rspec/its' 8 | require 'rspec/collection_matchers' 9 | 10 | require 'timecop' 11 | 12 | Kaminari::Hooks.init if defined?(Kaminari::Hooks) 13 | 14 | require 'support/fail_helpers' 15 | require 'support/class_helpers' 16 | 17 | require 'chewy/rspec' 18 | 19 | host = ENV['ES_HOST'] || 'localhost' 20 | port = ENV['ES_PORT'] || 9250 21 | 22 | Chewy.settings = { 23 | host: "#{host}:#{port}", 24 | wait_for_status: 'green', 25 | index: { 26 | number_of_shards: 1, 27 | number_of_replicas: 0 28 | } 29 | } 30 | 31 | # To work with security enabled: 32 | # 33 | # user = ENV['ES_USER'] || 'elastic' 34 | # password = ENV['ES_PASSWORD'] || '' 35 | # ca_cert = ENV['ES_CA_CERT'] || './tmp/http_ca.crt' 36 | # 37 | # Chewy.settings.merge!( 38 | # user: user, 39 | # password: password, 40 | # transport_options: { 41 | # ssl: { 42 | # ca_file: ca_cert 43 | # } 44 | # } 45 | # ) 46 | 47 | # Low-level substitute for now-obsolete drop_indices 48 | def drop_indices 49 | response = Chewy.client.cat.indices 50 | indices = response.body.lines.map { |line| line.split[2] } 51 | return if indices.blank? 52 | 53 | Chewy.client.indices.delete(index: indices) 54 | Chewy.wait_for_status 55 | end 56 | 57 | # Chewy.transport_logger = Logger.new(STDERR) 58 | 59 | RSpec.configure do |config| 60 | config.mock_with :rspec 61 | config.order = :random 62 | config.filter_run focus: true 63 | config.run_all_when_everything_filtered = true 64 | 65 | config.include FailHelpers 66 | config.include ClassHelpers 67 | end 68 | 69 | require 'support/active_record' 70 | -------------------------------------------------------------------------------- /spec/support/class_helpers.rb: -------------------------------------------------------------------------------- 1 | module ClassHelpers 2 | extend ActiveSupport::Concern 3 | 4 | def stub_index(name, superclass = nil, &block) 5 | stub_class("#{name.to_s.camelize}Index", superclass || Chewy::Index) 6 | .tap { |i| i.class_eval(&block) if block } 7 | end 8 | 9 | def stub_class(name, superclass = nil, &block) 10 | stub_const(name.to_s.camelize, Class.new(superclass || Object, &block)) 11 | end 12 | 13 | def stub_model(_name, _superclass = nil) 14 | raise NotImplementedError, 'Seems like no ORM/ODM are loaded, please check your Gemfile' 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /spec/support/fail_helpers.rb: -------------------------------------------------------------------------------- 1 | module FailHelpers 2 | def fail 3 | raise_error(RSpec::Expectations::ExpectationNotMetError) 4 | end 5 | 6 | def fail_with(message) 7 | raise_error(RSpec::Expectations::ExpectationNotMetError, message) 8 | end 9 | 10 | def fail_matching(message) 11 | raise_error(RSpec::Expectations::ExpectationNotMetError, /#{Regexp.escape(message)}/) 12 | end 13 | end 14 | --------------------------------------------------------------------------------