sunspot 2.1.0 → 2.1.1

Sign up to get free protection for your applications and to get access to all the features.
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