ruben-sunspot 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (188) hide show
  1. data/Gemfile +9 -0
  2. data/Gemfile.lock +22 -0
  3. data/History.txt +198 -0
  4. data/LICENSE +18 -0
  5. data/README.rdoc +244 -0
  6. data/Rakefile +11 -0
  7. data/TODO +13 -0
  8. data/VERSION.yml +4 -0
  9. data/bin/sunspot-installer +19 -0
  10. data/bin/sunspot-solr +74 -0
  11. data/installer/config/schema.yml +95 -0
  12. data/lib/light_config.rb +40 -0
  13. data/lib/sunspot.rb +529 -0
  14. data/lib/sunspot/adapters.rb +265 -0
  15. data/lib/sunspot/composite_setup.rb +202 -0
  16. data/lib/sunspot/configuration.rb +46 -0
  17. data/lib/sunspot/data_extractor.rb +50 -0
  18. data/lib/sunspot/dsl.rb +5 -0
  19. data/lib/sunspot/dsl/adjustable.rb +47 -0
  20. data/lib/sunspot/dsl/field_query.rb +266 -0
  21. data/lib/sunspot/dsl/fields.rb +113 -0
  22. data/lib/sunspot/dsl/fulltext.rb +243 -0
  23. data/lib/sunspot/dsl/function.rb +14 -0
  24. data/lib/sunspot/dsl/functional.rb +41 -0
  25. data/lib/sunspot/dsl/more_like_this_query.rb +56 -0
  26. data/lib/sunspot/dsl/paginatable.rb +28 -0
  27. data/lib/sunspot/dsl/query_facet.rb +36 -0
  28. data/lib/sunspot/dsl/restriction.rb +25 -0
  29. data/lib/sunspot/dsl/scope.rb +229 -0
  30. data/lib/sunspot/dsl/search.rb +30 -0
  31. data/lib/sunspot/dsl/standard_query.rb +125 -0
  32. data/lib/sunspot/field.rb +192 -0
  33. data/lib/sunspot/field_factory.rb +147 -0
  34. data/lib/sunspot/indexer.rb +131 -0
  35. data/lib/sunspot/installer.rb +31 -0
  36. data/lib/sunspot/installer/library_installer.rb +45 -0
  37. data/lib/sunspot/installer/schema_builder.rb +219 -0
  38. data/lib/sunspot/installer/solrconfig_updater.rb +106 -0
  39. data/lib/sunspot/installer/task_helper.rb +18 -0
  40. data/lib/sunspot/query.rb +10 -0
  41. data/lib/sunspot/query/abstract_field_facet.rb +52 -0
  42. data/lib/sunspot/query/boost_query.rb +24 -0
  43. data/lib/sunspot/query/common_query.rb +85 -0
  44. data/lib/sunspot/query/composite_fulltext.rb +31 -0
  45. data/lib/sunspot/query/connective.rb +195 -0
  46. data/lib/sunspot/query/date_field_facet.rb +14 -0
  47. data/lib/sunspot/query/dismax.rb +126 -0
  48. data/lib/sunspot/query/field_facet.rb +41 -0
  49. data/lib/sunspot/query/filter.rb +38 -0
  50. data/lib/sunspot/query/function_query.rb +52 -0
  51. data/lib/sunspot/query/highlighting.rb +55 -0
  52. data/lib/sunspot/query/local.rb +26 -0
  53. data/lib/sunspot/query/more_like_this.rb +60 -0
  54. data/lib/sunspot/query/more_like_this_query.rb +12 -0
  55. data/lib/sunspot/query/pagination.rb +38 -0
  56. data/lib/sunspot/query/query_facet.rb +16 -0
  57. data/lib/sunspot/query/restriction.rb +265 -0
  58. data/lib/sunspot/query/scope.rb +16 -0
  59. data/lib/sunspot/query/sort.rb +95 -0
  60. data/lib/sunspot/query/sort_composite.rb +33 -0
  61. data/lib/sunspot/query/standard_query.rb +20 -0
  62. data/lib/sunspot/query/text_field_boost.rb +17 -0
  63. data/lib/sunspot/schema.rb +151 -0
  64. data/lib/sunspot/search.rb +9 -0
  65. data/lib/sunspot/search/abstract_search.rb +302 -0
  66. data/lib/sunspot/search/date_facet.rb +35 -0
  67. data/lib/sunspot/search/facet_row.rb +27 -0
  68. data/lib/sunspot/search/field_facet.rb +88 -0
  69. data/lib/sunspot/search/highlight.rb +38 -0
  70. data/lib/sunspot/search/hit.rb +136 -0
  71. data/lib/sunspot/search/more_like_this_search.rb +31 -0
  72. data/lib/sunspot/search/query_facet.rb +62 -0
  73. data/lib/sunspot/search/standard_search.rb +21 -0
  74. data/lib/sunspot/server.rb +152 -0
  75. data/lib/sunspot/session.rb +252 -0
  76. data/lib/sunspot/session_proxy.rb +71 -0
  77. data/lib/sunspot/session_proxy/abstract_session_proxy.rb +29 -0
  78. data/lib/sunspot/session_proxy/class_sharding_session_proxy.rb +66 -0
  79. data/lib/sunspot/session_proxy/id_sharding_session_proxy.rb +89 -0
  80. data/lib/sunspot/session_proxy/master_slave_session_proxy.rb +43 -0
  81. data/lib/sunspot/session_proxy/sharding_session_proxy.rb +215 -0
  82. data/lib/sunspot/session_proxy/thread_local_session_proxy.rb +37 -0
  83. data/lib/sunspot/setup.rb +360 -0
  84. data/lib/sunspot/text_field_setup.rb +29 -0
  85. data/lib/sunspot/type.rb +342 -0
  86. data/lib/sunspot/util.rb +253 -0
  87. data/lib/sunspot/version.rb +3 -0
  88. data/solr/etc/jetty.xml +214 -0
  89. data/solr/etc/webdefault.xml +379 -0
  90. data/solr/lib/jetty-6.1.3.jar +0 -0
  91. data/solr/lib/jetty-util-6.1.3.jar +0 -0
  92. data/solr/lib/jsp-2.1/ant-1.6.5.jar +0 -0
  93. data/solr/lib/jsp-2.1/core-3.1.1.jar +0 -0
  94. data/solr/lib/jsp-2.1/jsp-2.1.jar +0 -0
  95. data/solr/lib/jsp-2.1/jsp-api-2.1.jar +0 -0
  96. data/solr/lib/servlet-api-2.5-6.1.3.jar +0 -0
  97. data/solr/solr/conf/admin-extra.html +31 -0
  98. data/solr/solr/conf/elevate.xml +36 -0
  99. data/solr/solr/conf/mapping-ISOLatin1Accent.txt +246 -0
  100. data/solr/solr/conf/protwords.txt +21 -0
  101. data/solr/solr/conf/schema.xml +238 -0
  102. data/solr/solr/conf/scripts.conf +24 -0
  103. data/solr/solr/conf/solrconfig.xml +938 -0
  104. data/solr/solr/conf/spellings.txt +2 -0
  105. data/solr/solr/conf/stopwords.txt +58 -0
  106. data/solr/solr/conf/synonyms.txt +31 -0
  107. data/solr/solr/lib/lucene-spatial-2.9.1.jar +0 -0
  108. data/solr/solr/lib/solr-spatial-light-0.0.6.jar +0 -0
  109. data/solr/start.jar +0 -0
  110. data/solr/webapps/solr.war +0 -0
  111. data/spec/api/adapters_spec.rb +33 -0
  112. data/spec/api/binding_spec.rb +38 -0
  113. data/spec/api/indexer/attributes_spec.rb +149 -0
  114. data/spec/api/indexer/batch_spec.rb +46 -0
  115. data/spec/api/indexer/dynamic_fields_spec.rb +42 -0
  116. data/spec/api/indexer/fixed_fields_spec.rb +57 -0
  117. data/spec/api/indexer/fulltext_spec.rb +43 -0
  118. data/spec/api/indexer/removal_spec.rb +53 -0
  119. data/spec/api/indexer/spec_helper.rb +1 -0
  120. data/spec/api/indexer_spec.rb +14 -0
  121. data/spec/api/query/advanced_manipulation_examples.rb +35 -0
  122. data/spec/api/query/connectives_examples.rb +186 -0
  123. data/spec/api/query/dsl_spec.rb +18 -0
  124. data/spec/api/query/dynamic_fields_examples.rb +165 -0
  125. data/spec/api/query/faceting_examples.rb +399 -0
  126. data/spec/api/query/fulltext_examples.rb +315 -0
  127. data/spec/api/query/function_spec.rb +70 -0
  128. data/spec/api/query/highlighting_examples.rb +225 -0
  129. data/spec/api/query/local_examples.rb +38 -0
  130. data/spec/api/query/more_like_this_spec.rb +140 -0
  131. data/spec/api/query/ordering_pagination_examples.rb +97 -0
  132. data/spec/api/query/scope_examples.rb +263 -0
  133. data/spec/api/query/spec_helper.rb +1 -0
  134. data/spec/api/query/standard_spec.rb +28 -0
  135. data/spec/api/query/text_field_scoping_examples.rb +30 -0
  136. data/spec/api/query/types_spec.rb +20 -0
  137. data/spec/api/search/dynamic_fields_spec.rb +33 -0
  138. data/spec/api/search/faceting_spec.rb +356 -0
  139. data/spec/api/search/highlighting_spec.rb +69 -0
  140. data/spec/api/search/hits_spec.rb +149 -0
  141. data/spec/api/search/results_spec.rb +79 -0
  142. data/spec/api/search/search_spec.rb +23 -0
  143. data/spec/api/search/spec_helper.rb +1 -0
  144. data/spec/api/server_spec.rb +91 -0
  145. data/spec/api/session_proxy/class_sharding_session_proxy_spec.rb +85 -0
  146. data/spec/api/session_proxy/id_sharding_session_proxy_spec.rb +30 -0
  147. data/spec/api/session_proxy/master_slave_session_proxy_spec.rb +41 -0
  148. data/spec/api/session_proxy/sharding_session_proxy_spec.rb +77 -0
  149. data/spec/api/session_proxy/spec_helper.rb +9 -0
  150. data/spec/api/session_proxy/thread_local_session_proxy_spec.rb +50 -0
  151. data/spec/api/session_spec.rb +198 -0
  152. data/spec/api/spec_helper.rb +3 -0
  153. data/spec/api/sunspot_spec.rb +18 -0
  154. data/spec/ext.rb +11 -0
  155. data/spec/helpers/indexer_helper.rb +29 -0
  156. data/spec/helpers/query_helper.rb +38 -0
  157. data/spec/helpers/search_helper.rb +80 -0
  158. data/spec/integration/dynamic_fields_spec.rb +55 -0
  159. data/spec/integration/faceting_spec.rb +238 -0
  160. data/spec/integration/highlighting_spec.rb +22 -0
  161. data/spec/integration/indexing_spec.rb +33 -0
  162. data/spec/integration/keyword_search_spec.rb +317 -0
  163. data/spec/integration/local_search_spec.rb +91 -0
  164. data/spec/integration/more_like_this_spec.rb +43 -0
  165. data/spec/integration/scoped_search_spec.rb +349 -0
  166. data/spec/integration/spec_helper.rb +7 -0
  167. data/spec/integration/stored_fields_spec.rb +10 -0
  168. data/spec/integration/test_pagination.rb +32 -0
  169. data/spec/mocks/adapters.rb +32 -0
  170. data/spec/mocks/blog.rb +3 -0
  171. data/spec/mocks/comment.rb +21 -0
  172. data/spec/mocks/connection.rb +126 -0
  173. data/spec/mocks/mock_adapter.rb +30 -0
  174. data/spec/mocks/mock_class_sharding_session_proxy.rb +24 -0
  175. data/spec/mocks/mock_record.rb +52 -0
  176. data/spec/mocks/mock_sharding_session_proxy.rb +15 -0
  177. data/spec/mocks/photo.rb +12 -0
  178. data/spec/mocks/post.rb +76 -0
  179. data/spec/mocks/super_class.rb +2 -0
  180. data/spec/mocks/user.rb +8 -0
  181. data/spec/spec_helper.rb +52 -0
  182. data/tasks/gemspec.rake +33 -0
  183. data/tasks/rcov.rake +28 -0
  184. data/tasks/rdoc.rake +27 -0
  185. data/tasks/schema.rake +19 -0
  186. data/tasks/spec.rake +24 -0
  187. data/tasks/todo.rake +4 -0
  188. metadata +356 -0
@@ -0,0 +1,229 @@
1
+ module Sunspot
2
+ module DSL #:nodoc:
3
+ #
4
+ # This DSL presents methods for constructing restrictions and other query
5
+ # elements that are specific to fields. As well as being a superclass of
6
+ # Sunspot::DSL::Query, which presents the main query block, this DSL class
7
+ # is also used directly inside the #dynamic() block, which only allows
8
+ # operations on specific fields.
9
+ #
10
+ class Scope
11
+ NONE = Object.new
12
+
13
+ def initialize(scope, setup) #:nodoc:
14
+ @scope, @setup = scope, setup
15
+ end
16
+
17
+ #
18
+ # Build a positive restriction. With one argument, this method returns
19
+ # another DSL object which presents methods for attaching various
20
+ # restriction types. With two arguments, this creates a shorthand
21
+ # restriction: if the second argument is a scalar, an equality restriction
22
+ # is created; if it is a Range, a between restriction will be created; and
23
+ # if it is an Array, an any_of restriction will be created.
24
+ #
25
+ # ==== Parameters
26
+ #
27
+ # field_name<Symbol>:: Name of the field on which to place the restriction
28
+ # value<Object,Range,Array>::
29
+ # If passed, creates an equality, range, or any-of restriction based on
30
+ # the type of value passed.
31
+ #
32
+ # ==== Returns
33
+ #
34
+ # Sunspot::DSL::Query::Restriction::
35
+ # Restriction DSL object (if only one argument is passed)
36
+ #
37
+ # ==== Examples
38
+ #
39
+ # An equality restriction:
40
+ #
41
+ # Sunspot.search do
42
+ # with(:blog_id, 1)
43
+ # end
44
+ #
45
+ # Restrict by range:
46
+ #
47
+ # Sunspot.search do
48
+ # with(:average_rating, 3.0..5.0)
49
+ # end
50
+ #
51
+ # Restrict by a set of allowed values:
52
+ #
53
+ # Sunspot.search do
54
+ # with(:category_ids, [1, 5, 9])
55
+ # end
56
+ #
57
+ # Other restriction types:
58
+ #
59
+ # Sunspot.search(Post) do
60
+ # with(:average_rating).greater_than(3.0)
61
+ # end
62
+ #
63
+ def with(field_name, value = NONE)
64
+ if value == NONE
65
+ DSL::Restriction.new(@setup.field(field_name.to_sym), @scope, false)
66
+ else
67
+ @scope.add_shorthand_restriction(@setup.field(field_name), value)
68
+ end
69
+ end
70
+
71
+ #
72
+ # Build a negative restriction (exclusion). This method can take three
73
+ # forms: equality exclusion, exclusion by another restriction, or identity
74
+ # exclusion. The first two forms work the same way as the #with method;
75
+ # the third excludes a specific instance from the search results.
76
+ #
77
+ # ==== Parameters (exclusion by field value)
78
+ #
79
+ # field_name<Symbol>:: Name of the field on which to place the exclusion
80
+ # value<Symbol>::
81
+ # If passed, creates an equality exclusion with this value
82
+ #
83
+ # ==== Parameters (exclusion by identity)
84
+ #
85
+ # args<Object>...::
86
+ # One or more instances that should be excluded from the results
87
+ #
88
+ # ==== Examples
89
+ #
90
+ # An equality exclusion:
91
+ #
92
+ # Sunspot.search(Post) do
93
+ # without(:blog_id, 1)
94
+ # end
95
+ #
96
+ # Other restriction types:
97
+ #
98
+ # Sunspot.search(Post) do
99
+ # without(:average_rating).greater_than(3.0)
100
+ # end
101
+ #
102
+ # Exclusion by identity:
103
+ #
104
+ # Sunspot.search(Post) do
105
+ # without(some_post_instance)
106
+ # end
107
+ #
108
+ def without(*args)
109
+ case args.first
110
+ when String, Symbol
111
+ field_name = args[0]
112
+ value = args.length > 1 ? args[1] : NONE
113
+ if value == NONE
114
+ DSL::Restriction.new(@setup.field(field_name.to_sym), @scope, true)
115
+ else
116
+ @scope.add_negated_shorthand_restriction(@setup.field(field_name.to_sym), value)
117
+ end
118
+ else
119
+ instances = args
120
+ instances.flatten.each do |instance|
121
+ @scope.add_negated_restriction(
122
+ IdField.instance,
123
+ Sunspot::Query::Restriction::EqualTo,
124
+ Sunspot::Adapters::InstanceAdapter.adapt(instance).index_id
125
+ )
126
+ end
127
+ end
128
+ end
129
+
130
+ #
131
+ # Create a disjunction, scoping the results to documents that match any
132
+ # of the enclosed restrictions.
133
+ #
134
+ # ==== Example
135
+ #
136
+ # Sunspot.search(Post) do
137
+ # any_of do
138
+ # with(:expired_at).greater_than Time.now
139
+ # with :expired_at, nil
140
+ # end
141
+ # end
142
+ #
143
+ # This will return all documents who either have an expiration time in the
144
+ # future, or who do not have any expiration time at all.
145
+ #
146
+ def any_of(&block)
147
+ disjunction = @scope.add_disjunction
148
+ Util.instance_eval_or_call(Scope.new(disjunction, @setup), &block)
149
+ disjunction
150
+ end
151
+
152
+ #
153
+ # Create a conjunction, scoping the results to documents that match all of
154
+ # the enclosed restrictions. When called from the top level of a search
155
+ # block, this has no effect, but can be useful for grouping a conjunction
156
+ # inside a disjunction.
157
+ #
158
+ # ==== Example
159
+ #
160
+ # Sunspot.search(Post) do
161
+ # any_of do
162
+ # with(:blog_id, 1)
163
+ # all_of do
164
+ # with(:blog_id, 2)
165
+ # with(:category_ids, 3)
166
+ # end
167
+ # end
168
+ # end
169
+ #
170
+ def all_of(&block)
171
+ conjunction = @scope.add_conjunction
172
+ Util.instance_eval_or_call(Scope.new(conjunction, @setup), &block)
173
+ conjunction
174
+ end
175
+
176
+ #
177
+ # Apply restrictions, facets, and ordering to dynamic field instances.
178
+ # The block API is implemented by Sunspot::DSL::FieldQuery, which is a
179
+ # superclass of the Query DSL (thus providing a subset of the API, in
180
+ # particular only methods that refer to particular fields).
181
+ #
182
+ # ==== Parameters
183
+ #
184
+ # base_name<Symbol>:: The base name for the dynamic field definition
185
+ #
186
+ # ==== Example
187
+ #
188
+ # Sunspot.search Post do
189
+ # dynamic :custom do
190
+ # with :cuisine, 'Pizza'
191
+ # facet :atmosphere
192
+ # order_by :chef_name
193
+ # end
194
+ # end
195
+ #
196
+ def dynamic(base_name, &block)
197
+ Sunspot::Util.instance_eval_or_call(
198
+ Scope.new(@scope, @setup.dynamic_field_factory(base_name)),
199
+ &block
200
+ )
201
+ end
202
+
203
+ #
204
+ # Apply scope-type restrictions on fulltext fields. In certain situations,
205
+ # it may be desirable to place logical restrictions on text fields.
206
+ # Remember that text fields are tokenized; your mileage may very.
207
+ #
208
+ # The block works exactly like a normal scope, except that the field names
209
+ # refer to text fields instead of attribute fields.
210
+ #
211
+ # === Example
212
+ #
213
+ # Sunspot.search(Post) do
214
+ # text_fields do
215
+ # with :body, nil
216
+ # end
217
+ # end
218
+ #
219
+ # This will return all documents that do not have a body.
220
+ #
221
+ def text_fields(&block)
222
+ Sunspot::Util.instance_eval_or_call(
223
+ Scope.new(@scope, TextFieldSetup.new(@setup)),
224
+ &block
225
+ )
226
+ end
227
+ end
228
+ end
229
+ end
@@ -0,0 +1,30 @@
1
+ module Sunspot
2
+ module DSL
3
+ #
4
+ # This top-level DSL class is the context in which the block passed to
5
+ # Sunspot.query. See Sunspot::DSL::Query, Sunspot::DSL::FieldQuery, and
6
+ # Sunspot::DSL::Scope for the full API presented.
7
+ #
8
+ class Search < StandardQuery
9
+ def initialize(search, setup) #:nodoc:
10
+ @search = search
11
+ super(search, search.query, setup)
12
+ end
13
+
14
+ #
15
+ # Retrieve the data accessor used to load instances of the given class
16
+ # out of persistent storage. Data accessors are free to implement any
17
+ # extra methods that may be useful in this context.
18
+ #
19
+ # ==== Example
20
+ #
21
+ # Sunspot.search Post do
22
+ # data_acccessor_for(Post).includes = [:blog, :comments]
23
+ # end
24
+ #
25
+ def data_accessor_for(clazz)
26
+ @search.data_accessor_for(clazz)
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,125 @@
1
+ module Sunspot
2
+ module DSL #:nodoc:
3
+ #
4
+ # This class presents a DSL for constructing queries using the
5
+ # Sunspot.search method. Methods of this class are available inside the
6
+ # search block. Much of the DSL's functionality is implemented by this
7
+ # class's superclasses, Sunspot::DSL::FieldQuery and Sunspot::DSL::Scope
8
+ #
9
+ # See Sunspot.search for usage examples
10
+ #
11
+ class StandardQuery < FieldQuery
12
+ include Paginatable, Adjustable
13
+
14
+ # Specify a phrase that should be searched as fulltext. Only +text+
15
+ # fields are searched - see DSL::Fields.text
16
+ #
17
+ # Keyword search is executed using Solr's dismax handler, which strikes
18
+ # a good balance between powerful and foolproof. In particular,
19
+ # well-matched quotation marks can be used to group phrases, and the
20
+ # + and - modifiers work as expected. All other special Solr boolean
21
+ # syntax is escaped, and mismatched quotes are ignored entirely.
22
+ #
23
+ # This method can optionally take a block, which is evaluated by the
24
+ # Fulltext DSL class, and exposes several powerful dismax features.
25
+ #
26
+ # ==== Parameters
27
+ #
28
+ # keywords<String>:: phrase to perform fulltext search on
29
+ #
30
+ # ==== Options
31
+ #
32
+ # :fields<Array>::
33
+ # List of fields that should be searched for keywords. Defaults to all
34
+ # fields configured for the types under search.
35
+ # :highlight<Boolean,Array>::
36
+ # If true, perform keyword highlighting on all searched fields. If an
37
+ # array of field names, perform highlighting on the specified fields.
38
+ # This can also be called from within the fulltext block.
39
+ # :minimum_match<Integer>::
40
+ # The minimum number of search terms that a result must match. By
41
+ # default, all search terms must match; if the number of search terms
42
+ # is less than this number, the default behavior applies.
43
+ # :tie<Float>::
44
+ # A tiebreaker coefficient for scores derived from subqueries that are
45
+ # lower-scoring than the maximum score subquery. Typically a near-zero
46
+ # value is useful. See
47
+ # http://wiki.apache.org/solr/DisMaxRequestHandler#tie_.28Tie_breaker.29
48
+ # for more information.
49
+ # :query_phrase_slop<Integer>::
50
+ # The number of words that can appear between the words in a
51
+ # user-entered phrase (i.e., keywords in quotes) and still match. For
52
+ # instance, in a search for "\"great pizza\"" with a phrase slop of 1,
53
+ # "great pizza" and "great big pizza" will match, but "great monster of
54
+ # a pizza" will not. Default behavior is a query phrase slop of zero.
55
+ #
56
+ def fulltext(keywords, options = {}, &block)
57
+ if keywords && !(keywords.to_s =~ /^\s*$/)
58
+ fulltext_query = @query.add_fulltext(keywords)
59
+ if field_names = options.delete(:fields)
60
+ Util.Array(field_names).each do |field_name|
61
+ @setup.text_fields(field_name).each do |field|
62
+ fulltext_query.add_fulltext_field(field, field.default_boost)
63
+ end
64
+ end
65
+ end
66
+ if minimum_match = options.delete(:minimum_match)
67
+ fulltext_query.minimum_match = minimum_match.to_i
68
+ end
69
+ if tie = options.delete(:tie)
70
+ fulltext_query.tie = tie.to_f
71
+ end
72
+ if query_phrase_slop = options.delete(:query_phrase_slop)
73
+ fulltext_query.query_phrase_slop = query_phrase_slop.to_i
74
+ end
75
+ if highlight_field_names = options.delete(:highlight)
76
+ if highlight_field_names == true
77
+ fulltext_query.add_highlight
78
+ else
79
+ highlight_fields = []
80
+ Util.Array(highlight_field_names).each do |field_name|
81
+ highlight_fields.concat(@setup.text_fields(field_name))
82
+ end
83
+ fulltext_query.add_highlight(highlight_fields)
84
+ end
85
+ end
86
+ if block && fulltext_query
87
+ fulltext_dsl = Fulltext.new(fulltext_query, @setup)
88
+ Util.instance_eval_or_call(
89
+ fulltext_dsl,
90
+ &block
91
+ )
92
+ end
93
+ if !field_names && (!fulltext_dsl || !fulltext_dsl.fields_added?)
94
+ @setup.all_text_fields.each do |field|
95
+ unless fulltext_query.has_fulltext_field?(field)
96
+ unless fulltext_dsl && fulltext_dsl.exclude_fields.include?(field.name)
97
+ fulltext_query.add_fulltext_field(field, field.default_boost)
98
+ end
99
+ end
100
+ end
101
+ end
102
+ end
103
+ end
104
+ alias_method :keywords, :fulltext
105
+
106
+ #
107
+ # Scope the search by geographical distance from a given point.
108
+ # +coordinates+ should either respond to #first and #last (e.g. a
109
+ # two-element array), or to #lat and one of #lng, #lon, or #long.
110
+ # +options+ should be one or both of the following:
111
+ #
112
+ # :distance:: The maximum distance in miles from which results can come
113
+ # :sort::
114
+ # Whether to sort by distance from these coordinates. If other sorts are
115
+ # specified, they take precedence over distance sort.
116
+ #
117
+ def near(coordinates, options)
118
+ if options.respond_to?(:to_f)
119
+ options = { :distance => options }
120
+ end
121
+ @query.add_location_restriction(coordinates, options)
122
+ end
123
+ end
124
+ end
125
+ end
@@ -0,0 +1,192 @@
1
+ module Sunspot
2
+ class Field #:nodoc:
3
+ attr_accessor :name # The public-facing name of the field
4
+ attr_accessor :type # The Type of the field
5
+ attr_accessor :reference # Model class that the value of this field refers to
6
+ attr_reader :boost
7
+
8
+ #
9
+ #
10
+ def initialize(name, type, options = {}) #:nodoc
11
+ @name, @type = name.to_sym, type
12
+ @stored = !!options.delete(:stored)
13
+ @more_like_this = !!options.delete(:more_like_this)
14
+ raise ArgumentError, "Field of type #{type} cannot be used for more_like_this" unless type.accepts_more_like_this? or !@more_like_this
15
+ end
16
+
17
+ # Convert a value to its representation for Solr indexing. This delegates
18
+ # to the #to_indexed method on the field's type.
19
+ #
20
+ # ==== Parameters
21
+ #
22
+ # value<Object>:: Value to convert to Solr representation
23
+ #
24
+ # ==== Returns
25
+ #
26
+ # String:: Solr representation of the object
27
+ #
28
+ # ==== Raises
29
+ #
30
+ # ArgumentError::
31
+ # the value is an array, but this field does not allow multiple values
32
+ #
33
+ def to_indexed(value)
34
+ if value.is_a? Array
35
+ if @multiple
36
+ value.map { |val| to_indexed(val) }
37
+ else
38
+ raise ArgumentError, "#{name} is not a multiple-value field, so it cannot index values #{value.inspect}"
39
+ end
40
+ else
41
+ @type.to_indexed(value)
42
+ end
43
+ end
44
+
45
+ # Cast the value into the appropriate Ruby class for the field's type
46
+ #
47
+ # ==== Parameters
48
+ #
49
+ # value<String>:: Solr's representation of the value
50
+ #
51
+ # ==== Returns
52
+ #
53
+ # Object:: The cast value
54
+ #
55
+ def cast(value)
56
+ @type.cast(value)
57
+ end
58
+
59
+ #
60
+ # Name with which this field is indexed internally. Based on public name and
61
+ # type.
62
+ #
63
+ # ==== Returns
64
+ #
65
+ # String:: Internal name of the field
66
+ #
67
+ def indexed_name
68
+ @type.indexed_name(@name)
69
+ end
70
+
71
+ #
72
+ # Whether this field accepts multiple values.
73
+ #
74
+ # ==== Returns
75
+ #
76
+ # Boolean:: True if this field accepts multiple values.
77
+ #
78
+ def multiple?
79
+ !!@multiple
80
+ end
81
+
82
+ #
83
+ # Whether this field can be used for more_like_this queries.
84
+ # If true, the field is configured to store termVectors.
85
+ #
86
+ # ==== Returns
87
+ #
88
+ # Boolean:: True if this field can be used for more_like_this queries.
89
+ #
90
+ def more_like_this?
91
+ !!@more_like_this
92
+ end
93
+
94
+ def hash
95
+ indexed_name.hash
96
+ end
97
+
98
+ def eql?(field)
99
+ indexed_name == field.indexed_name
100
+ end
101
+ alias_method :==, :eql?
102
+ end
103
+
104
+ #
105
+ # FulltextField instances represent fields that are indexed as fulltext.
106
+ # These fields are tokenized in the index, and can have boost applied to
107
+ # them. They also always allow multiple values (since the only downside of
108
+ # allowing multiple values is that it prevents the field from being sortable,
109
+ # and sorting on tokenized fields is nonsensical anyway, there is no reason
110
+ # to do otherwise). FulltextField instances always have the type TextType.
111
+ #
112
+ class FulltextField < Field #:nodoc:
113
+ attr_reader :default_boost
114
+
115
+ def initialize(name, options = {})
116
+ super(name, Type::TextType.instance, options)
117
+ @multiple = true
118
+ @boost = options.delete(:boost)
119
+ @default_boost = options.delete(:default_boost)
120
+ raise ArgumentError, "Unknown field option #{options.keys.first.inspect} provided for field #{name.inspect}" unless options.empty?
121
+ end
122
+
123
+ def indexed_name
124
+ "#{super}#{'s' if @stored}#{'v' if more_like_this?}"
125
+ end
126
+ end
127
+
128
+ #
129
+ # AttributeField instances encapsulate non-tokenized attribute data.
130
+ # AttributeFields can have any type except TextType, and can also have
131
+ # a reference (for instantiated facets), optionally allow multiple values
132
+ # (false by default), and can store their values (false by default). All
133
+ # scoping, sorting, and faceting is done with attribute fields.
134
+ #
135
+ class AttributeField < Field #:nodoc:
136
+ def initialize(name, type, options = {})
137
+ super(name, type, options)
138
+ @multiple = !!options.delete(:multiple)
139
+ @reference =
140
+ if (reference = options.delete(:references)).respond_to?(:name)
141
+ reference.name
142
+ elsif reference.respond_to?(:to_sym)
143
+ reference.to_sym
144
+ end
145
+ raise ArgumentError, "Unknown field option #{options.keys.first.inspect} provided for field #{name.inspect}" unless options.empty?
146
+ end
147
+
148
+ # The name of the field as it is indexed in Solr. The indexed name
149
+ # contains a suffix that contains information about the type as well as
150
+ # whether the field allows multiple values for a document.
151
+ #
152
+ # ==== Returns
153
+ #
154
+ # String:: The field's indexed name
155
+ #
156
+ def indexed_name
157
+ "#{super}#{'m' if @multiple}#{'s' if @stored}#{'v' if more_like_this?}"
158
+ end
159
+ end
160
+
161
+ class TypeField #:nodoc:
162
+ class <<self
163
+ def instance
164
+ @instance ||= new
165
+ end
166
+ end
167
+
168
+ def indexed_name
169
+ 'type'
170
+ end
171
+
172
+ def to_indexed(clazz)
173
+ clazz.name
174
+ end
175
+ end
176
+
177
+ class IdField #:nodoc:
178
+ class <<self
179
+ def instance
180
+ @instance ||= new
181
+ end
182
+ end
183
+
184
+ def indexed_name
185
+ 'id'
186
+ end
187
+
188
+ def to_indexed(id)
189
+ id.to_s
190
+ end
191
+ end
192
+ end