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,327 @@
1
+ module Sunspot
2
+ module DSL
3
+ #
4
+ # Provides an API for areas of the query DSL that operate on specific
5
+ # fields. This functionality is provided by the query DSL and the dynamic
6
+ # query DSL.
7
+ #
8
+ class FieldQuery < Scope
9
+ def initialize(search, query, setup) #:nodoc:
10
+ @search, @query = search, query
11
+ super(query.scope, setup)
12
+ end
13
+
14
+ # Specify the order that results should be returned in. This method can
15
+ # be called multiple times; precedence will be in the order given.
16
+ #
17
+ # ==== Parameters
18
+ #
19
+ # field_name<Symbol>:: the field to use for ordering
20
+ # direction<Symbol>:: :asc or :desc (default :asc)
21
+ #
22
+ def order_by(field_name, direction = nil)
23
+ sort =
24
+ if special = Sunspot::Query::Sort.special(field_name)
25
+ special.new(direction)
26
+ else
27
+ Sunspot::Query::Sort::FieldSort.new(
28
+ @setup.field(field_name), direction
29
+ )
30
+ end
31
+ @query.add_sort(sort)
32
+ end
33
+
34
+ #
35
+ # Specify that the results should be ordered based on their
36
+ # distance from a given point.
37
+ #
38
+ # ==== Parameters
39
+ #
40
+ # field_name<Symbol>::
41
+ # the field that stores the location (declared as `latlon`)
42
+ # lat<Numeric>::
43
+ # the reference latitude
44
+ # lon<Numeric>::
45
+ # the reference longitude
46
+ # direction<Symbol>::
47
+ # :asc or :desc (default :asc)
48
+ #
49
+ def order_by_geodist(field_name, lat, lon, direction = nil)
50
+ @query.add_sort(
51
+ Sunspot::Query::Sort::GeodistSort.new(@setup.field(field_name), lat, lon, direction)
52
+ )
53
+ end
54
+
55
+ #
56
+ # DEPRECATED Use <code>order_by(:random)</code>
57
+ #
58
+ def order_by_random
59
+ order_by(:random)
60
+ end
61
+
62
+ # Specify a field for result grouping. Grouping groups documents
63
+ # with a common field value, return only the top document per
64
+ # group.
65
+ #
66
+ # More information in the Solr documentation:
67
+ # <http://wiki.apache.org/solr/FieldCollapsing>
68
+ #
69
+ # ==== Parameters
70
+ #
71
+ # field_name<Symbol>:: the field to use for grouping
72
+ def group(*field_names, &block)
73
+ options = Sunspot::Util.extract_options_from(field_names)
74
+
75
+ field_names.each do |field_name|
76
+ field = @setup.field(field_name)
77
+ group = @query.add_group(Sunspot::Query::FieldGroup.new(field))
78
+ @search.add_field_group(field)
79
+
80
+ if block
81
+ Sunspot::Util.instance_eval_or_call(
82
+ FieldGroup.new(@query, @setup, group),
83
+ &block
84
+ )
85
+ end
86
+ end
87
+ end
88
+
89
+ #
90
+ # Request a facet on the search query. A facet is a feature of Solr that
91
+ # determines the number of documents that match the existing search *and*
92
+ # an additional criterion. This allows you to build powerful drill-down
93
+ # interfaces for search, at each step presenting the searcher with a set
94
+ # of refinements that are known to return results.
95
+ #
96
+ # In Sunspot, each facet returns zero or more rows, each of which
97
+ # represents a particular criterion conjoined with the actual query being
98
+ # performed. For _field_ _facets_, each row represents a particular value
99
+ # for a given field. For _query_ _facets_, each row represents an
100
+ # arbitrary scope; the facet itself is just a means of logically grouping
101
+ # the scopes.
102
+ #
103
+ # === Examples
104
+ #
105
+ # ==== Field Facets
106
+ #
107
+ # A field facet is specified by passing one or more Symbol arguments to
108
+ # this method:
109
+ #
110
+ # Sunspot.search(Post) do
111
+ # with(:blog_id, 1)
112
+ # facet(:category_id)
113
+ # end
114
+ #
115
+ # The facet specified above will have a row for each category_id that is
116
+ # present in a document which also has a blog_id of 1.
117
+ #
118
+ # ==== Multiselect Facets
119
+ #
120
+ # In certain circumstances, it is beneficial to exclude certain query
121
+ # scopes from a facet; the most common example is multi-select faceting,
122
+ # where the user has selected a certain value, but the facet should still
123
+ # show all options that would be available if they had not:
124
+ #
125
+ # Sunspot.search(Post) do
126
+ # with(:blog_id, 1)
127
+ # category_filter = with(:category_id, 2)
128
+ # facet(:category_id, :exclude => category_filter)
129
+ # end
130
+ #
131
+ # Although the results of the above search will be restricted to those
132
+ # with a category_id of 2, the category_id facet will operate as if a
133
+ # category had not been selected, allowing the user to select additional
134
+ # categories (which will presumably be ORed together).
135
+ #
136
+ # It possible to exclude multiple filters by passing an array:
137
+ #
138
+ # Sunspot.search(Post) do
139
+ # with(:blog_id, 1)
140
+ # category_filter = with(:category_id, 2)
141
+ # author_filter = with(:author_id, 3)
142
+ # facet(:category_id,
143
+ # :exclude => [category_filter, author_filter].compact)
144
+ # end
145
+ #
146
+ # You should consider using +.compact+ to ensure that the array does not
147
+ # contain any nil values.
148
+ #
149
+ # <strong>As far as I can tell, Solr only supports multi-select with
150
+ # field facets; if +:exclude+ is passed to a query facet, this method will
151
+ # raise an error. Also, the +:only+ and +:extra+ options use query
152
+ # faceting under the hood, so these can't be used with +:extra+ either.
153
+ # </strong>
154
+ #
155
+ # ==== Query Facets
156
+ #
157
+ # A query facet is a collection of arbitrary scopes, each of which
158
+ # represents a row. This is specified by passing a block into the #facet
159
+ # method; the block then contains one or more +row+ blocks, each of which
160
+ # creates a query facet row. The +row+ blocks follow the usual Sunspot
161
+ # scope DSL.
162
+ #
163
+ # For example, a query facet can be used to facet over a set of ranges:
164
+ #
165
+ # Sunspot.search(Post) do
166
+ # facet(:average_rating) do
167
+ # row(1.0..2.0) do
168
+ # with(:average_rating, 1.0..2.0)
169
+ # end
170
+ # row(2.0..3.0) do
171
+ # with(:average_rating, 2.0..3.0)
172
+ # end
173
+ # row(3.0..4.0) do
174
+ # with(:average_rating, 3.0..4.0)
175
+ # end
176
+ # row(4.0..5.0) do
177
+ # with(:average_rating, 4.0..5.0)
178
+ # end
179
+ # end
180
+ # end
181
+ #
182
+ # Note that the arguments to the +facet+ and +row+ methods simply provide
183
+ # labels for the facet and its rows, so that they can be retrieved and
184
+ # identified from the Search object. They are not passed to Solr and no
185
+ # semantic meaning is attached to them. The label for +facet+ should be
186
+ # a symbol; the label for +row+ can be whatever you'd like.
187
+ #
188
+ # ==== Parameters
189
+ #
190
+ # field_names...<Symbol>:: fields for which to return field facets
191
+ #
192
+ # ==== Options
193
+ #
194
+ # :sort<Symbol>::
195
+ # Either :count (values matching the most terms first) or :index (lexical)
196
+ # :limit<Integer>::
197
+ # The maximum number of facet rows to return
198
+ # :minimum_count<Integer>::
199
+ # The minimum count a facet row must have to be returned
200
+ # :zeros<Boolean>::
201
+ # Return facet rows for which there are no matches (equivalent to
202
+ # :minimum_count => 0). Default is false.
203
+ # :exclude<Object,Array>::
204
+ # Exclude one or more filters when performing the faceting (see
205
+ # Multiselect Faceting above). The object given for this argument should
206
+ # be the return value(s) of a scoping method (+with+, +any_of+,
207
+ # +all_of+, etc.). <strong>Only can be used for field facets that do not
208
+ # use the +:extra+ or +:only+ options.</strong>
209
+ # :name<Symbol>::
210
+ # Give a custom name to a field facet. The main use case for this option
211
+ # is for requesting the same field facet multiple times, using different
212
+ # filter exclusions (see Multiselect Faceting above). If you pass this
213
+ # option, it is also the argument that should be passed to Search#facet
214
+ # when retrieving the facet result.
215
+ # :only<Array>::
216
+ # Only return facet rows for the given values. Useful if you are only
217
+ # interested in faceting on a subset of values for a given field.
218
+ # <strong>Only applies to field facets.</strong>
219
+ # :extra<Symbol,Array>::
220
+ # One or more of :any and :none. :any returns a facet row with a count
221
+ # of all matching documents that have some value for this field. :none
222
+ # returns a facet row with a count of all matching documents that have
223
+ # no value for this field. The facet row(s) corresponding to the extras
224
+ # have a value of the symbol passed. <strong>Only applies to field
225
+ # facets.</strong>
226
+ #
227
+ def facet(*field_names, &block)
228
+ options = Sunspot::Util.extract_options_from(field_names)
229
+
230
+ if block
231
+ if field_names.length != 1
232
+ raise(
233
+ ArgumentError,
234
+ "wrong number of arguments (#{field_names.length} for 1)"
235
+ )
236
+ end
237
+ if options.has_key?(:exclude)
238
+ raise(
239
+ ArgumentError,
240
+ "can't use :exclude with query facets"
241
+ )
242
+ end
243
+ search_facet = @search.add_query_facet(field_names.first, options)
244
+ Sunspot::Util.instance_eval_or_call(
245
+ QueryFacet.new(@query, @setup, search_facet),
246
+ &block
247
+ )
248
+ elsif options[:only]
249
+ if options.has_key?(:exclude)
250
+ raise(
251
+ ArgumentError,
252
+ "can't use :exclude with :only (see documentation)"
253
+ )
254
+ end
255
+ field_names.each do |field_name|
256
+ field = @setup.field(field_name)
257
+ search_facet = @search.add_field_facet(field, options)
258
+ Util.Array(options[:only]).each do |value|
259
+ facet = Sunspot::Query::QueryFacet.new
260
+ facet.add_positive_restriction(field, Sunspot::Query::Restriction::EqualTo, value)
261
+ @query.add_query_facet(facet)
262
+ search_facet.add_row(value, facet.to_boolean_phrase)
263
+ end
264
+ end
265
+ else
266
+ field_names.each do |field_name|
267
+ search_facet = nil
268
+ field = @setup.field(field_name)
269
+ facet =
270
+ if options[:time_range]
271
+ unless field.type.is_a?(Sunspot::Type::TimeType)
272
+ raise(
273
+ ArgumentError,
274
+ ':time_range can only be specified for Date or Time fields'
275
+ )
276
+ end
277
+ search_facet = @search.add_date_facet(field, options)
278
+ Sunspot::Query::DateFieldFacet.new(field, options)
279
+ else
280
+ search_facet = @search.add_field_facet(field, options)
281
+ Sunspot::Query::FieldFacet.new(field, options)
282
+ end
283
+ @query.add_field_facet(facet)
284
+ Util.Array(options[:extra]).each do |extra|
285
+ if options.has_key?(:exclude)
286
+ raise(
287
+ ArgumentError,
288
+ "can't use :exclude with :extra (see documentation)"
289
+ )
290
+ end
291
+ extra_facet = Sunspot::Query::QueryFacet.new
292
+ case extra
293
+ when :any
294
+ extra_facet.add_negated_restriction(
295
+ field,
296
+ Sunspot::Query::Restriction::EqualTo,
297
+ nil
298
+ )
299
+ when :none
300
+ extra_facet.add_positive_restriction(
301
+ field,
302
+ Sunspot::Query::Restriction::EqualTo,
303
+ nil
304
+ )
305
+ else
306
+ raise(
307
+ ArgumentError,
308
+ "Allowed values for :extra are :any and :none"
309
+ )
310
+ end
311
+ search_facet.add_row(extra, extra_facet.to_boolean_phrase)
312
+ @query.add_query_facet(extra_facet)
313
+ end
314
+ end
315
+ end
316
+ end
317
+
318
+ def dynamic(base_name, &block)
319
+ dynamic_field_factory = @setup.dynamic_field_factory(base_name)
320
+ Sunspot::Util.instance_eval_or_call(
321
+ FieldQuery.new(@search, @query, dynamic_field_factory),
322
+ &block
323
+ )
324
+ end
325
+ end
326
+ end
327
+ end
@@ -0,0 +1,103 @@
1
+ module Sunspot
2
+ module DSL #:nodoc:
3
+ # The Fields class provides a DSL for specifying field definitions in the
4
+ # Sunspot.setup block. As well as the #text method, which creates fulltext
5
+ # fields, uses #method_missing to allow definition of typed fields. The
6
+ # available methods are determined by the constants defined in
7
+ # Sunspot::Type - in theory (though this is untested), plugin developers
8
+ # should be able to add support for new types simply by creating new
9
+ # implementations in Sunspot::Type
10
+ #
11
+ class Fields
12
+ def initialize(setup) #:nodoc:
13
+ @setup = setup
14
+ end
15
+
16
+ # Add a text field. Text fields are tokenized before indexing and are
17
+ # the only fields searched in fulltext searches. If a block is passed,
18
+ # create a virtual field; otherwise create an attribute field.
19
+ #
20
+ # If options are passed, they will be applied to all the given fields.
21
+ #
22
+ # ==== Parameters
23
+ #
24
+ # names...<Symbol>:: One or more field names
25
+ #
26
+ # ==== Options
27
+ #
28
+ # :boost<Float>::
29
+ # Index-time boost that should be applied to this field for keyword search
30
+ # :default_boost<Float>::
31
+ # Default search-time boost to apply to this field during keyword
32
+ # search. Can be overriden with DSL::Fulltext#fields or
33
+ # DSL::Fulltext#boost_fields method.
34
+ #
35
+ def text(*names, &block)
36
+ options = names.pop if names.last.is_a?(Hash)
37
+ names.each do |name|
38
+ @setup.add_text_field_factory(
39
+ name,
40
+ options || {},
41
+ &block
42
+ )
43
+ end
44
+ end
45
+
46
+ #
47
+ # Specify a document-level boost. As with fields, you have the option of
48
+ # passing an attribute name which will be called on each model, or a block
49
+ # to be evaluated in the model's context. As well as these two options,
50
+ # this method can also take a constant number, meaning that all indexed
51
+ # documents of this class will have the specified boost.
52
+ #
53
+ # ==== Parameters
54
+ #
55
+ # attr_name<Symbol,~.to_f>:: Attribute name to call or a numeric constant
56
+ #
57
+ def boost(attr_name = nil, &block)
58
+ @setup.add_document_boost(attr_name, &block)
59
+ end
60
+
61
+ # method_missing is used to provide access to typed fields, because
62
+ # developers should be able to add new Sunspot::Type implementations
63
+ # dynamically and have them recognized inside the Fields DSL. Like #text,
64
+ # these methods will create a VirtualField if a block is passed, or an
65
+ # AttributeField if not.
66
+ #
67
+ # ==== Example
68
+ #
69
+ # Sunspot.setup(File) do
70
+ # time :mtime
71
+ # end
72
+ #
73
+ # The call to +time+ will create a field of type Sunspot::Types::TimeType
74
+ #
75
+ def method_missing(method, *args, &block)
76
+ options = Util.extract_options_from(args)
77
+ type_const_name = "#{Util.camel_case(method.to_s.sub(/^dynamic_/, ''))}Type"
78
+ trie = options.delete(:trie)
79
+ type_const_name = "Trie#{type_const_name}" if trie
80
+ begin
81
+ type_class = Type.const_get(type_const_name)
82
+ rescue(NameError)
83
+ if trie
84
+ raise ArgumentError, "Trie fields are only valid for numeric and time types"
85
+ else
86
+ super(method, *args, &block)
87
+ end
88
+ end
89
+ type = type_class.instance
90
+ name = args.shift
91
+ if method.to_s =~ /^dynamic_/
92
+ if type.accepts_dynamic?
93
+ @setup.add_dynamic_field_factory(name, type, options, &block)
94
+ else
95
+ super(method, *args, &block)
96
+ end
97
+ else
98
+ @setup.add_field_factory(name, type, options, &block)
99
+ end
100
+ end
101
+ end
102
+ end
103
+ end
@@ -0,0 +1,243 @@
1
+ module Sunspot
2
+ module DSL
3
+ #
4
+ # This DSL exposes the functionality provided by Solr's fulltext Dismax
5
+ # handler.
6
+ #
7
+ class Fulltext
8
+ attr_reader :exclude_fields #:nodoc:
9
+
10
+ # accept function in boost
11
+ include Functional
12
+
13
+ def initialize(query, setup) #:nodoc:
14
+ @query, @setup = query, setup
15
+ @fields_added = false
16
+ @exclude_fields = []
17
+ end
18
+
19
+ #
20
+ # Specify which fields to search. Field names specified as arguments are
21
+ # given default boost; field boosts can be specified by passing a hash of
22
+ # field names keyed to boost values as the last argument.
23
+ #
24
+ # If you wish to boost certain fields without restricting which fields are
25
+ # searched, use #boost_fields
26
+ #
27
+ # === Example
28
+ #
29
+ # Sunspot.search(Post) do
30
+ # keywords 'search is cool' do
31
+ # fields(:body, :title => 2.0)
32
+ # end
33
+ # end
34
+ #
35
+ # This would search the :body field with default boost (1.0), and the :title
36
+ # field with a boost of 2.0
37
+ #
38
+ def fields(*field_names)
39
+ @fields_added = true
40
+ boosted_fields = field_names.pop if field_names.last.is_a?(Hash)
41
+ field_names.each do |field_name|
42
+ @setup.text_fields(field_name).each do |field|
43
+ @query.add_fulltext_field(field, field.default_boost)
44
+ end
45
+ end
46
+ if boosted_fields
47
+ boosted_fields.each_pair do |field_name, boost|
48
+ @setup.text_fields(field_name).each do |field|
49
+ @query.add_fulltext_field(field, boost)
50
+ end
51
+ end
52
+ end
53
+ end
54
+
55
+ #
56
+ # Exclude the given fields from the search. All fields that are configured
57
+ # for the types under search and not listed here will be searched.
58
+ #
59
+ def exclude_fields(*field_names)
60
+ @exclude_fields.concat(field_names)
61
+ end
62
+
63
+ #
64
+ # Enable keyword highlighting for this search. By default, the fields
65
+ # under search will be highlighted; you may also may pass one or more
66
+ # symbol arguments indicating fields to be highlighted (they don't even
67
+ # have to be the same fields you're searching).
68
+ #
69
+ # === Example
70
+ #
71
+ # Sunspot.search(Post) do
72
+ # keywords 'show me the highlighting' do
73
+ # highlight :title, :body
74
+ # end
75
+ # end
76
+ #
77
+ # You may also pass a hash of options as the last argument. Options are
78
+ # the following:
79
+ #
80
+ # Full disclosure: I barely understand what these options actually do;
81
+ # this documentation is pretty much just copied from the
82
+ # (http://wiki.apache.org/solr/HighlightingParameters#head-23ecd5061bc2c86a561f85dc1303979fe614b956)[Solr Wiki]
83
+ #
84
+ # :max_snippets::
85
+ # The maximum number of highlighted snippets to generate per field
86
+ # :fragment_size::
87
+ # The number of characters to consider for a highlighted fragment
88
+ # :merge_continuous_fragments::
89
+ # Collapse continuous fragments into a single fragment
90
+ # :phrase_highlighter::
91
+ # Highlight phrase terms only when they appear within the query phrase
92
+ # in the document
93
+ # :require_field_match::
94
+ # If true, a field will only be highlighted if the query matched in
95
+ # this particular field (only has an effect if :phrase_highlighter is
96
+ # true as well)
97
+ #
98
+ def highlight(*args)
99
+ options = args.last.kind_of?(Hash) ? args.pop : {}
100
+ fields = []
101
+ args.each { |field_name| fields.concat(@setup.text_fields(field_name)) }
102
+
103
+ @query.add_highlight(fields, options)
104
+ end
105
+
106
+ #
107
+ # Phrase fields are an awesome dismax feature that adds extra boost to
108
+ # documents for which all the fulltext keywords appear in close proximity
109
+ # in one of the given fields. Excellent for titles, headlines, etc.
110
+ #
111
+ # Boosted fields are specified in a hash of field names to a boost, as
112
+ # with the #fields and #boost_fields methods.
113
+ #
114
+ # === Example
115
+ #
116
+ # Sunspot.search(Post) do
117
+ # keywords 'nothing reveals like relevance' do
118
+ # phrase_fields :title => 2.0
119
+ # end
120
+ # end
121
+ #
122
+ def phrase_fields(boosted_fields)
123
+ if boosted_fields
124
+ boosted_fields.each_pair do |field_name, boost|
125
+ @setup.text_fields(field_name).each do |field|
126
+ @query.add_phrase_field(field, boost)
127
+ end
128
+ end
129
+ end
130
+ end
131
+
132
+ #
133
+ # The maximum number of words that can appear between search terms for a
134
+ # field to qualify for phrase field boost. See #query_phrase_slop for
135
+ # examples. Phrase slop is only meaningful if phrase fields are specified
136
+ # (see #phrase_fields), and it does not have an effect on which results
137
+ # are returned; only on what their respective boosts are.
138
+ #
139
+ def phrase_slop(slop)
140
+ @query.phrase_slop = slop
141
+ end
142
+
143
+ #
144
+ # Boost queries allow specification of an arbitrary scope for which
145
+ # matching documents should receive an extra boost. You can either specify
146
+ # a boost factor and a block, or a boost function. The block is evaluated
147
+ # in the usual scope DSL, and field names are attribute fields, not text
148
+ # fields, as in other scope.
149
+ #
150
+ # The boost function can be a constant (numeric or string literal),
151
+ # a field name or another function. You can build arbitrarily complex
152
+ # functions, which are passed transparently to solr.
153
+ #
154
+ # This method can be called more than once for different boost queries
155
+ # with different boosts.
156
+ #
157
+ # === Example
158
+ #
159
+ # Sunspot.search(Post) do
160
+ # keywords 'super fan' do
161
+ # boost(2.0) do
162
+ # with(:featured, true)
163
+ # end
164
+ #
165
+ # boost(function { sum(:average_rating, product(:popularity, 10)) })
166
+ # end
167
+ # end
168
+ #
169
+ # In the above search, featured posts will receive a boost of 2.0 and all posts
170
+ # will be boosted by (average_rating + popularity * 10).
171
+ #
172
+ def boost(factor_or_function, &block)
173
+ if factor_or_function.is_a?(Sunspot::Query::FunctionQuery)
174
+ @query.add_boost_function(factor_or_function)
175
+ else
176
+ Sunspot::Util.instance_eval_or_call(
177
+ Scope.new(@query.create_boost_query(factor_or_function), @setup),
178
+ &block
179
+ )
180
+ end
181
+ end
182
+
183
+ #
184
+ # Add boost to certain fields, without restricting which fields are
185
+ # searched.
186
+ #
187
+ # === Example
188
+ #
189
+ # Sunspot.search(Post) do
190
+ # keywords('pork sandwich') do
191
+ # boost_fields :title => 1.5
192
+ # end
193
+ # end
194
+ #
195
+ def boost_fields(boosts)
196
+ boosts.each_pair do |field_name, boost|
197
+ begin
198
+ @setup.text_fields(field_name).each do |field|
199
+ @query.add_fulltext_field(field, boost)
200
+ end
201
+ rescue Sunspot::UnrecognizedFieldError
202
+ # We'll let this one slide.
203
+ end
204
+ end
205
+ end
206
+
207
+ #
208
+ # The minimum number of search terms that a result must match. By
209
+ # default, all search terms must match; if the number of search terms
210
+ # is less than this number, the default behavior applies.
211
+ #
212
+ def minimum_match(minimum_match)
213
+ @query.minimum_match = minimum_match
214
+ end
215
+
216
+ #
217
+ # The number of words that can appear between the words in a
218
+ # user-entered phrase (i.e., keywords in quotes) and still match. For
219
+ # instance, in a search for "\"great pizza\"" with a query phrase slop of
220
+ # 1, "great pizza" and "great big pizza" will match, but "great monster of
221
+ # a pizza" will not. Default behavior is a query phrase slop of zero.
222
+ #
223
+ def query_phrase_slop(slop)
224
+ @query.query_phrase_slop = slop
225
+ end
226
+
227
+ #
228
+ # A tiebreaker coefficient for scores derived from subqueries that are
229
+ # lower-scoring than the maximum score subquery. Typically a near-zero
230
+ # value is useful. See
231
+ # http://wiki.apache.org/solr/DisMaxRequestHandler#tie_.28Tie_breaker.29
232
+ # for more information.
233
+ #
234
+ def tie(tie)
235
+ @query.tie = tie
236
+ end
237
+
238
+ def fields_added? #:nodoc:
239
+ @fields_added
240
+ end
241
+ end
242
+ end
243
+ end
@@ -0,0 +1,27 @@
1
+ module Sunspot
2
+ module DSL
3
+ class Function #:nodoc:
4
+ def initialize(functional) #:nodoc:
5
+ @functional = functional
6
+ end
7
+
8
+ # Special case to handle <http://wiki.apache.org/solr/FunctionQuery#sub>
9
+ # because `Kernel#sub` exists so `method_missing` will not be called
10
+ # for this function.
11
+ def sub(*args) #:nodoc:
12
+ create_function_query(:sub, *args)
13
+ end
14
+
15
+ def method_missing(method, *args, &block)
16
+ create_function_query(method, *args)
17
+ end
18
+
19
+ private
20
+
21
+ def create_function_query(method, *args)
22
+ function_args = args.map { |arg| @functional.create_function_query(arg) }
23
+ Sunspot::Query::FunctionalFunctionQuery.new(method, function_args)
24
+ end
25
+ end
26
+ end
27
+ end