sunspot 0.9.7

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 (101) hide show
  1. data/History.txt +83 -0
  2. data/LICENSE +18 -0
  3. data/README.rdoc +154 -0
  4. data/Rakefile +9 -0
  5. data/TODO +9 -0
  6. data/VERSION.yml +4 -0
  7. data/bin/sunspot-configure-solr +46 -0
  8. data/bin/sunspot-solr +62 -0
  9. data/lib/light_config.rb +40 -0
  10. data/lib/sunspot.rb +469 -0
  11. data/lib/sunspot/adapters.rb +265 -0
  12. data/lib/sunspot/composite_setup.rb +186 -0
  13. data/lib/sunspot/configuration.rb +38 -0
  14. data/lib/sunspot/data_extractor.rb +47 -0
  15. data/lib/sunspot/dsl.rb +3 -0
  16. data/lib/sunspot/dsl/field_query.rb +72 -0
  17. data/lib/sunspot/dsl/fields.rb +86 -0
  18. data/lib/sunspot/dsl/query.rb +59 -0
  19. data/lib/sunspot/dsl/query_facet.rb +31 -0
  20. data/lib/sunspot/dsl/restriction.rb +25 -0
  21. data/lib/sunspot/dsl/scope.rb +193 -0
  22. data/lib/sunspot/dsl/search.rb +30 -0
  23. data/lib/sunspot/facet.rb +16 -0
  24. data/lib/sunspot/facet_data.rb +120 -0
  25. data/lib/sunspot/facet_row.rb +10 -0
  26. data/lib/sunspot/field.rb +157 -0
  27. data/lib/sunspot/field_factory.rb +126 -0
  28. data/lib/sunspot/indexer.rb +123 -0
  29. data/lib/sunspot/instantiated_facet.rb +42 -0
  30. data/lib/sunspot/instantiated_facet_row.rb +22 -0
  31. data/lib/sunspot/query.rb +191 -0
  32. data/lib/sunspot/query/base_query.rb +90 -0
  33. data/lib/sunspot/query/connective.rb +126 -0
  34. data/lib/sunspot/query/dynamic_query.rb +69 -0
  35. data/lib/sunspot/query/field_facet.rb +151 -0
  36. data/lib/sunspot/query/field_query.rb +63 -0
  37. data/lib/sunspot/query/pagination.rb +39 -0
  38. data/lib/sunspot/query/query_facet.rb +73 -0
  39. data/lib/sunspot/query/query_facet_row.rb +19 -0
  40. data/lib/sunspot/query/query_field_facet.rb +13 -0
  41. data/lib/sunspot/query/restriction.rb +233 -0
  42. data/lib/sunspot/query/scope.rb +165 -0
  43. data/lib/sunspot/query/sort.rb +36 -0
  44. data/lib/sunspot/query/sort_composite.rb +33 -0
  45. data/lib/sunspot/schema.rb +165 -0
  46. data/lib/sunspot/search.rb +219 -0
  47. data/lib/sunspot/search/hit.rb +66 -0
  48. data/lib/sunspot/session.rb +201 -0
  49. data/lib/sunspot/setup.rb +271 -0
  50. data/lib/sunspot/type.rb +200 -0
  51. data/lib/sunspot/util.rb +164 -0
  52. data/solr/etc/jetty.xml +212 -0
  53. data/solr/etc/webdefault.xml +379 -0
  54. data/solr/lib/jetty-6.1.3.jar +0 -0
  55. data/solr/lib/jetty-util-6.1.3.jar +0 -0
  56. data/solr/lib/jsp-2.1/ant-1.6.5.jar +0 -0
  57. data/solr/lib/jsp-2.1/core-3.1.1.jar +0 -0
  58. data/solr/lib/jsp-2.1/jsp-2.1.jar +0 -0
  59. data/solr/lib/jsp-2.1/jsp-api-2.1.jar +0 -0
  60. data/solr/lib/servlet-api-2.5-6.1.3.jar +0 -0
  61. data/solr/solr/conf/elevate.xml +36 -0
  62. data/solr/solr/conf/protwords.txt +21 -0
  63. data/solr/solr/conf/schema.xml +50 -0
  64. data/solr/solr/conf/solrconfig.xml +696 -0
  65. data/solr/solr/conf/stopwords.txt +57 -0
  66. data/solr/solr/conf/synonyms.txt +31 -0
  67. data/solr/start.jar +0 -0
  68. data/solr/webapps/solr.war +0 -0
  69. data/spec/api/adapters_spec.rb +33 -0
  70. data/spec/api/build_search_spec.rb +1039 -0
  71. data/spec/api/indexer_spec.rb +311 -0
  72. data/spec/api/query_spec.rb +153 -0
  73. data/spec/api/search_retrieval_spec.rb +362 -0
  74. data/spec/api/session_spec.rb +157 -0
  75. data/spec/api/spec_helper.rb +1 -0
  76. data/spec/api/sunspot_spec.rb +18 -0
  77. data/spec/integration/dynamic_fields_spec.rb +55 -0
  78. data/spec/integration/faceting_spec.rb +169 -0
  79. data/spec/integration/keyword_search_spec.rb +83 -0
  80. data/spec/integration/scoped_search_spec.rb +289 -0
  81. data/spec/integration/spec_helper.rb +1 -0
  82. data/spec/integration/stored_fields_spec.rb +10 -0
  83. data/spec/integration/test_pagination.rb +32 -0
  84. data/spec/mocks/adapters.rb +32 -0
  85. data/spec/mocks/blog.rb +3 -0
  86. data/spec/mocks/comment.rb +19 -0
  87. data/spec/mocks/connection.rb +84 -0
  88. data/spec/mocks/mock_adapter.rb +30 -0
  89. data/spec/mocks/mock_record.rb +48 -0
  90. data/spec/mocks/photo.rb +8 -0
  91. data/spec/mocks/post.rb +73 -0
  92. data/spec/mocks/user.rb +8 -0
  93. data/spec/spec_helper.rb +47 -0
  94. data/tasks/gemspec.rake +25 -0
  95. data/tasks/rcov.rake +28 -0
  96. data/tasks/rdoc.rake +22 -0
  97. data/tasks/schema.rake +19 -0
  98. data/tasks/spec.rake +24 -0
  99. data/tasks/todo.rake +4 -0
  100. data/templates/schema.xml.haml +24 -0
  101. metadata +246 -0
@@ -0,0 +1,362 @@
1
+ require File.join(File.dirname(__FILE__), 'spec_helper')
2
+
3
+ describe 'retrieving search' do
4
+ it 'should load search result' do
5
+ post = Post.new
6
+ stub_results(post)
7
+ session.search(Post).results.should == [post]
8
+ end
9
+
10
+ it 'should load multiple search results in order' do
11
+ post_1, post_2 = Post.new, Post.new
12
+ stub_results(post_1, post_2)
13
+ session.search(Post).results.should == [post_1, post_2]
14
+ stub_results(post_2, post_1)
15
+ session.search(Post).results.should == [post_2, post_1]
16
+ end
17
+
18
+ # This is a reduction of a crazy bug I found in production where some hits
19
+ # were inexplicably not being populated.
20
+ it 'properly loads results of multiple classes that have the same primary key' do
21
+ Post.reset!
22
+ Namespaced::Comment.reset!
23
+ results = [Post.new, Namespaced::Comment.new]
24
+ stub_results(*results)
25
+ session.search(Post, Namespaced::Comment).results.should == results
26
+ end
27
+
28
+ if ENV['USE_WILL_PAGINATE']
29
+
30
+ it 'should return search total as attribute of results if pagination is provided' do
31
+ stub_results(Post.new, 4)
32
+ session.search(Post, :page => 1).results.total_entries.should == 4
33
+ end
34
+
35
+ else
36
+
37
+ it 'should return vanilla array if pagination is provided but WillPaginate is not available' do
38
+ stub_results(Post.new)
39
+ session.search(Post, :page => 1).results.should_not respond_to(:total_entries)
40
+ end
41
+
42
+ end
43
+
44
+ it 'should return hits without loading instances' do
45
+ post_1, post_2 = Array.new(2) { Post.new }
46
+ stub_results(post_1, post_2)
47
+ %w(load load_all).each do |message|
48
+ MockAdapter::DataAccessor.should_not_receive(message)
49
+ end
50
+ session.search(Post).hits.map do |hit|
51
+ [hit.class_name, hit.primary_key]
52
+ end.should == [['Post', post_1.id.to_s], ['Post', post_2.id.to_s]]
53
+ end
54
+
55
+ it 'should return instance from hit' do
56
+ posts = Array.new(2) { Post.new }
57
+ stub_results(*posts)
58
+ session.search(Post).hits.first.instance.should == posts.first
59
+ end
60
+
61
+ it 'should hydrate all hits when an instance is requested from a hit' do
62
+ posts = Array.new(2) { Post.new }
63
+ stub_results(*posts)
64
+ search = session.search(Post)
65
+ search.hits.first.instance
66
+ %w(load load_all).each do |message|
67
+ MockAdapter::DataAccessor.should_not_receive(message)
68
+ end
69
+ search.hits.last.instance.should == posts.last
70
+ end
71
+
72
+ it 'should attach score to hits' do
73
+ stub_full_results('instance' => Post.new, 'score' => 1.23)
74
+ session.search(Post).hits.first.score.should == 1.23
75
+ end
76
+
77
+ it 'should return stored field values in hits' do
78
+ stub_full_results('instance' => Post.new, 'title_ss' => 'Title')
79
+ session.search(Post).hits.first.stored(:title).should == 'Title'
80
+ end
81
+
82
+ it 'should return stored field values for searches against multiple types' do
83
+ stub_full_results('instance' => Post.new, 'title_ss' => 'Title')
84
+ session.search(Post, Namespaced::Comment).hits.first.stored(:title).should == 'Title'
85
+ end
86
+
87
+ it 'should typecast stored field values in hits' do
88
+ time = Time.utc(2008, 7, 8, 2, 45)
89
+ stub_full_results('instance' => Post.new, 'last_indexed_at_ds' => time.xmlschema)
90
+ session.search(Post).hits.first.stored(:last_indexed_at).should == time
91
+ end
92
+
93
+ it 'should allow access to the data accessor' do
94
+ stub_results(posts = Post.new)
95
+ search = session.search Post do
96
+ data_accessor_for(Post).custom_title = 'custom title'
97
+ end
98
+ search.results.first.title.should == 'custom title'
99
+ end
100
+
101
+ it 'should return total' do
102
+ stub_results(Post.new, Post.new, 4)
103
+ session.search(Post, :page => 1).total.should == 4
104
+ end
105
+
106
+ it 'should return field name for facet' do
107
+ stub_facet(:title_ss, {})
108
+ result = session.search Post do
109
+ facet :title
110
+ end
111
+ result.facet(:title).field_name.should == :title
112
+ end
113
+
114
+ it 'should return string facet' do
115
+ stub_facet(:title_ss, 'Author 1' => 2, 'Author 2' => 1)
116
+ result = session.search Post do
117
+ facet :title
118
+ end
119
+ facet_values(result, :title).should == ['Author 1', 'Author 2']
120
+ end
121
+
122
+ it 'should return counts for facet' do
123
+ stub_facet(:title_ss, 'Author 1' => 2, 'Author 2' => 1)
124
+ result = session.search Post do
125
+ facet :title
126
+ end
127
+ facet_counts(result, :title).should == [2, 1]
128
+ end
129
+
130
+ it 'should return integer facet' do
131
+ stub_facet(:blog_id_i, '3' => 2, '1' => 1)
132
+ result = session.search Post do
133
+ facet :blog_id
134
+ end
135
+ facet_values(result, :blog_id).should == [3, 1]
136
+ end
137
+
138
+ it 'should return float facet' do
139
+ stub_facet(:average_rating_f, '9.3' => 2, '1.1' => 1)
140
+ result = session.search Post do
141
+ facet :average_rating
142
+ end
143
+ facet_values(result, :average_rating).should == [9.3, 1.1]
144
+ end
145
+
146
+ it 'should return time facet' do
147
+ stub_facet(
148
+ :published_at_d,
149
+ '2009-04-07T20:25:23Z' => 3,
150
+ '2009-04-07T20:26:19Z' => 1
151
+ )
152
+ result = session.search Post do
153
+ facet :published_at
154
+ end
155
+ facet_values(result, :published_at).should ==
156
+ [Time.gm(2009, 04, 07, 20, 25, 23),
157
+ Time.gm(2009, 04, 07, 20, 26, 19)]
158
+ end
159
+
160
+ it 'should return date facet' do
161
+ stub_facet(
162
+ :expire_date_d,
163
+ '2009-07-13T00:00:00Z' => 3,
164
+ '2009-04-01T00:00:00Z' => 1
165
+ )
166
+ result = session.search(Post) do
167
+ facet :expire_date
168
+ end
169
+ facet_values(result, :expire_date).should ==
170
+ [Date.new(2009, 07, 13),
171
+ Date.new(2009, 04, 01)]
172
+ end
173
+
174
+ it 'should return boolean facet' do
175
+ stub_facet(:featured_b, 'true' => 3, 'false' => 1)
176
+ result = session.search(Post) { facet(:featured) }
177
+ facet_values(result, :featured).should == [true, false]
178
+ end
179
+
180
+ it 'should return class facet' do
181
+ stub_facet(:class_name, 'Post' => 3, 'Namespaced::Comment' => 1)
182
+ result = session.search(Post) { facet(:class) }
183
+ facet_values(result, :class).should == [Post, Namespaced::Comment]
184
+ end
185
+
186
+ it 'should return date range facet' do
187
+ stub_date_facet(:published_at_d, 60*60*24, '2009-07-08T04:00:00Z' => 2, '2009-07-07T04:00:00Z' => 1)
188
+ start_time = Time.utc(2009, 7, 7, 4)
189
+ end_time = start_time + 2*24*60*60
190
+ result = session.search(Post) { facet(:published_at, :time_range => start_time..end_time) }
191
+ facet = result.facet(:published_at)
192
+ facet.rows.first.value.should == (start_time..(start_time+24*60*60))
193
+ facet.rows.last.value.should == ((start_time+24*60*60)..end_time)
194
+ end
195
+
196
+ it 'should return query facet' do
197
+ stub_query_facet(
198
+ 'average_rating_f:[3\.0 TO 5\.0]' => 3,
199
+ 'average_rating_f:[1\.0 TO 3\.0]' => 1
200
+ )
201
+ search = session.search(Post) do
202
+ facet :average_rating do
203
+ row 3.0..5.0 do
204
+ with :average_rating, 3.0..5.0
205
+ end
206
+ row 1.0..3.0 do
207
+ with :average_rating, 1.0..3.0
208
+ end
209
+ end
210
+ end
211
+ facet = search.facet(:average_rating)
212
+ facet.rows.first.value.should == (3.0..5.0)
213
+ facet.rows.first.count.should == 3
214
+ facet.rows.last.value.should == (1.0..3.0)
215
+ facet.rows.last.count.should == 1
216
+ end
217
+
218
+ it 'should return query facet specified in dynamic call' do
219
+ stub_query_facet(
220
+ 'custom_string\:test_s:(foo OR bar)' => 3
221
+ )
222
+ search = session.search(Post) do
223
+ dynamic :custom_string do
224
+ facet :test do
225
+ row :foo_bar do
226
+ with :test, %w(foo bar)
227
+ end
228
+ end
229
+ end
230
+ end
231
+ facet = search.facet(:test)
232
+ facet.rows.first.value.should == :foo_bar
233
+ facet.rows.first.count.should == 3
234
+ end
235
+
236
+ it 'returns limited field facet' do
237
+ stub_query_facet(
238
+ 'category_ids_im:1' => 3,
239
+ 'category_ids_im:3' => 1
240
+ )
241
+ search = session.search(Post) do
242
+ facet :category_ids, :only => [1, 3, 5]
243
+ end
244
+ facet = search.facet(:category_ids)
245
+ facet.rows.first.value.should == 1
246
+ facet.rows.first.count.should == 3
247
+ facet.rows.last.value.should == 3
248
+ facet.rows.last.count.should == 1
249
+ end
250
+
251
+ it 'should return dynamic string facet' do
252
+ stub_facet(:"custom_string:test_s", 'two' => 2, 'one' => 1)
253
+ result = session.search(Post) { dynamic(:custom_string) { facet(:test) }}
254
+ result.dynamic_facet(:custom_string, :test).rows.map { |row| row.value }.should == ['two', 'one']
255
+ end
256
+
257
+ it 'should return instantiated facet values' do
258
+ blogs = Array.new(2) { Blog.new }
259
+ stub_facet(:blog_id_i, blogs[0].id.to_s => 2, blogs[1].id.to_s => 1)
260
+ result = session.search(Post) { facet(:blog_id) }
261
+ result.facet(:blog_id).rows.map { |row| row.instance }.should == blogs
262
+ end
263
+
264
+ it 'returns instantiated facet values for limited field facet' do
265
+ blogs = Array.new(2) { Blog.new }
266
+ stub_query_facet(
267
+ "blog_id_i:#{blogs[0].id}" => 3,
268
+ "blog_id_i:#{blogs[1].id}" => 1
269
+ )
270
+ search = session.search(Post) do
271
+ facet(:blog_id, :only => blogs.map { |blog| blog.id })
272
+ end
273
+ search.facet(:blog_id).rows.map { |row| row.instance }.should == blogs
274
+ end
275
+
276
+ it 'should only query the persistent store once for an instantiated facet' do
277
+ query_count = Blog.query_count
278
+ blogs = Array.new(2) { Blog.new }
279
+ stub_facet(:blog_id_i, blogs[0].id.to_s => 2, blogs[1].id.to_s => 1)
280
+ result = session.search(Post) { facet(:blog_id) }
281
+ result.facet(:blog_id).rows.each { |row| row.instance }
282
+ (Blog.query_count - query_count).should == 1
283
+ end
284
+
285
+ private
286
+
287
+ def stub_full_results(*results)
288
+ count =
289
+ if results.last.is_a?(Integer) then results.pop
290
+ else results.length
291
+ end
292
+ docs = results.map do |result|
293
+ instance = result.delete('instance')
294
+ result.merge('id' => "#{instance.class.name} #{instance.id}")
295
+ end
296
+ response = {
297
+ 'response' => {
298
+ 'docs' => docs,
299
+ 'numFound' => count
300
+ }
301
+ }
302
+ connection.stub!(:select).and_return(response)
303
+ end
304
+
305
+ def stub_results(*results)
306
+ stub_full_results(
307
+ *results.map do |result|
308
+ if result.is_a?(Integer)
309
+ result
310
+ else
311
+ { 'instance' => result }
312
+ end
313
+ end
314
+ )
315
+ end
316
+
317
+ def stub_facet(name, values)
318
+ connection.stub!(:select).and_return(
319
+ 'facet_counts' => {
320
+ 'facet_fields' => {
321
+ name.to_s => values.to_a.sort_by { |value, count| -count }.flatten
322
+ }
323
+ }
324
+ )
325
+ end
326
+
327
+ def stub_date_facet(name, gap, values)
328
+ connection.stub!(:select).and_return(
329
+ 'facet_counts' => {
330
+ 'facet_dates' => {
331
+ name.to_s => { 'gap' => "+#{gap}SECONDS" }.merge(values)
332
+ }
333
+ }
334
+ )
335
+ end
336
+
337
+ def stub_query_facet(values)
338
+ connection.stub!(:select).and_return(
339
+ 'facet_counts' => { 'facet_queries' => values }
340
+ )
341
+ end
342
+
343
+ def facet_values(result, field_name)
344
+ result.facet(field_name).rows.map { |row| row.value }
345
+ end
346
+
347
+ def facet_counts(result, field_name)
348
+ result.facet(field_name).rows.map { |row| row.count }
349
+ end
350
+
351
+ def config
352
+ @config ||= Sunspot::Configuration.build
353
+ end
354
+
355
+ def connection
356
+ @connection ||= mock('connection')
357
+ end
358
+
359
+ def session
360
+ @session ||= Sunspot::Session.new(config, connection)
361
+ end
362
+ end
@@ -0,0 +1,157 @@
1
+ require File.join(File.dirname(__FILE__), 'spec_helper')
2
+
3
+ shared_examples_for 'all sessions' do
4
+ context '#index()' do
5
+ before :each do
6
+ @session.index(Post.new)
7
+ end
8
+
9
+ it 'should add document to connection' do
10
+ connection.should have(1).adds
11
+ end
12
+ end
13
+
14
+ context '#index!()' do
15
+ before :each do
16
+ @session.index!(Post.new)
17
+ end
18
+
19
+ it 'should add document to connection' do
20
+ connection.should have(1).adds
21
+ end
22
+
23
+ it 'should commit' do
24
+ connection.should have(1).commits
25
+ end
26
+ end
27
+
28
+ context '#commit()' do
29
+ before :each do
30
+ @session.commit
31
+ end
32
+
33
+ it 'should commit' do
34
+ connection.should have(1).commits
35
+ end
36
+ end
37
+
38
+ context '#search()' do
39
+ before :each do
40
+ @session.search(Post)
41
+ end
42
+
43
+ it 'should search' do
44
+ connection.should have(1).searches
45
+ end
46
+ end
47
+ end
48
+
49
+ describe 'Session' do
50
+ before :each do
51
+ @connection_factory = Mock::ConnectionFactory.new
52
+ Sunspot::Session.connection_class = @connection_factory
53
+ end
54
+
55
+ after :each do
56
+ Sunspot::Session.connection_class = nil
57
+ Sunspot.reset!
58
+ end
59
+
60
+ context 'singleton session' do
61
+ before :each do
62
+ Sunspot.reset!
63
+ @session = Sunspot
64
+ end
65
+
66
+ it_should_behave_like 'all sessions'
67
+
68
+ it 'should open connection with defaults if nothing specified' do
69
+ Sunspot.commit
70
+ connection.adapter.opts[:url].should == 'http://127.0.0.1:8983/solr'
71
+ end
72
+
73
+ it 'should open a connection with custom host' do
74
+ Sunspot.config.solr.url = 'http://127.0.0.1:8981/solr'
75
+ Sunspot.commit
76
+ connection.adapter.opts[:url].should == 'http://127.0.0.1:8981/solr'
77
+ end
78
+
79
+ it 'should use Net::HTTP adapter by default' do
80
+ Sunspot.commit
81
+ connection.adapter.connector.adapter_name.should == :net_http
82
+ end
83
+
84
+ it 'should use Net::HTTP adapter when specified' do
85
+ Sunspot.config.http_client = :curb
86
+ Sunspot.commit
87
+ connection.adapter.connector.adapter_name.should == :curb
88
+ end
89
+ end
90
+
91
+ context 'custom session' do
92
+ before :each do
93
+ @session = Sunspot::Session.new
94
+ end
95
+
96
+ it_should_behave_like 'all sessions'
97
+
98
+ it 'should open a connection with custom host' do
99
+ session = Sunspot::Session.new do |config|
100
+ config.solr.url = 'http://127.0.0.1:8982/solr'
101
+ end
102
+ session.commit
103
+ connection.adapter.opts[:url].should == 'http://127.0.0.1:8982/solr'
104
+ end
105
+ end
106
+
107
+ context 'dirty sessions' do
108
+ before :each do
109
+ @session = Sunspot::Session.new
110
+ end
111
+
112
+ it 'should start out not dirty' do
113
+ @session.dirty?.should be_false
114
+ end
115
+
116
+ it 'should be dirty after adding an item' do
117
+ @session.index(Post.new)
118
+ @session.dirty?.should be_true
119
+ end
120
+
121
+ it 'should be dirty after deleting an item' do
122
+ @session.remove(Post.new)
123
+ @session.dirty?.should be_true
124
+ end
125
+
126
+ it 'should be dirty after a remove_all for a class' do
127
+ @session.remove_all(Post)
128
+ @session.dirty?.should be_true
129
+ end
130
+
131
+ it 'should be dirty after a global remove_all' do
132
+ @session.remove_all
133
+ @session.dirty?.should be_true
134
+ end
135
+
136
+ it 'should not be dirty after a commit' do
137
+ @session.index(Post.new)
138
+ @session.commit
139
+ @session.dirty?.should be_false
140
+ end
141
+
142
+ it 'should not commit when commit_if_dirty called on clean session' do
143
+ @session.commit_if_dirty
144
+ connection.should have(0).commits
145
+ end
146
+
147
+ it 'should commit when commit_if_dirty called on dirty session' do
148
+ @session.index(Post.new)
149
+ @session.commit_if_dirty
150
+ connection.should have(1).commits
151
+ end
152
+ end
153
+
154
+ def connection
155
+ @connection_factory.instance
156
+ end
157
+ end