Skip to content

Instantly share code, notes, and snippets.

@halilim
Last active November 29, 2015 23:44
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save halilim/42769dfe03b361560078 to your computer and use it in GitHub Desktop.
Save halilim/42769dfe03b361560078 to your computer and use it in GitHub Desktop.
A stab at Elastic script filters for a multi-currency price filter

Here we are trying to implement price filtering for a currency-per-instance model (i.e. an item is priced at ₺100, another at $100 etc).

Option 1 - Use a base currency

  1. Convert all prices to a base price (price_cents_try here)
  2. Periodically update that field as the exchange rates are updated

Pros

  • Simpler to implement
  • Better query performance (no script filter)

Cons

  • The cron job can become unwieldy if there are lots of records and/or the update frequency is high

Option 2 - Use scripting

  1. Use script fields and script filter (Option 2 part in the code).

Pros

  • No need for a cron job

Cons

  • Needs scripting enabled
  • Possible performance degradation
# Assumes gems: Chewy, Money and eu_central_bank
class SearchForm
include ActiveModel::Model
DEFAULTS = { 'currency' => 'TRY' }
attr_accessor :price_min, :price_max, :currency
# ...
def price_filter
currency = self.currency || DEFAULTS[:currency]
# Option 1
body = {}.tap do |h|
h.merge!(gte: Money.new(price_min, currency).exchange_to('TRY').cents) if price_min
h.merge!(lte: Money.new(price_max, currency).exchange_to('TRY').cents) if price_max
end
index.filter(range: { price_cents_try: body }) if body.present?
# Option 2
# TODO (Halil Özgür): Note: this method is probably faulty for currencies with subunit_to_unit != 100
script_rules = []
params = {}
if price_min
script_rules << 'price_cents_try >= price_min'
params[:price_min] = Money.new(price_min, currency).exchange_to('TRY').cents
end
if price_max
script_rules << 'price_cents_try <= price_max'
params[:price_max] = Money.new(price_max, currency).exchange_to('TRY').cents
end
if script_rules.present?
eur_to_try = Money.default_bank.rates['EUR_TO_TRY'].to_f
rates = {}
Product::CURRENCIES.each do |currency|
rates[currency] = eur_to_try / Money.default_bank.rates["EUR_TO_#{currency}"].to_f
end
index
.filter
.script_fields(
price_cents_try: {
params: { rates: rates },
script: 'doc["price_cents"].value * rates[doc["price_currency"].value]'
}
)
.filter { s(script_rules.join(' && '), params) }
end
end
end
class Product < ActiveRecord::Base
CURRENCIES = %w(TRY USD EUR)
monetize :price_cents, with_model_currency: :currency, numericality: { greater_than: 0 }
end
class ProductIndex < Chewy::Index
define_type Product do
field :price_cents, type: 'integer'
field :price_currency, index: 'not_analyzed'
# Only needed for the Option 1
field :price_cents_try, type: 'integer', value: -> { price.exchange_to('TRY').cents }
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment