Skip to content

Commit

Permalink
Add before request filter (#919)
Browse files Browse the repository at this point in the history
  • Loading branch information
Danil Nurgaliev authored Jan 15, 2024
1 parent ea0ed0a commit cf07086
Show file tree
Hide file tree
Showing 7 changed files with 87 additions and 11 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
### New Features

* [#894](https://github.com/toptal/chewy/pull/894): Way of cleaning redis from artifacts left by `delayed_sidekiq` strategy which could potentially cause flaky tests. ([@Drowze](https://github.com/Drowze))
* [#919](https://github.com/toptal/chewy/pull/919): Add pre-request filter ([@konalegi][https://github.com/konalegi])

### Changes

Expand Down
17 changes: 17 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1281,6 +1281,23 @@ If you use `DatabaseCleaner` in your tests with [the `transaction` strategy](htt
Chewy.use_after_commit_callbacks = !Rails.env.test?
```

### Pre-request Filter

Should you need to inspect the query prior to it being dispatched to ElasticSearch during any queries, you can use the `before_es_request_filter`. `before_es_request_filter` is a callable object, as demonstrated below:

```ruby
Chewy.before_es_request_filter = -> (method_name, args, kw_args) { ... }
```

While using the `before_es_request_filter`, please consider the following:

* `before_es_request_filter` acts as a simple proxy before any request made via the `ElasticSearch::Client`. The arguments passed to this filter include:
* `method_name` - The name of the method being called. Examples are search, count, bulk and etc.
* `args` and `kw_args` - These are the positional arguments provided in the method call.
* The operation is synchronous, so avoid executing any heavy or time-consuming operations within the filter to prevent performance degradation.
* The return value of the proc is disregarded. This filter is intended for inspection or modification of the query rather than generating a response.
* Any exception raised inside the callback will propagate upward and halt the execution of the query. It is essential to handle potential errors adequately to ensure the stability of your search functionality.

## Contributing

1. Fork it (http://github.com/toptal/chewy/fork)
Expand Down
8 changes: 2 additions & 6 deletions lib/chewy.rb
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ def try_require(path)
require 'chewy/fields/root'
require 'chewy/journal'
require 'chewy/railtie' if defined?(Rails::Railtie)
require 'chewy/elastic_client'

ActiveSupport.on_load(:active_record) do
include Chewy::Index::Observe::ActiveRecordMethods
Expand Down Expand Up @@ -97,12 +98,7 @@ def derive_name(index_name)
# Main elasticsearch-ruby client instance
#
def client
Chewy.current[:chewy_client] ||= begin
client_configuration = configuration.deep_dup
client_configuration.delete(:prefix) # used by Chewy, not relevant to Elasticsearch::Client
block = client_configuration[:transport_options].try(:delete, :proc)
::Elasticsearch::Client.new(client_configuration, &block)
end
Chewy.current[:chewy_client] ||= Chewy::ElasticClient.new
end

# Sends wait_for_status request to ElasticSearch with status
Expand Down
4 changes: 3 additions & 1 deletion lib/chewy/config.rb
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,9 @@ class Config
# for type mappings like `_all`.
:default_root_options,
# Default field type for any field in any Chewy type. Defaults to 'text'.
:default_field_type
:default_field_type,
# Callback called on each search request to be done into ES
:before_es_request_filter

attr_reader :transport_logger, :transport_tracer,
# Chewy search request DSL base class, used by every index.
Expand Down
31 changes: 31 additions & 0 deletions lib/chewy/elastic_client.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
module Chewy
# Replacement for Chewy.client
class ElasticClient
def self.build_es_client(configuration = Chewy.configuration)
client_configuration = configuration.deep_dup
client_configuration.delete(:prefix) # used by Chewy, not relevant to Elasticsearch::Client
block = client_configuration[:transport_options].try(:delete, :proc)
::Elasticsearch::Client.new(client_configuration, &block)
end

def initialize(elastic_client = self.class.build_es_client)
@elastic_client = elastic_client
end

private

def method_missing(name, *args, **kwargs, &block)
inspect_payload(name, args, kwargs)

@elastic_client.__send__(name, *args, **kwargs, &block)
end

def respond_to_missing?(name, _include_private = false)
@elastic_client.respond_to?(name) || super
end

def inspect_payload(name, args, kwargs)
Chewy.config.before_es_request_filter&.call(name, args, kwargs)
end
end
end
26 changes: 26 additions & 0 deletions spec/chewy/elastic_client_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
require 'spec_helper'

describe Chewy::ElasticClient do
describe 'payload inspection' do
let(:filter) { instance_double('Proc') }
let!(:filter_previous_value) { Chewy.before_es_request_filter }

before do
Chewy.massacre
stub_index(:products) do
field :id, type: :integer
end
ProductsIndex.create
Chewy.before_es_request_filter = filter
end

after do
Chewy.before_es_request_filter = filter_previous_value
end

it 'call filter with the request body' do
expect(filter).to receive(:call).with(:search, [{body: {size: 0}, index: ['products']}], {})
Chewy.client.search({index: ['products'], body: {size: 0}}).to_a
end
end
end
11 changes: 7 additions & 4 deletions spec/chewy_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -57,18 +57,21 @@

before do
Chewy.current[:chewy_client] = nil
allow(Chewy).to receive_messages(configuration: {transport_options: {proc: faraday_block}})
end

specify do
expect(Chewy).to receive_messages(configuration: {transport_options: {proc: faraday_block}})

allow(Elasticsearch::Client).to receive(:new).with(expected_client_config) do |*_args, &passed_block|
expect(Elasticsearch::Client).to receive(:new).with(expected_client_config) do |*_args, &passed_block|
# RSpec's `with(..., &block)` was used previously, but doesn't actually do
# any verification of the passed block (even of its presence).
expect(passed_block.source_location).to eq(faraday_block.source_location)

mock_client
end
end

its(:client) { is_expected.to eq(mock_client) }
expect(Chewy.client).to be_a(Chewy::ElasticClient)
end

after { Chewy.current[:chewy_client] = initial_client }
end
Expand Down

0 comments on commit cf07086

Please sign in to comment.