UnderpantsGnome-sunspot 0.9.1.1

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