tanker 0.5.6 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- data/Gemfile +1 -1
- data/README.rdoc +116 -2
- data/Rakefile +7 -6
- data/VERSION +1 -1
- data/lib/indextank_client.rb +5 -0
- data/lib/tanker/tasks/tanker.rake +36 -1
- data/lib/tanker/utilities.rb +2 -5
- data/lib/tanker.rb +200 -49
- data/spec/spec_helper.rb +30 -8
- data/spec/tanker_spec.rb +133 -9
- data/spec/utilities_spec.rb +1 -1
- data/tanker.gemspec +27 -30
- metadata +11 -37
- data/.gitignore +0 -21
- data/Gemfile.lock +0 -32
data/Gemfile
CHANGED
data/README.rdoc
CHANGED
@@ -1,7 +1,5 @@
|
|
1
1
|
= Tanker - IndexTank integration to your favorite orm
|
2
2
|
|
3
|
-
*Note:* This gem is still quite alpha but please feel free to send comments
|
4
|
-
|
5
3
|
IndexTank is a great search indexing service, this gem tries to make
|
6
4
|
any orm keep in sync with indextank with ease. This gem has a simple
|
7
5
|
but powerful approach to integrating IndexTank to any orm. The gem
|
@@ -39,6 +37,7 @@ in the IndexTank Dashboard
|
|
39
37
|
|
40
38
|
class Topic < ActiveRecord::Base
|
41
39
|
acts_as_taggable
|
40
|
+
has_many :authors
|
42
41
|
|
43
42
|
# just include the Tanker module
|
44
43
|
include Tanker
|
@@ -50,7 +49,32 @@ in the IndexTank Dashboard
|
|
50
49
|
tankit 'my_index' do
|
51
50
|
indexes :title
|
52
51
|
indexes :content
|
52
|
+
indexes :id
|
53
53
|
indexes :tag_list #NOTICE this is an array of Tags! Awesome!
|
54
|
+
|
55
|
+
# you may also dynamically retrieve field data
|
56
|
+
indexes :author_names do
|
57
|
+
authors.map {|author| author.name }
|
58
|
+
end
|
59
|
+
|
60
|
+
# to pass in a list of variables with your document,
|
61
|
+
# supply a hash with the variable integers as keys:
|
62
|
+
variables do
|
63
|
+
{
|
64
|
+
0 => authors.first.id, # these will be available in your queries
|
65
|
+
1 => id # as doc.var[0] and doc.var[1]
|
66
|
+
}
|
67
|
+
end
|
68
|
+
|
69
|
+
# You may defined some server-side functions that can be
|
70
|
+
# referenced in your queries. They're always referenced by
|
71
|
+
# their integer key:
|
72
|
+
functions do
|
73
|
+
{
|
74
|
+
1 => '-age',
|
75
|
+
2 => 'doc.var[0] - doc.var[1]'
|
76
|
+
}
|
77
|
+
end
|
54
78
|
end
|
55
79
|
|
56
80
|
# define the callbacks to update or delete the index
|
@@ -78,10 +102,83 @@ Search Topics
|
|
78
102
|
@topics = Topic.search_tank('blah', :conditions => {:tag_list => 'tag1'}) # Gets 2 results!
|
79
103
|
@topics = Topic.search_tank('blah', :conditions => {:title => 'Awesome', :tag_list => 'tag1'}) # Gets 1 result!
|
80
104
|
|
105
|
+
Search multiple models
|
106
|
+
|
107
|
+
@results = Tanker.search([Topic, Post], 'blah') # Gets any Topic OR Post results that match!
|
108
|
+
|
109
|
+
Search with negative conditions
|
110
|
+
|
111
|
+
@topics = Topic.search_tank('keyword', :conditions => {'tag_list' => 'tag1'}) # Gets 1 result!
|
112
|
+
@topics = Topic.search_tank('keyword', :conditions => {'-id' => [5,9]}) # Ignores documents with ids of '5' or '9'
|
113
|
+
|
114
|
+
Search with query variables
|
115
|
+
|
116
|
+
@topics = Topic.search_tank('keyword', :variables => {0 => 5.6}) # Makes 5.6 available server-side as q[0]
|
117
|
+
|
81
118
|
Paginate Results
|
82
119
|
|
83
120
|
<% will_paginate(@topics) %>
|
84
121
|
|
122
|
+
== Geospatial Search Example
|
123
|
+
|
124
|
+
IndexTank and the Tanker gem support native geographic calculations. All you need is to define two document variables with your latitude and your longitude
|
125
|
+
|
126
|
+
class Restaurant < ActiveRecord::Base
|
127
|
+
include Tanker
|
128
|
+
|
129
|
+
tankit 'my_index' do
|
130
|
+
indexes :name
|
131
|
+
|
132
|
+
# You may define lat/lng at any variable number you like, as long
|
133
|
+
# as you refer to them consistently with the same integers
|
134
|
+
variables do
|
135
|
+
{
|
136
|
+
0 => lat
|
137
|
+
1 => lng
|
138
|
+
}
|
139
|
+
end
|
140
|
+
|
141
|
+
functions do
|
142
|
+
{
|
143
|
+
# This function is good for sorting your results. It will
|
144
|
+
# rank relevant, close results higher and irrelevant, distant results lower
|
145
|
+
1 => "relevance / miles(d[0], d[1], q[0], q[1])",
|
146
|
+
|
147
|
+
# This function is useful for limiting the results that your query returns.
|
148
|
+
# It just calculates the distance of each document in miles, which you can use
|
149
|
+
# in your query. Notice that we're using 0 and 1 consistently as 'lat' and 'lng'
|
150
|
+
2 => "miles(d[0], d[1], q[0], q[1])"
|
151
|
+
}
|
152
|
+
end
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
# When searching, you need to provide a latitude and longitude in the query so that
|
157
|
+
# these variables are accessible on the server.
|
158
|
+
Restaurant.search("tasty",
|
159
|
+
:var0 => 47.689948,
|
160
|
+
:var1 => -122.363684,
|
161
|
+
# And we'll return all results sorted by distance
|
162
|
+
:function => 1)
|
163
|
+
|
164
|
+
# Or we can use just function 2 to return results in the default order, while
|
165
|
+
# limiting the total results to just those within 50 miles from our lat/lng:
|
166
|
+
Restaurant.search("tasty",
|
167
|
+
:var0 => 47.689948,
|
168
|
+
:var1 => -122.363684,
|
169
|
+
# the following is a fancy way of saying "return all results where
|
170
|
+
the result of function 2 is at least anything and less than 50"
|
171
|
+
:filter_functions => {2 => [['*', 50]]})
|
172
|
+
# And we can combine these two function calls to return only results within 50 miles
|
173
|
+
# and sort them by relevance / distance:
|
174
|
+
Restaurant.search("tasty",
|
175
|
+
:var0 => 47.689948,
|
176
|
+
:var1 => -122.363684,
|
177
|
+
:function => 1,
|
178
|
+
:filter_functions => {2 => [['*', 50]]})
|
179
|
+
|
180
|
+
|
181
|
+
|
85
182
|
== Reindex your data
|
86
183
|
|
87
184
|
If you are using rails 3 there are of couple of rake tasks included in your project
|
@@ -93,11 +190,28 @@ the indexes in the Index Tank interface this task creates them for you provided
|
|
93
190
|
you have enough indexes in your account.
|
94
191
|
|
95
192
|
rake tanker:reindex
|
193
|
+
|
194
|
+
This task pushes any functions you've defined in your tankit() blocks to indextank.com
|
195
|
+
|
196
|
+
rake tanker:functions
|
96
197
|
|
97
198
|
This task re-indexes all the your models that have the Tanker module included.
|
98
199
|
Usually you will only need a single Index Tank index but if you want to separate
|
99
200
|
your indexes please use different index names in the tankit method call in your models
|
100
201
|
|
202
|
+
To reindex just one class rather than all the classes in your application just call:
|
203
|
+
|
204
|
+
Model.tanker_reindex
|
205
|
+
|
206
|
+
And if you'd like to specify a custom number of records to PUT with each call to indextank.com you may specify it as the first argument:
|
207
|
+
|
208
|
+
Model.tanker_reindex(:batch_size => 1000) # defaults to batch size of 200
|
209
|
+
|
210
|
+
Or you can restrict indexing to a subset of records. This is particularly helpful for massive datasets where only a small portion of the records is useful.
|
211
|
+
|
212
|
+
Model.tanker_reindex(:batch_size => 1000, :scope => :not_deleted) # will call Model.not_deleted.all internally
|
213
|
+
|
214
|
+
|
101
215
|
== TODO
|
102
216
|
* Documentation
|
103
217
|
* More fancy stuff (Suggestions?)
|
data/Rakefile
CHANGED
@@ -9,9 +9,9 @@ begin
|
|
9
9
|
gem.description = %Q{IndexTank is a great search indexing service, this gem tries to make any orm keep in sync with indextank with ease}
|
10
10
|
gem.email = "kidpollo@gmail.com"
|
11
11
|
gem.homepage = "http://github.com/kidpollo/tanker"
|
12
|
-
gem.authors = ["@kidpollo"]
|
13
|
-
gem.add_development_dependency "rspec", ">= 2.0.0.beta.22"
|
14
|
-
gem.add_dependency 'will_paginate', '>= 2.3.15'
|
12
|
+
gem.authors = ["@kidpollo", "Jack Danger Canty"]
|
13
|
+
#gem.add_development_dependency "rspec", ">= 2.0.0.beta.22"
|
14
|
+
#gem.add_dependency 'will_paginate', '>= 2.3.15'
|
15
15
|
# gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
|
16
16
|
end
|
17
17
|
Jeweler::GemcutterTasks.new
|
@@ -21,10 +21,11 @@ end
|
|
21
21
|
|
22
22
|
require 'rake/testtask'
|
23
23
|
Rake::TestTask.new(:test) do |test|
|
24
|
-
test.libs << 'lib' << '
|
25
|
-
test.pattern = '
|
24
|
+
test.libs << 'lib' << 'spec'
|
25
|
+
test.pattern = 'spec/*_spec.rb'
|
26
26
|
test.verbose = true
|
27
27
|
end
|
28
|
+
task :spec => :test
|
28
29
|
|
29
30
|
begin
|
30
31
|
require 'rcov/rcovtask'
|
@@ -39,7 +40,7 @@ rescue LoadError
|
|
39
40
|
end
|
40
41
|
end
|
41
42
|
|
42
|
-
task :test => :check_dependencies
|
43
|
+
#task :test => :check_dependencies
|
43
44
|
|
44
45
|
task :default => :test
|
45
46
|
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.
|
1
|
+
1.0.0
|
data/lib/indextank_client.rb
CHANGED
@@ -139,6 +139,11 @@ module IndexTank
|
|
139
139
|
return r
|
140
140
|
end
|
141
141
|
|
142
|
+
def add_documents(data_array)
|
143
|
+
code, r = PUT "/docs", data_array
|
144
|
+
return r
|
145
|
+
end
|
146
|
+
|
142
147
|
def update_variables(docid, variables, options={})
|
143
148
|
options.merge!( :docid => docid, :variables => variables )
|
144
149
|
code, r = PUT "/docs/variables", options
|
@@ -2,11 +2,46 @@ namespace :tanker do
|
|
2
2
|
|
3
3
|
desc "Reindex all models"
|
4
4
|
task :reindex => :environment do
|
5
|
-
puts "
|
5
|
+
puts "reindexing all models"
|
6
6
|
load_models
|
7
7
|
Tanker::Utilities.reindex_all_models
|
8
8
|
end
|
9
9
|
|
10
|
+
desc "Update IndexTank functions"
|
11
|
+
task :functions => :environment do
|
12
|
+
puts "reindexing all IndexTank functions"
|
13
|
+
load_models
|
14
|
+
indexes = {}
|
15
|
+
Tanker::Utilities.get_model_classes.each do |model|
|
16
|
+
model.tanker_config.functions.each do |idx, definition|
|
17
|
+
indexes[model.tanker_config.index_name] ||= {}
|
18
|
+
indexes[model.tanker_config.index_name][idx] = definition
|
19
|
+
end
|
20
|
+
end
|
21
|
+
if indexes.blank?
|
22
|
+
puts <<-HELP
|
23
|
+
No IndexTank functions defined.
|
24
|
+
Define your server-side functions inside your model's tankit block like so:
|
25
|
+
tankit 'myindex' do
|
26
|
+
functions do
|
27
|
+
{
|
28
|
+
1 => "-age",
|
29
|
+
2 => "relevance / miles(d[0], d[1], q[0], q[1])"
|
30
|
+
}
|
31
|
+
end
|
32
|
+
end
|
33
|
+
HELP
|
34
|
+
else
|
35
|
+
indexes.each do |index_name, functions|
|
36
|
+
index = Tanker.api.get_index(index_name)
|
37
|
+
functions.each do |idx, definition|
|
38
|
+
index.add_function(idx, definition)
|
39
|
+
puts "Index #{index_name.inspect} function: #{idx} => #{definition.inspect}"
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
10
45
|
desc "Clear all Index Tank indexes"
|
11
46
|
task :clear_indexes => :environment do
|
12
47
|
puts "clearing all indexes"
|
data/lib/tanker/utilities.rb
CHANGED
@@ -6,7 +6,7 @@ module Tanker
|
|
6
6
|
end
|
7
7
|
|
8
8
|
def get_available_indexes
|
9
|
-
get_model_classes.map{|model| model.index_name}.uniq.compact
|
9
|
+
get_model_classes.map{|model| model.tanker_config.index_name}.uniq.compact
|
10
10
|
end
|
11
11
|
|
12
12
|
def clear_all_indexes
|
@@ -32,10 +32,7 @@ module Tanker
|
|
32
32
|
|
33
33
|
def reindex_all_models
|
34
34
|
get_model_classes.each do |klass|
|
35
|
-
|
36
|
-
klass.all.each do |model_instance|
|
37
|
-
model_instance.update_tank_indexes
|
38
|
-
end
|
35
|
+
klass.tanker_reindex
|
39
36
|
end
|
40
37
|
end
|
41
38
|
end
|
data/lib/tanker.rb
CHANGED
@@ -1,8 +1,12 @@
|
|
1
|
-
require "rubygems"
|
2
|
-
require "bundler"
|
3
1
|
|
4
|
-
|
2
|
+
begin
|
3
|
+
require "rubygems"
|
4
|
+
require "bundler"
|
5
5
|
|
6
|
+
Bundler.setup :default
|
7
|
+
rescue => e
|
8
|
+
puts "Tanker: #{e.message}"
|
9
|
+
end
|
6
10
|
require 'indextank_client'
|
7
11
|
require 'tanker/configuration'
|
8
12
|
require 'tanker/utilities'
|
@@ -10,7 +14,10 @@ require 'will_paginate/collection'
|
|
10
14
|
|
11
15
|
|
12
16
|
if defined? Rails
|
13
|
-
|
17
|
+
begin
|
18
|
+
require 'tanker/railtie'
|
19
|
+
rescue LoadError
|
20
|
+
end
|
14
21
|
end
|
15
22
|
|
16
23
|
module Tanker
|
@@ -33,8 +40,7 @@ module Tanker
|
|
33
40
|
@included_in << klass
|
34
41
|
@included_in.uniq!
|
35
42
|
|
36
|
-
|
37
|
-
klass.instance_variable_set('@tanker_indexes', [])
|
43
|
+
configuration # raises error if not defined
|
38
44
|
klass.send :include, InstanceMethods
|
39
45
|
klass.extend ClassMethods
|
40
46
|
|
@@ -42,62 +48,177 @@ module Tanker
|
|
42
48
|
define_method(:per_page) { 10 } unless respond_to?(:per_page)
|
43
49
|
end
|
44
50
|
end
|
51
|
+
|
52
|
+
def batch_update(records)
|
53
|
+
return false if records.empty?
|
54
|
+
data = records.map do |record|
|
55
|
+
options = record.tanker_index_options
|
56
|
+
options.merge!( :docid => record.it_doc_id, :fields => record.tanker_index_data )
|
57
|
+
options
|
58
|
+
end
|
59
|
+
records.first.class.tanker_index.add_documents(data)
|
60
|
+
end
|
61
|
+
|
62
|
+
def search(models, query, options = {})
|
63
|
+
ids = []
|
64
|
+
models = [models].flatten.uniq
|
65
|
+
page = (options.delete(:page) || 1).to_i
|
66
|
+
per_page = (options.delete(:per_page) || models.first.per_page).to_i
|
67
|
+
index = models.first.tanker_index
|
68
|
+
query = query.join(' ') if Array === query
|
69
|
+
|
70
|
+
if (index_names = models.map(&:tanker_config).map(&:index_name).uniq).size > 1
|
71
|
+
raise "You can't search across multiple indexes in one call (#{index_names.inspect})"
|
72
|
+
end
|
73
|
+
|
74
|
+
|
75
|
+
# move conditions into the query body
|
76
|
+
if conditions = options.delete(:conditions)
|
77
|
+
conditions.each do |field, value|
|
78
|
+
value = [value].flatten.compact
|
79
|
+
value.each do |item|
|
80
|
+
query += " #{field}:(#{item})"
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
# rephrase filter_functions
|
86
|
+
if filter_functions = options.delete(:filter_functions)
|
87
|
+
filter_functions.each do |function_number, ranges|
|
88
|
+
options[:"filter_function#{function_number}"] = ranges.map{|r|r.join(':')}.join(',')
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
# rephrase filter_docvars
|
93
|
+
if filter_docvars = options.delete(:filter_docvars)
|
94
|
+
filter_docvars.each do |var_number, ranges|
|
95
|
+
options[:"filter_docvar#{var_number}"] = ranges.map{|r|r.join(':')}.join(',')
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
query = "__any:(#{query.to_s}) __type:(#{models.map(&:name).join(' OR ')})"
|
100
|
+
options = { :start => per_page * (page - 1), :len => per_page }.merge(options)
|
101
|
+
results = index.search(query, options)
|
102
|
+
|
103
|
+
@entries = WillPaginate::Collection.create(page, per_page) do |pager|
|
104
|
+
# inject the result array into the paginated collection:
|
105
|
+
pager.replace instantiate_results(results)
|
106
|
+
|
107
|
+
unless pager.total_entries
|
108
|
+
# the pager didn't manage to guess the total count, do it manually
|
109
|
+
pager.total_entries = results["matches"]
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
protected
|
115
|
+
|
116
|
+
def instantiate_results(index_result)
|
117
|
+
results = index_result['results']
|
118
|
+
return [] if results.empty?
|
119
|
+
|
120
|
+
id_map = results.inject({}) do |acc, result|
|
121
|
+
model, id = result["docid"].split(" ", 2)
|
122
|
+
acc[model] ||= []
|
123
|
+
acc[model] << id.to_i
|
124
|
+
acc
|
125
|
+
end
|
126
|
+
|
127
|
+
if 1 == id_map.size # check for simple case, just one model involved
|
128
|
+
klass = constantize(id_map.keys.first)
|
129
|
+
# eager-load and return just this model's records
|
130
|
+
klass.find(id_map.values.flatten)
|
131
|
+
else # complex case, multiple models involved
|
132
|
+
id_map.each do |klass, ids|
|
133
|
+
# replace the id list with an eager-loaded list of records for this model
|
134
|
+
id_map[klass] = constantize(klass).find(ids)
|
135
|
+
end
|
136
|
+
# return them in order
|
137
|
+
results.map do |result|
|
138
|
+
model, id = result["docid"].split(" ", 2)
|
139
|
+
id_map[model].detect {|record| id.to_i == record.id }
|
140
|
+
end
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
def constantize(klass_name)
|
145
|
+
Object.const_defined?(klass_name) ?
|
146
|
+
Object.const_get(klass_name) :
|
147
|
+
Object.const_missing(klass_name)
|
148
|
+
end
|
45
149
|
end
|
46
150
|
|
47
151
|
# these are the class methods added when Tanker is included
|
152
|
+
# They're kept to a minimum to prevent namespace pollution
|
48
153
|
module ClassMethods
|
49
154
|
|
50
|
-
|
155
|
+
attr_accessor :tanker_config
|
51
156
|
|
52
157
|
def tankit(name, &block)
|
53
158
|
if block_given?
|
54
|
-
|
55
|
-
self.instance_exec(&block)
|
159
|
+
self.tanker_config = ModelConfig.new(name, block)
|
56
160
|
else
|
57
161
|
raise(NoBlockGiven, 'Please provide a block')
|
58
162
|
end
|
59
163
|
end
|
60
164
|
|
61
|
-
def
|
62
|
-
|
165
|
+
def search_tank(query, options = {})
|
166
|
+
Tanker.search([self], query, options)
|
63
167
|
end
|
64
168
|
|
65
|
-
def
|
66
|
-
|
169
|
+
def tanker_index
|
170
|
+
tanker_config.index
|
67
171
|
end
|
68
172
|
|
69
|
-
def
|
70
|
-
|
71
|
-
page = options.delete(:page) || 1
|
72
|
-
per_page = options.delete(:per_page) || self.per_page
|
173
|
+
def tanker_reindex(options = {})
|
174
|
+
puts "Indexing #{self} model"
|
73
175
|
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
end
|
79
|
-
end
|
176
|
+
batches = []
|
177
|
+
options[:batch_size] ||= 200
|
178
|
+
records = options[:scope] ? send(options[:scope]).all : all
|
179
|
+
record_size = records.size
|
80
180
|
|
81
|
-
|
82
|
-
|
83
|
-
|
181
|
+
records.each_with_index do |model_instance, idx|
|
182
|
+
batch_num = idx / options[:batch_size]
|
183
|
+
(batches[batch_num] ||= []) << model_instance
|
184
|
+
end
|
84
185
|
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
[]
|
186
|
+
timer = Time.now
|
187
|
+
batches.each_with_index do |batch, idx|
|
188
|
+
Tanker.batch_update(batch)
|
189
|
+
puts "Indexed #{batch.size} records #{(idx * options[:batch_size]) + batch.size}/#{record_size}"
|
89
190
|
end
|
191
|
+
puts "Indexed #{record_size} #{self} records in #{Time.now - timer} seconds"
|
192
|
+
end
|
193
|
+
end
|
90
194
|
|
91
|
-
|
92
|
-
|
93
|
-
# inject the result array into the paginated collection:
|
94
|
-
pager.replace(result)
|
195
|
+
class ModelConfig
|
196
|
+
attr_reader :index_name
|
95
197
|
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
198
|
+
def initialize(index_name, block)
|
199
|
+
@index_name = index_name
|
200
|
+
@indexes = []
|
201
|
+
@functions = {}
|
202
|
+
instance_exec &block
|
203
|
+
end
|
204
|
+
|
205
|
+
def indexes(field = nil, &block)
|
206
|
+
@indexes << [field, block] if field
|
207
|
+
@indexes
|
208
|
+
end
|
209
|
+
|
210
|
+
def variables(&block)
|
211
|
+
@variables = block if block
|
212
|
+
@variables
|
213
|
+
end
|
214
|
+
|
215
|
+
def functions(&block)
|
216
|
+
@functions = block.call if block
|
217
|
+
@functions
|
218
|
+
end
|
219
|
+
|
220
|
+
def index
|
221
|
+
@index ||= Tanker.api.get_index(index_name)
|
101
222
|
end
|
102
223
|
|
103
224
|
end
|
@@ -105,28 +226,58 @@ module Tanker
|
|
105
226
|
# these are the instance methods included
|
106
227
|
module InstanceMethods
|
107
228
|
|
229
|
+
def tanker_config
|
230
|
+
self.class.tanker_config || raise(NotConfigured, "Please configure Tanker for #{self.class.inspect} with the 'tankit' block")
|
231
|
+
end
|
232
|
+
|
108
233
|
def tanker_indexes
|
109
|
-
|
234
|
+
tanker_config.indexes
|
235
|
+
end
|
236
|
+
|
237
|
+
def tanker_variables
|
238
|
+
tanker_config.variables
|
110
239
|
end
|
111
240
|
|
112
241
|
# update a create instance from index tank
|
113
242
|
def update_tank_indexes
|
243
|
+
tanker_config.index.add_document(
|
244
|
+
it_doc_id, tanker_index_data, tanker_index_options
|
245
|
+
)
|
246
|
+
end
|
247
|
+
|
248
|
+
# delete instance from index tank
|
249
|
+
def delete_tank_indexes
|
250
|
+
tanker_config.index.delete_document(it_doc_id)
|
251
|
+
end
|
252
|
+
|
253
|
+
def tanker_index_data
|
114
254
|
data = {}
|
115
255
|
|
116
|
-
|
117
|
-
|
118
|
-
data[
|
256
|
+
# attempt to autodetect timestamp
|
257
|
+
if respond_to?(:created_at)
|
258
|
+
data[:timestamp] = created_at.to_i
|
119
259
|
end
|
120
260
|
|
121
|
-
|
261
|
+
tanker_indexes.each do |field, block|
|
262
|
+
val = block ? instance_exec(&block) : send(field)
|
263
|
+
val = val.join(' ') if Array === val
|
264
|
+
data[field.to_sym] = val.to_s unless val.nil?
|
265
|
+
end
|
266
|
+
|
267
|
+
data[:__any] = data.values.sort_by{|v| v.to_s}.join " . "
|
122
268
|
data[:__type] = self.class.name
|
123
269
|
|
124
|
-
|
270
|
+
data
|
125
271
|
end
|
126
272
|
|
127
|
-
|
128
|
-
|
129
|
-
|
273
|
+
def tanker_index_options
|
274
|
+
options = {}
|
275
|
+
|
276
|
+
if tanker_variables
|
277
|
+
options[:variables] = instance_exec(&tanker_variables)
|
278
|
+
end
|
279
|
+
|
280
|
+
options
|
130
281
|
end
|
131
282
|
|
132
283
|
# create a unique index based on the model name and unique id
|
@@ -134,4 +285,4 @@ module Tanker
|
|
134
285
|
self.class.name + ' ' + self.id.to_s
|
135
286
|
end
|
136
287
|
end
|
137
|
-
end
|
288
|
+
end
|
data/spec/spec_helper.rb
CHANGED
@@ -16,28 +16,45 @@ class Dummy
|
|
16
16
|
|
17
17
|
end
|
18
18
|
|
19
|
+
$frozen_moment = Time.now
|
20
|
+
|
19
21
|
class Person
|
22
|
+
|
23
|
+
attr_accessor :name, :last_name
|
24
|
+
|
25
|
+
def initialize(attrs = {})
|
26
|
+
attrs.each {|k,v| self.send "#{k}=", v }
|
27
|
+
self.name ||= 'paco'
|
28
|
+
self.last_name ||= 'viramontes'
|
29
|
+
end
|
30
|
+
|
20
31
|
include Tanker
|
21
32
|
|
22
33
|
tankit 'people' do
|
23
34
|
indexes :name
|
24
35
|
indexes :last_name
|
36
|
+
variables do
|
37
|
+
{0 => 1.0,
|
38
|
+
1 => 20.0,
|
39
|
+
2 => 300.0}
|
40
|
+
end
|
25
41
|
end
|
26
42
|
|
27
|
-
def
|
28
|
-
|
43
|
+
def created_at
|
44
|
+
$frozen_moment
|
29
45
|
end
|
30
46
|
|
31
|
-
def
|
32
|
-
|
33
|
-
end
|
34
|
-
|
35
|
-
def last_name
|
36
|
-
'viramontes'
|
47
|
+
def id
|
48
|
+
1
|
37
49
|
end
|
38
50
|
end
|
39
51
|
|
40
52
|
class Dog
|
53
|
+
attr_accessor :name, :id
|
54
|
+
def initialize(attrs = {})
|
55
|
+
attrs.each {|k,v| self.send "#{k}=", v }
|
56
|
+
end
|
57
|
+
|
41
58
|
include Tanker
|
42
59
|
|
43
60
|
tankit 'animals' do
|
@@ -47,6 +64,11 @@ class Dog
|
|
47
64
|
end
|
48
65
|
|
49
66
|
class Cat
|
67
|
+
attr_accessor :name, :id
|
68
|
+
def initialize(attrs = {})
|
69
|
+
attrs.each {|k,v| self.send "#{k}=", v }
|
70
|
+
end
|
71
|
+
|
50
72
|
include Tanker
|
51
73
|
|
52
74
|
tankit 'animals' do
|
data/spec/tanker_spec.rb
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
require 'spec_helper'
|
1
|
+
require File.expand_path(File.join(File.dirname(__FILE__), 'spec_helper'))
|
2
2
|
|
3
3
|
describe Tanker do
|
4
4
|
|
@@ -33,7 +33,20 @@ describe Tanker do
|
|
33
33
|
end
|
34
34
|
|
35
35
|
dummy_instance = Dummy.new
|
36
|
-
dummy_instance.
|
36
|
+
dummy_instance.tanker_config.indexes.any? {|field, block| field == :field }.should == true
|
37
|
+
end
|
38
|
+
|
39
|
+
it 'should allow blocks for indexable field data' do
|
40
|
+
Tanker.configuration = {:url => 'http://api.indextank.com'}
|
41
|
+
Dummy.send(:include, Tanker)
|
42
|
+
Dummy.send(:tankit, 'dummy index') do
|
43
|
+
indexes :class_name do
|
44
|
+
self.class.name
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
dummy_instance = Dummy.new
|
49
|
+
dummy_instance.tanker_config.indexes.any? {|field, block| field == :class_name }.should == true
|
37
50
|
end
|
38
51
|
|
39
52
|
describe 'tanker instance' do
|
@@ -42,11 +55,11 @@ describe Tanker do
|
|
42
55
|
end
|
43
56
|
|
44
57
|
it 'should create a connexion to index tank' do
|
45
|
-
Person.
|
58
|
+
Person.tanker_index.class.should == IndexTank::IndexClient
|
46
59
|
end
|
47
60
|
|
48
|
-
it 'should be able to perform a seach query' do
|
49
|
-
Person.
|
61
|
+
it 'should be able to perform a seach query directly on the model' do
|
62
|
+
Person.tanker_index.should_receive(:search).and_return(
|
50
63
|
{
|
51
64
|
"matches" => 1,
|
52
65
|
"results" => [{
|
@@ -69,18 +82,129 @@ describe Tanker do
|
|
69
82
|
collection.current_page.should == 1
|
70
83
|
end
|
71
84
|
|
85
|
+
it 'should be able to use multi-value query phrases' do
|
86
|
+
Person.tanker_index.should_receive(:search).with(
|
87
|
+
"__any:(hey! location_id:(1) location_id:(2)) __type:(Person)",
|
88
|
+
{:start => 0, :len => 10}
|
89
|
+
).and_return({'results' => [], 'matches' => 0})
|
90
|
+
|
91
|
+
collection = Person.search_tank('hey!', :conditions => {:location_id => [1,2]})
|
92
|
+
end
|
93
|
+
|
94
|
+
it 'should be able to use filter_functions' do
|
95
|
+
Person.tanker_index.should_receive(:search).with(
|
96
|
+
"__any:(hey!) __type:(Person)",
|
97
|
+
{ :start => 0,
|
98
|
+
:len => 10,
|
99
|
+
:filter_function2 => "0:10,20:40"
|
100
|
+
}
|
101
|
+
).and_return({'results' => [], 'matches' => 0})
|
102
|
+
|
103
|
+
collection = Person.search_tank('hey!',
|
104
|
+
:filter_functions => {
|
105
|
+
2 => [[0,10], [20,40]]
|
106
|
+
})
|
107
|
+
end
|
108
|
+
it 'should be able to use filter_docvars' do
|
109
|
+
Person.tanker_index.should_receive(:search).with(
|
110
|
+
"__any:(hey!) __type:(Person)",
|
111
|
+
{ :start => 0,
|
112
|
+
:len => 10,
|
113
|
+
:filter_docvar3 => "*:7,80:100"
|
114
|
+
}
|
115
|
+
).and_return({'results' => [], 'matches' => 0})
|
116
|
+
|
117
|
+
collection = Person.search_tank('hey!',
|
118
|
+
:filter_docvars => {
|
119
|
+
3 => [['*',7], [80,100]]
|
120
|
+
})
|
121
|
+
end
|
122
|
+
|
123
|
+
it 'should be able to perform a seach query over several models' do
|
124
|
+
index = Tanker.api.get_index('animals')
|
125
|
+
Dog.should_receive(:tanker_index).and_return(index)
|
126
|
+
index.should_receive(:search).and_return(
|
127
|
+
{
|
128
|
+
"matches" => 2,
|
129
|
+
"results" => [{
|
130
|
+
"docid" => 'Dog 7',
|
131
|
+
"name" => 'fido'
|
132
|
+
},
|
133
|
+
{
|
134
|
+
"docid" => 'Cat 9',
|
135
|
+
"name" => 'fluffy'
|
136
|
+
}],
|
137
|
+
"search_time" => 1
|
138
|
+
}
|
139
|
+
)
|
140
|
+
|
141
|
+
Dog.should_receive(:find).and_return(
|
142
|
+
[Dog.new(:name => 'fido', :id => 7)]
|
143
|
+
)
|
144
|
+
Cat.should_receive(:find).and_return(
|
145
|
+
[Cat.new(:name => 'fluffy', :id => 9)]
|
146
|
+
)
|
147
|
+
|
148
|
+
collection = Tanker.search([Dog, Cat], 'hey!')
|
149
|
+
collection.class.should == WillPaginate::Collection
|
150
|
+
collection.total_entries.should == 2
|
151
|
+
collection.total_pages.should == 1
|
152
|
+
collection.per_page.should == 10
|
153
|
+
collection.current_page.should == 1
|
154
|
+
end
|
155
|
+
|
72
156
|
it 'should be able to update the index' do
|
73
|
-
person = Person.new
|
157
|
+
person = Person.new(:name => 'Name', :last_name => 'Last Name')
|
74
158
|
|
75
|
-
Person.
|
159
|
+
Person.tanker_index.should_receive(:add_document).with(
|
160
|
+
Person.new.it_doc_id,
|
161
|
+
{
|
162
|
+
:__any => "#{$frozen_moment.to_i} . Last Name . Name",
|
163
|
+
:__type => 'Person',
|
164
|
+
:name => 'Name',
|
165
|
+
:last_name => 'Last Name',
|
166
|
+
:timestamp => $frozen_moment.to_i
|
167
|
+
},
|
168
|
+
{
|
169
|
+
:variables => {
|
170
|
+
0 => 1.0,
|
171
|
+
1 => 20.0,
|
172
|
+
2 => 300.0
|
173
|
+
}
|
174
|
+
}
|
175
|
+
)
|
76
176
|
|
77
177
|
person.update_tank_indexes
|
78
178
|
end
|
79
179
|
|
80
|
-
it 'should be able to
|
180
|
+
it 'should be able to batch update the index' do
|
181
|
+
person = Person.new(:name => 'Name', :last_name => 'Last Name')
|
182
|
+
|
183
|
+
Person.tanker_index.should_receive(:add_documents).with(
|
184
|
+
[ {
|
185
|
+
:docid => Person.new.it_doc_id,
|
186
|
+
:fields => {
|
187
|
+
:__any => "#{$frozen_moment.to_i} . Last Name . Name",
|
188
|
+
:__type => 'Person',
|
189
|
+
:name => 'Name',
|
190
|
+
:last_name => 'Last Name',
|
191
|
+
:timestamp => $frozen_moment.to_i
|
192
|
+
},
|
193
|
+
:variables => {
|
194
|
+
0 => 1.0,
|
195
|
+
1 => 20.0,
|
196
|
+
2 => 300.0
|
197
|
+
}
|
198
|
+
} ]
|
199
|
+
)
|
200
|
+
|
201
|
+
Tanker.batch_update([person])
|
202
|
+
end
|
203
|
+
|
204
|
+
it 'should be able to delete the document from the index' do
|
81
205
|
person = Person.new
|
82
206
|
|
83
|
-
Person.
|
207
|
+
Person.tanker_index.should_receive(:delete_document)
|
84
208
|
|
85
209
|
person.delete_tank_indexes
|
86
210
|
end
|
data/spec/utilities_spec.rb
CHANGED
data/tanker.gemspec
CHANGED
@@ -1,65 +1,62 @@
|
|
1
1
|
# Generated by jeweler
|
2
2
|
# DO NOT EDIT THIS FILE DIRECTLY
|
3
|
-
# Instead, edit Jeweler::Tasks in Rakefile, and run
|
3
|
+
# Instead, edit Jeweler::Tasks in Rakefile, and run 'rake gemspec'
|
4
4
|
# -*- encoding: utf-8 -*-
|
5
5
|
|
6
6
|
Gem::Specification.new do |s|
|
7
7
|
s.name = %q{tanker}
|
8
|
-
s.version = "0.
|
8
|
+
s.version = "1.0.0"
|
9
9
|
|
10
10
|
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
11
|
-
s.authors = ["@kidpollo"]
|
12
|
-
s.date = %q{2011-
|
11
|
+
s.authors = ["@kidpollo", "Jack Danger Canty"]
|
12
|
+
s.date = %q{2011-04-02}
|
13
13
|
s.description = %q{IndexTank is a great search indexing service, this gem tries to make any orm keep in sync with indextank with ease}
|
14
14
|
s.email = %q{kidpollo@gmail.com}
|
15
15
|
s.extra_rdoc_files = [
|
16
16
|
"LICENSE",
|
17
|
-
|
17
|
+
"README.rdoc"
|
18
18
|
]
|
19
19
|
s.files = [
|
20
20
|
".document",
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
"spec/utilities_spec.rb",
|
38
|
-
"tanker.gemspec"
|
21
|
+
"Gemfile",
|
22
|
+
"LICENSE",
|
23
|
+
"README.rdoc",
|
24
|
+
"Rakefile",
|
25
|
+
"VERSION",
|
26
|
+
"autotest/discover.rb",
|
27
|
+
"lib/indextank_client.rb",
|
28
|
+
"lib/tanker.rb",
|
29
|
+
"lib/tanker/configuration.rb",
|
30
|
+
"lib/tanker/railtie.rb",
|
31
|
+
"lib/tanker/tasks/tanker.rake",
|
32
|
+
"lib/tanker/utilities.rb",
|
33
|
+
"spec/spec_helper.rb",
|
34
|
+
"spec/tanker_spec.rb",
|
35
|
+
"spec/utilities_spec.rb",
|
36
|
+
"tanker.gemspec"
|
39
37
|
]
|
40
38
|
s.homepage = %q{http://github.com/kidpollo/tanker}
|
41
|
-
s.rdoc_options = ["--charset=UTF-8"]
|
42
39
|
s.require_paths = ["lib"]
|
43
|
-
s.rubygems_version = %q{1.
|
40
|
+
s.rubygems_version = %q{1.7.1}
|
44
41
|
s.summary = %q{IndexTank integration to your favorite orm}
|
45
42
|
s.test_files = [
|
46
43
|
"spec/spec_helper.rb",
|
47
|
-
|
48
|
-
|
44
|
+
"spec/tanker_spec.rb",
|
45
|
+
"spec/utilities_spec.rb"
|
49
46
|
]
|
50
47
|
|
51
48
|
if s.respond_to? :specification_version then
|
52
49
|
s.specification_version = 3
|
53
50
|
|
54
51
|
if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
|
55
|
-
s.
|
52
|
+
s.add_runtime_dependency(%q<jeweler>, [">= 0"])
|
56
53
|
s.add_runtime_dependency(%q<will_paginate>, [">= 2.3.15"])
|
57
54
|
else
|
58
|
-
s.add_dependency(%q<
|
55
|
+
s.add_dependency(%q<jeweler>, [">= 0"])
|
59
56
|
s.add_dependency(%q<will_paginate>, [">= 2.3.15"])
|
60
57
|
end
|
61
58
|
else
|
62
|
-
s.add_dependency(%q<
|
59
|
+
s.add_dependency(%q<jeweler>, [">= 0"])
|
63
60
|
s.add_dependency(%q<will_paginate>, [">= 2.3.15"])
|
64
61
|
end
|
65
62
|
end
|
metadata
CHANGED
@@ -1,56 +1,39 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: tanker
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
hash: 7
|
5
4
|
prerelease:
|
6
|
-
|
7
|
-
- 0
|
8
|
-
- 5
|
9
|
-
- 6
|
10
|
-
version: 0.5.6
|
5
|
+
version: 1.0.0
|
11
6
|
platform: ruby
|
12
7
|
authors:
|
13
8
|
- "@kidpollo"
|
9
|
+
- Jack Danger Canty
|
14
10
|
autorequire:
|
15
11
|
bindir: bin
|
16
12
|
cert_chain: []
|
17
13
|
|
18
|
-
date: 2011-
|
19
|
-
default_executable:
|
14
|
+
date: 2011-04-02 00:00:00 Z
|
20
15
|
dependencies:
|
21
16
|
- !ruby/object:Gem::Dependency
|
22
|
-
name:
|
23
|
-
prerelease: false
|
17
|
+
name: jeweler
|
24
18
|
requirement: &id001 !ruby/object:Gem::Requirement
|
25
19
|
none: false
|
26
20
|
requirements:
|
27
21
|
- - ">="
|
28
22
|
- !ruby/object:Gem::Version
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
- 0
|
33
|
-
- 0
|
34
|
-
- beta
|
35
|
-
- 22
|
36
|
-
version: 2.0.0.beta.22
|
37
|
-
type: :development
|
23
|
+
version: "0"
|
24
|
+
type: :runtime
|
25
|
+
prerelease: false
|
38
26
|
version_requirements: *id001
|
39
27
|
- !ruby/object:Gem::Dependency
|
40
28
|
name: will_paginate
|
41
|
-
prerelease: false
|
42
29
|
requirement: &id002 !ruby/object:Gem::Requirement
|
43
30
|
none: false
|
44
31
|
requirements:
|
45
32
|
- - ">="
|
46
33
|
- !ruby/object:Gem::Version
|
47
|
-
hash: 29
|
48
|
-
segments:
|
49
|
-
- 2
|
50
|
-
- 3
|
51
|
-
- 15
|
52
34
|
version: 2.3.15
|
53
35
|
type: :runtime
|
36
|
+
prerelease: false
|
54
37
|
version_requirements: *id002
|
55
38
|
description: IndexTank is a great search indexing service, this gem tries to make any orm keep in sync with indextank with ease
|
56
39
|
email: kidpollo@gmail.com
|
@@ -63,9 +46,7 @@ extra_rdoc_files:
|
|
63
46
|
- README.rdoc
|
64
47
|
files:
|
65
48
|
- .document
|
66
|
-
- .gitignore
|
67
49
|
- Gemfile
|
68
|
-
- Gemfile.lock
|
69
50
|
- LICENSE
|
70
51
|
- README.rdoc
|
71
52
|
- Rakefile
|
@@ -81,13 +62,12 @@ files:
|
|
81
62
|
- spec/tanker_spec.rb
|
82
63
|
- spec/utilities_spec.rb
|
83
64
|
- tanker.gemspec
|
84
|
-
has_rdoc: true
|
85
65
|
homepage: http://github.com/kidpollo/tanker
|
86
66
|
licenses: []
|
87
67
|
|
88
68
|
post_install_message:
|
89
|
-
rdoc_options:
|
90
|
-
|
69
|
+
rdoc_options: []
|
70
|
+
|
91
71
|
require_paths:
|
92
72
|
- lib
|
93
73
|
required_ruby_version: !ruby/object:Gem::Requirement
|
@@ -95,23 +75,17 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
95
75
|
requirements:
|
96
76
|
- - ">="
|
97
77
|
- !ruby/object:Gem::Version
|
98
|
-
hash: 3
|
99
|
-
segments:
|
100
|
-
- 0
|
101
78
|
version: "0"
|
102
79
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
103
80
|
none: false
|
104
81
|
requirements:
|
105
82
|
- - ">="
|
106
83
|
- !ruby/object:Gem::Version
|
107
|
-
hash: 3
|
108
|
-
segments:
|
109
|
-
- 0
|
110
84
|
version: "0"
|
111
85
|
requirements: []
|
112
86
|
|
113
87
|
rubyforge_project:
|
114
|
-
rubygems_version: 1.
|
88
|
+
rubygems_version: 1.7.1
|
115
89
|
signing_key:
|
116
90
|
specification_version: 3
|
117
91
|
summary: IndexTank integration to your favorite orm
|
data/.gitignore
DELETED
data/Gemfile.lock
DELETED
@@ -1,32 +0,0 @@
|
|
1
|
-
GEM
|
2
|
-
remote: http://rubygems.org/
|
3
|
-
specs:
|
4
|
-
diff-lcs (1.1.2)
|
5
|
-
gemcutter (0.6.1)
|
6
|
-
git (1.2.5)
|
7
|
-
jeweler (1.4.0)
|
8
|
-
gemcutter (>= 0.1.0)
|
9
|
-
git (>= 1.2.5)
|
10
|
-
rubyforge (>= 2.0.0)
|
11
|
-
json_pure (1.4.6)
|
12
|
-
rspec (2.0.0.beta.22)
|
13
|
-
rspec-core (= 2.0.0.beta.22)
|
14
|
-
rspec-expectations (= 2.0.0.beta.22)
|
15
|
-
rspec-mocks (= 2.0.0.beta.22)
|
16
|
-
rspec-core (2.0.0.beta.22)
|
17
|
-
rspec-expectations (2.0.0.beta.22)
|
18
|
-
diff-lcs (>= 1.1.2)
|
19
|
-
rspec-mocks (2.0.0.beta.22)
|
20
|
-
rspec-core (= 2.0.0.beta.22)
|
21
|
-
rspec-expectations (= 2.0.0.beta.22)
|
22
|
-
rubyforge (2.0.4)
|
23
|
-
json_pure (>= 1.1.7)
|
24
|
-
will_paginate (2.3.15)
|
25
|
-
|
26
|
-
PLATFORMS
|
27
|
-
ruby
|
28
|
-
|
29
|
-
DEPENDENCIES
|
30
|
-
jeweler
|
31
|
-
rspec (>= 2.0.0.beta.22)
|
32
|
-
will_paginate (>= 2.3.15)
|