tanker 1.1.3 → 1.1.4

Sign up to get free protection for your applications and to get access to all the features.
data/README.rdoc CHANGED
@@ -54,6 +54,9 @@ or
54
54
 
55
55
  # just include the Tanker module
56
56
  include Tanker
57
+
58
+ # define a scope
59
+ scope :awesome, :conditions => {:title => 'Awesome'}
57
60
 
58
61
  # define the index by supplying the index name and the fields to index
59
62
  # this is the index name you create in the Index Tank dashboard
@@ -64,11 +67,17 @@ or
64
67
  indexes :content
65
68
  indexes :id
66
69
  indexes :tag_list #NOTICE this is an array of Tags! Awesome!
70
+ indexes :category, :category => true # make attributes also be categories (facets)
67
71
 
68
72
  # you may also dynamically retrieve field data
69
73
  indexes :author_names do
70
74
  authors.map {|author| author.name }
71
75
  end
76
+
77
+ # you cal also dynamically set categories
78
+ category :content_length do
79
+ content.length
80
+ end
72
81
 
73
82
  # to pass in a list of variables with your document,
74
83
  # supply a hash with the variable integers as keys:
@@ -115,6 +124,14 @@ Search Topics
115
124
  @topics = Topic.search_tank('blah', :conditions => {:tag_list => 'tag1'}) # Gets 2 results!
116
125
  @topics = Topic.search_tank('blah', :conditions => {:title => 'Awesome', :tag_list => 'tag1'}) # Gets 1 result!
117
126
 
127
+ Search with scope (Only tested on ActiveRecord)
128
+
129
+ @topics = Topic.awesome.search_tank('blah') # Gets 1 result!
130
+
131
+ Search with wildcards!
132
+
133
+ @topics = Topic.search_tank('Awe*', :page => 1, :per_page => 10) # Gets 1 result!
134
+
118
135
  Search multiple models
119
136
 
120
137
  @results = Tanker.search([Topic, Post], 'blah') # Gets any Topic OR Post results that match!
@@ -333,11 +350,6 @@ Or you can restrict indexing to a subset of records. This is particularly helpfu
333
350
 
334
351
  Model.tanker_reindex(:batch_size => 1000, :scope => :not_deleted) # will call Model.not_deleted.all internally
335
352
 
336
-
337
- == TODO
338
- * Documentation
339
- * More fancy stuff (Suggestions?)
340
-
341
353
  == Note on Patches/Pull Requests
342
354
 
343
355
  * Fork the project.
@@ -348,18 +360,34 @@ Or you can restrict indexing to a subset of records. This is particularly helpfu
348
360
  (if you want to have your own version, that is fine but bump version in a commit by itself I can ignore when I pull)
349
361
  * Send me a pull request. Bonus points for topic branches.
350
362
 
363
+ == Note on testing
364
+
365
+ Tanker is has a full suite of unit tests that cover configuration and the api of the library but it also inludes a battery of Integration tests!
366
+ These tests spec the behabiero between Index Tank and an ActiveRecord model with a Sqlite3 Backend.
367
+
368
+ If you want to run the specs you will need to provide an Index Tank Api key and user in the +spec/integration_spec_conf.rb+ file.
369
+ This file is ignored from the repo but you'll find an example file in the same path.
351
370
 
352
371
  == Acknowledgements
353
372
  The gem is based on a couple of projects:
354
373
 
355
- *{redis-textsearch}[http://github.com/nateware/redis-textsearch]
356
- *{mongomapper}[http://github.com/jnunemaker/mongomapper]
357
- *{thinkingtank}[http://github.com/flaptor/thinkingtank]
358
- *{will_paginate}[http://github.com/mislav/will_paginate]
374
+ * {redis-textsearch}[http://github.com/nateware/redis-textsearch]
375
+ * {mongomapper}[http://github.com/jnunemaker/mongomapper]
376
+ * {thinkingtank}[http://github.com/flaptor/thinkingtank]
377
+ * {will_paginate}[http://github.com/mislav/will_paginate]
359
378
 
360
379
  Kudos to this awesome projects and developers
361
380
 
381
+ There is a growing list of colabrators to this Gem so I would briefly mention them so if you know them and use this gem buy them a beer!
382
+
383
+ * {@JackDanger}[http://github.com/JackDanger]
384
+ * {@edgarjs}[http://github.com/edgarjs]
385
+ * {@slothbear}[http://github.com/slothbear]
386
+ * {@dabit}[http://github.com/dabit]
387
+ * {@coryschires}[http://github.com/coryschires]
388
+ * {@arvida}[http://github.com/arvida]
389
+ * {@joeljunstrom}[http://github.com/joeljunstrom]
362
390
 
363
391
  == Copyright
364
392
 
365
- Copyright (c) 2010 @kidpollo. See LICENSE for details.
393
+ Copyright (c) 2011 @kidpollo. See LICENSE for details.
data/Rakefile CHANGED
@@ -18,30 +18,13 @@ rescue LoadError
18
18
  puts "Jeweler (or a dependency) not available. Install it with: gem install jeweler"
19
19
  end
20
20
 
21
- require 'rake/testtask'
22
- Rake::TestTask.new(:test) do |test|
23
- test.libs << 'lib' << 'spec'
24
- test.pattern = 'spec/*_spec.rb'
25
- test.verbose = true
21
+ require "rspec/core/rake_task"
22
+ # RSpec 2.0
23
+ RSpec::Core::RakeTask.new(:spec) do |spec|
24
+ spec.pattern = 'spec/*_spec.rb'
25
+ spec.rspec_opts = ['--backtrace']
26
26
  end
27
- task :spec => :test
28
-
29
- begin
30
- require 'rcov/rcovtask'
31
- Rcov::RcovTask.new do |test|
32
- test.libs << 'test'
33
- test.pattern = 'test/**/test_*.rb'
34
- test.verbose = true
35
- end
36
- rescue LoadError
37
- task :rcov do
38
- abort "RCov is not available. In order to run rcov, you must: sudo gem install spicycode-rcov"
39
- end
40
- end
41
-
42
- #task :test => :check_dependencies
43
-
44
- task :default => :test
27
+ task :default => :spec
45
28
 
46
29
  require 'rake/rdoctask'
47
30
  Rake::RDocTask.new do |rdoc|
data/VERSION CHANGED
@@ -1 +1 @@
1
- 1.1.3
1
+ 1.1.4
@@ -30,9 +30,10 @@ module Tanker
30
30
 
31
31
  class << self
32
32
  def create(results, total_hits, options = {})
33
- new(results, options[:per_page], options[:page]-1, total_hits)
33
+ instance = new(results, options[:per_page], options[:page]-1, total_hits)
34
+ instance
34
35
  end
35
36
  end
36
37
  end
37
38
  end
38
- end
39
+ end
@@ -12,4 +12,4 @@ module Tanker
12
12
  end
13
13
  end
14
14
  end
15
- end
15
+ end
@@ -4,13 +4,26 @@ module Tanker
4
4
  autoload :WillPaginate, 'tanker/pagination/will_paginate'
5
5
  autoload :Kaminari, 'tanker/pagination/kaminari'
6
6
 
7
- def self.create(results, total_hits, options = {})
7
+ def self.create(results, total_hits, options = {}, categories = {})
8
8
  begin
9
9
  backend = Tanker.configuration[:pagination_backend].to_s.gsub(/\/(.?)/) { "::#{$1.upcase}" }.gsub(/(?:^|_)(.)/) { $1.upcase } # classify pagination backend name
10
- Object.const_get(:Tanker).const_get(:Pagination).const_get(backend).create(results, total_hits, options)
10
+ page = Object.const_get(:Tanker).const_get(:Pagination).const_get(backend).create(results, total_hits, options)
11
+ page.extend Categories
12
+ page.categories = categories
13
+ page
11
14
  rescue NameError
12
15
  raise(BadConfiguration, "Unknown pagination backend")
13
16
  end
14
17
  end
18
+
19
+ module Categories
20
+ def categories
21
+ @categories
22
+ end
23
+
24
+ def categories=(val)
25
+ @categories = val
26
+ end
27
+ end
15
28
  end
16
- end
29
+ end
data/lib/tanker.rb CHANGED
@@ -103,18 +103,22 @@ module Tanker
103
103
  options[:fetch] += ",#{fetch.join(',')}" if fetch
104
104
  options[:snippet] = snippets.join(',') if snippets
105
105
 
106
+ # convert category_filters to a json string
107
+ options[:category_filters] = options[:category_filters].to_json if options[:category_filters]
108
+
106
109
  search_on_fields = models.map{|model| model.tanker_config.indexes.map{|arr| arr[0]}.uniq}.flatten.uniq.join(":(#{query.to_s}) OR ")
107
110
 
108
- query = "#{search_on_fields}:(#{query.to_s}) OR __any:(#{query.to_s}) __type:(#{models.map(&:name).map {|name| "\"#{name.split('::').join(' ')}\"" }.join(' OR ')})"
111
+ query = "(#{search_on_fields}:(#{query.to_s}) OR __any:(#{query.to_s})) __type:(#{models.map(&:name).map {|name| "\"#{name.split('::').join(' ')}\"" }.join(' OR ')})"
109
112
  options = { :start => paginate[:per_page] * (paginate[:page] - 1), :len => paginate[:per_page] }.merge(options) if paginate
110
113
  results = index.search(query, options)
114
+ categories = results['facets'] if results.has_key?('facets')
111
115
 
112
116
  instantiated_results = if (fetch || snippets)
113
117
  instantiate_results_from_results(results, fetch, snippets)
114
118
  else
115
119
  instantiate_results_from_db(results)
116
120
  end
117
- paginate === false ? instantiated_results : Pagination.create(instantiated_results, results['matches'], paginate)
121
+ paginate === false ? instantiated_results : Pagination.create(instantiated_results, results['matches'], paginate, categories)
118
122
  end
119
123
 
120
124
  protected
@@ -133,13 +137,19 @@ module Tanker
133
137
 
134
138
  id_map.each do |klass, ids|
135
139
  # replace the id list with an eager-loaded list of records for this model
136
- id_map[klass] = constantize(klass).find(ids)
140
+ klass_const = constantize(klass)
141
+ if klass_const.respond_to?('find_all_by_id')
142
+ id_map[klass] = klass_const.find_all_by_id(ids)
143
+ else
144
+ id_map[klass] = klass_const.find(ids)
145
+ end
137
146
  end
138
147
  # return them in order
139
- results.map do |result|
148
+ results = results.map do |result|
140
149
  model, id = result["__type"], result["__id"]
141
150
  id_map[model].detect {|record| id == record.id.to_s }
142
151
  end
152
+ results.compact
143
153
  end
144
154
 
145
155
  def instantiate_results_from_results(index_result, fetch = false, snippets = false)
@@ -270,18 +280,33 @@ module Tanker
270
280
  attr_accessor :options
271
281
 
272
282
  def initialize(index_name, options, block)
273
- @index_name = index_name
274
- @options = options
275
- @indexes = []
276
- @variables = []
277
- @functions = {}
283
+ @index_name = index_name
284
+ @options = options
285
+ @indexes = []
286
+ @categories = []
287
+ @variables = []
288
+ @functions = {}
278
289
  instance_exec &block
279
290
  end
280
291
 
281
- def indexes(field = nil, &block)
282
- @indexes << [field, block] if field
292
+ def indexes(field = nil, options = {}, &block)
293
+ if field
294
+ @indexes << [field, block]
295
+ @categories << [field, block] if options[:category]
296
+ end
283
297
  @indexes
284
298
  end
299
+
300
+ def category(field = nil, options = {}, &block)
301
+ categories field, options, &block
302
+ end
303
+
304
+ def categories(field = nil, options = {}, &block)
305
+ if field
306
+ @categories << [field, block]
307
+ end
308
+ @categories
309
+ end
285
310
 
286
311
  def variables(&block)
287
312
  @variables << block if block
@@ -310,6 +335,10 @@ module Tanker
310
335
  tanker_config.indexes
311
336
  end
312
337
 
338
+ def tanker_categories
339
+ tanker_config.categories
340
+ end
341
+
313
342
  def tanker_variables
314
343
  tanker_config.variables
315
344
  end
@@ -333,7 +362,7 @@ module Tanker
333
362
  if respond_to?(:created_at)
334
363
  data[:timestamp] = created_at.to_i
335
364
  end
336
-
365
+
337
366
  tanker_indexes.each do |field, block|
338
367
  val = block ? instance_exec(&block) : send(field)
339
368
  val = val.join(' ') if Array === val
@@ -342,7 +371,7 @@ module Tanker
342
371
 
343
372
  data[:__any] = data.values.sort_by{|v| v.to_s}.join " . "
344
373
  data[:__type] = type_name
345
- data[:__id] = self.id
374
+ data[:__id] = self.id.is_a?(Fixnum) ? self.id : self.id.to_s
346
375
 
347
376
  data
348
377
  end
@@ -363,6 +392,15 @@ module Tanker
363
392
  end
364
393
  end
365
394
 
395
+ unless tanker_categories.empty?
396
+ options[:categories] = {}
397
+ tanker_categories.each do |field, block|
398
+ val = block ? instance_exec(&block) : send(field)
399
+ val = val.join(' ') if Array === val
400
+ options[:categories][field] = val.to_s unless val.nil?
401
+ end
402
+ end
403
+
366
404
  options
367
405
  end
368
406
 
@@ -21,19 +21,33 @@ ActiveRecord::Schema.define do
21
21
  t.string :tags
22
22
  t.text :description
23
23
  end
24
+ create_table :companies do |t|
25
+ t.string :name
26
+ end
24
27
  end
25
28
 
26
29
  class Product < ActiveRecord::Base
27
30
  include Tanker
28
31
 
32
+ scope :amazon, :conditions => {:href => "amazon"}
33
+
29
34
  tankit 'tanker_integration_tests' do
30
35
  indexes :name
31
- indexes :href
36
+ indexes :href, :category => true
32
37
  indexes :tags
33
38
  indexes :description
34
39
  end
35
40
  end
36
41
 
42
+ class Company < ActiveRecord::Base
43
+ include Tanker
44
+
45
+ tankit 'tanker_integration_tests' do
46
+ indexes :name
47
+ end
48
+ end
49
+
50
+
37
51
  describe 'An imaginary store' do
38
52
 
39
53
  before(:all) do
@@ -67,6 +81,9 @@ describe 'An imaginary store' do
67
81
  @products_in_database = Product.all
68
82
 
69
83
  Product.tanker_reindex
84
+
85
+ @apple = Company.create(:name => 'apple')
86
+ Company.tanker_reindex
70
87
  end
71
88
 
72
89
  describe 'basic searching' do
@@ -106,6 +123,11 @@ describe 'An imaginary store' do
106
123
  results.should include(@iphone)
107
124
  results.should have(1).product
108
125
  end
126
+
127
+ it "should not find a Company when searching Product" do
128
+ results = Product.search_tank("apple")
129
+ results.should_not include(@apple)
130
+ end
109
131
  end
110
132
 
111
133
  describe 'searching by tag' do
@@ -241,5 +263,32 @@ describe 'An imaginary store' do
241
263
  end
242
264
  end
243
265
 
266
+ describe 'categories' do
267
+ it 'should find categories for query features' do
268
+ @results = Product.search_tank('features', :snippets => [:description], :fetch => [:name, :href])
269
+ @results.categories.should == {"href"=>{"amazon"=>1, "apple"=>1}}
270
+ end
271
+
272
+ it 'should find categories for query decent' do
273
+ @results = Product.search_tank('decent', :snippets => [:description], :fetch => [:name, :href])
274
+ @results.categories.should == {"href"=>{"amazon"=>2, "google"=>2, "yahoo"=>3, "ebay"=>1}}
275
+ end
276
+
277
+ it 'should apply actegory filters to search on products filtered by yahoo' do
278
+ category_filters = {
279
+ 'href' => ['yahoo']
280
+ }
281
+ @results = Product.search_tank('decent', :snippets => [:description], :fetch => [:name, :href], :category_filters => category_filters )
282
+ @results.count.should == 3
283
+ end
284
+ end
285
+
286
+ describe 'on scope' do
287
+ it 'should return amazon product only', :focus => true do
288
+ results = Product.amazon.search_tank('decent')
289
+ results.should include(@samsung, @motorola)
290
+ results.should have_exactly(2).products
291
+ end
292
+ end
244
293
  end
245
294
 
data/spec/tanker_spec.rb CHANGED
@@ -60,11 +60,33 @@ describe Tanker do
60
60
  @dummy_class.send(:tankit, 'dummy index') do
61
61
  indexes :field
62
62
  end
63
-
63
+
64
64
  dummy_instance = @dummy_class.new
65
65
  dummy_instance.tanker_config.indexes.any? {|field, block| field == :field }.should == true
66
66
  end
67
67
 
68
+ it 'should allow set category values for indexable fields inline' do
69
+ @dummy_class.send(:tankit, 'dummy index') do
70
+ indexes :category, :category => true
71
+ end
72
+
73
+ dummy_instance = @dummy_class.new
74
+ dummy_instance.tanker_config.categories.any? {|field, block| field == :category }.should == true
75
+ end
76
+
77
+ it 'should allow set category values for indexable fields from dynamic attributes' do
78
+ @dummy_class.send(:tankit, 'dummy index') do
79
+ indexes :category, :category => true
80
+ category :category_2 do
81
+ 'blah'
82
+ end
83
+ end
84
+
85
+ dummy_instance = @dummy_class.new
86
+ dummy_instance.tanker_config.categories.any? {|field, block| field == :category }.should == true
87
+ dummy_instance.tanker_config.categories.any? {|field, block| field == :category_2 }.should == true
88
+ end
89
+
68
90
  it 'should allow blocks for indexable field data' do
69
91
  @dummy_class.send(:tankit, 'dummy index') do
70
92
  indexes :class_name do
@@ -207,7 +229,7 @@ describe Tanker do
207
229
  }
208
230
  )
209
231
 
210
- Person.should_receive(:find).and_return(
232
+ Person.should_receive(:find_all_by_id).and_return(
211
233
  [Person.new]
212
234
  )
213
235
 
@@ -238,7 +260,7 @@ describe Tanker do
238
260
  }
239
261
  )
240
262
 
241
- Person.should_receive(:find).with(['mystring1d', '1']).and_return(
263
+ Person.should_receive(:find_all_by_id).with(['mystring1d', '1']).and_return(
242
264
  [Person.new, Person.new]
243
265
  )
244
266
 
@@ -249,7 +271,7 @@ describe Tanker do
249
271
 
250
272
  it 'should be able to use multi-value query phrases' do
251
273
  Person.tanker_index.should_receive(:search).with(
252
- 'name:(hey! location_id:(1) location_id:(2)) OR last_name:(hey! location_id:(1) location_id:(2)) OR __any:(hey! location_id:(1) location_id:(2)) __type:("Person")',
274
+ '(name:(hey! location_id:(1) location_id:(2)) OR last_name:(hey! location_id:(1) location_id:(2)) OR __any:(hey! location_id:(1) location_id:(2))) __type:("Person")',
253
275
  anything
254
276
  ).and_return({'results' => [], 'matches' => 0})
255
277
 
@@ -301,10 +323,10 @@ describe Tanker do
301
323
  }
302
324
  )
303
325
 
304
- Dog.should_receive(:find).and_return(
326
+ Dog.should_receive(:find_all_by_id).and_return(
305
327
  [Dog.new(:name => 'fido', :id => 7)]
306
328
  )
307
- Cat.should_receive(:find).and_return(
329
+ Cat.should_receive(:find_all_by_id).and_return(
308
330
  [Cat.new(:name => 'fluffy', :id => 9)]
309
331
  )
310
332
 
@@ -328,7 +350,7 @@ describe Tanker do
328
350
  }]
329
351
  })
330
352
 
331
- Foo::Bar.should_receive(:find).and_return([stub(:id => 42)])
353
+ Foo::Bar.should_receive(:find_all_by_id).and_return([stub(:id => 42)])
332
354
 
333
355
  Foo::Bar.search_tank('bar')
334
356
  end
@@ -352,10 +374,10 @@ describe Tanker do
352
374
  }
353
375
  )
354
376
 
355
- Person.should_receive(:find).with(['1', '2']).and_return(
356
- [Person.new, Person.new]
377
+ Person.should_receive(:find_all_by_id).with(['1', '2']).and_return(
378
+ [Person.new(:id => 1), Person.new(:id => 2)]
357
379
  )
358
-
380
+
359
381
  collection = Person.search_tank('hey!', :paginate => false)
360
382
  collection.class.should == Array
361
383
  collection.size.should == 2
@@ -380,7 +402,7 @@ describe Tanker do
380
402
  }
381
403
  )
382
404
 
383
- Person.should_receive(:find).with(['1', '2']).and_return(
405
+ Person.should_receive(:find_all_by_id).with(['1', '2']).and_return(
384
406
  [Person.new, Person.new]
385
407
  )
386
408
 
@@ -396,7 +418,7 @@ describe Tanker do
396
418
  person = Person.new(:name => 'Name', :last_name => 'Last Name')
397
419
 
398
420
  Person.tanker_index.should_receive(:add_document).with(
399
- Person.new.it_doc_id,
421
+ Person.new(:id => 1).it_doc_id,
400
422
  {
401
423
  :__any => "#{$frozen_moment.to_i} . Last Name . Name",
402
424
  :__type => 'Person',
@@ -518,7 +540,7 @@ describe Tanker do
518
540
  "search_time" => 1
519
541
  }
520
542
  )
521
- Person.should_receive(:find).and_return([Person.new])
543
+ Person.should_receive(:find_all_by_id).and_return([Person.new])
522
544
 
523
545
  lambda { Person.search_tank('test') }.should raise_error(Tanker::BadConfiguration)
524
546
  end
@@ -538,7 +560,7 @@ describe Tanker do
538
560
  }
539
561
  )
540
562
 
541
- Person.should_receive(:find).and_return([Person.new])
563
+ Person.should_receive(:find_all_by_id).and_return([Person.new])
542
564
 
543
565
  array = Person.search_tank('hey!')
544
566
  array.class.should == Tanker::Pagination::Kaminari
@@ -548,4 +570,5 @@ describe Tanker do
548
570
  array.current_page.should == 1
549
571
  end
550
572
  end
573
+
551
574
  end
data/tanker.gemspec CHANGED
@@ -5,11 +5,11 @@
5
5
 
6
6
  Gem::Specification.new do |s|
7
7
  s.name = %q{tanker}
8
- s.version = "1.1.3"
8
+ s.version = "1.1.4"
9
9
 
10
10
  s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
11
  s.authors = [%q{Francisco Viramontes}, %q{Jack Danger Canty}]
12
- s.date = %q{2011-07-01}
12
+ s.date = %q{2011-08-31}
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 = [
metadata CHANGED
@@ -2,7 +2,7 @@
2
2
  name: tanker
3
3
  version: !ruby/object:Gem::Version
4
4
  prerelease:
5
- version: 1.1.3
5
+ version: 1.1.4
6
6
  platform: ruby
7
7
  authors:
8
8
  - Francisco Viramontes
@@ -11,7 +11,7 @@ autorequire:
11
11
  bindir: bin
12
12
  cert_chain: []
13
13
 
14
- date: 2011-07-01 00:00:00 Z
14
+ date: 2011-08-31 00:00:00 Z
15
15
  dependencies:
16
16
  - !ruby/object:Gem::Dependency
17
17
  name: json