Search code examples
ruby-on-railsrubychewy-gem

How to access Chewy results with the dot notation?


I'm using Toptal's Chewy gem to connect and query my Elasticsearch, just like an ODM.

I'm using Chewy along with Elasticsearch 6, Ruby on Rails 5.2 and Active Record.

I've defined my index just like this:

class OrdersIndex < Chewy::Index
  define_type Order.includes(:customer) do

    field :id, type: "keyword"

    field :customer do
      field :id, type: "keyword"
      field :name, type: "text"
      field :email, type: "keyword"
    end
  end
end

And my model:

class Order < ApplicationRecord
  belongs_to :customer
end

The problem here is that when I perform any query using Chewy, the customer data gets deserialized as a hash instead of an Object, and I can't use the dot notation to access the nested data.

results = OrdersIndex.query(query_string: { query: "test" })
results.first.id
# => "594d8e8b2cc640bb78bd115ae644637a1cc84dd460be6f69"

results.first.customer.name
# => NoMethodError: undefined method `name' for #<Hash:0x000000000931d928>

results.first.customer["name"]
# => "Frederique Schaefer"

How can I access the nested association using the dot notation (result.customer.name)? Or to deserialize the nested data inside an Object such as a Struct, that allows me to use the dot notation?


Solution

  • Converting the just-deserialized results to JSON string and deserializing it again with OpenStruct as an object_class can be a bad idea and has a great CPU cost.

    I've solved it differently, using recursion and the Ruby's native Struct, preserving the laziness of the Chewy gem.

    def convert_to_object(keys, values)
      schema = Struct.new(*keys.map(&:to_sym))
      object = schema.new(*values)
    
      object.each_pair do |key, value|
        if value.is_a?(Hash)
          object.send("#{key}=", convert_to_object(value.keys, value.values))
        end
      end
    
      object
    end
    
    OrdersIndex.query(query_string: { query: "test" }).lazy.map do |item|
      convert_to_object(item.attributes.keys, item.attributes.values)
    end
    

    convert_to_object takes an array of keys and another one of values and creates a struct from it. Whenever the class of one of the array of values items is a Hash, then it converts to a struct, recursively, passing the hash keys and values.

    To presence the laziness, that is the coolest part of Chewy, I've used Enumerator::Lazy and Enumerator#map. Mapping every value returned by the ES query into the convert_to_object function, makes every entry a complete struct.

    The code is very generic and works to every index I've got.