Skip to content

Commit

Permalink
Merge branch 'master' of git://github.com/dandorman/tanker
Browse files Browse the repository at this point in the history
Conflicts:
	lib/tanker.rb
	spec/tanker_spec.rb

Merging my branch with dandorman's. Very little of the code actually
conflicts, my work was mostly cleanup and his was adding new features.

Currently 14 of the specs are failing.
  • Loading branch information
JackDanger committed Apr 27, 2011
2 parents 58c0eae + 9447241 commit 325a4fa
Show file tree
Hide file tree
Showing 7 changed files with 462 additions and 103 deletions.
2 changes: 1 addition & 1 deletion Gemfile
@@ -1,9 +1,9 @@
source "http://rubygems.org"

gem 'jeweler'
gem 'json', '>= 1.5.1'
gem 'will_paginate', '>= 2.3.15'
gem 'activesupport' # <- required by will_paginate but not in will_paginate's listed dependencies
gem 'json'

group :test do
gem 'rspec', '>= 2.5.0'
Expand Down
99 changes: 99 additions & 0 deletions README.rdoc
Expand Up @@ -177,7 +177,106 @@ IndexTank and the Tanker gem support native geographic calculations. All you nee
:function => 1,
:filter_functions => {2 => [['*', 50]]})

== Categories and faceted search example

If your IndexTank indexes have faceting enabled, you can do this:

class Part < ActiveRecord::Base
include Tanker

tankit 'index' do
indexes :brand
indexes :part_number

# return a hash of category/value pairs
categories do
{
'price_range' => (price > 5 ? 'expensive' : 'cheap')
}
end
end
end

To retrieve facets in a search:

@parts, @facets = Part.search_tank('widget', :facets => true)

# @facets =
# { 'price_range' => { 'expensive' => 93, 'cheap' => 47 } }

At some point you will probably also want to filter what categories to search.
That's faceted searching, after all!

# search only cheap parts:
@parts = Part.search_tank('widget', :category_filters => { 'price_range' => 'cheap' })

# highly contrived, but to show how you could filter multiple values for a
# given category:
@parts = Part.search_tank('widget', :category_filters => { 'price_range' => ['cheap', 'expensive'] })

# or multiple categories:
@parts = Part.search_tank('widget', :category_filters => { 'price_range' => ['cheap', 'expensive'],
'type' => 'Part' })


== Extend your index definitions

If you have a bunch of models with a lot of overlapping indexed fields,
variables, or categories, you might want to abstract those out into a module
that you can include and then extend in the including classes. Something like:

module TankerDefaults
def self.included(base)
base.send(:include, ::Tanker) # include the actual Tanker module

# provide a default index name
base.tankit 'my_index' do
# index some common fields
indexes :tag_list

# set some common variables
variables do
{
0 => view_count
1 => foo
}
end

# set some common categories
categories do
{
"type" => self.class.name
}
end
end
end
end

class SuperModel
include TankerDefaults

# no need to respecify the index if it's the same
# (but you can override it)
tankit do
# `indexes :tag_list` is inherited
indexes :name
indexes :boyfriend_names do
boyfriends.map(&:name)
end

variables do
{
# `0 => view_count` is inherited
1 => iq, # overwrites "foo"
2 => endorsements.count # adds new variables
}
end

# and the same can be done with categories
end
end

You currently can't remove previously defined stuff, though.

== An index for each environment

Expand Down
72 changes: 65 additions & 7 deletions lib/tanker.rb
Expand Up @@ -36,11 +36,12 @@ def api
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

Expand Down Expand Up @@ -81,7 +82,6 @@ def search(models, query, options = {})
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|
Expand All @@ -106,6 +106,11 @@ def search(models, query, options = {})
end
end

# IndexTank expects a JSON_formatted map in the GET query variable
options[:category_filters] = options[:category_filters].to_json if options[:category_filters]

options[:fetch] = "__type,__id"

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)
Expand All @@ -119,6 +124,10 @@ def search(models, query, options = {})
pager.total_entries = results["matches"]
end
end

@entries.extend ResultsMethods
@entries.results = results
@entries
end
end

Expand All @@ -131,10 +140,27 @@ module ClassMethods
def tankit(name = nil, &block)
name ||= Tanker.guess_index_name

raise NoIndexName, "Please provide a name for this index" unless name
raise NoIndexName, "Please provide a name for this index" unless name || tanker_config

if block_given?
self.tanker_config = ModelConfig.new(name.to_s, block)

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

%w[variables categories].each do |method|
unless config.send(method).empty?
self.tanker_config.send(method) do
instance_exec &config.send(method).first
end
end
end
else
raise NoBlockGiven, 'Please provide a block'
end
Expand Down Expand Up @@ -173,15 +199,21 @@ def tanker_reindex(options = {})
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

def initialize(index_name, block)
@index_name = index_name
@indexes = []
@variables = []
@functions = {}
@categories = []
instance_exec &block
end

Expand All @@ -191,7 +223,7 @@ def indexes(field = nil, &block)
end

def variables(&block)
@variables = block if block
@variables << block if block
@variables
end

Expand All @@ -200,6 +232,11 @@ def functions(&block)
@functions
end

def categories(&block)
@categories << block if block
@categories
end

def index
@index ||= Tanker.api.get_index(index_name)
end
Expand All @@ -221,6 +258,10 @@ def tanker_variables
tanker_config.variables
end

def tanker_categories
tanker_config.categories
end

# update a create instance from index tank
def update_tank_indexes
tanker_config.index.add_document(
Expand Down Expand Up @@ -249,15 +290,24 @@ def tanker_index_data

data[:__any] = data.values.sort_by{|v| v.to_s}.join " . "
data[:__type] = self.class.name
data[:__id] = self.id

data
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

unless tanker_categories.empty?
options[:categories] = tanker_categories.inject({}) do |hash, categories|
hash.merge(instance_exec(&categories))
end
end

options
Expand All @@ -268,4 +318,12 @@ def it_doc_id
self.class.name + ' ' + self.id.to_s
end
end

module ResultsMethods
attr_accessor :results

def facets
@results['facets']
end
end
end
24 changes: 10 additions & 14 deletions lib/tanker/utilities.rb
Expand Up @@ -54,25 +54,21 @@ def instantiate_results(index_result)
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
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
ensure_order klass.find(id_map.values.first), id_map.values.first
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] = ensure_order constantize(klass).find(ids), ids
end
results.map do |result|
model, id = result["docid"].split(" ", 2)
id_map[model].detect {|record| id.to_i == record.id }
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.to_i == record.id }
end
end

Expand Down
4 changes: 0 additions & 4 deletions spec/spec_helper.rb
Expand Up @@ -13,10 +13,6 @@

Tanker.configuration = {:url => 'http://api.indextank.com'}

class Dummy

end

$frozen_moment = Time.now

class Person
Expand Down

0 comments on commit 325a4fa

Please sign in to comment.