sunspot 0.9.7

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