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.
- data/.gitignore +12 -0
- data/Gemfile +5 -0
- data/History.txt +252 -0
- data/LICENSE +18 -0
- data/Rakefile +13 -0
- data/TODO +13 -0
- data/lib/light_config.rb +40 -0
- data/lib/sunspot/adapters.rb +265 -0
- data/lib/sunspot/batcher.rb +62 -0
- data/lib/sunspot/class_set.rb +23 -0
- data/lib/sunspot/composite_setup.rb +202 -0
- data/lib/sunspot/configuration.rb +53 -0
- data/lib/sunspot/data_extractor.rb +50 -0
- data/lib/sunspot/dsl/adjustable.rb +47 -0
- data/lib/sunspot/dsl/field_group.rb +57 -0
- data/lib/sunspot/dsl/field_query.rb +327 -0
- data/lib/sunspot/dsl/fields.rb +103 -0
- data/lib/sunspot/dsl/fulltext.rb +243 -0
- data/lib/sunspot/dsl/function.rb +27 -0
- data/lib/sunspot/dsl/functional.rb +44 -0
- data/lib/sunspot/dsl/more_like_this_query.rb +56 -0
- data/lib/sunspot/dsl/paginatable.rb +32 -0
- data/lib/sunspot/dsl/query_facet.rb +36 -0
- data/lib/sunspot/dsl/restriction.rb +25 -0
- data/lib/sunspot/dsl/restriction_with_near.rb +160 -0
- data/lib/sunspot/dsl/scope.rb +217 -0
- data/lib/sunspot/dsl/search.rb +30 -0
- data/lib/sunspot/dsl/standard_query.rb +123 -0
- data/lib/sunspot/dsl.rb +5 -0
- data/lib/sunspot/field.rb +193 -0
- data/lib/sunspot/field_factory.rb +129 -0
- data/lib/sunspot/indexer.rb +136 -0
- data/lib/sunspot/query/abstract_field_facet.rb +52 -0
- data/lib/sunspot/query/bbox.rb +15 -0
- data/lib/sunspot/query/boost_query.rb +24 -0
- data/lib/sunspot/query/common_query.rb +96 -0
- data/lib/sunspot/query/composite_fulltext.rb +36 -0
- data/lib/sunspot/query/connective.rb +206 -0
- data/lib/sunspot/query/date_field_facet.rb +14 -0
- data/lib/sunspot/query/dismax.rb +132 -0
- data/lib/sunspot/query/field_facet.rb +41 -0
- data/lib/sunspot/query/field_group.rb +36 -0
- data/lib/sunspot/query/filter.rb +38 -0
- data/lib/sunspot/query/function_query.rb +52 -0
- data/lib/sunspot/query/geo.rb +53 -0
- data/lib/sunspot/query/geofilt.rb +16 -0
- data/lib/sunspot/query/highlighting.rb +62 -0
- data/lib/sunspot/query/more_like_this.rb +61 -0
- data/lib/sunspot/query/more_like_this_query.rb +12 -0
- data/lib/sunspot/query/pagination.rb +42 -0
- data/lib/sunspot/query/query_facet.rb +16 -0
- data/lib/sunspot/query/restriction.rb +262 -0
- data/lib/sunspot/query/scope.rb +9 -0
- data/lib/sunspot/query/sort.rb +109 -0
- data/lib/sunspot/query/sort_composite.rb +34 -0
- data/lib/sunspot/query/standard_query.rb +16 -0
- data/lib/sunspot/query/text_field_boost.rb +17 -0
- data/lib/sunspot/query.rb +11 -0
- data/lib/sunspot/schema.rb +151 -0
- data/lib/sunspot/search/abstract_search.rb +281 -0
- data/lib/sunspot/search/date_facet.rb +35 -0
- data/lib/sunspot/search/facet_row.rb +27 -0
- data/lib/sunspot/search/field_facet.rb +88 -0
- data/lib/sunspot/search/field_group.rb +32 -0
- data/lib/sunspot/search/group.rb +50 -0
- data/lib/sunspot/search/highlight.rb +38 -0
- data/lib/sunspot/search/hit.rb +150 -0
- data/lib/sunspot/search/hit_enumerable.rb +72 -0
- data/lib/sunspot/search/more_like_this_search.rb +31 -0
- data/lib/sunspot/search/paginated_collection.rb +57 -0
- data/lib/sunspot/search/query_facet.rb +67 -0
- data/lib/sunspot/search/standard_search.rb +21 -0
- data/lib/sunspot/search.rb +9 -0
- data/lib/sunspot/session.rb +262 -0
- data/lib/sunspot/session_proxy/abstract_session_proxy.rb +29 -0
- data/lib/sunspot/session_proxy/class_sharding_session_proxy.rb +66 -0
- data/lib/sunspot/session_proxy/id_sharding_session_proxy.rb +89 -0
- data/lib/sunspot/session_proxy/master_slave_session_proxy.rb +43 -0
- data/lib/sunspot/session_proxy/multicore_session_proxy.rb +67 -0
- data/lib/sunspot/session_proxy/sharding_session_proxy.rb +222 -0
- data/lib/sunspot/session_proxy/silent_fail_session_proxy.rb +42 -0
- data/lib/sunspot/session_proxy/thread_local_session_proxy.rb +37 -0
- data/lib/sunspot/session_proxy.rb +95 -0
- data/lib/sunspot/setup.rb +350 -0
- data/lib/sunspot/text_field_setup.rb +29 -0
- data/lib/sunspot/type.rb +393 -0
- data/lib/sunspot/util.rb +252 -0
- data/lib/sunspot/version.rb +3 -0
- data/lib/sunspot.rb +579 -0
- data/log/.gitignore +1 -0
- data/pkg/.gitignore +1 -0
- data/script/console +10 -0
- data/spec/api/adapters_spec.rb +33 -0
- data/spec/api/batcher_spec.rb +112 -0
- data/spec/api/binding_spec.rb +50 -0
- data/spec/api/class_set_spec.rb +24 -0
- data/spec/api/hit_enumerable_spec.rb +47 -0
- data/spec/api/indexer/attributes_spec.rb +149 -0
- data/spec/api/indexer/batch_spec.rb +72 -0
- data/spec/api/indexer/dynamic_fields_spec.rb +42 -0
- data/spec/api/indexer/fixed_fields_spec.rb +57 -0
- data/spec/api/indexer/fulltext_spec.rb +43 -0
- data/spec/api/indexer/removal_spec.rb +53 -0
- data/spec/api/indexer/spec_helper.rb +1 -0
- data/spec/api/indexer_spec.rb +14 -0
- data/spec/api/query/advanced_manipulation_examples.rb +35 -0
- data/spec/api/query/connectives_examples.rb +189 -0
- data/spec/api/query/dsl_spec.rb +18 -0
- data/spec/api/query/dynamic_fields_examples.rb +165 -0
- data/spec/api/query/faceting_examples.rb +397 -0
- data/spec/api/query/fulltext_examples.rb +313 -0
- data/spec/api/query/function_spec.rb +79 -0
- data/spec/api/query/geo_examples.rb +68 -0
- data/spec/api/query/group_spec.rb +32 -0
- data/spec/api/query/highlighting_examples.rb +245 -0
- data/spec/api/query/more_like_this_spec.rb +140 -0
- data/spec/api/query/ordering_pagination_examples.rb +116 -0
- data/spec/api/query/scope_examples.rb +275 -0
- data/spec/api/query/spatial_examples.rb +27 -0
- data/spec/api/query/spec_helper.rb +1 -0
- data/spec/api/query/standard_spec.rb +29 -0
- data/spec/api/query/text_field_scoping_examples.rb +30 -0
- data/spec/api/query/types_spec.rb +20 -0
- data/spec/api/search/dynamic_fields_spec.rb +33 -0
- data/spec/api/search/faceting_spec.rb +360 -0
- data/spec/api/search/highlighting_spec.rb +69 -0
- data/spec/api/search/hits_spec.rb +131 -0
- data/spec/api/search/paginated_collection_spec.rb +36 -0
- data/spec/api/search/results_spec.rb +72 -0
- data/spec/api/search/search_spec.rb +23 -0
- data/spec/api/search/spec_helper.rb +1 -0
- data/spec/api/session_proxy/class_sharding_session_proxy_spec.rb +85 -0
- data/spec/api/session_proxy/id_sharding_session_proxy_spec.rb +30 -0
- data/spec/api/session_proxy/master_slave_session_proxy_spec.rb +41 -0
- data/spec/api/session_proxy/sharding_session_proxy_spec.rb +77 -0
- data/spec/api/session_proxy/silent_fail_session_proxy_spec.rb +24 -0
- data/spec/api/session_proxy/spec_helper.rb +9 -0
- data/spec/api/session_proxy/thread_local_session_proxy_spec.rb +39 -0
- data/spec/api/session_spec.rb +232 -0
- data/spec/api/spec_helper.rb +3 -0
- data/spec/api/sunspot_spec.rb +29 -0
- data/spec/ext.rb +11 -0
- data/spec/helpers/indexer_helper.rb +17 -0
- data/spec/helpers/integration_helper.rb +8 -0
- data/spec/helpers/mock_session_helper.rb +13 -0
- data/spec/helpers/query_helper.rb +26 -0
- data/spec/helpers/search_helper.rb +68 -0
- data/spec/integration/dynamic_fields_spec.rb +57 -0
- data/spec/integration/faceting_spec.rb +251 -0
- data/spec/integration/field_grouping_spec.rb +66 -0
- data/spec/integration/geospatial_spec.rb +85 -0
- data/spec/integration/highlighting_spec.rb +44 -0
- data/spec/integration/indexing_spec.rb +55 -0
- data/spec/integration/keyword_search_spec.rb +317 -0
- data/spec/integration/local_search_spec.rb +64 -0
- data/spec/integration/more_like_this_spec.rb +43 -0
- data/spec/integration/scoped_search_spec.rb +354 -0
- data/spec/integration/stored_fields_spec.rb +12 -0
- data/spec/integration/test_pagination.rb +43 -0
- data/spec/integration/unicode_spec.rb +15 -0
- data/spec/mocks/adapters.rb +32 -0
- data/spec/mocks/blog.rb +3 -0
- data/spec/mocks/comment.rb +21 -0
- data/spec/mocks/connection.rb +126 -0
- data/spec/mocks/mock_adapter.rb +30 -0
- data/spec/mocks/mock_class_sharding_session_proxy.rb +24 -0
- data/spec/mocks/mock_record.rb +52 -0
- data/spec/mocks/mock_sharding_session_proxy.rb +15 -0
- data/spec/mocks/photo.rb +11 -0
- data/spec/mocks/post.rb +86 -0
- data/spec/mocks/super_class.rb +2 -0
- data/spec/mocks/user.rb +13 -0
- data/spec/spec_helper.rb +40 -0
- data/sunspot.gemspec +42 -0
- data/tasks/rdoc.rake +27 -0
- data/tasks/schema.rake +19 -0
- data/tasks/todo.rake +4 -0
- 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
|