sunspot 1.1.0 → 1.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (66) hide show
  1. data/Gemfile +10 -0
  2. data/Gemfile.lock +32 -0
  3. data/History.txt +24 -0
  4. data/README.rdoc +18 -5
  5. data/lib/sunspot.rb +40 -0
  6. data/lib/sunspot/dsl.rb +2 -2
  7. data/lib/sunspot/dsl/field_query.rb +2 -2
  8. data/lib/sunspot/dsl/fields.rb +0 -10
  9. data/lib/sunspot/dsl/restriction.rb +4 -4
  10. data/lib/sunspot/dsl/restriction_with_near.rb +121 -0
  11. data/lib/sunspot/dsl/scope.rb +55 -67
  12. data/lib/sunspot/dsl/standard_query.rb +11 -15
  13. data/lib/sunspot/field.rb +30 -29
  14. data/lib/sunspot/field_factory.rb +0 -18
  15. data/lib/sunspot/installer/solrconfig_updater.rb +0 -30
  16. data/lib/sunspot/query.rb +4 -3
  17. data/lib/sunspot/query/common_query.rb +2 -2
  18. data/lib/sunspot/query/composite_fulltext.rb +7 -2
  19. data/lib/sunspot/query/connective.rb +21 -6
  20. data/lib/sunspot/query/dismax.rb +1 -0
  21. data/lib/sunspot/query/geo.rb +53 -0
  22. data/lib/sunspot/query/more_like_this.rb +1 -0
  23. data/lib/sunspot/query/restriction.rb +5 -5
  24. data/lib/sunspot/query/standard_query.rb +0 -4
  25. data/lib/sunspot/search/abstract_search.rb +1 -7
  26. data/lib/sunspot/search/hit.rb +10 -10
  27. data/lib/sunspot/search/query_facet.rb +8 -3
  28. data/lib/sunspot/session.rb +10 -2
  29. data/lib/sunspot/session_proxy.rb +16 -0
  30. data/lib/sunspot/session_proxy/master_slave_session_proxy.rb +1 -1
  31. data/lib/sunspot/session_proxy/sharding_session_proxy.rb +7 -0
  32. data/lib/sunspot/session_proxy/silent_fail_session_proxy.rb +42 -0
  33. data/lib/sunspot/session_proxy/thread_local_session_proxy.rb +1 -1
  34. data/lib/sunspot/setup.rb +1 -17
  35. data/lib/sunspot/type.rb +38 -6
  36. data/lib/sunspot/util.rb +21 -31
  37. data/lib/sunspot/version.rb +1 -1
  38. data/solr/solr/conf/solrconfig.xml +0 -4
  39. data/spec/api/binding_spec.rb +12 -0
  40. data/spec/api/indexer/attributes_spec.rb +22 -22
  41. data/spec/api/query/connectives_examples.rb +14 -1
  42. data/spec/api/query/fulltext_examples.rb +3 -3
  43. data/spec/api/query/geo_examples.rb +69 -0
  44. data/spec/api/query/scope_examples.rb +32 -13
  45. data/spec/api/query/standard_spec.rb +1 -1
  46. data/spec/api/search/faceting_spec.rb +5 -1
  47. data/spec/api/search/hits_spec.rb +14 -12
  48. data/spec/api/session_proxy/class_sharding_session_proxy_spec.rb +1 -1
  49. data/spec/api/session_proxy/sharding_session_proxy_spec.rb +1 -1
  50. data/spec/api/session_proxy/silent_fail_session_proxy_spec.rb +24 -0
  51. data/spec/api/session_spec.rb +22 -0
  52. data/spec/integration/local_search_spec.rb +42 -69
  53. data/spec/integration/scoped_search_spec.rb +30 -0
  54. data/spec/mocks/connection.rb +6 -2
  55. data/spec/mocks/photo.rb +0 -1
  56. data/spec/mocks/post.rb +11 -2
  57. data/spec/mocks/user.rb +6 -1
  58. data/spec/spec_helper.rb +2 -12
  59. metadata +209 -177
  60. data/lib/sunspot/query/local.rb +0 -26
  61. data/solr/solr/lib/lucene-spatial-2.9.1.jar +0 -0
  62. data/solr/solr/lib/solr-spatial-light-0.0.6.jar +0 -0
  63. data/spec/api/query/local_examples.rb +0 -38
  64. data/tasks/gemspec.rake +0 -33
  65. data/tasks/rcov.rake +0 -28
  66. data/tasks/spec.rake +0 -24
@@ -1,4 +1,5 @@
1
1
  require File.join(File.dirname(__FILE__), 'spec_helper')
2
+ require 'bigdecimal'
2
3
 
3
4
  describe 'indexing attribute fields', :type => :indexer do
4
5
  it 'should correctly index a stored string attribute field' do
@@ -72,41 +73,29 @@ describe 'indexing attribute fields', :type => :indexer do
72
73
 
73
74
  it 'should correctly index a boolean field' do
74
75
  session.index(post(:featured => true))
75
- connection.should have_add_with(:featured_b => 'true')
76
+ connection.should have_add_with(:featured_bs => 'true')
76
77
  end
77
78
 
78
79
  it 'should correctly index a false boolean field' do
79
80
  session.index(post(:featured => false))
80
- connection.should have_add_with(:featured_b => 'false')
81
+ connection.should have_add_with(:featured_bs => 'false')
81
82
  end
82
83
 
83
84
  it 'should not index a nil boolean field' do
84
85
  session.index(post)
85
- connection.should_not have_add_with(:featured_b)
86
+ connection.should_not have_add_with(:featured_bs)
86
87
  end
87
88
 
88
89
  it 'should index latitude and longitude as a pair' do
89
- session.index(post(:coordinates => [40.7, -73.5]))
90
- connection.should have_add_with(:lat => 40.7, :lng => -73.5)
90
+ session.index(post(:coordinates => Sunspot::Util::Coordinates.new(40.7, -73.5)))
91
+ connection.should have_add_with(:coordinates_s => 'dr5xx3nytvgs')
91
92
  end
92
93
 
93
- [
94
- [:lat, :lng],
95
- [:lat, :lon],
96
- [:lat, :long],
97
- [:latitude, :longitude]
98
- ].each do |lat_attr, lng_attr|
99
- it "should index latitude and longitude from #{lat_attr.inspect}, #{lng_attr.inspect}" do
100
- session.index(post(
101
- :coordinates => OpenStruct.new(lat_attr => 40.7, lng_attr => -73.5)
102
- ))
103
- connection.should have_add_with(:lat => 40.7, :lng => -73.5)
104
- end
105
- end
106
-
107
- it 'should index latitude and longitude from a block' do
108
- session.index(Photo.new(:lat => 30, :lng => -60))
109
- connection.should have_add_with(:lat => 30.0, :lng => -60.0)
94
+ it 'should index latitude and longitude passed as non-Floats' do
95
+ coordinates = Sunspot::Util::Coordinates.new(
96
+ BigDecimal.new('40.7'), BigDecimal.new('-73.5'))
97
+ session.index(post(:coordinates => coordinates))
98
+ connection.should have_add_with(:coordinates_s => 'dr5xx3nytvgs')
110
99
  end
111
100
 
112
101
  it 'should correctly index an attribute field with block access' do
@@ -146,4 +135,15 @@ describe 'indexing attribute fields', :type => :indexer do
146
135
  Sunspot.setup(Post) { integer :popularity, :more_like_this => true }
147
136
  end.should raise_error(ArgumentError)
148
137
  end
138
+
139
+ it 'should use a specified field name when the :as option is set' do
140
+ session.index(post(:title => 'A Title'))
141
+ connection.should have_add_with(:legacy_field_s => 'legacy A Title')
142
+ end
143
+
144
+ it 'should use a specified field name when the :as option is set for array values' do
145
+ session.index(post(:title => 'Another Title'))
146
+ connection.should have_add_with(:legacy_array_field_sm => ['first string', 'second string'])
147
+ end
149
148
  end
149
+
@@ -137,7 +137,7 @@ shared_examples_for "query with connective scope" do
137
137
  end
138
138
  end
139
139
  connection.should have_last_search_including(
140
- :fq, "-(id:Post\\ #{post.id} AND -category_ids_im:1)"
140
+ :fq, "-(id:(Post\\ #{post.id}) AND -category_ids_im:1)"
141
141
  )
142
142
  end
143
143
 
@@ -153,6 +153,19 @@ shared_examples_for "query with connective scope" do
153
153
  )
154
154
  end
155
155
 
156
+ it 'creates a disjunction with instance inclusion' do
157
+ post = Post.new
158
+ search do
159
+ any_of do
160
+ with(post)
161
+ with(:average_rating).greater_than(3.0)
162
+ end
163
+ end
164
+ connection.should have_last_search_including(
165
+ :fq, "(id:(Post\\ #{post.id}) OR average_rating_ft:[3\\.0 TO *])"
166
+ )
167
+ end
168
+
156
169
  it 'creates a disjunction with some text field components' do
157
170
  search do
158
171
  any_of do
@@ -70,8 +70,8 @@ shared_examples_for 'fulltext query' do
70
70
  subqueries(:q).last[:qf].split(' ').sort.should == %w(backwards_title_text body_textsv tags_textv title_text)
71
71
  end
72
72
 
73
- it 'puts automatic dismax parameters in subquery' do
74
- subqueries(:q).each { |subquery| subquery[:fl].should == '* score' }
73
+ it 'puts field list in main query' do
74
+ connection.should have_last_search_with(:fl => '* score')
75
75
  end
76
76
  end
77
77
 
@@ -243,7 +243,7 @@ shared_examples_for 'fulltext query' do
243
243
  connection.should have_last_search_with(
244
244
  :bq => [
245
245
  'average_rating_ft:[2\.0 TO *]^2.0',
246
- 'featured_b:true^1.5'
246
+ 'featured_bs:true^1.5'
247
247
  ]
248
248
  )
249
249
  end
@@ -0,0 +1,69 @@
1
+ require File.join(File.dirname(__FILE__), 'spec_helper')
2
+ require 'bigdecimal'
3
+
4
+ shared_examples_for 'geohash query' do
5
+ it 'searches for nearby points with defaults' do
6
+ search do
7
+ with(:coordinates).near(40.7, -73.5)
8
+ end
9
+ connection.should have_last_search_including(:q, build_geo_query)
10
+ end
11
+
12
+ it 'searches for nearby points with non-Float arguments' do
13
+ search do
14
+ with(:coordinates).near(BigDecimal.new('40.7'), BigDecimal.new('-73.5'))
15
+ end
16
+ connection.should have_last_search_including(:q, build_geo_query)
17
+ end
18
+
19
+ it 'searches for nearby points with given precision' do
20
+ search do
21
+ with(:coordinates).near(40.7, -73.5, :precision => 10)
22
+ end
23
+ connection.should have_last_search_including(:q, build_geo_query(:precision => 10))
24
+ end
25
+
26
+ it 'searches for nearby points with given precision factor' do
27
+ search do
28
+ with(:coordinates).near(40.7, -73.5, :precision_factor => 1.5)
29
+ end
30
+ connection.should have_last_search_including(:q, build_geo_query(:precision_factor => 1.5))
31
+ end
32
+
33
+ it 'searches for nearby points with given boost' do
34
+ search do
35
+ with(:coordinates).near(40.7, -73.5, :boost => 2.0)
36
+ end
37
+ connection.should have_last_search_including(:q, build_geo_query(:boost => 2.0))
38
+ end
39
+
40
+ it 'performs both dismax search and location search' do
41
+ search do
42
+ fulltext 'pizza', :fields => :title
43
+ with(:coordinates).near(40.7, -73.5)
44
+ end
45
+ expected =
46
+ "{!dismax fl='* score' qf='title_text'}pizza (#{build_geo_query})"
47
+ connection.should have_last_search_including(
48
+ :q,
49
+ %Q(_query_:"{!dismax qf='title_text'}pizza" (#{build_geo_query}))
50
+ )
51
+ end
52
+
53
+ private
54
+
55
+ def build_geo_query(options = {})
56
+ precision = options[:precision] || 7
57
+ precision_factor = options[:precision_factor] || 16.0
58
+ boost = options[:boost] || 1.0
59
+ hash = 'dr5xx3nytvgs'
60
+ (precision..12).map do |i|
61
+ phrase =
62
+ if i == 12 then hash
63
+ else "#{hash[0, i]}*"
64
+ end
65
+ precision_boost = Sunspot::Util.format_float(boost*precision_factor**(i-12.0), 3)
66
+ "coordinates_s:#{phrase}^#{precision_boost}"
67
+ end.reverse.join(' OR ')
68
+ end
69
+ end
@@ -39,7 +39,7 @@ shared_examples_for "scoped query" do
39
39
  search do
40
40
  with :featured, false
41
41
  end
42
- connection.should have_last_search_including(:fq, 'featured_b:false')
42
+ connection.should have_last_search_including(:fq, 'featured_bs:false')
43
43
  end
44
44
 
45
45
  it 'scopes by less than match with float' do
@@ -77,13 +77,6 @@ shared_examples_for "scoped query" do
77
77
  connection.should have_last_search_including(:fq, 'average_rating_ft:[2\.0 TO 4\.0]')
78
78
  end
79
79
 
80
- it 'automatically sorts ranges in between matches' do
81
- search do
82
- with(:blog_id).between(4..2)
83
- end
84
- connection.should have_last_search_including(:fq, 'blog_id_i:[2 TO 4]')
85
- end
86
-
87
80
  it 'scopes by any match with integer' do
88
81
  search do
89
82
  with(:category_ids).any_of [2, 7, 12]
@@ -175,12 +168,40 @@ shared_examples_for "scoped query" do
175
168
  connection.should have_last_search_including(:fq, 'average_rating_ft:[* TO *]')
176
169
  end
177
170
 
171
+ it 'includes by object identity' do
172
+ post = Post.new
173
+ search do
174
+ with post
175
+ end
176
+ connection.should have_last_search_including(:fq, "id:(Post\\ #{post.id})")
177
+ end
178
+
179
+ it 'includes multiple objects passed as varargs by object identity' do
180
+ post1, post2 = Post.new, Post.new
181
+ search do
182
+ with post1, post2
183
+ end
184
+ connection.should have_last_search_including(
185
+ :fq, "id:(Post\\ #{post1.id} OR Post\\ #{post2.id})"
186
+ )
187
+ end
188
+
189
+ it 'includes multiple objects passed as array by object identity' do
190
+ posts = [Post.new, Post.new]
191
+ search do
192
+ with posts
193
+ end
194
+ connection.should have_last_search_including(
195
+ :fq, "id:(Post\\ #{posts.first.id} OR Post\\ #{posts.last.id})"
196
+ )
197
+ end
198
+
178
199
  it 'excludes by object identity' do
179
200
  post = Post.new
180
201
  search do
181
202
  without post
182
203
  end
183
- connection.should have_last_search_including(:fq, "-id:Post\\ #{post.id}")
204
+ connection.should have_last_search_including(:fq, "-id:(Post\\ #{post.id})")
184
205
  end
185
206
 
186
207
  it 'excludes multiple objects passed as varargs by object identity' do
@@ -190,8 +211,7 @@ shared_examples_for "scoped query" do
190
211
  end
191
212
  connection.should have_last_search_including(
192
213
  :fq,
193
- "-id:Post\\ #{post1.id}",
194
- "-id:Post\\ #{post2.id}"
214
+ "-id:(Post\\ #{post1.id} OR Post\\ #{post2.id})"
195
215
  )
196
216
  end
197
217
 
@@ -202,8 +222,7 @@ shared_examples_for "scoped query" do
202
222
  end
203
223
  connection.should have_last_search_including(
204
224
  :fq,
205
- "-id:Post\\ #{posts.first.id}",
206
- "-id:Post\\ #{posts.last.id}"
225
+ "-id:(Post\\ #{posts.first.id} OR Post\\ #{posts.last.id})"
207
226
  )
208
227
  end
209
228
 
@@ -9,8 +9,8 @@ describe 'standard query', :type => :query do
9
9
  it_should_behave_like "fulltext query"
10
10
  it_should_behave_like "query with highlighting support"
11
11
  it_should_behave_like "sortable query"
12
- it_should_behave_like "spatial query"
13
12
  it_should_behave_like "query with text field scoping"
13
+ it_should_behave_like "geohash query"
14
14
 
15
15
  it 'adds a no-op query to :q parameter when no :q provided' do
16
16
  session.search Post do
@@ -128,7 +128,7 @@ describe 'faceting', :type => :search do
128
128
  end
129
129
 
130
130
  it 'returns boolean facet' do
131
- stub_facet(:featured_b, 'true' => 3, 'false' => 1)
131
+ stub_facet(:featured_bs, 'true' => 3, 'false' => 1)
132
132
  result = session.search(Post) { facet(:featured) }
133
133
  facet_values(result, :featured).should == [true, false]
134
134
  end
@@ -267,6 +267,10 @@ describe 'faceting', :type => :search do
267
267
  facet_values_from_options(:limit => 1).should == [2]
268
268
  end
269
269
 
270
+ it 'does not limit facets if limit option is negative' do
271
+ facet_values_from_options(:limit => -2).should == [1, 3, 2]
272
+ end
273
+
270
274
  it 'returns all facets if limit greater than number of facets' do
271
275
  facet_values_from_options(:limit => 10).should == [2, 1, 3]
272
276
  end
@@ -106,11 +106,22 @@ describe 'hits', :type => :search do
106
106
  session.search(Post, Namespaced::Comment).hits.first.stored(:title).should == 'Title'
107
107
  end
108
108
 
109
+ it 'should return stored field values for searches against base type when subtype matches' do
110
+ class SubclassedPost < Post; end;
111
+ stub_full_results('instance' => SubclassedPost.new, 'title_ss' => 'Title')
112
+ session.search(Post).hits.first.stored(:title).should == 'Title'
113
+ end
114
+
109
115
  it 'should return stored text fields' do
110
116
  stub_full_results('instance' => Post.new, 'body_textsv' => 'Body')
111
117
  session.search(Post, Namespaced::Comment).hits.first.stored(:body).should == 'Body'
112
118
  end
113
119
 
120
+ it 'should return stored boolean fields' do
121
+ stub_full_results('instance' => Post.new, 'featured_bs' => true)
122
+ session.search(Post, Namespaced::Comment).hits.first.stored(:featured).should be_true
123
+ end
124
+
114
125
  it 'should return stored dynamic fields' do
115
126
  stub_full_results('instance' => Post.new, 'custom_string:test_ss' => 'Custom')
116
127
  session.search(Post, Namespaced::Comment).hits.first.stored(:custom_string, :test).should == 'Custom'
@@ -122,17 +133,8 @@ describe 'hits', :type => :search do
122
133
  session.search(Post).hits.first.stored(:last_indexed_at).should == time
123
134
  end
124
135
 
125
- it 'should return geo distance' do
126
- post = Post.new
127
- stub_results(post)
128
- connection.response['distances'] = {
129
- "Post #{post.id}" => 1.23
130
- }
131
- session.search(Post).hits.first.distance.should == 1.23
132
- end
133
-
134
- it 'should return nil if no geo distance' do
135
- stub_results(Post.new)
136
- session.search(Post).hits.first.distance.should be_nil
136
+ it 'should return stored values for multi-valued fields' do
137
+ stub_full_results('instance' => User.new, 'role_ids_ims' => %w(1 4 5))
138
+ session.search(User).hits.first.stored(:role_ids).should == [1, 4, 5]
137
139
  end
138
140
  end
@@ -40,7 +40,7 @@ describe Sunspot::SessionProxy::ClassShardingSessionProxy do
40
40
  end
41
41
  end
42
42
 
43
- [:commit, :commit_if_dirty, :commit_if_delete_dirty].each do |method|
43
+ [:commit, :commit_if_dirty, :commit_if_delete_dirty, :optimize].each do |method|
44
44
  it "should delegate #{method} to all sessions" do
45
45
  [@proxy.post_session, @proxy.photo_session].each do |session|
46
46
  session.should_receive(method)
@@ -32,7 +32,7 @@ describe Sunspot::SessionProxy::ShardingSessionProxy do
32
32
  end
33
33
  end
34
34
 
35
- [:commit, :commit_if_dirty, :commit_if_delete_dirty].each do |method|
35
+ [:commit, :commit_if_dirty, :commit_if_delete_dirty, :optimize].each do |method|
36
36
  it "should delegate #{method} to all sessions" do
37
37
  @proxy.sessions.each do |session|
38
38
  session.should_receive(method)
@@ -0,0 +1,24 @@
1
+ require File.join(File.dirname(__FILE__), 'spec_helper')
2
+
3
+ describe Sunspot::SessionProxy::ShardingSessionProxy do
4
+
5
+ FakeException = Class.new(StandardError)
6
+ SUPPORTED_METHODS = Sunspot::SessionProxy::SilentFailSessionProxy::SUPPORTED_METHODS
7
+
8
+ before do
9
+ @search_session = mock(Sunspot::Session.new)
10
+ @proxy = Sunspot::SessionProxy::SilentFailSessionProxy.new(@search_session)
11
+ end
12
+
13
+ it "should call rescued_exception when an exception is caught" do
14
+ SUPPORTED_METHODS.each do |method|
15
+ e = FakeException.new(method)
16
+ @search_session.stub!(method).and_raise(e)
17
+ @proxy.should_receive(:rescued_exception).with(method, e)
18
+ @proxy.send(method)
19
+ end
20
+ end
21
+
22
+ it_should_behave_like 'session proxy'
23
+
24
+ end
@@ -35,6 +35,16 @@ shared_examples_for 'all sessions' do
35
35
  end
36
36
  end
37
37
 
38
+ context '#optimize()' do
39
+ before :each do
40
+ @session.optimize
41
+ end
42
+
43
+ it 'should optimize' do
44
+ connection.should have(1).optims
45
+ end
46
+ end
47
+
38
48
  context '#search()' do
39
49
  before :each do
40
50
  @session.search(Post)
@@ -152,12 +162,24 @@ describe 'Session' do
152
162
  @session.dirty?.should be_false
153
163
  end
154
164
 
165
+ it 'should not be dirty after an optimize' do
166
+ @session.index(Post.new)
167
+ @session.optimize
168
+ @session.dirty?.should be_false
169
+ end
170
+
155
171
  it 'should not be delete_dirty after a commit' do
156
172
  @session.remove(Post.new)
157
173
  @session.commit
158
174
  @session.delete_dirty?.should be_false
159
175
  end
160
176
 
177
+ it 'should not be delete_dirty after an optimize' do
178
+ @session.remove(Post.new)
179
+ @session.optimize
180
+ @session.delete_dirty?.should be_false
181
+ end
182
+
161
183
  it 'should not commit when commit_if_dirty called on clean session' do
162
184
  @session.commit_if_dirty
163
185
  connection.should have(0).commits
@@ -1,91 +1,64 @@
1
1
  require File.join(File.dirname(__FILE__), 'spec_helper')
2
2
 
3
3
  describe 'local search' do
4
- ORIGIN = [40.6749113, -73.9648859]
4
+ ORIGIN = [40.7246062, -73.9969018]
5
+ LOCATIONS = [
6
+ [40.7246062, -73.9969018], # dr5rsjtn50yf
7
+ [40.724606, -73.996902], # dr5rsjtn50y9
8
+ [40.724606, -73.996901], # dr5rsjtn50z3
9
+ [40.72461, -73.996906] # dr5rsjtn51ec
10
+ ].map { |lat, lng| Sunspot::Util::Coordinates.new(lat, lng) }
11
+
5
12
  before :each do
6
13
  Sunspot.remove_all
7
- @posts = [
8
- Post.new(:coordinates => ORIGIN),
9
- Post.new(:coordinates => [40.725304, -73.997211], :title => 'teacup'),
10
- Post.new(:coordinates => [40.800069, -73.962283]),
11
- Post.new(:coordinates => [43.706488, -72.292233]),
12
- Post.new(:coordinates => [38.920303, -77.110934], :title => 'teacup'),
13
- Post.new(:coordinates => [47.661557, -122.349938])
14
- ]
15
- @posts.each_with_index { |post, i| post.blog_id = @posts.length - i }
16
- Sunspot.index!(@posts)
17
- end
18
-
19
- it 'should find all the posts within a given radius' do
20
- search = Sunspot.search(Post) { |query| query.near(ORIGIN, :distance => 20) }
21
- search.results.to_set.should == @posts[0..2].to_set
22
14
  end
23
15
 
24
- it 'should perform a radial search with fulltext matching' do
25
- search = Sunspot.search(Post) do |query|
26
- query.keywords 'teacup'
27
- query.near(ORIGIN, :distance => 20)
28
- end
29
- search.results.should == [@posts[1]]
30
- end
31
-
32
- it 'should use dismax for fulltext matching in local search' do
33
- lambda do
34
- search = Sunspot.new_search(Post)
35
- search.build do |query|
36
- query.keywords 'teacup['
37
- query.near(ORIGIN, :distance => 20)
16
+ describe 'without fulltext' do
17
+ before :each do
18
+ @posts = LOCATIONS.map do |location|
19
+ Post.new(:coordinates => location)
38
20
  end
39
- search.execute
40
- end.should_not raise_error
41
- end
21
+ Sunspot.index!(@posts)
22
+ @search = Sunspot.search(Post) do
23
+ with(:coordinates).near(ORIGIN[0], ORIGIN[1], :precision_factor => 4.0)
24
+ end
25
+ end
42
26
 
43
- it 'should perform a radial search with attribute scoping' do
44
- search = Sunspot.search(Post) do |query|
45
- query.near(ORIGIN, :distance => 20)
46
- query.with(:title, 'teacup')
27
+ it 'should return results in geo order' do
28
+ @search.results.should == @posts
47
29
  end
48
- search.results.should == [@posts[1]]
49
- end
50
30
 
51
- it 'should perform a radial search with attribute scoping and distance sorting' do
52
- search = Sunspot.search(Post) do |query|
53
- query.near(ORIGIN, :sort => true)
54
- query.with(:title, 'teacup')
31
+ it 'should asssign higher score to closer locations' do
32
+ hits = @search.hits
33
+ hits[1..-1].each_with_index do |hit, i|
34
+ hit.score.should < hits[i].score
35
+ end
55
36
  end
56
- search.results.should == [@posts[1], @posts[4]]
57
37
  end
58
38
 
59
- it 'should order by arbitrary field' do
60
- search = Sunspot.search(Post) do |query|
61
- query.near(ORIGIN, :distance => 20)
62
- query.order_by(:blog_id)
39
+ describe 'with fulltext' do
40
+ before :each do
41
+ @posts = [
42
+ Post.new(:title => 'pizza', :coordinates => LOCATIONS[0]),
43
+ Post.new(:title => 'pizza', :coordinates => LOCATIONS[1]),
44
+ Post.new(:title => 'pasta calzone pizza antipasti', :coordinates => LOCATIONS[1])
45
+ ]
46
+ Sunspot.index!(@posts)
47
+ @search = Sunspot.search(Post) do
48
+ keywords 'pizza'
49
+ with(:coordinates).near(ORIGIN[0], ORIGIN[1])
50
+ end
63
51
  end
64
- search.results.should == @posts[0..2].reverse
65
- end
66
52
 
67
- it 'should order by geo distance' do
68
- search = Sunspot.search(Post) do |query|
69
- query.near(ORIGIN, :distance => 20, :sort => true)
53
+ it 'should take both fulltext and distance into account in ordering' do
54
+ @search.results.should == @posts
70
55
  end
71
- search.results.should == @posts[0..2]
72
- end
73
56
 
74
- it 'should order by geo distance with fulltext' do
75
- lambda do
76
- search = Sunspot.search(Post) do |query|
77
- query.fulltext('teacup')
78
- query.near(ORIGIN, :sort => true)
57
+ it 'should take both fulltext and distance into account in scoring' do
58
+ hits = @search.hits
59
+ hits[1..-1].each_with_index do |hit, i|
60
+ hit.score.should < hits[i].score
79
61
  end
80
- end.should_not raise_error
81
- end
82
-
83
- it 'should return geographical distance from origin' do
84
- search = Sunspot.search(Post) do |query|
85
- query.near(ORIGIN, :sort => true)
86
- end
87
- search.hits.each do |hit|
88
- hit.distance.should_not be_nil
89
62
  end
90
63
  end
91
64
  end