sunspot 2.1.0 → 2.1.1

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.
Files changed (51) hide show
  1. checksums.yaml +7 -0
  2. data/Gemfile +4 -0
  3. data/History.txt +10 -0
  4. data/lib/sunspot.rb +6 -6
  5. data/lib/sunspot/batcher.rb +1 -1
  6. data/lib/sunspot/dsl.rb +1 -1
  7. data/lib/sunspot/dsl/field_query.rb +47 -7
  8. data/lib/sunspot/dsl/field_stats.rb +18 -0
  9. data/lib/sunspot/dsl/fields.rb +10 -1
  10. data/lib/sunspot/field.rb +15 -0
  11. data/lib/sunspot/field_factory.rb +33 -0
  12. data/lib/sunspot/indexer.rb +1 -0
  13. data/lib/sunspot/query.rb +1 -1
  14. data/lib/sunspot/query/common_query.rb +5 -0
  15. data/lib/sunspot/query/field_stats.rb +28 -0
  16. data/lib/sunspot/query/geofilt.rb +8 -3
  17. data/lib/sunspot/query/restriction.rb +5 -2
  18. data/lib/sunspot/query/sort.rb +35 -0
  19. data/lib/sunspot/search.rb +2 -1
  20. data/lib/sunspot/search/abstract_search.rb +57 -35
  21. data/lib/sunspot/search/field_facet.rb +4 -4
  22. data/lib/sunspot/search/field_stats.rb +21 -0
  23. data/lib/sunspot/search/stats_facet.rb +25 -0
  24. data/lib/sunspot/search/stats_row.rb +66 -0
  25. data/lib/sunspot/session.rb +6 -6
  26. data/lib/sunspot/session_proxy/class_sharding_session_proxy.rb +6 -4
  27. data/lib/sunspot/session_proxy/id_sharding_session_proxy.rb +16 -8
  28. data/lib/sunspot/setup.rb +6 -0
  29. data/lib/sunspot/version.rb +1 -1
  30. data/spec/api/class_set_spec.rb +3 -3
  31. data/spec/api/indexer/fixed_fields_spec.rb +5 -0
  32. data/spec/api/indexer/removal_spec.rb +13 -3
  33. data/spec/api/query/faceting_examples.rb +19 -0
  34. data/spec/api/query/join_spec.rb +19 -0
  35. data/spec/api/query/standard_spec.rb +1 -0
  36. data/spec/api/query/stats_examples.rb +66 -0
  37. data/spec/api/search/paginated_collection_spec.rb +1 -1
  38. data/spec/api/search/stats_spec.rb +94 -0
  39. data/spec/api/session_proxy/class_sharding_session_proxy_spec.rb +8 -2
  40. data/spec/api/session_proxy/id_sharding_session_proxy_spec.rb +14 -2
  41. data/spec/api/session_proxy/retry_5xx_session_proxy_spec.rb +3 -3
  42. data/spec/api/session_proxy/silent_fail_session_proxy_spec.rb +1 -1
  43. data/spec/helpers/search_helper.rb +30 -0
  44. data/spec/integration/highlighting_spec.rb +3 -3
  45. data/spec/integration/indexing_spec.rb +3 -2
  46. data/spec/integration/scoped_search_spec.rb +30 -0
  47. data/spec/integration/stats_spec.rb +47 -0
  48. data/spec/mocks/photo.rb +14 -1
  49. data/sunspot.gemspec +1 -2
  50. data/tasks/rdoc.rake +22 -14
  51. metadata +32 -41
@@ -45,23 +45,31 @@ module Sunspot
45
45
  #
46
46
  # See Sunspot.remove_by_id
47
47
  #
48
- def remove_by_id(clazz, id)
49
- session_for_index_id(
50
- Adapters::InstanceAdapter.index_id_for(clazz, id)
51
- ).remove_by_id(clazz, id)
48
+ def remove_by_id(clazz, *ids)
49
+ ids.flatten!
50
+ ids_by_session(clazz, ids).each do |session, ids|
51
+ session.remove_by_id(clazz, ids)
52
+ end
52
53
  end
53
54
 
54
55
  #
55
56
  # See Sunspot.remove_by_id!
56
57
  #
57
- def remove_by_id!(clazz, id)
58
- session_for_index_id(
59
- Adapters::InstanceAdapter.index_id_for(clazz, id)
60
- ).remove_by_id!(clazz, id)
58
+ def remove_by_id!(clazz, *ids)
59
+ ids.flatten!
60
+ ids_by_session(clazz, ids).each do |session, ids|
61
+ session.remove_by_id!(clazz, ids)
62
+ end
61
63
  end
62
64
 
63
65
  private
64
66
 
67
+ def ids_by_session(clazz, ids)
68
+ ids.group_by do |id|
69
+ session_for_index_id(Adapters::InstanceAdapter.index_id_for(clazz, id))
70
+ end
71
+ end
72
+
65
73
  def session_for_index_id(index_id)
66
74
  @sessions[id_hash(index_id) % @sessions.length]
67
75
  end
@@ -38,6 +38,12 @@ module Sunspot
38
38
  end
39
39
  end
40
40
 
41
+ def add_join_field_factory(name, type, options = {}, &block)
42
+ field_factory = FieldFactory::Join.new(name, type, options, &block)
43
+ @field_factories[field_factory.signature] = field_factory
44
+ @field_factories_cache[field_factory.name] = field_factory
45
+ end
46
+
41
47
  #
42
48
  # Add field_factories for fulltext search
43
49
  #
@@ -1,3 +1,3 @@
1
1
  module Sunspot
2
- VERSION = '2.1.0'
2
+ VERSION = '2.1.1'
3
3
  end
@@ -2,7 +2,7 @@ require "spec_helper"
2
2
 
3
3
  describe Sunspot::ClassSet do
4
4
  it "is enumerable" do
5
- class1, class2 = stub(:name => "Class1"), stub(:name => "Class2")
5
+ class1, class2 = double(:name => "Class1"), double(:name => "Class2")
6
6
 
7
7
  set = described_class.new
8
8
  set << class1 << class2
@@ -13,11 +13,11 @@ describe Sunspot::ClassSet do
13
13
  it "replaces classes with the same name" do
14
14
  set = described_class.new
15
15
 
16
- class1 = stub(:name => "Class1")
16
+ class1 = double(:name => "Class1")
17
17
  set << class1
18
18
  set.to_a.should == [class1]
19
19
 
20
- class1_dup = stub(:name => "Class1")
20
+ class1_dup = double(:name => "Class1")
21
21
  set << class1_dup
22
22
  set.to_a.should == [class1_dup]
23
23
  end
@@ -11,6 +11,11 @@ describe 'indexing fixed fields', :type => :indexer do
11
11
  connection.should have_add_with(:type => ['Post', 'SuperClass', 'MockRecord'])
12
12
  end
13
13
 
14
+ it 'should not index join fields' do
15
+ session.index PhotoContainer.new
16
+ connection.should_not have_add_with(:photo_caption => 'blah')
17
+ end
18
+
14
19
  it 'should index class name' do
15
20
  session.index post
16
21
  connection.should have_add_with(:class_name => 'Post')
@@ -11,10 +11,20 @@ describe 'document removal', :type => :indexer do
11
11
  connection.should have_delete('Post 1')
12
12
  end
13
13
 
14
- it 'removes an object by type and id and immediately commits' do
15
- connection.should_receive(:delete_by_id).with(['Post 1']).ordered
14
+ it 'removes an object by type and ids' do
15
+ session.remove_by_id(Post, 1, 2)
16
+ connection.should have_delete('Post 1', 'Post 2')
17
+ end
18
+
19
+ it 'removes an object by type and ids array' do
20
+ session.remove_by_id(Post, [1, 2])
21
+ connection.should have_delete('Post 1', 'Post 2')
22
+ end
23
+
24
+ it 'removes an object by type and ids and immediately commits' do
25
+ connection.should_receive(:delete_by_id).with(['Post 1', 'Post 2', 'Post 3']).ordered
16
26
  connection.should_receive(:commit).ordered
17
- session.remove_by_id!(Post, 1)
27
+ session.remove_by_id!(Post, 1, 2, 3)
18
28
  end
19
29
 
20
30
  it 'removes an object from the index and immediately commits' do
@@ -112,6 +112,25 @@ shared_examples_for "facetable query" do
112
112
  end.should raise_error(ArgumentError)
113
113
  end
114
114
 
115
+ it 'tags and excludes a geofilt in a field facet' do
116
+ search do
117
+ post_geo = with(:coordinates_new).in_radius(32, -68, 1)
118
+ facet(:coordinates_new, :exclude => post_geo) do
119
+ row(0..10) do
120
+ with(:coordinates_new).in_radius(32, -68, 10)
121
+ end
122
+ end
123
+ end
124
+ if connection.searches.last.has_key?(:"mlt.fl")
125
+ filter_tag = get_filter_tag('_query_:"{!geofilt sfield=coordinates_new_ll pt=32,-68 d=1}"')
126
+ else
127
+ filter_tag = get_filter_tag('{!geofilt sfield=coordinates_new_ll pt=32,-68 d=1}')
128
+ end
129
+ connection.should have_last_search_with(
130
+ :"facet.query" => "{!ex=#{filter_tag}}_query_:\"{!geofilt sfield=coordinates_new_ll pt=32,-68 d=10}\""
131
+ )
132
+ end
133
+
115
134
  it 'tags and excludes a scope filter in a field facet' do
116
135
  search do
117
136
  blog_filter = with(:blog_id, 1)
@@ -0,0 +1,19 @@
1
+ require File.expand_path('spec_helper', File.dirname(__FILE__))
2
+
3
+ describe 'join' do
4
+ it 'should search by join' do
5
+ session.search PhotoContainer do
6
+ with(:caption, 'blah')
7
+ end
8
+ connection.should have_last_search_including(
9
+ :fq, "{!join from=photo_container_id to=id}caption_s:blah")
10
+ end
11
+
12
+ it 'should greater_than search by join' do
13
+ session.search PhotoContainer do
14
+ with(:photo_rating).greater_than(3)
15
+ end
16
+ connection.should have_last_search_including(
17
+ :fq, "{!join from=photo_container_id to=id}average_rating_ft:{3\\.0 TO *}")
18
+ end
19
+ end
@@ -12,6 +12,7 @@ describe 'standard query', :type => :query do
12
12
  it_should_behave_like "query with text field scoping"
13
13
  it_should_behave_like "geohash query"
14
14
  it_should_behave_like "spatial query"
15
+ it_should_behave_like "stats query"
15
16
 
16
17
  it 'adds a no-op query to :q parameter when no :q provided' do
17
18
  session.search Post do
@@ -0,0 +1,66 @@
1
+ shared_examples_for 'stats query' do
2
+ it 'does not use stats unless requested' do
3
+ search
4
+ connection.should_not have_last_search_with(:stats)
5
+ end
6
+
7
+ it 'uses stats when requested' do
8
+ search do
9
+ stats :average_rating
10
+ end
11
+ connection.should have_last_search_with(:stats => true)
12
+ end
13
+
14
+ it 'requests single field stats' do
15
+ search do
16
+ stats :average_rating
17
+ end
18
+ connection.should have_last_search_with(:"stats.field" => %w{average_rating_ft})
19
+ end
20
+
21
+ it 'requests multiple field stats' do
22
+ search do
23
+ stats :average_rating, :published_at
24
+ end
25
+ connection.should have_last_search_with(:"stats.field" => %w{average_rating_ft published_at_dt})
26
+ end
27
+
28
+ it 'facets on a stats field' do
29
+ search do
30
+ stats :average_rating do
31
+ facet :featured
32
+ end
33
+ end
34
+ connection.should have_last_search_with(:"f.average_rating_ft.stats.facet" => %w{featured_bs})
35
+ end
36
+
37
+ it 'only facets on a stats field when requested' do
38
+ search do
39
+ stats :average_rating
40
+ end
41
+ connection.should_not have_last_search_with(:"f.average_rating_ft.stats.facet")
42
+ end
43
+
44
+ it 'facets on multiple stats fields' do
45
+ search do
46
+ stats :average_rating, :published_at do
47
+ facet :featured
48
+ end
49
+ end
50
+ connection.should have_last_search_with(
51
+ :"f.average_rating_ft.stats.facet" => %w{featured_bs},
52
+ :"f.published_at_dt.stats.facet" => %w{featured_bs}
53
+ )
54
+ end
55
+
56
+ it 'supports facets on stats field' do
57
+ search do
58
+ stats :average_rating do
59
+ facet :featured, :primary_category_id
60
+ end
61
+ end
62
+ connection.should have_last_search_with(
63
+ :"f.average_rating_ft.stats.facet" => %w{featured_bs primary_category_id_i}
64
+ )
65
+ end
66
+ end
@@ -7,7 +7,7 @@ describe "PaginatedCollection" do
7
7
 
8
8
  describe "#send" do
9
9
  it 'should allow send' do
10
- expect { subject.send(:current_page) }.not_to raise_error(NoMethodError)
10
+ expect { subject.send(:current_page) }.to_not raise_error
11
11
  end
12
12
  end
13
13
 
@@ -0,0 +1,94 @@
1
+ require File.expand_path('spec_helper', File.dirname(__FILE__))
2
+
3
+ describe 'stats', :type => :search do
4
+ it 'returns field name for stats field' do
5
+ stub_stats(:average_rating_ft, {})
6
+ result = session.search Post do
7
+ stats :average_rating
8
+ end
9
+ result.stats(:average_rating).field_name.should == :average_rating
10
+ end
11
+
12
+ it 'returns min for stats field' do
13
+ stub_stats(:average_rating_ft, { 'min' => 1.0 })
14
+ result = session.search Post do
15
+ stats :average_rating
16
+ end
17
+ result.stats(:average_rating).min.should == 1.0
18
+ end
19
+
20
+ it 'returns max for stats field' do
21
+ stub_stats(:average_rating_ft, { 'max' => 5.0 })
22
+ result = session.search Post do
23
+ stats :average_rating
24
+ end
25
+ result.stats(:average_rating).max.should == 5.0
26
+ end
27
+
28
+ it 'returns count for stats field' do
29
+ stub_stats(:average_rating_ft, { 'count' => 120 })
30
+ result = session.search Post do
31
+ stats :average_rating
32
+ end
33
+ result.stats(:average_rating).count.should == 120
34
+ end
35
+
36
+ it 'returns sum for stats field' do
37
+ stub_stats(:average_rating_ft, { 'sum' => 2200.0 })
38
+ result = session.search Post do
39
+ stats :average_rating
40
+ end
41
+ result.stats(:average_rating).sum.should == 2200.0
42
+ end
43
+
44
+ it 'returns facet rows for stats field' do
45
+ stub_stats_facets(:average_rating_ft, 'featured_bs' => {
46
+ 'false' => {},
47
+ 'true' => {}
48
+ })
49
+ result = session.search Post do
50
+ stats :average_rating do
51
+ facet :featured
52
+ end
53
+ end
54
+ stats_facet_values(result, :average_rating, :featured).should == [false, true]
55
+ end
56
+
57
+ it 'returns facet stats for stats field' do
58
+ stub_stats_facets(:average_rating_ft, 'featured_bs' => {
59
+ 'true' => { 'min' => 2.0, 'max' => 4.0 }
60
+ })
61
+ result = session.search Post do
62
+ stats :average_rating do
63
+ facet :featured
64
+ end
65
+ end
66
+ stats_facet_stats(result, :average_rating, :featured, true).min.should == 2.0
67
+ stats_facet_stats(result, :average_rating, :featured, true).max.should == 4.0
68
+ end
69
+
70
+ it 'returns instantiated stats facet values' do
71
+ blogs = 2.times.map { Blog.new }
72
+ stub_stats_facets(:average_rating_ft, 'blog_id_i' => {
73
+ blogs[0].id.to_s => {}, blogs[1].id.to_s => {} })
74
+ search = session.search(Post) do
75
+ stats :average_rating do
76
+ facet :blog_id
77
+ end
78
+ end
79
+ search.stats(:average_rating).facet(:blog_id).rows.map { |row| row.instance }.should == blogs
80
+ end
81
+
82
+ it 'only returns verified instances when requested' do
83
+ blog = Blog.new
84
+ stub_stats_facets(:average_rating_ft, 'blog_id_i' => {
85
+ blog.id.to_s => {}, '0' => {} })
86
+
87
+ search = session.search(Post) do
88
+ stats :average_rating do
89
+ facet :blog_id
90
+ end
91
+ end
92
+ search.stats(:average_rating).facet(:blog_id).rows(:verified => true).map { |row| row.instance }.should == [blog]
93
+ end
94
+ end
@@ -18,11 +18,17 @@ describe Sunspot::SessionProxy::ClassShardingSessionProxy do
18
18
 
19
19
  [:remove_by_id, :remove_by_id!].each do |method|
20
20
  it "should delegate #{method} to appropriate shard" do
21
- @proxy.post_session.should_receive(method).with(Post, 1)
22
- @proxy.photo_session.should_receive(method).with(Photo, 1)
21
+ @proxy.post_session.should_receive(method).with(Post, [1])
22
+ @proxy.photo_session.should_receive(method).with(Photo, [1])
23
23
  @proxy.send(method, Post, 1)
24
24
  @proxy.send(method, Photo, 1)
25
25
  end
26
+ it "should delegate #{method} to appropriate shard given ids" do
27
+ @proxy.post_session.should_receive(method).with(Post, [1, 2])
28
+ @proxy.photo_session.should_receive(method).with(Photo, [1, 2])
29
+ @proxy.send(method, Post, 1, 2)
30
+ @proxy.send(method, Photo, [1, 2])
31
+ end
26
32
  end
27
33
 
28
34
  [:remove_all, :remove_all!].each do |method|
@@ -19,10 +19,22 @@ describe Sunspot::SessionProxy::ShardingSessionProxy do
19
19
 
20
20
  [:remove_by_id, :remove_by_id!].each do |method|
21
21
  it "should delegate #{method} to appropriate session" do
22
- @proxy.sessions[0].should_receive(method).with(Post, 2)
23
- @proxy.sessions[1].should_receive(method).with(Post, 1)
22
+ @proxy.sessions[1].should_receive(method).with(Post, [3])
23
+ @proxy.sessions[0].should_receive(method).with(Post, [2])
24
+ @proxy.sessions[1].should_receive(method).with(Post, [1])
24
25
  @proxy.send(method, Post, 1)
25
26
  @proxy.send(method, Post, 2)
27
+ @proxy.send(method, Post, 3)
28
+ end
29
+ it "should delegate #{method} to appropriate session given splatted index ids" do
30
+ @proxy.sessions[0].should_receive(method).with(Post, [2])
31
+ @proxy.sessions[1].should_receive(method).with(Post, [1, 3])
32
+ @proxy.send(method, Post, 1, 2, 3)
33
+ end
34
+ it "should delegate #{method} to appropriate session given array of index ids" do
35
+ @proxy.sessions[0].should_receive(method).with(Post, [2])
36
+ @proxy.sessions[1].should_receive(method).with(Post, [1, 3])
37
+ @proxy.send(method, Post, [1, 2, 3])
26
38
  end
27
39
  end
28
40
 
@@ -33,14 +33,14 @@ describe Sunspot::SessionProxy::Retry5xxSessionProxy do
33
33
  end
34
34
 
35
35
  it "should behave normally without a stubbed exception" do
36
- @sunspot_session.should_receive(:index).and_return(mock)
36
+ @sunspot_session.should_receive(:index).and_return(double)
37
37
  Sunspot.index(post)
38
38
  end
39
39
 
40
40
  it "should be successful with a single exception followed by a sucess" do
41
41
  e = FakeRSolrErrorHttp.new(fake_rsolr_request, fake_rsolr_response(503))
42
42
  @sunspot_session.should_receive(:index).and_return do
43
- @sunspot_session.should_receive(:index).and_return(mock)
43
+ @sunspot_session.should_receive(:index).and_return(double)
44
44
  raise e
45
45
  end
46
46
  Sunspot.index(post)
@@ -49,7 +49,7 @@ describe Sunspot::SessionProxy::Retry5xxSessionProxy do
49
49
  it "should return the error response after two exceptions" do
50
50
  fake_response = fake_rsolr_response(503)
51
51
  e = FakeRSolrErrorHttp.new(fake_rsolr_request, fake_response)
52
- fake_success = mock('success')
52
+ fake_success = double('success')
53
53
 
54
54
  @sunspot_session.should_receive(:index).and_return do
55
55
  @sunspot_session.should_receive(:index).and_return do
@@ -6,7 +6,7 @@ describe Sunspot::SessionProxy::ShardingSessionProxy do
6
6
  SUPPORTED_METHODS = Sunspot::SessionProxy::SilentFailSessionProxy::SUPPORTED_METHODS
7
7
 
8
8
  before do
9
- @search_session = mock(Sunspot::Session.new)
9
+ @search_session = double(Sunspot::Session.new)
10
10
  @proxy = Sunspot::SessionProxy::SilentFailSessionProxy.new(@search_session)
11
11
  end
12
12
 
@@ -54,6 +54,28 @@ module SearchHelper
54
54
  }
55
55
  end
56
56
 
57
+ def stub_stats(name, values)
58
+ connection.response = {
59
+ 'stats' => {
60
+ 'stats_fields' => {
61
+ name.to_s => { :facets => {} }.merge(values)
62
+ }
63
+ }
64
+ }
65
+ end
66
+
67
+ def stub_stats_facets(name, facets)
68
+ connection.response = {
69
+ 'stats' => {
70
+ 'stats_fields' => {
71
+ name.to_s => {
72
+ 'facets' => facets
73
+ }
74
+ }
75
+ }
76
+ }
77
+ end
78
+
57
79
  def stub_query_facet(values)
58
80
  connection.response = { 'facet_counts' => { 'facet_queries' => values } }
59
81
  end
@@ -65,4 +87,12 @@ module SearchHelper
65
87
  def facet_counts(result, field_name)
66
88
  result.facet(field_name).rows.map { |row| row.count }
67
89
  end
90
+
91
+ def stats_facet_values(result, field_name, facet_name)
92
+ result.stats(field_name).facet(facet_name).rows.map(&:value)
93
+ end
94
+
95
+ def stats_facet_stats(result, field_name, facet_name, value)
96
+ result.stats(field_name).facet(facet_name).rows.find { |r| r.value == value }
97
+ end
68
98
  end