tanker 0.5.6 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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)
|