lib/tanker.rb in tanker-1.0.0 vs lib/tanker.rb in tanker-1.1.0
- old
+ new
@@ -1,6 +1,5 @@
-
begin
require "rubygems"
require "bundler"
Bundler.setup :default
@@ -10,39 +9,43 @@
require 'indextank_client'
require 'tanker/configuration'
require 'tanker/utilities'
require 'will_paginate/collection'
-
if defined? Rails
begin
require 'tanker/railtie'
rescue LoadError
end
end
module Tanker
class NotConfigured < StandardError; end
+ class BadConfiguration < StandardError; end
class NoBlockGiven < StandardError; end
+ class NoIndexName < StandardError; end
autoload :Configuration, 'tanker/configuration'
extend Configuration
+ autoload :KaminariPaginatedArray, 'tanker/paginated_array'
+
class << self
attr_reader :included_in
def api
@api ||= IndexTank::ApiClient.new(Tanker.configuration[:url])
end
def included(klass)
+ configuration # raises error if not defined
+
@included_in ||= []
@included_in << klass
@included_in.uniq!
- configuration # raises error if not defined
klass.send :include, InstanceMethods
klass.extend ClassMethods
class << klass
define_method(:per_page) { 10 } unless respond_to?(:per_page)
@@ -60,20 +63,20 @@
end
def search(models, query, options = {})
ids = []
models = [models].flatten.uniq
- page = (options.delete(:page) || 1).to_i
- per_page = (options.delete(:per_page) || models.first.per_page).to_i
index = models.first.tanker_index
query = query.join(' ') if Array === query
+ snippets = options.delete(:snippets)
+ fetch = options.delete(:fetch)
+ paginate = extract_setup_paginate_options(options, :page => 1, :per_page => models.first.per_page)
if (index_names = models.map(&:tanker_config).map(&:index_name).uniq).size > 1
raise "You can't search across multiple indexes in one call (#{index_names.inspect})"
end
-
# move conditions into the query body
if conditions = options.delete(:conditions)
conditions.each do |field, value|
value = [value].flatten.compact
value.each do |item|
@@ -94,71 +97,159 @@
filter_docvars.each do |var_number, ranges|
options[:"filter_docvar#{var_number}"] = ranges.map{|r|r.join(':')}.join(',')
end
end
- query = "__any:(#{query.to_s}) __type:(#{models.map(&:name).join(' OR ')})"
- options = { :start => per_page * (page - 1), :len => per_page }.merge(options)
- results = index.search(query, options)
+ # fetch values from index tank or just the type and id to instace results localy
+ options[:fetch] = "__type,__id"
+ options[:fetch] += ",#{fetch.join(',')}" if fetch
+ options[:snippet] = snippets.join(',') if snippets
+
+ search_on_fields = models.map{|model| model.tanker_config.indexes.map{|arr| arr[0]}.uniq}.flatten.uniq.join(":(#{query.to_s}) OR ")
+ query = "#{search_on_fields}:(#{query.to_s}) __type:(#{models.map(&:name).map {|name| "\"#{name.split('::').join(' ')}\"" }.join(' OR ')})"
- @entries = WillPaginate::Collection.create(page, per_page) do |pager|
- # inject the result array into the paginated collection:
- pager.replace instantiate_results(results)
-
- unless pager.total_entries
- # the pager didn't manage to guess the total count, do it manually
- pager.total_entries = results["matches"]
- end
+ options = { :start => paginate[:per_page] * (paginate[:page] - 1), :len => paginate[:per_page] }.merge(options) if paginate
+ results = index.search(query, options)
+
+ instantiated_results = if (fetch || snippets)
+ instantiate_results_from_results(results, fetch, snippets)
+ else
+ instantiate_results_from_db(results)
end
+ paginate === false ? instantiated_results : paginate_results(instantiated_results, paginate, results['matches'])
end
protected
- def instantiate_results(index_result)
+ def instantiate_results_from_db(index_result)
results = index_result['results']
return [] if results.empty?
id_map = results.inject({}) do |acc, result|
- model, id = result["docid"].split(" ", 2)
+ model = result["__type"]
+ id = constantize(model).tanker_parse_doc_id(result)
acc[model] ||= []
- acc[model] << id.to_i
+ acc[model] << id
acc
end
+
+ id_map.each do |klass, ids|
+ # replace the id list with an eager-loaded list of records for this model
+ id_map[klass] = constantize(klass).find(ids)
+ end
+ # return them in order
+ results.map do |result|
+ model, id = result["__type"], result["__id"]
+ id_map[model].detect {|record| id == record.id.to_s }
+ end
+ end
- if 1 == id_map.size # check for simple case, just one model involved
- klass = constantize(id_map.keys.first)
- # eager-load and return just this model's records
- klass.find(id_map.values.flatten)
- else # complex case, multiple models involved
- id_map.each do |klass, ids|
- # replace the id list with an eager-loaded list of records for this model
- id_map[klass] = constantize(klass).find(ids)
+ def paginate_results(results, pagination_options, total_hits)
+ case Tanker.configuration[:pagination_backend]
+ when :will_paginate
+ WillPaginate::Collection.create(pagination_options[:page],
+ pagination_options[:per_page],
+ total_hits) { |pager| pager.replace results }
+ when :kaminari
+ Tanker::KaminariPaginatedArray.new(results,
+ pagination_options[:per_page],
+ pagination_options[:page]-1,
+ total_hits)
+ else
+ raise(BadConfiguration, "Unknown pagination backend")
+ end
+ end
+
+ def instantiate_results_from_results(index_result, fetch = false, snippets = false)
+ results = index_result['results']
+ return [] if results.empty?
+ instances = []
+ id_map = results.inject({}) do |acc, result|
+ model = result["__type"]
+ instance = constantize(model).new()
+ result.each do |key, value|
+ case key
+ when /snippet/
+ # create snippet reader attribute (method)
+ instance.create_snippet_attribute(key, value)
+ when '__id'
+ # assign id attribute to the model
+ instance.id = value
+ when '__type', 'docid'
+ # do nothing
+ else
+ #assign attributes that are fetched if they match attributes in the model
+ if instance.respond_to?("#{key}=".to_sym)
+ instance.send("#{key}=", value)
+ end
+ end
end
- # return them in order
- results.map do |result|
- model, id = result["docid"].split(" ", 2)
- id_map[model].detect {|record| id.to_i == record.id }
- end
+ instances << instance
end
+ instances
end
def constantize(klass_name)
Object.const_defined?(klass_name) ?
- Object.const_get(klass_name) :
- Object.const_missing(klass_name)
+ Object.const_get(klass_name) :
+ Object.const_missing(klass_name)
end
+
+ # borrowed from Rails' ActiveSupport::Inflector
+ def constantize(camel_cased_word)
+ names = camel_cased_word.split('::')
+ names.shift if names.empty? || names.first.empty?
+
+ constant = Object
+ names.each do |name|
+ constant = constant.const_defined?(name) ? constant.const_get(name) : constant.const_missing(name)
+ end
+ constant
+ end
+
+ def extract_setup_paginate_options(options, defaults)
+ # extract
+ paginate_options = if options[:paginate] or options[:paginate] === false
+ options.delete(:paginate)
+ else
+ { :page => options.delete(:page), :per_page => options.delete(:per_page) }
+ end
+ # setup defaults and ensure we got integer values
+ unless paginate_options === false
+ paginate_options[:page] = defaults[:page] unless paginate_options[:page]
+ paginate_options[:per_page] = defaults[:per_page] unless paginate_options[:per_page]
+ paginate_options.each { |key, value| paginate_options[key] = value.to_i }
+ end
+ paginate_options
+ end
end
# these are the class methods added when Tanker is included
# They're kept to a minimum to prevent namespace pollution
module ClassMethods
attr_accessor :tanker_config
- def tankit(name, &block)
+ def tankit(name = nil, options = {}, &block)
if block_given?
- self.tanker_config = ModelConfig.new(name, block)
+ raise(NoIndexName, 'Please provide an index name') if name.nil? && self.tanker_config.nil?
+
+ self.tanker_config ||= ModelConfig.new(name, options, Proc.new)
+ name ||= self.tanker_config.index_name
+
+ self.tanker_config.index_name = name
+
+ config = ModelConfig.new(name, block)
+ config.indexes.each do |key, value|
+ self.tanker_config.indexes << [key, value]
+ end
+
+ unless config.variables.empty?
+ self.tanker_config.variables do
+ instance_exec &config.variables.first
+ end
+ end
else
raise(NoBlockGiven, 'Please provide a block')
end
end
@@ -174,43 +265,51 @@
puts "Indexing #{self} model"
batches = []
options[:batch_size] ||= 200
records = options[:scope] ? send(options[:scope]).all : all
- record_size = records.size
+ record_size = 0
records.each_with_index do |model_instance, idx|
batch_num = idx / options[:batch_size]
(batches[batch_num] ||= []) << model_instance
+ record_size += 1
end
timer = Time.now
batches.each_with_index do |batch, idx|
Tanker.batch_update(batch)
puts "Indexed #{batch.size} records #{(idx * options[:batch_size]) + batch.size}/#{record_size}"
end
puts "Indexed #{record_size} #{self} records in #{Time.now - timer} seconds"
end
+
+ def tanker_parse_doc_id(result)
+ result['docid'].split(' ').last
+ end
end
class ModelConfig
- attr_reader :index_name
+ attr_accessor :index_name
+ attr_accessor :options
- def initialize(index_name, block)
+ def initialize(index_name, options = {}, block)
@index_name = index_name
+ @options = options
@indexes = []
+ @variables = []
@functions = {}
instance_exec &block
end
def indexes(field = nil, &block)
@indexes << [field, block] if field
@indexes
end
def variables(&block)
- @variables = block if block
+ @variables << block if block
@variables
end
def functions(&block)
@functions = block.call if block
@@ -263,26 +362,41 @@
val = val.join(' ') if Array === val
data[field.to_sym] = val.to_s unless val.nil?
end
data[:__any] = data.values.sort_by{|v| v.to_s}.join " . "
- data[:__type] = self.class.name
+ data[:__type] = type_name
+ data[:__id] = self.id
data
end
+ #dynamically create a snippet read attribute (method)
+ def create_snippet_attribute(key, value)
+ # the method name should something_snippet not snippet_something as the api returns it
+ self.class.send(:define_method, "#{key.match(/snippet_(\w+)/)[1]}_snippet") do
+ value
+ end
+ end
+
def tanker_index_options
options = {}
- if tanker_variables
- options[:variables] = instance_exec(&tanker_variables)
+ unless tanker_variables.empty?
+ options[:variables] = tanker_variables.inject({}) do |hash, variables|
+ hash.merge(instance_exec(&variables))
+ end
end
options
end
# create a unique index based on the model name and unique id
def it_doc_id
- self.class.name + ' ' + self.id.to_s
+ type_name + ' ' + self.id.to_s
+ end
+
+ def type_name
+ tanker_config.options[:as] || self.class.name
end
end
end