Skip to content

Commit

Permalink
Meilisearch extra (#316)
Browse files Browse the repository at this point in the history
  • Loading branch information
molfar committed Jun 19, 2021
1 parent fb65260 commit 744717f
Show file tree
Hide file tree
Showing 14 changed files with 375 additions and 9 deletions.
1 change: 1 addition & 0 deletions README.md
Expand Up @@ -109,6 +109,7 @@ Use the official extras, or write your own in just a few lines. Extras add speci
- [countless](http://ddnexus.github.io/pagy/extras/countless): Paginate without the need of any count, saving one query per rendering
- [elasticsearch_rails](http://ddnexus.github.io/pagy/extras/elasticsearch_rails): Paginate `ElasticsearchRails` response objects
- [headers](http://ddnexus.github.io/pagy/extras/headers): Add RFC-8288 compliant http response headers (and other helpers) useful for API pagination
- [meilisearch](http://ddnexus.github.io/pagy/extras/meilisearch): Paginate `Meilisearch` results
- [metadata](http://ddnexus.github.io/pagy/extras/metadata): Provides the pagination metadata to Javascript frameworks like Vue.js, react.js, etc.
- [searchkick](http://ddnexus.github.io/pagy/extras/searchkick): Paginate `Searchkick::Results` objects

Expand Down
1 change: 1 addition & 0 deletions docs/_layouts/default.html
Expand Up @@ -118,6 +118,7 @@ <h1 id="site-title">{{ site.title | default: site.github.repository_name }}
<a href="{{ site.baseurl }}/extras/items"><p class="indent1" {% if page.title == 'Items' %}id="active"{% endif %} >Items</p></a>
<a href="{{ site.baseurl }}/extras/overflow"><p class="indent1" {% if page.title == 'Overflow' %}id="active"{% endif %} >Overflow</p></a>
<a href="{{ site.baseurl }}/extras/materialize"><p class="indent1" {% if page.title == 'Materialize' %}id="active"{% endif %} >Materialize</p></a>
<a href="{{ site.baseurl }}/extras/meilisearch"><p class="indent1" {% if page.title == 'Meilisearch' %}id="active"{% endif %} >Meilisearch</p></a>
<a href="{{ site.baseurl }}/extras/metadata"><p class="indent1" {% if page.title == 'Metadata' %}id="active"{% endif %} >Metadata</p></a>
<a href="{{ site.baseurl }}/extras/navs"><p class="indent1" {% if page.title == 'Navs' %}id="active"{% endif %} >Navs</p></a>
<a href="{{ site.baseurl }}/extras/searchkick"><p class="indent1" {% if page.title == 'Searchkick' %}id="active"{% endif %} >Searchkick</p></a>
Expand Down
2 changes: 1 addition & 1 deletion docs/api/backend.md
Expand Up @@ -9,7 +9,7 @@ For overriding convenience, the `pagy` method calls two sub-methods that you may

**Notice**: Keep in mind that the whole module is basically providing a single functionality: getting a Pagy instance and the paginated items. You could re-write the whole module as one single and simpler method specific to your need, eventually gaining a few IPS in the process. If you seek a bit more performance you are encouraged to [write your own Pagy methods](#writing-your-own-pagy-methods).

Check also the [array](../extras/array.md), [searchkick](../extras/searchkick.md) and [elasticsearch_rails](../extras/elasticsearch_rails.md) extras for specific backend customizations.
Check also the [array](../extras/array.md), [searchkick](../extras/searchkick.md), [elasticsearch_rails](../extras/elasticsearch_rails.md) and [meilisearch](extras/meilisearch.md) extras for specific backend customizations.

## Synopsis

Expand Down
1 change: 1 addition & 0 deletions docs/extras.md
Expand Up @@ -18,6 +18,7 @@ Pagy comes with a few optional extensions/extras:
| `i18n` | Use the `I18n` gem instead of the pagy implementation | [i18n.rb](https://github.com/ddnexus/pagy/blob/master/lib/pagy/extras/i81n.rb), [documentation](extras/i18n.md) |
| `items` | Allow the client to request a custom number of items per page with a ready to use selector UI | [items.rb](https://github.com/ddnexus/pagy/blob/master/lib/pagy/extras/items.rb), [documentation](extras/items.md) |
| `materialize` | Add nav, nav_js and combo_nav_js helpers for the Materialize CSS [pagination component](https://materializecss.com/pagination.html) | [materialize.rb](https://github.com/ddnexus/pagy/blob/master/lib/pagy/extras/materialize.rb), [documentation](extras/materialize.md) |
| `meilisearch` | Paginate `Meilisearch` results efficiently | [meilisearch.rb](https://github.com/ddnexus/pagy/blob/master/lib/pagy/extras/meilisearch.rb), [documentation](extras/meilisearch.md) |
| `metadata` | Provides the pagination metadata to Javascript frameworks like Vue.js, react.js, etc. | [metadata.rb](https://github.com/ddnexus/pagy/blob/master/lib/pagy/extras/metadata.rb), [documentation](extras/metadata.md) |
| `navs` | Add nav_js and combo_nav_js javascript unstyled helpers | [navs.rb](https://github.com/ddnexus/pagy/blob/master/lib/pagy/extras/navs.rb), [documentation](extras/navs.md) |
| `overflow` | Allow for easy handling of overflowing pages | [overflow.rb](https://github.com/ddnexus/pagy/blob/master/lib/pagy/extras/overflow.rb), [documentation](extras/overflow.md) |
Expand Down
9 changes: 4 additions & 5 deletions docs/extras/items.md
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 user-customizable UIs.

It works also with the [countless](countless.md), [searchkick](searchkick.md) and [elasticsearch_rails](elasticsearch_rails.md) extras.
It works also with the [countless](countless.md), [searchkick](searchkick.md), [elasticsearch_rails](elasticsearch_rails.md) and [meilisearch](extras/meilisearch.md) extras.

## Synopsis

Expand All @@ -20,9 +20,9 @@ require 'pagy/extras/items'

# you can disable it explicitly for specific requests
@pagy, @records = pagy(Product.all, enable_items_extra: false)
# or...

# or...

# disable it by default (opt-in)
Pagy::VARS[:enable_items_extra] = false # default true
# in this case you have to enable it explicitly when you want it
Expand Down Expand Up @@ -111,4 +111,3 @@ When the items number is changed with the selector, pagy will reload the paginat
This method can take an extra `id` argument, which is used to build the `id` attribute of the `nav` tag. Since the internal automatic id generation is based on the code line where you use the helper, you _must_ pass an explicit id if you are going to use more than one `*_js` call in the same line for the same file.

**Notice**: passing an explicit id is also a bit faster than having pagy to generate one.

100 changes: 100 additions & 0 deletions docs/extras/meilisearch.md
@@ -0,0 +1,100 @@
---
title: Meilisearch
---
# Meilisearch Extra

This extra deals with the pagination of `Meilisearch` results either by creating a `Pagy` object out of an (already paginated) `Meilisearch` results or by creating the `Pagy` and `Meilisearch` results from the backend params.

## Synopsis

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

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

```ruby
require 'pagy/extras/meilisearch'
```

### Passive mode

If you have an already paginated `Meilisearch` results, you can get the `Pagy` object out of it:

```ruby
@results = Model.search(nil, offset: 10, limit: 10, ...)
@pagy = Pagy.new_from_meilisearch(@results, ...)
```

### Active Mode

If you want Pagy to control the pagination, getting the page from the params, and returning both the `Pagy` and the Meilisearch results automatically (from the backend params):

Extend your model:

```ruby
extend Pagy::Meilisearch
```

In a controller use `pagy_search` in place of `search`:

```ruby
results = Article.pagy_search(params[:q])
@pagy, @results = pagy_meilisearch(results, items: 10)
```

## Files

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

## Pagy.new_from_meilisearch

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

```ruby
@results = Model.search(nil, offset: 10, limit: 10, ...)
@pagy = Pagy.new_from_meilisearch(@results, ...)
```

**Notice**: you have to take care of manually manage all the params for your search, however the method extracts the `:items`, `:page` and `:count` from the results object, so you don't need to pass that again. If you prefer to manage the pagination automatically, see below.

## Pagy::Meilisearch

Extend your model with the Pagy::Meilisearch` micro-moudule:

```ruby
extend Pagy::Meilisearch
```

The `Pagy::ElasticsearchRails::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(...)

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 `:offset` and `:limit` options before passing them to the standard `search` method internally.

## Variables

| Variable | | Description | Default |
|:----------------------------|:-----------------------------------------------|:---------------|:--------|
| `:meilisearch_search_method` | customizable name of the `:pagy_search` method | `:pagy_search` | |

## Methods

This extra adds the `pagy_meilisearch` method to the `Pagy::Backend` to be used when you have to paginate a Meilisearch object. It also adds a `pagy_meilisearch_get_vars` sub-method, used for easy customization of variables by overriding.

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

This method is similar to the generic `pagy` method, but specialized for Meilisearch. (see the [pagy doc](../api/backend.md#pagycollection-varsnil))

It expects to receive a `Model.pagy_search(...)` result and returns a paginated response. You can use it in a couple of ways:

```ruby
@pagy, @results = pagy_meilisearch(Model.pagy_search(params[:q]), ...)
...
@records = @results.results

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

### pagy_meilisearch_get_vars(array)

This sub-method is similar to the `pagy_get_vars` sub-method, but it is called only by the `pagy_meilisearch` method. (see the [pagy_get_vars doc](../api/backend.md#pagy_get_varscollection-vars)).
4 changes: 2 additions & 2 deletions docs/how-to.md
Expand Up @@ -133,7 +133,7 @@ Notice: Older versions run on ruby 1.9+ or jruby 1.7+ till ruby <3.0
Pagy works out of the box in a web app assuming that:

- You are using a `Rack` based framework (Rails, Sinatra, Padrino, etc.)
- The collection to paginate is an ORM collection (e.g. ActiveRecord scope) or other collections supported by some backend extra ([array](extras/array.md), [elasticsearch_rails](extras/elasticsearch_rails.md), [searchkick](extras/searchkick.md), ...)
- The collection to paginate is an ORM collection (e.g. ActiveRecord scope) or other collections supported by some backend extra ([array](extras/array.md), [elasticsearch_rails](extras/elasticsearch_rails.md), [searchkick](extras/searchkick.md), [meilisearch](extras/meilisearch.md), ...)
- The controller where you include `Pagy::Backend` responds to a `params` method
- The view where you include `Pagy::Frontend` responds to a `request` method returning a `Rack::Request` instance.

Expand Down Expand Up @@ -402,7 +402,7 @@ Ransack `result` returns an `ActiveRecord` collection, which can be paginated ou

## Paginate Elasticsearch results

Pagy has a couple of extras for gems returning elasticsearch results: [elasticsearch_rails](extras/elasticsearch_rails.md) and [searchkick](extras/searchkick.md)
Pagy has a couple of extras for gems returning elasticsearch results: [elasticsearch_rails](extras/elasticsearch_rails.md), [searchkick](extras/searchkick.md) and [meilisearch](extras/meilisearch.md)

## Paginate pre-offsetted and pre-limited collections

Expand Down
55 changes: 55 additions & 0 deletions lib/pagy/extras/meilisearch.rb
@@ -0,0 +1,55 @@
# frozen_string_literal: true

class Pagy

VARS[:meilisearch_search_method] ||= :pagy_search

module Meilisearch
# returns an array used to delay the call of #search
# after the pagination variables are merged to the options
def pagy_meilisearch(term = nil, **vars)
[self, term, vars]
end
alias_method VARS[:meilisearch_search_method], :pagy_meilisearch
end

# create a Pagy object from a Meilisearch results
def self.new_from_meilisearch(results, vars={})
vars[:items] = results.raw_answer[:limit]
vars[:page] = [results.raw_answer[:offset] / vars[:items], 1].max
vars[:count] = results.raw_answer[:nbHits]
new(vars)
end

# Add specialized backend methods to paginate Meilisearch results
module Backend
private

# Return Pagy object and results
def pagy_meilisearch(pagy_search_args, vars = {})
model, term, options = pagy_search_args
vars = pagy_meilisearch_get_vars(nil, vars)
options[:limit] = vars[:items]
options[:offset] = (vars[:page] - 1) * vars[:items]
results = model.search(term, **options)
vars[:count] = results.raw_answer[:nbHits]

pagy = Pagy.new(vars)
# with :last_page overflow we need to re-run the method in order to get the hits
return pagy_meilisearch(pagy_search_args, vars.merge(page: pagy.page)) \
if defined?(Pagy::UseOverflowExtra) && pagy.overflow? && pagy.vars[:overflow] == :last_page

[ pagy, results ]
end

# Sub-method called only by #pagy_meilisearch: here for easy customization of variables by overriding
# the _collection argument is not available when the method is called
def pagy_meilisearch_get_vars(_collection, vars)
pagy_set_items_from_params(vars) if defined?(UseItemsExtra)
vars[:items] ||= VARS[:items]
vars[:page] ||= (params[ vars[:page_param] || VARS[:page_param] ] || 1).to_i
vars
end

end
end
1 change: 1 addition & 0 deletions pagy.manifest
Expand Up @@ -50,6 +50,7 @@ lib/pagy/extras/headers.rb
lib/pagy/extras/i18n.rb
lib/pagy/extras/items.rb
lib/pagy/extras/materialize.rb
lib/pagy/extras/meilisearch.rb
lib/pagy/extras/metadata.rb
lib/pagy/extras/navs.rb
lib/pagy/extras/overflow.rb
Expand Down
1 change: 1 addition & 0 deletions tasks/test.rake
Expand Up @@ -11,6 +11,7 @@ test_tasks = {}
i18n
items
items_trim
meilisearch
overflow
searchkick
shared_json
Expand Down
36 changes: 36 additions & 0 deletions test/mock_helpers/meilisearch.rb
@@ -0,0 +1,36 @@
# frozen_string_literal: true

require 'pagy/extras/meilisearch'

module MockMeilisearch

RESULTS = { 'a' => ('a-1'..'a-1000').to_a,
'b' => ('b-1'..'b-1000').to_a }.freeze

class Results < Array

def initialize(query, params = {})
@query = query
@params = { offset: 0, limit: 10_000 }.merge(params)
super RESULTS[@query].slice(@params[:offset], @params[:limit]) || []
end

def raw_answer
{
hits: self,
offset: @params[:offset],
limit: @params[:limit],
nbHits: RESULTS[@query].length
}
end
end

class Model

def self.search(*args)
Results.new(*args)
end

extend Pagy::Meilisearch
end
end
8 changes: 7 additions & 1 deletion test/pagy/extras/items_test.rb
Expand Up @@ -3,6 +3,7 @@
require_relative '../../test_helper'
require_relative '../../mock_helpers/elasticsearch_rails'
require_relative '../../mock_helpers/searchkick'
require_relative '../../mock_helpers/meilisearch'
require_relative '../../mock_helpers/arel'
require 'pagy/extras/countless'
require 'pagy/extras/arel'
Expand All @@ -18,6 +19,11 @@ def test_items_vars_params(items, vars, params)
_(pagy.items).must_equal items
_(records.size).must_equal items
end
[[:pagy_meilisearch, MockMeilisearch::Model]].each do |meth, mod|
pagy, records = controller.send meth, mod.pagy_search('a'), vars
_(pagy.items).must_equal items
_(records.size).must_equal items
end
%i[pagy pagy_countless pagy_array pagy_arel].each do |meth|
pagy, records = controller.send meth, @collection, vars
_(pagy.items).must_equal items
Expand All @@ -34,7 +40,7 @@ def test_items_vars_params(items, vars, params)
it 'uses the defaults' do
vars = {}
controller = MockController.new
%i[pagy_elasticsearch_rails_get_vars pagy_searchkick_get_vars].each do |method|
%i[pagy_elasticsearch_rails_get_vars pagy_searchkick_get_vars pagy_meilisearch_get_vars].each do |method|
merged = controller.send method, nil, vars
_(merged[:items]).must_equal 20
end
Expand Down

0 comments on commit 744717f

Please sign in to comment.