lib/chewy/index/adapter/object.rb
require 'chewy/index/adapter/base'
module Chewy
class Index
module Adapter
# This adapter provides an ability to import documents from any
# source. You can actually use any class or even a symbol as
# a target.
#
# In case if a class is used - some of the additional features
# are available: it is possible to provide the default import
# data (used on reset) and source objects loading logic.
#
# @see #import
# @see #load
class Object < Base
# The signature of the index scope definition.
#
# @example
# index_scope :geoname
# index_scope Geoname
# index_scope -> { Geoname.all_the_places }, name: 'geoname'
#
# @param target [Class, Symbol, String, Proc] a source of data and everything
# @option options [String, Symbol] :name redefines the inferred name if necessary
# @option options [String, Symbol] :import_all_method redefines import method name
# @option options [String, Symbol] :load_all_method redefines batch load method name
# @option options [String, Symbol] :load_one_method redefines per-object load method name
def initialize(target, **options)
@target = target
@options = options
end
# Inferred from the target by default if possible.
#
# @example
# # defines name = Geoname
# index_scope :geoname
# # still defines name = Geoname
# index_scope -> { Geoname.all_the_places }, name: 'geoname'
#
# @return [String]
def name
@name ||= (options[:name] || @target).to_s.camelize.demodulize
end
# While for ORM adapters it returns an array of ids for the passed
# collection, for the object adapter it returns the collection itself.
#
# @param collection [Array<Object>, Object] a collection or an object
# @return [Array<Object>]
def identify(collection)
Array.wrap(collection)
end
# This method is used internally by `Chewy::Index.import`.
#
# The idea is that any object can be imported to ES if
# it responds to `#to_json` method.
#
# If method `destroyed?` is defined for object (or, in case of hash object,
# it has `:_destroyed` or `'_destroyed'` key) and returns `true` or object
# satisfy `delete_if` option then object will be deleted from index.
# But in order to be destroyable, objects need to respond to `id` method
# or have an `id` key so ElasticSearch could know which one to delete.
#
# If nothing is passed the method tries to call `import_all_method`,
# which is `call` by default, on target to get the default objects batch.
#
# @example
# class Geoname
# self < class
# def self.call
# FancyGeoAPI.all_points_collection
# end
# alias_method :import_all, :call
# end
# end
#
# # All the following variants will work:
# index_scope Geoname
# index_scope Geoname, import_all_method: 'import_all'
# index_scope -> { FancyGeoAPI.all_points_collection }, name: 'geoname'
#
# @param args [Array<#to_json>]
# @option options [Integer] :batch_size import processing batch size
# @return [true, false]
def import(*args, &block)
collection, options = import_args(*args)
import_objects(collection, options, &block)
end
# For the object adapter this method tries to fetch :id and requested
# fields from the passed collection or the target's `import_all_method`
# when defined. Otherwise it tries to call the target `pluck_method`,
# which is configurable and `pluck` by default. The `pluck_method` have
# to act exactly the same way as the AR one. It returns an empty array
# when none of the methods are found.
#
# @example
# class Geoname
# self < class
# def self.pluck(*fields)
# if fields.one?
# whatever_source.map { |object| object.send(fields.first) }
# else
# whatever_source.map do |object|
# fields.map { |field| object.send(field) }
# end
# end
# end
# end
# end
#
# @see Chewy::Index::Adapter::Base#import_fields
def import_fields(*args, &block)
return enum_for(:import_fields, *args) unless block_given?
options = args.extract_options!
options[:batch_size] ||= BATCH_SIZE
if args.empty? && @target.respond_to?(pluck_method)
@target.send(pluck_method, :id, *options[:fields]).each_slice(options[:batch_size], &block)
elsif options[:fields].blank?
import_references(*args, options) do |batch|
yield batch.map { |object| object_field(object, :id) || object }
end
else
import_references(*args, options) do |batch|
batch = batch.map do |object|
options[:fields].map { |field| object_field(object, field) }
.unshift(object_field(object, :id) || object)
end
yield batch
end
end
end
# For the Object adapter returns the objects themselves in batches.
#
# @see Chewy::Index::Adapter::Base#import_references
def import_references(*args, &block)
return enum_for(:import_references, *args) unless block_given?
collection, options = import_args(*args)
collection.each_slice(options[:batch_size], &block)
end
# This method is used internally by the request DSL when the
# collection of ORM/ODM objects is requested.
#
# Options usage is implemented by `load_all_method` and `load_one_method`.
#
# If none of the `load_all_method` or `load_one_method` is implemented
# for the target - the method will return nil. This means that the
# loader will return an array `Chewy::Index` objects that actually was passed.
#
# To use loading for objects it is obviously required to provide
# some meaningful ids for ES documents.
#
# @example
# class Geoname
# def self.load_all(wrappers, options)
# if options[:additional_data]
# wrappers.map do |wrapper|
# FancyGeoAPI.point_by_name(wrapper.name)
# end
# else
# wrappers
# end
# end
# end
#
# MyIndex.load(additional_data: true).objects
#
# @param ids [Array<Hash>] an array of ids from ES hits
# @param options [Hash] any options passed here with the request DSL `load` method.
# @return [Array<Object>, nil]
def load(ids, **options)
if target.respond_to?(load_all_method)
if target.method(load_all_method).arity == 1
target.send(load_all_method, ids)
else
target.send(load_all_method, ids, options)
end
elsif target.respond_to?(load_one_method)
if target.method(load_one_method).arity == 1
ids.map { |hit| target.send(load_one_method, hit) }
else
ids.map { |hit| target.send(load_one_method, hit, options) }
end
end
end
private
def import_objects(objects, options)
objects.each_slice(options[:batch_size]).map do |group|
yield grouped_objects(group)
end.all?
end
def delete_from_index?(object)
delete = super
delete ||= object.destroyed? if object.respond_to?(:destroyed?)
delete ||= object[:_destroyed] || object['_destroyed'] if object.is_a?(Hash)
!!delete
end
def object_field(object, name)
if object.respond_to?(name)
object.send(name)
elsif object.is_a?(Hash)
object[name.to_sym] || object[name.to_s]
end
end
def import_all_method
@import_all_method ||= options[:import_all_method] || :call
end
def pluck_method
@pluck_method ||= options[:pluck_method] || :pluck
end
def load_all_method
@load_all_method ||= options[:load_all_method] || :load_all
end
def load_one_method
@load_one_method ||= options[:load_one_method] || :load_one
end
def import_args(*args)
options = args.extract_options!
options[:batch_size] ||= BATCH_SIZE
collection = if args.empty? && @target.respond_to?(import_all_method)
@target.send(import_all_method)
else
args.flatten(1).compact
end
[collection, options]
end
end
end
end
end