2019-12-22 22:52:31 +05:30
module Elasticsearch
module Model
# Provides the necessary support to set up index options (mappings, settings)
# as well as instance methods to create, update or delete documents in the index.
#
# @see ClassMethods#settings
# @see ClassMethods#mapping
#
# @see InstanceMethods#index_document
# @see InstanceMethods#update_document
# @see InstanceMethods#delete_document
#
module Indexing
# Wraps the [index settings](http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/setup-configuration.html#configuration-index-settings)
#
class Settings
attr_accessor :settings
def initialize ( settings = { } )
@settings = settings
end
def to_hash
@settings
end
def as_json ( options = { } )
to_hash
end
end
# Wraps the [index mappings](http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/mapping.html)
#
class Mappings
attr_accessor :options , :type
# @private
TYPES_WITH_EMBEDDED_PROPERTIES = %w( object nested )
def initialize ( type , options = { } )
raise ArgumentError , " `type` is missing " if type . nil?
@type = type
@options = options
@mapping = { }
end
def indexes ( name , options = { } , & block )
@mapping [ name ] = options
if block_given?
@mapping [ name ] [ :type ] || = 'object'
properties = TYPES_WITH_EMBEDDED_PROPERTIES . include? ( @mapping [ name ] [ :type ] . to_s ) ? :properties : :fields
@mapping [ name ] [ properties ] || = { }
previous = @mapping
begin
@mapping = @mapping [ name ] [ properties ]
self . instance_eval ( & block )
ensure
@mapping = previous
end
end
2020-03-09 13:42:32 +05:30
# Set the type to `text` by default
@mapping [ name ] [ :type ] || = 'text'
2019-12-22 22:52:31 +05:30
self
end
def to_hash
{ @type . to_sym = > @options . merge ( properties : @mapping ) }
end
def as_json ( options = { } )
to_hash
end
end
module ClassMethods
# Defines mappings for the index
#
# @example Define mapping for model
#
# class Article
# mapping dynamic: 'strict' do
# indexes :foo do
# indexes :bar
# end
# indexes :baz
# end
# end
#
# Article.mapping.to_hash
#
# # => { :article =>
# # { :dynamic => "strict",
# # :properties=>
# # { :foo => {
# # :type=>"object",
# # :properties => {
# # :bar => { :type => "string" }
# # }
# # }
# # },
# # :baz => { :type=> "string" }
# # }
# # }
#
# @example Define index settings and mappings
#
# class Article
# settings number_of_shards: 1 do
# mappings do
# indexes :foo
# end
# end
# end
#
# @example Call the mapping method directly
#
# Article.mapping(dynamic: 'strict') { indexes :foo, type: 'long' }
#
# Article.mapping.to_hash
#
# # => {:article=>{:dynamic=>"strict", :properties=>{:foo=>{:type=>"long"}}}}
#
# The `mappings` and `settings` methods are accessible directly on the model class,
# when it doesn't already define them. Use the `__elasticsearch__` proxy otherwise.
#
def mapping ( options = { } , & block )
@mapping || = Mappings . new ( document_type , options )
@mapping . options . update ( options ) unless options . empty?
if block_given?
@mapping . instance_eval ( & block )
return self
else
@mapping
end
end ; alias_method :mappings , :mapping
# Define settings for the index
#
# @example Define index settings
#
# Article.settings(index: { number_of_shards: 1 })
#
# Article.settings.to_hash
#
# # => {:index=>{:number_of_shards=>1}}
#
# You can read settings from any object that responds to :read
# as long as its return value can be parsed as either YAML or JSON.
#
# @example Define index settings from YAML file
#
# # config/elasticsearch/articles.yml:
# #
# # index:
# # number_of_shards: 1
# #
#
# Article.settings File.open("config/elasticsearch/articles.yml")
#
# Article.settings.to_hash
#
# # => { "index" => { "number_of_shards" => 1 } }
#
#
# @example Define index settings from JSON file
#
# # config/elasticsearch/articles.json:
# #
# # { "index": { "number_of_shards": 1 } }
# #
#
# Article.settings File.open("config/elasticsearch/articles.json")
#
# Article.settings.to_hash
#
# # => { "index" => { "number_of_shards" => 1 } }
#
def settings ( settings = { } , & block )
settings = YAML . load ( settings . read ) if settings . respond_to? ( :read )
@settings || = Settings . new ( settings )
@settings . settings . update ( settings ) unless settings . empty?
if block_given?
self . instance_eval ( & block )
return self
else
@settings
end
end
def load_settings_from_io ( settings )
YAML . load ( settings . read )
end
# Creates an index with correct name, automatically passing
# `settings` and `mappings` defined in the model
#
# @example Create an index for the `Article` model
#
# Article.__elasticsearch__.create_index!
#
# @example Forcefully create (delete first) an index for the `Article` model
#
# Article.__elasticsearch__.create_index! force: true
#
# @example Pass a specific index name
#
# Article.__elasticsearch__.create_index! index: 'my-index'
#
def create_index! ( options = { } )
2020-03-09 13:42:32 +05:30
options = options . clone
target_index = options . delete ( :index ) || self . index_name
settings = options . delete ( :settings ) || self . settings . to_hash
mappings = options . delete ( :mappings ) || self . mappings . to_hash
2019-12-22 22:52:31 +05:30
delete_index! ( options . merge index : target_index ) if options [ :force ]
unless index_exists? ( index : target_index )
self . client . indices . create index : target_index ,
body : {
2020-03-09 13:42:32 +05:30
settings : settings ,
mappings : mappings }
2019-12-22 22:52:31 +05:30
end
end
# Returns true if the index exists
#
# @example Check whether the model's index exists
#
# Article.__elasticsearch__.index_exists?
#
# @example Check whether a specific index exists
#
# Article.__elasticsearch__.index_exists? index: 'my-index'
#
def index_exists? ( options = { } )
target_index = options [ :index ] || self . index_name
self . client . indices . exists ( index : target_index ) rescue false
end
# Deletes the index with corresponding name
#
# @example Delete the index for the `Article` model
#
# Article.__elasticsearch__.delete_index!
#
# @example Pass a specific index name
#
# Article.__elasticsearch__.delete_index! index: 'my-index'
#
def delete_index! ( options = { } )
target_index = options . delete ( :index ) || self . index_name
begin
self . client . indices . delete index : target_index
rescue Exception = > e
if e . class . to_s =~ / NotFound / && options [ :force ]
2020-03-09 13:42:32 +05:30
client . transport . logger . debug ( " [!!!] Index does not exist ( #{ e . class } ) " ) if client . transport . logger
nil
2019-12-22 22:52:31 +05:30
else
raise e
end
end
end
# Performs the "refresh" operation for the index (useful e.g. in tests)
#
# @example Refresh the index for the `Article` model
#
# Article.__elasticsearch__.refresh_index!
#
# @example Pass a specific index name
#
# Article.__elasticsearch__.refresh_index! index: 'my-index'
#
# @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/indices-refresh.html
#
def refresh_index! ( options = { } )
target_index = options . delete ( :index ) || self . index_name
begin
self . client . indices . refresh index : target_index
rescue Exception = > e
if e . class . to_s =~ / NotFound / && options [ :force ]
2020-03-09 13:42:32 +05:30
client . transport . logger . debug ( " [!!!] Index does not exist ( #{ e . class } ) " ) if client . transport . logger
nil
2019-12-22 22:52:31 +05:30
else
raise e
end
end
end
end
module InstanceMethods
def self . included ( base )
# Register callback for storing changed attributes for models
2020-03-09 13:42:32 +05:30
# which implement `before_save` and return changed attributes
# (ie. when `Elasticsearch::Model` is included)
2019-12-22 22:52:31 +05:30
#
# @note This is typically triggered only when the module would be
# included in the model directly, not within the proxy.
#
# @see #update_document
#
2020-03-09 13:42:32 +05:30
base . before_save do | i |
if i . class . instance_methods . include? ( :changes_to_save ) # Rails 5.1
i . instance_variable_set ( :@__changed_model_attributes ,
Hash [ i . changes_to_save . map { | key , value | [ key , value . last ] } ] )
elsif i . class . instance_methods . include? ( :changes )
i . instance_variable_set ( :@__changed_model_attributes ,
Hash [ i . changes . map { | key , value | [ key , value . last ] } ] )
end
end if base . respond_to? ( :before_save )
2019-12-22 22:52:31 +05:30
end
# Serializes the model instance into JSON (by calling `as_indexed_json`),
# and saves the document into the Elasticsearch index.
#
# @param options [Hash] Optional arguments for passing to the client
#
# @example Index a record
#
# @article.__elasticsearch__.index_document
# 2013-11-20 16:25:57 +0100: PUT http://localhost:9200/articles/article/1 ...
#
# @return [Hash] The response from Elasticsearch
#
# @see http://rubydoc.info/gems/elasticsearch-api/Elasticsearch/API/Actions:index
#
def index_document ( options = { } )
document = self . as_indexed_json
client . index (
{ index : index_name ,
type : document_type ,
id : self . id ,
body : document } . merge ( options )
)
end
# Deletes the model instance from the index
#
# @param options [Hash] Optional arguments for passing to the client
#
# @example Delete a record
#
# @article.__elasticsearch__.delete_document
# 2013-11-20 16:27:00 +0100: DELETE http://localhost:9200/articles/article/1
#
# @return [Hash] The response from Elasticsearch
#
# @see http://rubydoc.info/gems/elasticsearch-api/Elasticsearch/API/Actions:delete
#
def delete_document ( options = { } )
client . delete (
{ index : index_name ,
type : document_type ,
id : self . id } . merge ( options )
)
end
# Tries to gather the changed attributes of a model instance
# (via [ActiveModel::Dirty](http://api.rubyonrails.org/classes/ActiveModel/Dirty.html)),
# performing a _partial_ update of the document.
#
# When the changed attributes are not available, performs full re-index of the record.
#
# See the {#update_document_attributes} method for updating specific attributes directly.
#
# @param options [Hash] Optional arguments for passing to the client
#
# @example Update a document corresponding to the record
#
# @article = Article.first
# @article.update_attribute :title, 'Updated'
# # SQL (0.3ms) UPDATE "articles" SET "title" = ?...
#
# @article.__elasticsearch__.update_document
# # 2013-11-20 17:00:05 +0100: POST http://localhost:9200/articles/article/1/_update ...
# # 2013-11-20 17:00:05 +0100: > {"doc":{"title":"Updated"}}
#
# @return [Hash] The response from Elasticsearch
#
# @see http://rubydoc.info/gems/elasticsearch-api/Elasticsearch/API/Actions:update
#
def update_document ( options = { } )
2020-03-09 13:42:32 +05:30
if attributes_in_database = self . instance_variable_get ( :@__changed_model_attributes ) . presence
2019-12-22 22:52:31 +05:30
attributes = if respond_to? ( :as_indexed_json )
2020-03-09 13:42:32 +05:30
self . as_indexed_json . select { | k , v | attributes_in_database . keys . map ( & :to_s ) . include? k . to_s }
2019-12-22 22:52:31 +05:30
else
2020-03-09 13:42:32 +05:30
attributes_in_database
2019-12-22 22:52:31 +05:30
end
client . update (
{ index : index_name ,
type : document_type ,
id : self . id ,
body : { doc : attributes } } . merge ( options )
2020-03-09 13:42:32 +05:30
) unless attributes . empty?
2019-12-22 22:52:31 +05:30
else
index_document ( options )
end
end
# Perform a _partial_ update of specific document attributes
# (without consideration for changed attributes as in {#update_document})
#
# @param attributes [Hash] Attributes to be updated
# @param options [Hash] Optional arguments for passing to the client
#
# @example Update the `title` attribute
#
# @article = Article.first
# @article.title = "New title"
# @article.__elasticsearch__.update_document_attributes title: "New title"
#
# @return [Hash] The response from Elasticsearch
#
def update_document_attributes ( attributes , options = { } )
client . update (
{ index : index_name ,
type : document_type ,
id : self . id ,
body : { doc : attributes } } . merge ( options )
)
end
end
end
end
end