Elasticsearch provides a powerful, RESTful HTTP interface for indexing and querying data, built on top of the Apache Lucene library. Right out of the box, it provides scalable, efficient, and robust search, with UTF-8 support. It’s a powerful tool for indexing and querying massive amounts of structured data and, here at Toptal, it powers our platform search and will soon be used for autocompletion as well. We’re huge fans.

Chewy extends the Elasticsearch-Ruby client, making it more powerful and providing tighter integration with Rails.

Since our platform is built using Ruby on Rails, our integration of Elasticsearch takes advantage of the elasticsearch-ruby project (a Ruby integration framework for Elasticsearch that provides a client for connecting to an Elasticsearch cluster, a Ruby API for the Elasticsearch’s REST API, and various extensions and utilities). Building on this foundation, we’ve developed and released our own improvement (and simplification) of the Elasticsearch application search architecture, packaged as a Ruby gem that we’ve named Chewy (with an example app available here).

Chewy extends the Elasticsearch-Ruby client, making it more powerful and providing tighter integration with Rails. In this post, I discuss (through usage examples) how we accomplished this, including the technical obstacles that emerged during implementation.

Just a couple of quick notes before proceeding:

  • Both Chewy and a Chewy demo application are available on GitHub.
  • For those interested in more “under the hood” info about Elasticsearch, I’ve included a brief write-up as an Appendix to this post.

Why Chewy?

Despite Elasticsearch’s scalability and efficiency, integrating it with Rails didn’t turn out to be quite as simple as anticipated. At Toptal, we found ourselves needing to significantly augment the basic Elasticsearch-Ruby client to make it more performant and to support additional operations.

Despite Elasticsearch's scalability and efficiency, integrating it with Rails didn't turn out to be quite as simple as anticipated.

And thus, Chewy was born.

A few particularly noteworthy features of Chewy include:

  1. Every index is observable by all the related models.

    Most indexed models are related to each other. And sometimes, it’s necessary to denormalize this related data and bind it to the same object (e.g., if you want to index an array of tags together with their associated article). Chewy allows you to specify an updatable index for every model, so corresponding articles will be reindexed whenever a relevant tag is updated.

  2. Index classes are independent from ORM/ODM models.

    With this enhancement, implementing cross-model autocompletion, for example, is much easier. You can just define an index and work with it in object-oriented fashion. Unlike other clients, Chewy removes the need to manually implement index classes, data import callbacks, and other components.

  3. Bulk import is everywhere.

    Chewy utilizes the bulk Elasticsearch API for full reindexing and index updates. It also utilizes the concept of atomic updates, collecting changed objects within an atomic block and updating them all at once.

  4. Chewy provides an AR-style query DSL.

    By being chainable, mergable, and lazy, this enhancement allows queries to be produced in a more efficient manner.

OK, so let’s see how this all plays out in the gem…

The basics

Elasticsearch has several document-related concepts. The first is that of an index (the analogue of a database in RDBMS), which consists of a set of documents, which can be of several types (where a type is a kind of RDBMS table).

Every document has a set of fields. Each field is analyzed independently and its analysis options are stored in the mapping for its type. Chewy utilizes this structure “as is” in its object model:

class EntertainmentIndex < Chewy::Index
  settings analysis: {
    analyzer: {
      title: {
        tokenizer: 'standard',
        filter: ['lowercase', 'asciifolding']
      }
    }
  }

  define_type Book.includes(:author, :tags) do
    field :title, analyzer: 'title'
    field :year, type: 'integer'
    field :author, value: ->{ author.name }
    field :author_id, type: 'integer'
    field :description
    field :tags, index: 'not_analyzed', value: ->{ tags.map(&:name) }
  end

  {movie: Video.movies, cartoon: Video.cartoons}.each do |type_name, scope|
    define_type scope.includes(:director, :tags), name: type_name do
      field :title, analyzer: 'title'
      field :year, type: 'integer'
      field :author, value: ->{ director.name }
      field :author_id, type: 'integer', value: ->{ director_id }
      field :description
      field :tags, index: 'not_analyzed', value: ->{ tags.map(&:name) }
    end
  end
end

Above, we defined an Elasticsearch index called entertainment with three types: book, movie, and cartoon. For each type, we defined some field mappings and a hash of settings for the whole index.

So, we’ve defined the EntertainmentIndex and we want to execute some queries. As a first step, we need to create the index and import our data:

EntertainmentIndex.create!
EntertainmentIndex.import
# EntertainmentIndex.reset! (which includes deletion,
# creation, and import) could be used instead

The .import method is aware of imported data because we passed in scopes when we defined our types; thus, it will import all the books, movies, and cartoons stored in the persistent storage.

With that done, we can perform some queries:

EntertainmentIndex.query(match: {author: 'Tarantino'}).filter{ year > 1990 }
EntertainmentIndex.query(match: {title: 'Shawshank'}).types(:movie)
EntertainmentIndex.query(match: {author: 'Tarantino'}).only(:id).limit(10).load
# the last one loads ActiveRecord objects for documents found

Now our index is almost ready to be used in our search implementation.

Rails integration

For integration with Rails, the first thing we need is to be able to react to RDBMS object changes. Chewy supports this behavior via callbacks defined within the update_index class method. update_index takes two arguments:

  1. A type identifier supplied in the "index_name#type_name" format
  2. A method name or block to execute, which represents a back-reference to the updated object or object collection

We need to define these callbacks for each dependent model:

class Book < ActiveRecord::Base
  acts_as_taggable

  belongs_to :author, class_name: 'Dude'
  # We update the book itself on-change
  update_index 'entertainment#book', :self
end

class Video < ActiveRecord::Base
  acts_as_taggable

  belongs_to :director, class_name: 'Dude'
  # Update video types when changed, depending on the category
  update_index('entertainment#movie') { self if movie? }
  update_index('entertainment#cartoon') { self if cartoon? }
end

class Dude < ActiveRecord::Base
  acts_as_taggable

  has_many :books
  has_many :videos
  # If author or director was changed, all the corresponding
  # books, movies and cartoons are updated
  update_index 'entertainment#book', :books
  update_index('entertainment#movie') { videos.movies }
  update_index('entertainment#cartoon') { videos.cartoons }
end

Since tags are also indexed, we next need to monkey-patch some external models so that they react to changes:

ActsAsTaggableOn::Tag.class_eval do
  has_many :books, through: :taggings, source: :taggable, source_type: 'Book'
  has_many :videos, through: :taggings, source: :taggable, source_type: 'Video'

  # Updating all tag-related objects
  update_index 'entertainment#book', :books
  update_index('entertainment#movie') { videos.movies }
  update_index('entertainment#cartoon') { videos.cartoons }
end

ActsAsTaggableOn::Tagging.class_eval do
  # Same goes for the intermediate model
  update_index('entertainment#book') { taggable if taggable_type == 'Book' }
  update_index('entertainment#movie') { taggable if taggable_type == 'Video' &&
                                        taggable.movie? }
  update_index('entertainment#cartoon') { taggable if taggable_type == 'Video' &&
                                          taggable.cartoon? }
end

At this point, every object save or destroy will update the corresponding Elasticsearch index type.

Atomicity

We still have one lingering problem. If we do something like books.map(&:save) to save multiple books, we’ll request an update of the entertainment index every time an individual book is saved. Thus, if we save five books, we’ll update the Chewy index five times. This behavior is acceptable for REPL, but certainly not acceptable for controller actions in which performance is critical.

We address this issue with the Chewy.atomic block:

class ApplicationController < ActionController::Base
  around_action { |&block| Chewy.atomic(&block) }
end

In short, Chewy.atomic batches these updates as follows:

  1. Disables the after_save callback.
  2. Collects the IDs of saved books.
  3. On completion of the Chewy.atomic block, uses the collected IDs to make a single Elasticsearch index update request.

Searching

Now we’re ready to implement a search interface. Since our user interface is a form, the best way to build it is, of course, with FormBuilder and ActiveModel. (At Toptal, we use ActiveData to implement ActiveModel interfaces, but feel free to use your favorite gem.)

class EntertainmentSearch
  include ActiveData::Model

  attribute :query, type: String
  attribute :author_id, type: Integer
  attribute :min_year, type: Integer
  attribute :max_year, type: Integer
  attribute :tags, mode: :arrayed, type: String,
                   normalize: ->(value) { value.reject(&:blank?) }

  # This accessor is for the form. It will have a single text field
  # for comma-separated tag inputs.
  def tag_list= value
    self.tags = value.split(',').map(&:strip)
  end

  def tag_list
    self.tags.join(', ')
  end
end

Query and filters

Now that we have an ActiveModel-like object that can accept and typecast attributes, let’s implement search:

class EntertainmentSearch
  ...

  def index
    EntertainmentIndex
  end

  def search
    # We can merge multiple scopes
    [query_string, author_id_filter,
     year_filter, tags_filter].compact.reduce(:merge)
  end

  # Using query_string advanced query for the main query input
  def query_string
    index.query(query_string: {fields: [:title, :author, :description],
                               query: query, default_operator: 'and'}) if query?
  end

  # Simple term filter for author id. `:author_id` is already
  # typecasted to integer and ignored if empty.
  def author_id_filter
    index.filter(term: {author_id: author_id}) if author_id?
  end

  # For filtering on years, we will use range filter.
  # Returns nil if both min_year and max_year are not passed to the model.
  def year_filter
    body = {}.tap do |body|
      body.merge!(gte: min_year) if min_year?
      body.merge!(lte: max_year) if max_year?
    end
    index.filter(range: {year: body}) if body.present?
  end

  # Same goes for `author_id_filter`, but `terms` filter used.
  # Returns nil if no tags passed in.
  def tags_filter
    index.filter(terms: {tags: tags}) if tags?
  end
end

Controllers and views

At this point, our model can perform search requests with passed attributes. Usage will look something like:

EntertainmentSearch.new(query: 'Tarantino', min_year: 1990).search

Note that in the controller, we want to load exact ActiveRecord objects instead of Chewy document wrappers:

class EntertainmentController < ApplicationController
  def index
    @search = EntertainmentSearch.new(params[:search])
    # In case we want to load real objects, we don't need any other
    # fields except for `:id` retrieved from Elasticsearch index.
    # Chewy query DSL supports Kaminari gem and corresponding API.
    # Also, we pass scopes for every requested type to the `load` method.
    @entertainments = @search.search.only(:id).page(params[:page]).load(
      book: {scope: Book.includes(:author)},
      movie: {scope: Video.includes(:director)},
      cartoon: {scope: Video.includes(:director)}
    )
  end
end

Now, it’s time to write up some HAML at entertainment/index.html.haml:

= form_for @search, as: :search, url: entertainment_index_path, method: :get do |f|
  = f.text_field :query
  = f.select :author_id, Dude.all.map { |d| [d.name, d.id] }, include_blank: true
  = f.text_field :min_year
  = f.text_field :max_year
  = f.text_field :tag_list
  = f.submit

- if @entertainments.any?
  %dl
    - @entertainments.each do |entertainment|
      %dt
        %h1= entertainment.title
        %strong= entertainment.class
      %dd
        %p= entertainment.year
        %p= entertainment.description
        %p= entertainment.tag_list
    = paginate @entertainments
- else
  Nothing to see here

Sorting

As a bonus, we’ll also add sorting to our search functionality.

Assume that we need to sort on the title and year fields, as well as by relevance. Unfortunately, the title One Flew Over the Cuckoo's Nest will be split into individual terms, so sorting by these disparate terms will be too random; instead, we’d like to sort by the entire title.

The solution is to use a special title field and apply its own analyzer:

class EntertainmentIndex < Chewy::Index
  settings analysis: {
    analyzer: {
      ...
      sorted: {
        # `keyword` tokenizer will not split our titles and
        # will produce the whole phrase as the term, which
        # can be sorted easily
        tokenizer: 'keyword',
        filter: ['lowercase', 'asciifolding']
      }
    }
  }

  define_type Book.includes(:author, :tags) do
    # We use the `multi_field` type to add `title.sorted` field
    # to the type mapping. Also, will still use just the `title`
    # field for search.
    field :title, type: 'multi_field' do
      field :title, index: 'analyzed', analyzer: 'title'
      field :sorted, index: 'analyzed', analyzer: 'sorted'
    end
    ...
  end

  {movie: Video.movies, cartoon: Video.cartoons}.each do |type_name, scope|
    define_type scope.includes(:director, :tags), name: type_name do
      # For videos as well
      field :title, type: 'multi_field' do
        field :title, index: 'analyzed', analyzer: 'title'
        field :sorted, index: 'analyzed', analyzer: 'sorted'
      end
      ...
    end
  end
end

In addition, we’re going to add both these new attributes and the sort processing step to our search model:

class EntertainmentSearch
  # we are going to use `title.sorted` field for sort
  SORT = {title: {'title.sorted' => :asc}, year: {year: :desc}, relevance: :_score}
  ...
  attribute :sort, type: String, enum: %w(title year relevance),
  default_blank: 'relevance'
  ...
  def search
    # we have added `sorting` scope to merge list
    [query_string, author_id_filter, year_filter,
     tags_filter, sorting].compact.reduce(:merge)
  end

  def sorting
    # We have one of the 3 possible values in `sort` attribute
    # and `SORT` mapping returns actual sorting expression
    index.order(SORT[sort.to_sym])
  end
end

Finally, we’ll modify our form adding sort options selection box:

= form_for @search, as: :search, url: entertainment_index_path, method: :get do |f|
  ...
  / `EntertainmentSearch.sort_values` will just return
  / enum option content from the sort attribute definition.
  = f.select :sort, EntertainmentSearch.sort_values
  ...

Error handling

If your users perform incorrect queries like ( or AND, the Elasticsearch client will raise an error. To handle that, let’s make some changes to our controller:

class EntertainmentController < ApplicationController
  def index
    @search = EntertainmentSearch.new(params[:search])
    @entertainments = @search.search.only(:id).page(params[:page]).load(
      book: {scope: Book.includes(:author)},
      movie: {scope: Video.includes(:director)},
      cartoon: {scope: Video.includes(:director)}
    )
  rescue Elasticsearch::Transport::Transport::Errors::BadRequest => e
    @entertainments = []
    @error = e.message.match(/QueryParsingException\[([^;]+)\]/).try(:[], 1)
  end
end

Further, we need to render the error in the view:

...
- if @entertainments.any?
  ...
- else
  - if @error
    = @error
  - else
    Nothing to see here

Testing queries

The basic testing setup is as follows:

  1. Start the Elasticsearch server.
  2. Cleanup and create our indices.
  3. Import our data.
  4. Perform our query.
  5. Cross-reference the result with our expectations.

For step 1, it’s convenient to use the test cluster defined in the elasticsearch-extensions gem. Just add the following line to your project’s Rakefile post-gem installation:

require 'elasticsearch/extensions/test/cluster/tasks'

Then, you’ll get the following Rake tasks:

$ rake -T elasticsearch
rake elasticsearch:start  # Start Elasticsearch cluster for tests
rake elasticsearch:stop   # Stop Elasticsearch cluster for tests

Elasticsearch and Rspec

First, we need to make sure that our index is updated to be in-sync with our data changes. Luckily, Chewy comes with the helpful update_index rspec matcher:

describe EntertainmentIndex do
  # No need to cleanup Elasticsearch as requests are
  # stubbed in case of `update_index` matcher usage.
  describe 'Tag' do
    # We create several books with the same tag
    let(:books) { create_list :book, 2, tag_list: 'tag1' }

    specify do
      # We expect that after modifying the tag name...
      expect do
        ActsAsTaggableOn::Tag.where(name: 'tag1').update_attributes(name: 'tag2')
      # ... the corresponding type will be updated with previously-created books.
      end.to update_index('entertainment#book').and_reindex(books,
                                                            with: {tags: ['tag2']})
    end
  end
end

Next, we need to test that the actual search queries are performed properly and that they return the expected results:

describe EntertainmentSearch do
  # Just defining helpers for simplifying testing
  def search attributes = {}
    EntertainmentSearch.new(attributes).search
  end

  # Import helper as well
  def import *args
    # We are using `import!` here to be sure all the objects are imported
    # correctly before examples run.
    EntertainmentIndex.import! *args
  end

  # Deletes and recreates index before every example
  before { EntertainmentIndex.purge! }

  describe '#min_year, #max_year' do
    let(:book) { create(:book, year: 1925) }
    let(:movie) { create(:movie, year: 1970) }
    let(:cartoon) { create(:cartoon, year: 1995) }
    before { import book: book, movie: movie, cartoon: cartoon }

    # NOTE:  The sample code below provides a clear usage example but is not
    # optimized code.  Something along the following lines would perform better:
    # `specify { search(min_year: 1970).map(&:id).map(&:to_i)
    #                                  .should =~ [movie, cartoon].map(&:id) }`
    specify { search(min_year: 1970).load.should =~ [movie, cartoon] }
    specify { search(max_year: 1980).load.should =~ [book, movie] }
    specify { search(min_year: 1970, max_year: 1980).load.should == [movie] }
    specify { search(min_year: 1980, max_year: 1970).should == [] }
  end
end

Test cluster troubleshooting

Finally, here are a few tips for troubleshooting your test cluster:

  • To start, use an in-memory, one-node cluster. It will be much faster for specs. In our case: TEST_CLUSTER_NODES=1 rake elasticsearch:start

  • There are some existing issues with the elasticsearch-extensions test cluster implementation itself related to one-node cluster status check (it’s yellow in some cases and will never be green, so the green-status cluster start check will fail every time). The issue has been fixed in this fork, but hopefully it will be fixed in the main repo soon.

  • For each dataset, group your request in specs (i.e., import your data once and then perform several requests). Elasticsearch warms up for a long time and uses a lot of heap memory while importing data, so don’t overdo it, especially if you’ve got a bunch of specs.

  • Make sure your machine has sufficient memory or Elasticsearch will freeze (we required around 5GB for each testing virtual machine and around 1GB for Elasticsearch itself).

Wrapping up

Elasticsearch is self-described as “a flexible and powerful open source, distributed, real-time search, and analytics engine.” It’s the gold standard in search technologies.

With Chewy, we’ve packaged these benefits as a simple, easy-to-use, production quality, open source Ruby gem that provides tight integration with Rails. Elasticsearch and Rails – what an awesome combination!

Elasticsearch and Rails -- what an awesome combination!


Appendix: Elasticsearch internals

Here’s a very brief introduction to Elasticsearch “under the hood”…

Elasticsearch is built on Lucene, which itself uses inverted indices as its primary data structure. For example, if we have the strings “the dogs jump high”, “jump over the fence”, and “the fence was too high”, we get the following structure:

"the"       [0, 0], [1, 2], [2, 0]
"dogs"      [0, 1]
"jump"      [0, 2], [1, 0]
"high"      [0, 3], [2, 4]
"over"      [1, 1]
"fence"     [1, 3], [2, 1]
"was"       [2, 2]
"too"       [2, 3]

Thus, every term contains both references to, and positions in, the text. Furthermore, we choose to modify our terms (e.g., by removing stop-words like “the”) and apply phonetic hashing to every term (can you guess the algorithm?):

"DAG"       [0, 1]
"JANP"      [0, 2], [1, 0]
"HAG"       [0, 3], [2, 4]
"OVAR"      [1, 1]
"FANC"      [1, 3], [2, 1]
"W"         [2, 2]
"T"         [2, 3]

If we then query for “the dog jumps”, it’s analyzed in the same way as the source text, becoming “DAG JANP” after hashing (“dog” has the same hash as “dogs”, as is true with “jumps” and “jump”).

We also add some logic between the individual words in the string (based on configuration settings), choosing between (“DAG” AND “JANP”) or (“DAG” OR “JANP”). The former returns the intersection of [0] & [0, 1] (i.e., document 0) and the latter, [0] | [0, 1] (i.e., documents 0 and 1). The in-text positions can be used for scoring results and position-dependent queries.

Hiring? Meet the Top 10 Ruby on Rails Developers for Hire in October 2014
Like what you're reading?
Get the latest updates first.
Don't miss out.
Get the latest updates first.

Comments

Sergey Mostovoy
how is it different from searchkick gem?
Zabazhanov Arkady
There are a lot of differences from tire and searchkick. The most common is: Chewy fully supports cross-model indexing and search. Chewy was built to support it. Also it has advanced AR-like search DSL. And many other features such as bulk index updates and zero-downtime full reindexing.
David Tuite
How do I update the index after destroying records without setting <samp>urgent: true</samp>. For example, I have a method which deletes records which are more than a month old: <pre> <code> class Story < ActiveRecord::Base update_index('suggestions#story', :self) def self.clear where('created_at < ?', 1.months.ago).destroy_all end end </code> </pre> How do I then remove the deleted stories from the index without rebuilding the whole thing from scratch.
Zabazhanov Arkady
Unfortunately, Chewy doesn't support update_all and delete_all operations, but you can perform this manually: > def self.clear > scope = where('created_at < ?', 1.months.ago) > ids = scope.pluck(:id) > scope.destroy_all > SuggestionsIndex::Story.import(ids) > end
Gabe
Can you give an example of how you would create facets for your searches?
David Tuite
That works, thanks. Great gem by the way. I've integrated it into an app I'm building which relies heavily on Elasticsearch and it's been a super smooth process. I'm even doing complex scripted queries via Chewy.
Zabazhanov Arkady
No, import method deletes destroyed records from index also. Need to improve documentation about import :)
Zabazhanov Arkady
Unfortunately chewy doesn't support facets fully yet, but you can perform something like this: <pre> scope = ArticlesIndex.query(...).filter(...).facets(tags: {terms: {field: 'tags', size: 10}}) scope.send(:_response)['facets'] </pre> Actually it is about adding 'facets' method returning facets from the response, but I will do it a little bit later. Stay tuned.
Gabe
Such a quick response. Thanks a lot!
David Tuite
I'm still slightly confused unfortunately. If I change the title of a Story instance in the database, how do I update the Elasticsearch index?
Zabazhanov Arkady
If you mean in database directly, bypassing ActiveRecord - there is no way. Only rake chewy:update:all
David Tuite
Sorry, no I mean just: <pre><code> story.update_attributes(title: 'Some new title') </code></pre> Does the index update if I do that?
Zabazhanov Arkady
Sure, if there is <pre>update_index('suggestions#story', :self)</pre>
David Tuite
Ok. I think I have it now! ;-) Thanks for all your explanation!
Zabazhanov Arkady
You are welcome
mark_ellul
Is there a way I can use the same Index Definition but scoped to a attribute on the models being added? So I have a multi-tenent app, which uses site_id to separate the data for each site, is there a way I can create a Index class that adds 2 or 3 models, but uses separate indexes depending on the model objects site_id?
Zabazhanov Arkady
Why not? Such an example is shown in article - the Video model. Or didn't I get a question?
mark_ellul
In the Example above you have a EntertainmentIndex, which has multiple models, but imagine that each model belonged to a different tenant, so imagine you have a imdb tenant, and a netflix tenant. You have netflix to have its own index in elastic search imdb its own index, even though they are all stored within the same Database, you want movies from imdb stored in that index, and netflix in its own index. Effectively you would have to define the Indexes separately in Chewy. Why two indexes? well it allows them to move between nodes easier, if one becomes bigger than another.
Zabazhanov Arkady
Still can not understand, why not? :) <pre> class NetflixIndex < Chewy::Index define_type Video.netflix, name: :netflix do ... end end class ImdbIndex < Chewy::Index define_type Video.netflix, name: :imdb do ... end end </pre> The scope you use for type definition is just default scope used only in you perform ImdbIndex::Imdb.import parameterless. Unfortunately, you can't use Chewy DSL to perform cross-index queries, but you still can use elasticsearch-ruby methods. Also, I don't think it is a big deal to add cross-index quering support via patch.
mark_ellul
Thanks, but my tenants are database driven, so effectively. So I cannot predict my tenants at compile time, so I would need to generate index code when we add new tenants at runtime. I have not found any particular library to do this, so I will use elasticsearch gem with custom code. Chewy looks awesome otherwise, I just have unusual requirements.
Zabazhanov Arkady
Ah, now I've got it. Actually, nothing prevents you to generate index classes in run-time. But cross-index queries is still under a question mark.
mark_ellul
Yes, it seems like too much hacking, Chewy has different use cases to my project, in my case its quite easy to just do it manually, my searches, will be done over JSON from a different system anyway.
Zabazhanov Arkady
Sure. It is really unusual requirement, but I'll think about it and maybe will implement in in Chewy in future. Thanks for your comments
Zabazhanov Arkady
Do you know, what have I thought? I think you don't need to separate your data manually, ES doing it internally and effectively using shards.
mark_ellul
Using something like this http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/index-modules-allocation.html ?
Zabazhanov Arkady
Looks like a solution, but I've thought it divides data automatically between shards. Anyway, it looks much more convenient.
moosilauke
Anyway to get it to work with the will_paginate gem? Or does it only support kaminari?
Zabazhanov Arkady
The only way is to send a pull-request. Right now will_paginate is not supported yet, sorry.
Zabazhanov Arkady
Btw, chewy from master supports facets currently: <pre> ArticlesIndex.query(...).filter(...).facets(tags: {terms: {field: 'tags', size: 10}}).facets </pre>
Igor Alexandrov
What about filters as a top level option, that I described in this issue: https://github.com/toptal/chewy/issues/16 How can I write a filter that will not affect my facets? For example I want to take a list of popular brands but to filter by brand too.
JoseAlberto
Good artile, adn good job with Chewy, it's really useful. Actually I'm playing with Chewy and elasticsearch-model. I prefer Chewy DSL to define mapspping and indexes, but I find the query interface in es-model mode flexible. How you compare this 2 gems? What is the best approach to do things like highlitgh results in Chewy?
Zabazhanov Arkady
Hey, sorry, discuss doesn't notify me about new comments :( The best way - is to improve chewy, I suppose :) At this moment chewy has highlight support (was added not so long ago).
Darryl
Kudos on chewy, seems to me this is the way rails should integrate with elasticsearch! Question: given your define_type Book, how would you create a define_type Author where the author defines the Book as the parent and the Author is now its own elasticsearch document?
Darryl
Figured out the mapping, which would be like: define_type Author do root _parent: { type: 'book'} do field: name end end Which will create the correct mapping for author with book as its parent. The question now is how does one set the parent for import scripts and callbacks if this is supported?
Leon
I am new to chewy, so i am facing a problem when i try to index a field of my model. The field is a text field in the DB which i serialize as a Hash in my model. The hash is dynamic and might have 0 to n elements in it in the form {"0"=>{"property"=>"value","property"=>"value"},"1"=>{"property"=>"value","property"=>"value"}.......} how can I index such field in my index class when i do define_type?
Leon
Fixed it!
Jon F
You mention in the chewy README on github that "Now implementing, e.g. cross-model autocomplete is much easier". Can you give a short example of how to do this? Does it use the completion suggest feature of elastic search?
comments powered by Disqus
Subscribe
Free email updates
Get the latest content first.
Trending articles
Relevant technologies
Toptal Authors
Android Engineer
Software Engineer
Ruby Developer
Software Developer
iOS Developer