gojee-sunspot 2.0.3 → 2.0.4

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 (178) hide show
  1. data/.gitignore +12 -0
  2. data/Gemfile +5 -0
  3. data/History.txt +252 -0
  4. data/LICENSE +18 -0
  5. data/Rakefile +13 -0
  6. data/TODO +13 -0
  7. data/lib/light_config.rb +40 -0
  8. data/lib/sunspot/adapters.rb +265 -0
  9. data/lib/sunspot/batcher.rb +62 -0
  10. data/lib/sunspot/class_set.rb +23 -0
  11. data/lib/sunspot/composite_setup.rb +202 -0
  12. data/lib/sunspot/configuration.rb +53 -0
  13. data/lib/sunspot/data_extractor.rb +50 -0
  14. data/lib/sunspot/dsl/adjustable.rb +47 -0
  15. data/lib/sunspot/dsl/field_group.rb +57 -0
  16. data/lib/sunspot/dsl/field_query.rb +327 -0
  17. data/lib/sunspot/dsl/fields.rb +103 -0
  18. data/lib/sunspot/dsl/fulltext.rb +243 -0
  19. data/lib/sunspot/dsl/function.rb +27 -0
  20. data/lib/sunspot/dsl/functional.rb +44 -0
  21. data/lib/sunspot/dsl/more_like_this_query.rb +56 -0
  22. data/lib/sunspot/dsl/paginatable.rb +32 -0
  23. data/lib/sunspot/dsl/query_facet.rb +36 -0
  24. data/lib/sunspot/dsl/restriction.rb +25 -0
  25. data/lib/sunspot/dsl/restriction_with_near.rb +160 -0
  26. data/lib/sunspot/dsl/scope.rb +217 -0
  27. data/lib/sunspot/dsl/search.rb +30 -0
  28. data/lib/sunspot/dsl/standard_query.rb +123 -0
  29. data/lib/sunspot/dsl.rb +5 -0
  30. data/lib/sunspot/field.rb +193 -0
  31. data/lib/sunspot/field_factory.rb +129 -0
  32. data/lib/sunspot/indexer.rb +136 -0
  33. data/lib/sunspot/query/abstract_field_facet.rb +52 -0
  34. data/lib/sunspot/query/bbox.rb +15 -0
  35. data/lib/sunspot/query/boost_query.rb +24 -0
  36. data/lib/sunspot/query/common_query.rb +96 -0
  37. data/lib/sunspot/query/composite_fulltext.rb +36 -0
  38. data/lib/sunspot/query/connective.rb +206 -0
  39. data/lib/sunspot/query/date_field_facet.rb +14 -0
  40. data/lib/sunspot/query/dismax.rb +132 -0
  41. data/lib/sunspot/query/field_facet.rb +41 -0
  42. data/lib/sunspot/query/field_group.rb +36 -0
  43. data/lib/sunspot/query/filter.rb +38 -0
  44. data/lib/sunspot/query/function_query.rb +52 -0
  45. data/lib/sunspot/query/geo.rb +53 -0
  46. data/lib/sunspot/query/geofilt.rb +16 -0
  47. data/lib/sunspot/query/highlighting.rb +62 -0
  48. data/lib/sunspot/query/more_like_this.rb +61 -0
  49. data/lib/sunspot/query/more_like_this_query.rb +12 -0
  50. data/lib/sunspot/query/pagination.rb +42 -0
  51. data/lib/sunspot/query/query_facet.rb +16 -0
  52. data/lib/sunspot/query/restriction.rb +262 -0
  53. data/lib/sunspot/query/scope.rb +9 -0
  54. data/lib/sunspot/query/sort.rb +109 -0
  55. data/lib/sunspot/query/sort_composite.rb +34 -0
  56. data/lib/sunspot/query/standard_query.rb +16 -0
  57. data/lib/sunspot/query/text_field_boost.rb +17 -0
  58. data/lib/sunspot/query.rb +11 -0
  59. data/lib/sunspot/schema.rb +151 -0
  60. data/lib/sunspot/search/abstract_search.rb +281 -0
  61. data/lib/sunspot/search/date_facet.rb +35 -0
  62. data/lib/sunspot/search/facet_row.rb +27 -0
  63. data/lib/sunspot/search/field_facet.rb +88 -0
  64. data/lib/sunspot/search/field_group.rb +32 -0
  65. data/lib/sunspot/search/group.rb +50 -0
  66. data/lib/sunspot/search/highlight.rb +38 -0
  67. data/lib/sunspot/search/hit.rb +150 -0
  68. data/lib/sunspot/search/hit_enumerable.rb +72 -0
  69. data/lib/sunspot/search/more_like_this_search.rb +31 -0
  70. data/lib/sunspot/search/paginated_collection.rb +57 -0
  71. data/lib/sunspot/search/query_facet.rb +67 -0
  72. data/lib/sunspot/search/standard_search.rb +21 -0
  73. data/lib/sunspot/search.rb +9 -0
  74. data/lib/sunspot/session.rb +262 -0
  75. data/lib/sunspot/session_proxy/abstract_session_proxy.rb +29 -0
  76. data/lib/sunspot/session_proxy/class_sharding_session_proxy.rb +66 -0
  77. data/lib/sunspot/session_proxy/id_sharding_session_proxy.rb +89 -0
  78. data/lib/sunspot/session_proxy/master_slave_session_proxy.rb +43 -0
  79. data/lib/sunspot/session_proxy/multicore_session_proxy.rb +67 -0
  80. data/lib/sunspot/session_proxy/sharding_session_proxy.rb +222 -0
  81. data/lib/sunspot/session_proxy/silent_fail_session_proxy.rb +42 -0
  82. data/lib/sunspot/session_proxy/thread_local_session_proxy.rb +37 -0
  83. data/lib/sunspot/session_proxy.rb +95 -0
  84. data/lib/sunspot/setup.rb +350 -0
  85. data/lib/sunspot/text_field_setup.rb +29 -0
  86. data/lib/sunspot/type.rb +393 -0
  87. data/lib/sunspot/util.rb +252 -0
  88. data/lib/sunspot/version.rb +3 -0
  89. data/lib/sunspot.rb +579 -0
  90. data/log/.gitignore +1 -0
  91. data/pkg/.gitignore +1 -0
  92. data/script/console +10 -0
  93. data/spec/api/adapters_spec.rb +33 -0
  94. data/spec/api/batcher_spec.rb +112 -0
  95. data/spec/api/binding_spec.rb +50 -0
  96. data/spec/api/class_set_spec.rb +24 -0
  97. data/spec/api/hit_enumerable_spec.rb +47 -0
  98. data/spec/api/indexer/attributes_spec.rb +149 -0
  99. data/spec/api/indexer/batch_spec.rb +72 -0
  100. data/spec/api/indexer/dynamic_fields_spec.rb +42 -0
  101. data/spec/api/indexer/fixed_fields_spec.rb +57 -0
  102. data/spec/api/indexer/fulltext_spec.rb +43 -0
  103. data/spec/api/indexer/removal_spec.rb +53 -0
  104. data/spec/api/indexer/spec_helper.rb +1 -0
  105. data/spec/api/indexer_spec.rb +14 -0
  106. data/spec/api/query/advanced_manipulation_examples.rb +35 -0
  107. data/spec/api/query/connectives_examples.rb +189 -0
  108. data/spec/api/query/dsl_spec.rb +18 -0
  109. data/spec/api/query/dynamic_fields_examples.rb +165 -0
  110. data/spec/api/query/faceting_examples.rb +397 -0
  111. data/spec/api/query/fulltext_examples.rb +313 -0
  112. data/spec/api/query/function_spec.rb +79 -0
  113. data/spec/api/query/geo_examples.rb +68 -0
  114. data/spec/api/query/group_spec.rb +32 -0
  115. data/spec/api/query/highlighting_examples.rb +245 -0
  116. data/spec/api/query/more_like_this_spec.rb +140 -0
  117. data/spec/api/query/ordering_pagination_examples.rb +116 -0
  118. data/spec/api/query/scope_examples.rb +275 -0
  119. data/spec/api/query/spatial_examples.rb +27 -0
  120. data/spec/api/query/spec_helper.rb +1 -0
  121. data/spec/api/query/standard_spec.rb +29 -0
  122. data/spec/api/query/text_field_scoping_examples.rb +30 -0
  123. data/spec/api/query/types_spec.rb +20 -0
  124. data/spec/api/search/dynamic_fields_spec.rb +33 -0
  125. data/spec/api/search/faceting_spec.rb +360 -0
  126. data/spec/api/search/highlighting_spec.rb +69 -0
  127. data/spec/api/search/hits_spec.rb +131 -0
  128. data/spec/api/search/paginated_collection_spec.rb +36 -0
  129. data/spec/api/search/results_spec.rb +72 -0
  130. data/spec/api/search/search_spec.rb +23 -0
  131. data/spec/api/search/spec_helper.rb +1 -0
  132. data/spec/api/session_proxy/class_sharding_session_proxy_spec.rb +85 -0
  133. data/spec/api/session_proxy/id_sharding_session_proxy_spec.rb +30 -0
  134. data/spec/api/session_proxy/master_slave_session_proxy_spec.rb +41 -0
  135. data/spec/api/session_proxy/sharding_session_proxy_spec.rb +77 -0
  136. data/spec/api/session_proxy/silent_fail_session_proxy_spec.rb +24 -0
  137. data/spec/api/session_proxy/spec_helper.rb +9 -0
  138. data/spec/api/session_proxy/thread_local_session_proxy_spec.rb +39 -0
  139. data/spec/api/session_spec.rb +232 -0
  140. data/spec/api/spec_helper.rb +3 -0
  141. data/spec/api/sunspot_spec.rb +29 -0
  142. data/spec/ext.rb +11 -0
  143. data/spec/helpers/indexer_helper.rb +17 -0
  144. data/spec/helpers/integration_helper.rb +8 -0
  145. data/spec/helpers/mock_session_helper.rb +13 -0
  146. data/spec/helpers/query_helper.rb +26 -0
  147. data/spec/helpers/search_helper.rb +68 -0
  148. data/spec/integration/dynamic_fields_spec.rb +57 -0
  149. data/spec/integration/faceting_spec.rb +251 -0
  150. data/spec/integration/field_grouping_spec.rb +66 -0
  151. data/spec/integration/geospatial_spec.rb +85 -0
  152. data/spec/integration/highlighting_spec.rb +44 -0
  153. data/spec/integration/indexing_spec.rb +55 -0
  154. data/spec/integration/keyword_search_spec.rb +317 -0
  155. data/spec/integration/local_search_spec.rb +64 -0
  156. data/spec/integration/more_like_this_spec.rb +43 -0
  157. data/spec/integration/scoped_search_spec.rb +354 -0
  158. data/spec/integration/stored_fields_spec.rb +12 -0
  159. data/spec/integration/test_pagination.rb +43 -0
  160. data/spec/integration/unicode_spec.rb +15 -0
  161. data/spec/mocks/adapters.rb +32 -0
  162. data/spec/mocks/blog.rb +3 -0
  163. data/spec/mocks/comment.rb +21 -0
  164. data/spec/mocks/connection.rb +126 -0
  165. data/spec/mocks/mock_adapter.rb +30 -0
  166. data/spec/mocks/mock_class_sharding_session_proxy.rb +24 -0
  167. data/spec/mocks/mock_record.rb +52 -0
  168. data/spec/mocks/mock_sharding_session_proxy.rb +15 -0
  169. data/spec/mocks/photo.rb +11 -0
  170. data/spec/mocks/post.rb +86 -0
  171. data/spec/mocks/super_class.rb +2 -0
  172. data/spec/mocks/user.rb +13 -0
  173. data/spec/spec_helper.rb +40 -0
  174. data/sunspot.gemspec +42 -0
  175. data/tasks/rdoc.rake +27 -0
  176. data/tasks/schema.rake +19 -0
  177. data/tasks/todo.rake +4 -0
  178. metadata +261 -3
@@ -0,0 +1,360 @@
1
+ require File.expand_path('spec_helper', File.dirname(__FILE__))
2
+
3
+ describe 'faceting', :type => :search do
4
+ it 'returns field name for facet' do
5
+ stub_facet(:title_ss, {})
6
+ result = session.search Post do
7
+ facet :title
8
+ end
9
+ result.facet(:title).field_name.should == :title
10
+ end
11
+
12
+ it 'returns facet specified by string' do
13
+ stub_facet(:title_ss, {})
14
+ result = session.search Post do
15
+ facet :title
16
+ end
17
+ result.facet('title').field_name.should == :title
18
+ end
19
+
20
+ it 'returns all facets specified by search' do
21
+ stub_facet(:title_ss, { 'Author 1' => 1 })
22
+ stub_facet(:blog_id_i, { '1' => 3 })
23
+ result = session.search(Post) do
24
+ facet :title
25
+ facet :blog_id
26
+ end
27
+ result.facets.first.field_name.should == :title
28
+ result.facets.last.field_name.should == :blog_id
29
+ end
30
+
31
+ it 'returns string facet' do
32
+ stub_facet(:title_ss, 'Author 1' => 2, 'Author 2' => 1)
33
+ result = session.search Post do
34
+ facet :title
35
+ end
36
+ facet_values(result, :title).should == ['Author 1', 'Author 2']
37
+ end
38
+
39
+ it 'returns counts for facet' do
40
+ stub_facet(:title_ss, 'Author 1' => 2, 'Author 2' => 1)
41
+ result = session.search Post do
42
+ facet :title
43
+ end
44
+ facet_counts(result, :title).should == [2, 1]
45
+ end
46
+
47
+ it 'returns integer facet' do
48
+ stub_facet(:blog_id_i, '3' => 2, '1' => 1)
49
+ result = session.search Post do
50
+ facet :blog_id
51
+ end
52
+ facet_values(result, :blog_id).should == [3, 1]
53
+ end
54
+
55
+ it 'returns float facet' do
56
+ stub_facet(:average_rating_ft, '9.3' => 2, '1.1' => 1)
57
+ result = session.search Post do
58
+ facet :average_rating
59
+ end
60
+ facet_values(result, :average_rating).should == [9.3, 1.1]
61
+ end
62
+
63
+ it 'returns time facet' do
64
+ stub_facet(
65
+ :published_at_dt,
66
+ '2009-04-07T20:25:23Z' => 3,
67
+ '2009-04-07T20:26:19Z' => 2,
68
+ '2050-04-07T20:27:15Z' => 1
69
+ )
70
+ result = session.search Post do
71
+ facet :published_at
72
+ end
73
+ # In JRuby, Time doesn't have 32-bit range constraint, apparently.
74
+ future_time =
75
+ begin
76
+ Time.gm(2050, 4, 7, 20, 27, 15)
77
+ rescue ArgumentError
78
+ DateTime.civil(2050, 4, 7, 20, 27, 15)
79
+ end
80
+ facet_values(result, :published_at).should ==
81
+ [Time.gm(2009, 4, 7, 20, 25, 23),
82
+ Time.gm(2009, 4, 7, 20, 26, 19),
83
+ future_time]
84
+ end
85
+
86
+ it 'returns date facet' do
87
+ stub_facet(
88
+ :expire_date_d,
89
+ '2009-07-13T00:00:00Z' => 3,
90
+ '2009-04-01T00:00:00Z' => 1
91
+ )
92
+ result = session.search(Post) do
93
+ facet :expire_date
94
+ end
95
+ facet_values(result, :expire_date).should ==
96
+ [Date.new(2009, 07, 13),
97
+ Date.new(2009, 04, 01)]
98
+ end
99
+
100
+ it 'returns trie integer facet' do
101
+ stub_facet(:size_it, '3' => 2, '1' => 1)
102
+ result = session.search Photo do
103
+ facet :size
104
+ end
105
+ facet_values(result, :size).should == [3, 1]
106
+ end
107
+
108
+ it 'returns float facet' do
109
+ stub_facet(:average_rating_ft, '9.3' => 2, '1.1' => 1)
110
+ result = session.search Photo do
111
+ facet :average_rating
112
+ end
113
+ facet_values(result, :average_rating).should == [9.3, 1.1]
114
+ end
115
+
116
+ it 'returns time facet' do
117
+ stub_facet(
118
+ :created_at_dt,
119
+ '2009-04-07T20:25:23Z' => 3,
120
+ '2009-04-07T20:26:19Z' => 1
121
+ )
122
+ result = session.search Photo do
123
+ facet :created_at
124
+ end
125
+ facet_values(result, :created_at).should ==
126
+ [Time.gm(2009, 04, 07, 20, 25, 23),
127
+ Time.gm(2009, 04, 07, 20, 26, 19)]
128
+ end
129
+
130
+ it 'returns boolean facet' do
131
+ stub_facet(:featured_bs, 'true' => 3, 'false' => 1)
132
+ result = session.search(Post) { facet(:featured) }
133
+ facet_values(result, :featured).should == [true, false]
134
+ end
135
+
136
+
137
+ { 'string' => 'blog', 'symbol' => :blog }.each_pair do |type, name|
138
+ it "returns field facet with #{type} custom name" do
139
+ stub_facet(:blog, '2' => 1, '1' => 4)
140
+ result = session.search(Post) { facet(:blog_id, :name => name) }
141
+ facet_values(result, :blog).should == [1, 2]
142
+ end
143
+
144
+ it "assigns #{type} custom name to field facet" do
145
+ stub_facet(:blog, '2' => 1)
146
+ result = session.search(Post) { facet(:blog_id, :name => name) }
147
+ result.facet(:blog).name.should == :blog
148
+ end
149
+
150
+ it "retains field name for #{type} custom-named field facet" do
151
+ stub_facet(:blog, '2' => 1)
152
+ result = session.search(Post) { facet(:blog_id, :name => name) }
153
+ result.facet(:blog).field_name.should == :blog_id
154
+ end
155
+ end
156
+
157
+ it 'returns class facet' do
158
+ stub_facet(:class_name, 'Post' => 3, 'Namespaced::Comment' => 1)
159
+ result = session.search(Post) { facet(:class) }
160
+ facet_values(result, :class).should == [Post, Namespaced::Comment]
161
+ end
162
+
163
+ it 'returns special :any facet' do
164
+ stub_query_facet(
165
+ 'category_ids_im:[* TO *]' => 3
166
+ )
167
+ search = session.search(Post) { facet(:category_ids, :extra => :any) }
168
+ row = search.facet(:category_ids).rows.first
169
+ row.value.should == :any
170
+ row.count.should == 3
171
+ end
172
+
173
+ it 'returns special :none facet' do
174
+ stub_query_facet(
175
+ '-category_ids_im:[* TO *]' => 3
176
+ )
177
+ search = session.search(Post) { facet(:category_ids, :extra => :none) }
178
+ row = search.facet(:category_ids).rows.first
179
+ row.value.should == :none
180
+ row.count.should == 3
181
+ end
182
+
183
+ it 'returns date range facet' do
184
+ stub_date_facet(:published_at_dt, 60*60*24, '2009-07-08T04:00:00Z' => 2, '2009-07-07T04:00:00Z' => 1)
185
+ start_time = Time.utc(2009, 7, 7, 4)
186
+ end_time = start_time + 2*24*60*60
187
+ result = session.search(Post) { facet(:published_at, :time_range => start_time..end_time) }
188
+ facet = result.facet(:published_at)
189
+ facet.rows.first.value.should == (start_time..(start_time+24*60*60))
190
+ facet.rows.last.value.should == ((start_time+24*60*60)..end_time)
191
+ end
192
+
193
+ it 'returns date range facet sorted by count' do
194
+ stub_date_facet(:published_at_dt, 60*60*24, '2009-07-08T04:00:00Z' => 2, '2009-07-07T04:00:00Z' => 1)
195
+ start_time = Time.utc(2009, 7, 7, 4)
196
+ end_time = start_time + 2*24*60*60
197
+ result = session.search(Post) { facet(:published_at, :time_range => start_time..end_time, :sort => :count) }
198
+ facet = result.facet(:published_at)
199
+ facet.rows.first.value.should == ((start_time+24*60*60)..end_time)
200
+ facet.rows.last.value.should == (start_time..(start_time+24*60*60))
201
+ end
202
+
203
+ it 'returns query facet' do
204
+ stub_query_facet(
205
+ 'average_rating_ft:[3\.0 TO 5\.0]' => 3,
206
+ 'average_rating_ft:[1\.0 TO 3\.0]' => 1
207
+ )
208
+ search = session.search(Post) do
209
+ facet :average_rating do
210
+ row 3.0..5.0 do
211
+ with :average_rating, 3.0..5.0
212
+ end
213
+ row 1.0..3.0 do
214
+ with :average_rating, 1.0..3.0
215
+ end
216
+ end
217
+ end
218
+ facet = search.facet(:average_rating)
219
+ facet.rows.first.value.should == (3.0..5.0)
220
+ facet.rows.first.count.should == 3
221
+ facet.rows.last.value.should == (1.0..3.0)
222
+ facet.rows.last.count.should == 1
223
+ end
224
+
225
+ describe 'query facet option handling' do
226
+ def facet_values_from_options(options = {})
227
+ session.search(Post) do
228
+ facet :average_rating, options do
229
+ row(1) { with(:average_rating, 1.0..2.0) }
230
+ row(3) { with(:average_rating, 3.0..4.0) }
231
+ row(2) { with(:average_rating, 2.0..3.0) }
232
+ row(4) { with(:average_rating, 4.0..5.0) }
233
+ end
234
+ end.facet(:average_rating).rows.map { |row| row.value }
235
+ end
236
+
237
+ before :each do
238
+ stub_query_facet(
239
+ 'average_rating_ft:[1\.0 TO 2\.0]' => 2,
240
+ 'average_rating_ft:[2\.0 TO 3\.0]' => 3,
241
+ 'average_rating_ft:[3\.0 TO 4\.0]' => 1,
242
+ 'average_rating_ft:[4\.0 TO 5\.0]' => 0
243
+ )
244
+ end
245
+
246
+ it 'sorts in order of specification if no limit is given' do
247
+ facet_values_from_options.should == [1, 3, 2]
248
+ end
249
+
250
+ it 'sorts lexically if lexical option is specified' do
251
+ facet_values_from_options(:sort => :index).should == [1, 2, 3]
252
+ end
253
+
254
+ it 'sorts by count by default if limit is given' do
255
+ facet_values_from_options(:limit => 2).should == [2, 1]
256
+ end
257
+
258
+ it 'sorts by count if count option is specified' do
259
+ facet_values_from_options(:sort => :count).should == [2, 1, 3]
260
+ end
261
+
262
+ it 'sorts lexically if lexical option is specified even if limit is given' do
263
+ facet_values_from_options(:sort => :index, :limit => 2).should == [1, 2]
264
+ end
265
+
266
+ it 'limits facets if limit option is given' do
267
+ facet_values_from_options(:limit => 1).should == [2]
268
+ end
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
+
274
+ it 'returns all facets if limit greater than number of facets' do
275
+ facet_values_from_options(:limit => 10).should == [2, 1, 3]
276
+ end
277
+
278
+ it 'allows zero count if specified' do
279
+ facet_values_from_options(:zeros => true).should == [1, 3, 2, 4]
280
+ end
281
+
282
+ it 'sets minimum count' do
283
+ facet_values_from_options(:minimum_count => 2).should == [1, 2]
284
+ end
285
+ end
286
+
287
+ it 'returns limited field facet' do
288
+ stub_query_facet(
289
+ 'category_ids_im:1' => 3,
290
+ 'category_ids_im:3' => 1
291
+ )
292
+ search = session.search(Post) do
293
+ facet :category_ids, :only => [1, 3, 5]
294
+ end
295
+ facet = search.facet(:category_ids)
296
+ facet.rows.first.value.should == 1
297
+ facet.rows.first.count.should == 3
298
+ facet.rows.last.value.should == 3
299
+ facet.rows.last.count.should == 1
300
+ end
301
+
302
+ it 'returns instantiated facet values' do
303
+ blogs = Array.new(2) { Blog.new }
304
+ stub_facet(:blog_id_i, blogs[0].id.to_s => 2, blogs[1].id.to_s => 1)
305
+ search = session.search(Post) { facet(:blog_id) }
306
+ search.facet(:blog_id).rows.map { |row| row.instance }.should == blogs
307
+ end
308
+
309
+ it 'returns all instantiated facet rows, whether or not the instances exist' do
310
+ blogs = Array.new(2) { Blog.new }
311
+ blogs.last.destroy
312
+ stub_facet(:blog_id_i, blogs[0].id.to_s => 2, blogs[1].id.to_s => 1)
313
+ search = session.search(Post) { facet(:blog_id) }
314
+ search.facet(:blog_id).rows.map { |row| row.instance }.should == [blogs.first, nil]
315
+ end
316
+
317
+ it 'returns only rows with available instances if specified' do
318
+ blogs = Array.new(2) { Blog.new }
319
+ blogs.last.destroy
320
+ stub_facet(:blog_id_i, blogs[0].id.to_s => 2, blogs[1].id.to_s => 1)
321
+ search = session.search(Post) { facet(:blog_id) }
322
+ search.facet(:blog_id).rows(:verify => true).map { |row| row.instance }.should == blogs[0..0]
323
+ end
324
+
325
+ it 'returns both verified and unverified rows from the same facet' do
326
+ blogs = Array.new(2) { Blog.new }
327
+ blogs.last.destroy
328
+ stub_facet(:blog_id_i, blogs[0].id.to_s => 2, blogs[1].id.to_s => 1)
329
+ search = session.search(Post) { facet(:blog_id) }
330
+ search.facet(:blog_id).rows(:verify => true).map { |row| row.instance }.should == blogs[0..0]
331
+ search.facet(:blog_id).rows.map { |row| row.instance }.should == [blogs.first, nil]
332
+ end
333
+
334
+ it 'ignores :verify option if facet not a reference facet' do
335
+ stub_facet(:category_ids_im, '1' => 2, '2' => 1)
336
+ search = session.search(Post) { facet(:category_ids) }
337
+ search.facet(:category_ids).should have(2).rows(:verify => true)
338
+ end
339
+
340
+ it 'returns instantiated facet values for limited field facet' do
341
+ blogs = Array.new(2) { Blog.new }
342
+ stub_query_facet(
343
+ "blog_id_i:#{blogs[0].id}" => 3,
344
+ "blog_id_i:#{blogs[1].id}" => 1
345
+ )
346
+ search = session.search(Post) do
347
+ facet(:blog_id, :only => blogs.map { |blog| blog.id })
348
+ end
349
+ search.facet(:blog_id).rows.map { |row| row.instance }.should == blogs
350
+ end
351
+
352
+ it 'only queries the persistent store once for an instantiated facet' do
353
+ query_count = Blog.query_count
354
+ blogs = Array.new(2) { Blog.new }
355
+ stub_facet(:blog_id_i, blogs[0].id.to_s => 2, blogs[1].id.to_s => 1)
356
+ result = session.search(Post) { facet(:blog_id) }
357
+ result.facet(:blog_id).rows.each { |row| row.instance }
358
+ (Blog.query_count - query_count).should == 1
359
+ end
360
+ end
@@ -0,0 +1,69 @@
1
+ require File.expand_path('spec_helper', File.dirname(__FILE__))
2
+
3
+ describe 'search with highlighting results', :type => :search do
4
+ before :each do
5
+ @posts = Array.new(2) { Post.new }
6
+ stub_results_with_highlighting(
7
+ @posts[0],
8
+ { 'title_text' => ['one @@@hl@@@two@@@endhl@@@ three'] },
9
+ @posts[1],
10
+ { 'title_text' => ['three four @@@hl@@@five@@@endhl@@@'],
11
+ 'body_text' => ['@@@hl@@@five@@@ six seven', '@@@hl@@@eight@@@endhl@@@ nine @@@hl@@@ten@@@endhl@@@'] }
12
+ )
13
+ @search = session.search(Post)
14
+ end
15
+
16
+ it 'returns all highlights' do
17
+ @search.hits.last.should have(3).highlights
18
+ end
19
+
20
+ it 'returns all highlights for a specified field' do
21
+ @search.hits.last.should have(2).highlights(:body)
22
+ end
23
+
24
+ it 'returns first highlight for a specified field' do
25
+ @search.hits.first.highlight(:title).format.should == 'one <em>two</em> three'
26
+ end
27
+
28
+ it 'returns an empty array if a given field does not have a highlight' do
29
+ @search.hits.first.highlights(:body).should == []
30
+ end
31
+
32
+ it 'formats hits with <em> by default' do
33
+ highlight = @search.hits.first.highlights(:title).first.formatted
34
+ highlight.should == 'one <em>two</em> three'
35
+ end
36
+
37
+ it 'formats hits with provided block' do
38
+ highlight = @search.hits.first.highlights(:title).first.format do |word|
39
+ "<i>#{word}</i>"
40
+ end
41
+ highlight.should == 'one <i>two</i> three'
42
+ end
43
+
44
+ it 'handles multiple highlighted words' do
45
+ highlight = @search.hits.last.highlights(:body).last.format do |word|
46
+ "<b>#{word}</b>"
47
+ end
48
+ highlight.should == '<b>eight</b> nine <b>ten</b>'
49
+ end
50
+
51
+ private
52
+
53
+ def stub_results_with_highlighting(*instances_and_highlights)
54
+ docs, highlights = [], []
55
+ instances_and_highlights.each_slice(2) do |doc, highlight|
56
+ docs << doc
57
+ highlights << highlight
58
+ end
59
+ response = stub_full_results(*docs.map { |doc| { 'instance' => doc }})
60
+ highlighting = response['highlighting'] = {}
61
+ highlights.each_with_index do |highlight, i|
62
+ if highlight
63
+ instance = docs[i]
64
+ highlighting["#{instance.class.name} #{instance.id}"] = highlight
65
+ end
66
+ end
67
+ response
68
+ end
69
+ end
@@ -0,0 +1,131 @@
1
+ require File.expand_path('spec_helper', File.dirname(__FILE__))
2
+
3
+ describe 'hits', :type => :search do
4
+ it 'should return hits without loading instances' do
5
+ post_1, post_2 = Array.new(2) { Post.new }
6
+ stub_results(post_1, post_2)
7
+ %w(load load_all).each do |message|
8
+ MockAdapter::DataAccessor.should_not_receive(message)
9
+ end
10
+ session.search(Post).hits.map do |hit|
11
+ [hit.class_name, hit.primary_key]
12
+ end.should == [['Post', post_1.id.to_s], ['Post', post_2.id.to_s]]
13
+ end
14
+
15
+ it 'returns search total as attribute of hits' do
16
+ stub_results(Post.new, 4)
17
+ session.search(Post) do
18
+ paginate(:page => 1)
19
+ end.hits.total_entries.should == 4
20
+ end
21
+
22
+ it 'returns search total as attribute of verified hits' do
23
+ stub_results(Post.new, 4)
24
+ session.search(Post) do
25
+ paginate(:page => 1)
26
+ end.hits(:verify => true).total_entries.should == 4
27
+ end
28
+
29
+ it 'should return instance from hit' do
30
+ posts = Array.new(2) { Post.new }
31
+ stub_results(*posts)
32
+ session.search(Post).hits.first.instance.should == posts.first
33
+ end
34
+
35
+ it 'should return the instance primary key when you use it as a param' do
36
+ posts = Array.new(2) { Post.new }
37
+ stub_results(*posts)
38
+ session.search(Post).hits.first.to_param.should == posts.first.id.to_s
39
+ end
40
+
41
+ it 'should provide iterator over hits with instances' do
42
+ posts = Array.new(2) { Post.new }
43
+ stub_results(*posts)
44
+ search = session.search(Post)
45
+ hits, results = [], []
46
+ search.each_hit_with_result do |hit, result|
47
+ hits << hit
48
+ results << result
49
+ end
50
+ end
51
+
52
+ it 'should hydrate all hits when an instance is requested from a hit' do
53
+ posts = Array.new(2) { Post.new }
54
+ stub_results(*posts)
55
+ search = session.search(Post)
56
+ search.hits.first.instance
57
+ %w(load load_all).each do |message|
58
+ MockAdapter::DataAccessor.should_not_receive(message)
59
+ end
60
+ search.hits.last.instance.should == posts.last
61
+ end
62
+
63
+ it 'should return only hits whose referenced object exists in the data store if :verify option passed' do
64
+ posts = Array.new(2) { Post.new }
65
+ posts.last.destroy
66
+ stub_results(*posts)
67
+ search = session.search(Post)
68
+ search.hits(:verify => true).map { |hit| hit.instance }.should == posts[0..0]
69
+ end
70
+
71
+ it 'should return verified and unverified hits from the same search' do
72
+ posts = Array.new(2) { Post.new }
73
+ posts.last.destroy
74
+ stub_results(*posts)
75
+ search = session.search(Post)
76
+ search.hits(:verify => true).map { |hit| hit.instance }.should == posts[0..0]
77
+ search.hits.map { |hit| hit.instance }.should == [posts.first, nil]
78
+ end
79
+
80
+ it 'should attach score to hits' do
81
+ stub_full_results('instance' => Post.new, 'score' => 1.23)
82
+ session.search(Post).hits.first.score.should == 1.23
83
+ end
84
+
85
+ it 'should return stored field values in hits' do
86
+ stub_full_results('instance' => Post.new, 'title_ss' => 'Title')
87
+ session.search(Post).hits.first.stored(:title).should == 'Title'
88
+ end
89
+
90
+ it 'should return stored field values for searches against multiple types' do
91
+ stub_full_results('instance' => Post.new, 'title_ss' => 'Title')
92
+ session.search(Post, Namespaced::Comment).hits.first.stored(:title).should == 'Title'
93
+ end
94
+
95
+ it 'should return stored field values for searches against base type when subtype matches' do
96
+ class SubclassedPost < Post; end;
97
+ stub_full_results('instance' => SubclassedPost.new, 'title_ss' => 'Title')
98
+ session.search(Post).hits.first.stored(:title).should == 'Title'
99
+ end
100
+
101
+ it 'should return stored text fields' do
102
+ stub_full_results('instance' => Post.new, 'body_textsv' => 'Body')
103
+ session.search(Post, Namespaced::Comment).hits.first.stored(:body).should == 'Body'
104
+ end
105
+
106
+ it 'should return stored boolean fields' do
107
+ stub_full_results('instance' => Post.new, 'featured_bs' => true)
108
+ session.search(Post, Namespaced::Comment).hits.first.stored(:featured).should be_true
109
+ end
110
+
111
+ it 'should return stored boolean fields that evaluate to false' do
112
+ stub_full_results('instance' => Post.new, 'featured_bs' => false)
113
+ session.search(Post, Namespaced::Comment).hits.first.stored(:featured).should == false
114
+ end
115
+
116
+ it 'should return stored dynamic fields' do
117
+ stub_full_results('instance' => Post.new, 'custom_string:test_ss' => 'Custom')
118
+ session.search(Post, Namespaced::Comment).hits.first.stored(:custom_string, :test).should == 'Custom'
119
+ end
120
+
121
+ it 'should typecast stored field values in hits' do
122
+ time = Time.utc(2008, 7, 8, 2, 45)
123
+ stub_full_results('instance' => Post.new, 'last_indexed_at_ds' => time.xmlschema)
124
+ session.search(Post).hits.first.stored(:last_indexed_at).should == time
125
+ end
126
+
127
+ it 'should return stored values for multi-valued fields' do
128
+ stub_full_results('instance' => User.new, 'role_ids_ims' => %w(1 4 5))
129
+ session.search(User).hits.first.stored(:role_ids).should == [1, 4, 5]
130
+ end
131
+ end
@@ -0,0 +1,36 @@
1
+ require File.expand_path('spec_helper', File.dirname(__FILE__))
2
+
3
+ describe "PaginatedCollection" do
4
+ subject { Sunspot::Search::PaginatedCollection.new [], 1, 10, 20 }
5
+
6
+ it { subject.should be_an(Array) }
7
+
8
+ context "behaves like a WillPaginate::Collection" do
9
+ it { subject.total_entries.should eql(20) }
10
+ it { subject.total_pages.should eql(2) }
11
+ it { subject.current_page.should eql(1) }
12
+ it { subject.per_page.should eql(10) }
13
+ it { subject.previous_page.should be_nil }
14
+ it { subject.next_page.should eql(2) }
15
+ it { subject.out_of_bounds?.should_not be_true }
16
+ it { subject.offset.should eql(0) }
17
+
18
+ it 'should allow setting total_count' do
19
+ subject.total_count = 1
20
+ subject.total_count.should eql(1)
21
+ end
22
+
23
+ it 'should allow setting total_entries' do
24
+ subject.total_entries = 1
25
+ subject.total_entries.should eql(1)
26
+ end
27
+ end
28
+
29
+ context "behaves like Kaminari" do
30
+ it { subject.total_count.should eql(20) }
31
+ it { subject.num_pages.should eql(2) }
32
+ it { subject.limit_value.should eql(10) }
33
+ it { subject.first_page?.should be_true }
34
+ it { subject.last_page?.should_not be_true }
35
+ end
36
+ end
@@ -0,0 +1,72 @@
1
+ require File.expand_path('spec_helper', File.dirname(__FILE__))
2
+
3
+ describe 'search results', :type => :search do
4
+ it 'loads single result' do
5
+ post = Post.new
6
+ stub_results(post)
7
+ session.search(Post).results.should == [post]
8
+ end
9
+
10
+ it 'loads multiple 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
+ it 'gracefully returns empty results when response is nil' do
29
+ stub_nil_results
30
+ session.search(Post).results.should == []
31
+ end
32
+
33
+ it 'returns search total as attribute of results' do
34
+ stub_results(Post.new, 4)
35
+ session.search(Post) do
36
+ paginate(:page => 1)
37
+ end.results.total_entries.should == 4
38
+ end
39
+
40
+ it 'returns total' do
41
+ stub_results(Post.new, Post.new, 4)
42
+ session.search(Post) { paginate(:page => 1) }.total.should == 4
43
+ end
44
+
45
+ it 'returns query time' do
46
+ stub_nil_results
47
+ connection.response['responseHeader'] = { 'QTime' => 42 }
48
+ session.search(Post) { paginate(:page => 1) }.query_time.should == 42
49
+ end
50
+
51
+ it 'returns total for nil search' do
52
+ stub_nil_results
53
+ session.search(Post).total.should == 0
54
+ end
55
+
56
+ it 'returns available results if some results are not available from data store' do
57
+ posts = [Post.new, Post.new]
58
+ posts.last.destroy
59
+ stub_results(*posts)
60
+ session.search(Post).results.should == posts[0..0]
61
+ end
62
+
63
+ it 'does not attempt to query the data store more than once when results are unavailable' do
64
+ posts = [Post.new, Post.new]
65
+ posts.each { |post| post.destroy }
66
+ stub_results(*posts)
67
+ search = session.search(Post) do
68
+ data_accessor_for(Post).should_receive(:load_all).once.and_return([])
69
+ end
70
+ search.results.should == []
71
+ end
72
+ end
@@ -0,0 +1,23 @@
1
+ require File.expand_path('spec_helper', File.dirname(__FILE__))
2
+
3
+ describe Sunspot::Search do
4
+ it 'should allow access to the data accessor' do
5
+ stub_results(posts = Post.new)
6
+ search = session.search Post do
7
+ data_accessor_for(Post).custom_title = 'custom title'
8
+ end
9
+ search.results.first.title.should == 'custom title'
10
+ end
11
+
12
+ it 'should re-execute search' do
13
+ post_1, post_2 = Post.new, Post.new
14
+
15
+ stub_results(post_1)
16
+ search = session.search Post
17
+ search.results.should == [post_1]
18
+
19
+ stub_results(post_2)
20
+ search.execute!
21
+ search.results.should == [post_2]
22
+ end
23
+ end
@@ -0,0 +1 @@
1
+ require File.expand_path('spec_helper', File.join(File.dirname(__FILE__), '..'))