Elastic Search with Chewy
Chewy is one of the elastic search Ruby client.
Chewy usages:
-
Multi-model indices You can define several types for index one per indexed model.
-
Every index is observable by all the related models. Most of the indexed models are related to other and it is necessary to denormalize this related data and put at the same object. Chewy is useful for example when we need index for an array of tags together with an article since it specify updated index for every model seperately so corressponding articles will be reindexed on any tag update.
-
Bulk import everywhere It supports bulk elastic search api for full reindex and index updates.
-
Powerful querying DSL Chewy has an ActiveRecord style query DSL.
-
Support for ActiveRecord, Mongoid and Sequel.
Installation Steps:
gem 'chewy'
bundle install
or gem install chewy
Client settings:
Chewy.settings
hash and chewy.yml
are two ways in which Chewy client can be configured.
Run the command rails g chewy:install
to generate the file or create one manually.
# config/chewy.yml
# separate environment configs
test:
host: 'localhost:9250'
prefix: 'test'
development:
host: 'localhost:9200'
config/initializers/chewy.rb
Chewy.settings = {host: 'localhost:9250'} # do not use environments
Aws Elastic Search
Configuration for using AWS’s elastic search using an IAM user policy, sign your requests for the es:* action by injecting the headers passing a proc to transport_options.
Chewy.settings = {
host: 'http://my-es-instance-on-aws.us-east-1.es.amazonaws.com:80',
transport_options: {
headers: { content_type: 'application/json' },
proc: -> (f) do
f.request :aws_signers_v4,
service_name: 'es',
region: 'us-east-1',
credentials: Aws::Credentials.new(
ENV['AWS_ACCESS_KEY'],
ENV['AWS_SECRET_ACCESS_KEY'])
end
}
}
Type access
Following API is used to access index-defined types
UsersIndex::User
UsersIndex.type_hash['user']
UsersIndex.type('user')
UsersIndex.type('foo')
UsersIndex.types # [UserIndex::User]
UsersIndex.type_names # ["user"]
Index Manipulation
UsersIndex.delete # destroy existed index
UsersIndex.delete!
UsersIndex.create # create index
UsersIndex.create!
UsersIndex.purge
UsersIndex.purge! # deletes then creates index
UsersIndex::User.import # import with 0 arguments process all the data specified in type definition
UsersIndex::User.import User.where('rating > 100') # or import specified users scope
UsersIndex::User.import User.where('rating > 100').to_a # or import specified users array
UsersIndex::User.import [1, 2, 42] # pass even ids for import, it will be handled in the most effective way
UsersIndex::User.import user: User.where('rating > 100') # if update fields are specified - it will update their values only with the `update` bulk action.
UsersIndex.reset! # purges index and imports default data for all types
Practical on Ruby on Rails application
app/chewy/user_index.rb
class UserIndex < Chewy::Index
settings analysis: {
analyzer: {
email: {
tokenizer: 'keyword',
filter: ['lowercase']
}
}
}
define_type User do
field :name, {type: 'text'}
field :email, analyzer: 'email'
field :phone, {type: 'text'}
end
end
app/controllers/users_controller.rb
class UsersController < ApplicationController
def search
@users = UsersIndex.query(query_string: { fields: [:name, :email, :phone], query: search_params[:query], default_operator: 'and' })
render json: @users.to_json, status: :ok
end
private
def search_params
params.permit(:query, :page, :per)
end
end
app/models/user.rb
class User < ApplicationRecord
update_index('user') { self }
enum status: { unconfirmed: 0, confirmed: 1 }
end
routes.rb
resources :users do
get :search, on: :collection
end
If you access the url http://localhost:3000/users/search?query=test1
Following results are seen on the browser
0
id "18"
name "test1"
status "unconfirmed"
email "test1@example.com"
phone "090111111"
_score 0.5389965
_explanation null
1
id "3"
name "test1"
status "unconfirmed"
email "test1@example.com"
phone "090111112"
_score 0.5389965
_explanation null
2
id "45"
name "test1"
email "test1@example.com"
phone "090111111"
_score 0.5389965
_explanation null
and if we inspect the result of @users object on controller.first on console, we will see
@_data=
{"_index"=>"user",
"_type"=>"user",
"_id"=>"18",
"_score"=>0.5389965,
"_source"=>{"name"=>"test1", "status"=>"unconfirmed", "email"=>"test1@example.com", "phone"=>"090111111"}},
@attributes=
{"id"=>"18",
"name"=>"test1",
"status"=>"unconfirmed",
"email"=>"test1@example.com",
"phone"=>"090111111",
"_score"=>0.5389965,
"_explanation"=>nil}
We can refactor the searching as:
Create a dir called as app/searches/user_search.rb
# user_search.rb
# frozen_string_literal: true
class UserSearch
include ActiveModel::Model
DEFAULT_PER_PAGE = 10
DEFAULT_PAGE = 0
attr_accessor :query, :page, :per
def search
[query_string].compact.reduce(&:merge).page(page_num).per(per_page)
end
def query_string
index.query(query_string: { fields: [:name, :email, :phone], query: query, default_operator: 'and' }) if query.present?
end
private
def index
UsersIndex
end
def page_num
page || DEFAULT_PAGE
end
def per_page
per || DEFAULT_PER_PAGE
end
end
Now call the UserSearch class and implement it inside the UsersController
class UsersController < ApplicationController
def search
user_search = UserSearch.new(search_params)
@users = user_search.search
render json: @users, status: :ok
end
private
def search_params
params.permit(:query, :page, :per)
end
end
Now modify the search action as:
class UsersController < ApplicationController
def search
user_search = UserSearch.new(search_params)
@users = user_search.search
end
private
def search_params
params.permit(:query, :page, :per)
end
end
search.html.erb
<% if @users.any? %>
<table border="1">
<tr>
<th>Id</th>
<th>Name</th>
<th>Phone</th>
<th>Email</th>
<th>Status</th>
</tr>
<% @users.each do |user| %>
<% res = user.attributes %>
<tr>
<td><%= res["id"] %></td>
<td><%= res["name"] %></td>
<td><%= res["phone"] %></td>
<td><%= res["email"] %></td>
<td><%= res["status"] %></td>
</tr>
<% end %>
</table>
<% else %>
<p>No users found.</p>
<% end %>