Search code examples
ruby-on-railsjsonrubyelasticsearchactive-model-serializers

Rails: can I use elasticsearch-model and active_model_serializers together?


I'm building a JSON API in Rails and I'd like to use Elasticsearch in order to speed up responses and allow for search.

I'm just done implementing the elasticsearch-rails Gem for my first model and I can query ES from the console successfully.

Now I'd like to make results available to API consumers, so for example a GET request to /articles/index.json?q="blah" would retrieve the matching articles from ES and render them according to JSON:API standards.

Is it possible to use the rails active_model_serializers gem to achieve this? I'm asking because there (in contrast with jbuilder) the JSON:API format is already taken care of.

EDIT: Here's where I stand at the moment:

In my model I have the following:

require 'elasticsearch/rails'
require 'elasticsearch/model'
class Thing < ApplicationRecord
    validates :user_id, :active, :status, presence: true
    include Elasticsearch::Model
    include Elasticsearch::Model::Callbacks

    index_name Rails.application.class.parent_name.underscore
    document_type self.name.downcase

    settings index: { number_of_shards: 1, number_of_replicas: 1 } do 
        mapping dynamic: 'strict' do 
            indexes :id, type: :string
            indexes :user_id, type: :string
            indexes :active, type: :boolean
            indexes :status, type: :string
        end 
    end

    def as_indexed_json(options = nil)
      self.as_json({
        only: [:id, :user_id, :active, :status],
      })
    end

    def self.search(query) 
        __elasticsearch__.search( { 
            query: { 
                multi_match: { 
                    query: query, 
                    fields: ['id^5', 'user_id'] 
                } 
            }
        } )
    end
end

This correctly indexes the model in ES and makes it possible to search the ES index. In my controller I have:

class ThingsController < ApplicationController
    def index
        things = Thing.search(params[:query]).results.map{|m| m._source}
        render json: things, each_serializer: ThingSerializer
    end
end

In the serializer, at the moment, is the following:

class ThingSerializer < ActiveModel::Serializer
    attributes :id, :user_id, :active, :status
end

This unfortunately brings up the following JSON in the view:

{"data":[{"id":"","type":"hashie-mashes","attributes":{"user-id":null,"active":null,"status":null}}]}

So the serializer is not correctly parsing the result, which is wrapped from the ES gem into this Hashie::Mash object.


Solution

  • I finally managed to make it work nicely and without the need to fetch records from the DB. Here's the complete solution for future googlers:

    Serializer (probably better to create a dedicated one for search results):

    class SearchableThingSerializer < ActiveModel::Serializer
      type 'things' # This needs to be overridden, otherwise it will print "hashie-mashes"
      attributes :id # For some reason the mapping below doesn't work with :id
    
      [:user_id, :active, :status].map{|a| attribute(a) {object[:_source][a]}}
    
      def id
        object[:_source].id
      end
    end
    

    Controller:

    def index
      things = Thing.search(params[:query])
      render json: things, each_serializer: SearchableThingSerializer
    end
    

    With this you can build a JSON API as described in this guide, with the additional benefit of serving data straight from Elasticsearch:

    https://www.simplify.ba/articles/2016/06/18/creating-rails5-api-only-application-following-jsonapi-specification/