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