Skip to content

Commit

Permalink
refactoring of elasticsearch_rails extra
Browse files Browse the repository at this point in the history
  • Loading branch information
ddnexus committed Feb 22, 2019
1 parent fc608e9 commit 22afe14
Show file tree
Hide file tree
Showing 8 changed files with 316 additions and 102 deletions.
78 changes: 63 additions & 15 deletions docs/extras/elasticsearch_rails.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,29 +3,42 @@ title: Elasticsearch Rails
---
# Elasticsearch Rails Extra

Paginate `ElasticsearchRails::Results` objects efficiently avoiding expensive object-wrapping and without overriding.
This extra deals with the pagination of `ElasticsearchRails` response objects either by creating a `Pagy` object out of an (already paginated) `ElasticsearchRails` object or by creating the `Pagy` and `ElasticsearchRails` objects from the backend params.

## Synopsis

See [extras](../extras.md) for general usage info.

In the `pagy.rb` initializer:
Require the extra in the `pagy.rb` initializer:

```ruby
require 'pagy/extras/elasticsearch_rails'
```

In a controller:
### Passive mode

If you have an already paginated `Elasticsearch::Model::Response::Response` object, you can get the `Pagy` object out of it:

```ruby
@response = Model.search('*', from: 0; size: 10, ...)
@pagy = Pagy.new_from_elasticsearch_rails(@response, ...)
```

### Active Mode

If you want Pagy to control the pagination, getting the page from the params, and returning both the `Pagy` and the `Elasticsearch::Model::Response::Response` objects automatically (from the backend params):

Extend your model:

```ruby
def search
@pagy, @articles = pagy_elasticsearch_rails(Article.search(params[:q]).records, items: 10)
extend Pagy::Search
```

render action: "index"
end
In a controller use `pagy_search` in place of `search`:

# independently paginate some other collections as usual
@pagy_b, @records = pagy(some_scope, ...)
```ruby
records = Article.pagy_search(params[:q]).records
@pagy, @articles = pagy_elasticsearch_rails(records, items: 10)
```

## Files
Expand All @@ -34,16 +47,51 @@ This extra is composed of 1 file:

- [elasticsearch_rails.rb](https://github.com/ddnexus/pagy/blob/master/lib/pagy/extras/elasticsearch_rails.rb)

## Pagy.new_from_elasticsearch_rails

This constructor accepts an `Elasticsearch::Model::Response::Response` as the first argument, plus the usual optional variable hash. It sets the `:items`, `:page` and `:count` pagy variables extracted/calculated out of the `Elasticsearch::Model::Response::Response` object.

```ruby
@response = Model.search('*', from: 0; size: 10, ...)
@pagy = Pagy.new_from_elasticsearch_rails(@response, ...)
```

**Notice**: you have to take care of managing all the params manually. If you prefer to manage the pagination automatically, see below.

## Pagy::Search

Extend your model with the `Pagy::Search` micro-moudule (see [pagy_search.rb](https://github.com/ddnexus/pagy/blob/master/lib/pagy/extras/pagy_search.rb))

```ruby
extend Pagy::Search
```

The `Pagy::Search` adds the `pagy_search` class method that you must use in place of the standard `search` method when you want to paginate the search response.

### pagy_search(term, options={})

This method accepts the same arguments of the `search` method and you must use it in its place. This extra uses it in order to capture the arguments, automatically merging the calculated `:from` and `:size` options before passing them to the standard `search` method internally.

## Methods

This extra adds the `pagy_elasticsearch_rails` method to the `Pagy::Backend` to be used in place (or in parallel) of the `pagy` method when you have to paginate a `ElasticsearchRails::Results` object. It also adds a `pagy_elasticsearch_rails_get_variables` sub-method, used for easy customization of variables by overriding.
This extra adds the `pagy_elasticsearch_rails` method to the `Pagy::Backend` to be used when you have to paginate a `ElasticsearchRails` object. It also adds a `pagy_elasticsearch_rails_get_variables` sub-method, used for easy customization of variables by overriding.

### pagy_elasticsearch_rails(Model.pagy_search(...), vars={}})

**Notice**: there is no `pagy_elasticsearch_rails_get_items` method to override, since the items are fetched directly by Elasticsearch Rails.
This method is similar to the generic `pagy` method, but specialized for Elasticsearch Rails. (see the [pagy doc](../api/backend.md#pagycollection-varsnil))

### pagy_elasticsearch_rails(Model.search(...), vars=nil)
It expects to receive a `Model.pagy_search(...)` result and returns a paginated response. You can use it in a couple of ways:

This method is the same as the generic `pagy` method, but specialized for Elasticsearch Rails. (see the [pagy doc](../api/backend.md#pagycollection-varsnil))
```ruby
@pagy, @response = pagy_elasticsearch_rails(Model.pagy_search(params[:q]), ...)
...
records = @response.records
results = @response.results

# or directly with the collection you need (e.g. records)
@pagy, @records = pagy_elasticsearch_rails(Model.pagy_search(params[:q]).records, ...)
```

### pagy_elastic_search_rails_get_vars(array)
### pagy_elasticsearch_rails_get_vars(array)

This sub-method is the same as the `pagy_get_vars` sub-method, but it is called only by the `pagy_elasticsearch_rails` method. (see the [pagy_get_vars doc](../api/backend.md#pagy_get_varscollection-vars)).
This sub-method is similar to the `pagy_get_vars` sub-method, but it is called only by the `pagy_elasticsearch_rails` method. (see the [pagy_get_vars doc](../api/backend.md#pagy_get_varscollection-vars)).
2 changes: 1 addition & 1 deletion docs/extras/items.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ title: Items

Allow the client to request a custom number of items per page with an optional selector UI. It is useful with APIs or higly user-customizable apps.

It works also with the [countless extra](countless.md).
It works also with the [countless](countless.md) and with the [elasticsearch_rails](elasticsearch_rails.md) extras.

## Synopsis

Expand Down
36 changes: 29 additions & 7 deletions lib/pagy/extras/elasticsearch_rails.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,42 @@
# encoding: utf-8
# frozen_string_literal: true

require 'pagy/extras/pagy_search'

class Pagy
# Add specialized backend methods to paginate ElasticsearchRails::Results

# used by the items extra
ELASTICSEARCH_RAILS = true

# create a Pagy object from an Elasticsearch::Model::Response::Response object
def self.new_from_elasticsearch_rails(response, vars={})
vars[:items] = response.search.options[:size] || 10
vars[:page] = (response.search.options[:from] || 0) / vars[:items] + 1
vars[:count] = response.raw_response['hits']['total']
new(vars)
end

# Add specialized backend methods to paginate ElasticsearchRails searches
module Backend ; private

# Return Pagy object and items
def pagy_elasticsearch_rails(results, vars={})
pagy = Pagy.new(pagy_elasticsearch_rails_get_vars(results, vars))
return pagy, results.offset(pagy.offset).limit(pagy.items)
def pagy_elasticsearch_rails(search_args, vars={})
model, query_or_payload, options, _block, *called = search_args
vars = pagy_elasticsearch_rails_get_vars(nil, vars)
options[:size] = vars[:items]
options[:from] = vars[:items] * (vars[:page] - 1)
response = model.search(query_or_payload, options)
vars[:count] = response.raw_response['hits']['total']
return Pagy.new(vars), called.empty? ? response : response.send(*called)
end

# Sub-method called only by #pagy_elasticsearch_rails: here for easy customization of variables by overriding
def pagy_elasticsearch_rails_get_vars(results, vars)
vars[:count] ||= results.total
vars[:page] ||= params[:page] || 1
# the _collection argument is not available when the method is called
def pagy_elasticsearch_rails_get_vars(_collection, vars)
vars[:items] ||= VARS[:items]
vars[:page] ||= (params[ vars[:page_param] || VARS[:page_param] ] || 1).to_i
vars
end

end
end
28 changes: 13 additions & 15 deletions lib/pagy/extras/items.rb
Original file line number Diff line number Diff line change
Expand Up @@ -18,21 +18,19 @@ def pagy_with_items(vars)
[items.to_i, vars.key?(:max_items) ? vars[:max_items] : VARS[:max_items]].compact.min # :items capped to :max_items
end

alias_method :pagy_get_vars_without_items, :pagy_get_vars
def pagy_get_vars_with_items(collection, vars)
pagy_with_items(vars)
pagy_get_vars_without_items(collection, vars)
end
alias_method :pagy_get_vars, :pagy_get_vars_with_items

# support for countless extra
if defined?(Pagy::COUNTLESS) # defined in the countless extra
alias_method :pagy_countless_get_vars_without_items, :pagy_countless_get_vars
def pagy_countless_get_vars_with_items(collection, vars)
pagy_with_items(vars)
pagy_countless_get_vars_without_items(collection, vars)
end
alias_method :pagy_countless_get_vars, :pagy_countless_get_vars_with_items
# add the pagy*_get_vars alias-chained methods for frontend, and defined/required extras
[nil, 'countless', 'elasticsearch_rails'].each do |name|
prefix, if_start, if_end = "_#{name}", "if defined?(Pagy::#{name.upcase})", "end" if name
module_eval <<-RUBY
#{if_start}
alias_method :pagy#{prefix}_get_vars_without_items, :pagy#{prefix}_get_vars
def pagy#{prefix}_get_vars_with_items(collection, vars)
pagy_with_items(vars)
pagy#{prefix}_get_vars_without_items(collection, vars)
end
alias_method :pagy#{prefix}_get_vars, :pagy#{prefix}_get_vars_with_items
#{if_end}
RUBY
end

end
Expand Down
16 changes: 16 additions & 0 deletions lib/pagy/extras/pagy_search.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# Support module to capture search calls
# encoding: utf-8
# frozen_string_literal: true

class Pagy
module Search
# returns an array used to delay the call of #search
# after the pagination variables are merged to the options
# it also pushes to the same array an eventually called method and arguments
def pagy_search(arg, options={}, &block)
[self, arg, options, block].tap do |args|
args.define_singleton_method(:method_missing){|*a| args += a}
end
end
end
end
112 changes: 79 additions & 33 deletions test/pagy/extras/elasticsearch_rails_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,77 +2,123 @@
# frozen_string_literal: true

require_relative '../../test_helper'
require 'pagy/extras/elasticsearch_rails'
require_relative '../../test_helper/elasticsearch_rails'

SingleCov.covered! unless ENV['SKIP_SINGLECOV']

describe Pagy::Backend do
describe Pagy::Search do

let(:backend) { TestController.new }
describe '#pagy_search' do

class ElasticsearchRails
class Results
def initialize(params); @params = params; end
def total; 1000; end
def offset(*); self; end
def limit(*); 25; end
def page
@params[:page] || 1
end
it 'extends the class with #pagy_search' do
ElasticsearchRailsModel.must_respond_to :pagy_search
end

it 'returns class and arguments' do
ElasticsearchRailsModel.pagy_search('a', b:2).must_equal [ElasticsearchRailsModel, 'a', {b: 2}, nil]
args = ElasticsearchRailsModel.pagy_search('a', b:2){|a| a*2}
block = args[-1]
args.must_equal [ElasticsearchRailsModel, 'a', {b: 2}, block]
end

it 'adds the caller and arguments' do
ElasticsearchRailsModel.pagy_search('a', b:2).records.must_equal [ElasticsearchRailsModel, 'a', {b: 2}, nil, :records]
ElasticsearchRailsModel.pagy_search('a', b:2).a('b', 2).must_equal [ElasticsearchRailsModel, 'a', {b: 2}, nil, :a, 'b', 2]
end

end

end

describe Pagy::Backend do

let(:backend) { TestController.new }

describe "#pagy_elasticsearch_rails" do

before do
@collection = ElasticsearchRails::Results.new(backend.params)
@collection = TestCollection.new((1..1000).to_a)
end

it 'paginates with defaults' do
pagy, _items = backend.send(:pagy_elasticsearch_rails, @collection)
it 'paginates response with defaults' do
pagy, response = backend.send(:pagy_elasticsearch_rails, ElasticsearchRailsModel.pagy_search('a'))
records = response.records
pagy.must_be_instance_of Pagy
pagy.count.must_equal 1000
pagy.items.must_equal 20 # Pagy default
pagy.items.must_equal Pagy::VARS[:items]
pagy.page.must_equal backend.params[:page]
records.count.must_equal Pagy::VARS[:items]
records.must_equal ["R-a-41", "R-a-42", "R-a-43", "R-a-44", "R-a-45", "R-a-46", "R-a-47", "R-a-48", "R-a-49", "R-a-50", "R-a-51", "R-a-52", "R-a-53", "R-a-54", "R-a-55", "R-a-56", "R-a-57", "R-a-58", "R-a-59", "R-a-60"]
end

it 'paginates records with defaults' do
pagy, records = backend.send(:pagy_elasticsearch_rails, ElasticsearchRailsModel.pagy_search('a').records)
pagy.must_be_instance_of Pagy
pagy.count.must_equal 1000
pagy.items.must_equal Pagy::VARS[:items]
pagy.page.must_equal backend.params[:page]
records.count.must_equal Pagy::VARS[:items]
records.must_equal ["R-a-41", "R-a-42", "R-a-43", "R-a-44", "R-a-45", "R-a-46", "R-a-47", "R-a-48", "R-a-49", "R-a-50", "R-a-51", "R-a-52", "R-a-53", "R-a-54", "R-a-55", "R-a-56", "R-a-57", "R-a-58", "R-a-59", "R-a-60"]
end

it 'paginates with vars' do
pagy, _items = backend.send(:pagy_elasticsearch_rails, @collection, link_extra: 'X')
pagy, records = backend.send(:pagy_elasticsearch_rails, ElasticsearchRailsModel.pagy_search('b').records, page: 2, items: 10, link_extra: 'X')
pagy.must_be_instance_of Pagy
pagy.count.must_equal 1000
pagy.items.must_equal 20 # Pagy default
pagy.items.must_equal 10
pagy.page.must_equal 2
pagy.vars[:link_extra].must_equal 'X'
records.count.must_equal 10
records.must_equal ["R-b-11", "R-b-12", "R-b-13", "R-b-14", "R-b-15", "R-b-16", "R-b-17", "R-b-18", "R-b-19", "R-b-20"]
end

end

describe "#pagy_elasticsearch_rails_get_vars" do

before do
@collection = ElasticsearchRails::Results.new(backend.params)
end
describe '#pagy_elasticsearch_rails_get_vars' do

it 'gets defaults' do
vars = {}
merged = backend.send :pagy_elasticsearch_rails_get_vars, @collection, vars
merged.keys.must_include :count
vars = {}
merged = backend.send :pagy_elasticsearch_rails_get_vars, nil, vars
merged.keys.must_include :page
merged[:count].must_equal 1000
merged[:items].must_be_nil
merged.keys.must_include :items
merged[:page].must_equal 3
merged[:items].must_equal 20
end

it 'gets vars' do
vars = { items: 25, link_extra: 'X' }
merged = backend.send :pagy_elasticsearch_rails_get_vars, @collection, vars
merged.keys.must_include :count
vars = {page: 2, items: 10, link_extra: 'X'}
merged = backend.send :pagy_elasticsearch_rails_get_vars, nil, vars
merged.keys.must_include :page
merged.keys.must_include :items
merged.keys.must_include :link_extra
merged[:count].must_equal 1000
merged[:items].must_equal 25
merged[:page].must_equal 2
merged[:items].must_equal 10
merged[:link_extra].must_equal 'X'
end

end

describe 'Pagy.new_from_elasticsearch_rails' do

it 'paginates response with defaults' do
response = ElasticsearchRailsModel.search('a')
pagy = Pagy.new_from_elasticsearch_rails(response)
pagy.must_be_instance_of Pagy
pagy.count.must_equal 1000
pagy.items.must_equal 10
pagy.page.must_equal 1
end

it 'paginates response with vars' do
response = ElasticsearchRailsModel.search('b', from: 15, size: 15)
pagy = Pagy.new_from_elasticsearch_rails(response, link_extra: 'X')
pagy.must_be_instance_of Pagy
pagy.count.must_equal 1000
pagy.items.must_equal 15
pagy.page.must_equal 2
pagy.vars[:link_extra].must_equal 'X'
end

end

end
Loading

0 comments on commit 22afe14

Please sign in to comment.