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,96 @@
1
+ module Sunspot
2
+ module Query #:nodoc:
3
+ class CommonQuery
4
+ def initialize(types)
5
+ @scope = Scope.new
6
+ @sort = SortComposite.new
7
+ @components = [@scope, @sort]
8
+ if types.length == 1
9
+ @scope.add_positive_restriction(TypeField.instance, Restriction::EqualTo, types.first)
10
+ else
11
+ @scope.add_positive_restriction(TypeField.instance, Restriction::AnyOf, types)
12
+ end
13
+ end
14
+
15
+ def solr_parameter_adjustment=(block)
16
+ @parameter_adjustment = block
17
+ end
18
+
19
+ def add_sort(sort)
20
+ @sort << sort
21
+ end
22
+
23
+ def add_group(group)
24
+ @components << group
25
+ group
26
+ end
27
+
28
+ def add_field_facet(facet)
29
+ @components << facet
30
+ facet
31
+ end
32
+
33
+ def add_query_facet(facet)
34
+ @components << facet
35
+ facet
36
+ end
37
+
38
+ def add_function(function)
39
+ @components << function
40
+ function
41
+ end
42
+
43
+ def add_geo(geo)
44
+ @components << geo
45
+ geo
46
+ end
47
+
48
+ def paginate(page, per_page, offset = nil)
49
+ if @pagination
50
+ @pagination.offset = offset
51
+ @pagination.page = page
52
+ @pagination.per_page = per_page
53
+ else
54
+ @components << @pagination = Pagination.new(page, per_page, offset)
55
+ end
56
+ end
57
+
58
+ def to_params
59
+ params = {}
60
+ @components.each do |component|
61
+ Sunspot::Util.deep_merge!(params, component.to_params)
62
+ end
63
+ @parameter_adjustment.call(params) if @parameter_adjustment
64
+ params[:q] ||= '*:*'
65
+ params
66
+ end
67
+
68
+ def [](key)
69
+ to_params[key.to_sym]
70
+ end
71
+
72
+ def page
73
+ @pagination.page if @pagination
74
+ end
75
+
76
+ def per_page
77
+ @pagination.per_page if @pagination
78
+ end
79
+
80
+
81
+ private
82
+
83
+ #
84
+ # If we have a single fulltext query, merge is normally. If there are
85
+ # multiple nested queries, serialize them as `_query_` subqueries.
86
+ #
87
+ def merge_fulltext(params)
88
+ return nil if @fulltexts.empty?
89
+ return Sunspot::Util.deep_merge!(params, @fulltexts.first.to_params) if @fulltexts.length == 1
90
+ subqueries = @fulltexts.map {|fulltext| fulltext.to_subquery }.join(' ')
91
+ Sunspot::Util.deep_merge!(params, {:q => subqueries})
92
+ end
93
+
94
+ end
95
+ end
96
+ end
@@ -0,0 +1,36 @@
1
+ module Sunspot
2
+ module Query
3
+ class CompositeFulltext
4
+ def initialize
5
+ @components = []
6
+ end
7
+
8
+ def add(keywords)
9
+ @components << dismax = Dismax.new(keywords)
10
+ dismax
11
+ end
12
+
13
+ def add_location(field, lat, lng, options)
14
+ @components << location = Geo.new(field, lat, lng, options)
15
+ location
16
+ end
17
+
18
+ def to_params
19
+ case @components.length
20
+ when 0
21
+ {}
22
+ when 1
23
+ @components.first.to_params.merge(:fl => '* score')
24
+ else
25
+ to_subqueries.merge(:fl => '* score')
26
+ end
27
+ end
28
+
29
+ private
30
+
31
+ def to_subqueries
32
+ { :q => @components.map { |dismax| dismax.to_subquery }.join(' ') }
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,206 @@
1
+ module Sunspot
2
+ module Query
3
+ module Connective #:nodoc:all
4
+ #
5
+ # Base class for connectives (conjunctions and disjunctions).
6
+ #
7
+ class Abstract
8
+ include Filter
9
+
10
+ def initialize(negated = false) #:nodoc:
11
+ @negated = negated
12
+ @components = []
13
+ end
14
+
15
+ #
16
+ # Add a restriction to the connective.
17
+ #
18
+ def add_restriction(negated, field, restriction_type, *value)
19
+ add_component(restriction_type.new(negated, field, *value))
20
+ end
21
+
22
+ #
23
+ # Add a shorthand restriction; the restriction type is determined by
24
+ # the value.
25
+ #
26
+ def add_shorthand_restriction(negated, field, value)
27
+ restriction_type =
28
+ case value
29
+ when Array then Restriction::AnyOf
30
+ when Range then Restriction::Between
31
+ else Restriction::EqualTo
32
+ end
33
+ add_restriction(negated, field, restriction_type, value)
34
+ end
35
+
36
+ #
37
+ # Add a positive restriction. The restriction will match all
38
+ # documents who match the terms fo the restriction.
39
+ #
40
+ def add_positive_restriction(field, restriction_type, value)
41
+ add_restriction(false, field, restriction_type, value)
42
+ end
43
+
44
+ #
45
+ # Add a positive shorthand restriction (see add_shorthand_restriction)
46
+ #
47
+ def add_positive_shorthand_restriction(field, value)
48
+ add_shorthand_restriction(false, field, value)
49
+ end
50
+
51
+ #
52
+ # Add a negated restriction. The added restriction will match all
53
+ # documents who do not match the terms of the restriction.
54
+ #
55
+ def add_negated_restriction(field, restriction_type, value)
56
+ add_restriction(true, field, restriction_type, value)
57
+ end
58
+
59
+ #
60
+ # Add a negated shorthand restriction (see add_shorthand_restriction)
61
+ #
62
+ def add_negated_shorthand_restriction(field, value)
63
+ add_shorthand_restriction(true, field, value)
64
+ end
65
+
66
+ #
67
+ # Add a new conjunction and return it.
68
+ #
69
+ def add_conjunction
70
+ add_component(Conjunction.new)
71
+ end
72
+
73
+ #
74
+ # Add a new disjunction and return it.
75
+ #
76
+ def add_disjunction
77
+ add_component(Disjunction.new)
78
+ end
79
+
80
+ #
81
+ # Add an arbitrary component to the conjunction, and return it.
82
+ # The component must respond to #to_boolean_phrase
83
+ #
84
+ def add_component(component)
85
+ @components << component
86
+ component
87
+ end
88
+
89
+ #
90
+ # Express the connective as a Lucene boolean phrase.
91
+ #
92
+ def to_boolean_phrase #:nodoc:
93
+ unless @components.empty?
94
+ phrase =
95
+ if @components.length == 1
96
+ @components.first.to_boolean_phrase
97
+ else
98
+ component_phrases = @components.map do |component|
99
+ component.to_boolean_phrase
100
+ end
101
+ "(#{component_phrases.join(" #{connector} ")})"
102
+ end
103
+ if negated?
104
+ "-#{phrase}"
105
+ else
106
+ phrase
107
+ end
108
+ end
109
+ end
110
+
111
+ #
112
+ # Connectives can be negated during the process of denormalization that
113
+ # is performed when a disjunction contains a negated component. This
114
+ # method conforms to the duck type for all boolean query components.
115
+ #
116
+ def negated?
117
+ @negated
118
+ end
119
+
120
+ #
121
+ # Returns a new connective that's a negated version of this one.
122
+ #
123
+ def negate
124
+ negated = self.class.new(!negated?)
125
+ @components.each do |component|
126
+ negated.add_component(component)
127
+ end
128
+ negated
129
+ end
130
+ end
131
+
132
+ #
133
+ # Disjunctions combine their components with an OR operator.
134
+ #
135
+ class Disjunction < Abstract
136
+ class <<self
137
+ def inverse
138
+ Conjunction
139
+ end
140
+ end
141
+
142
+ #
143
+ # Express this disjunction as a Lucene boolean phrase
144
+ #
145
+ def to_boolean_phrase
146
+ if @components.any? { |component| component.negated? }
147
+ denormalize.to_boolean_phrase
148
+ else
149
+ super
150
+ end
151
+ end
152
+
153
+ #
154
+ # No-op - this is already a disjunction
155
+ #
156
+ def add_disjunction
157
+ self
158
+ end
159
+
160
+ private
161
+
162
+ def connector
163
+ 'OR'
164
+ end
165
+
166
+ #
167
+ # If a disjunction contains negated components, it must be
168
+ # "denormalized", because the Lucene parser interprets any negated
169
+ # boolean phrase using AND semantics (this isn't a bug, it's just a
170
+ # subtlety of how Lucene parses queries). So, per DeMorgan's law we
171
+ # create a negated conjunction and add to it all of our components,
172
+ # negated themselves, which creates a query whose Lucene semantics are
173
+ # in line with our intentions.
174
+ #
175
+ def denormalize
176
+ denormalized = self.class.inverse.new(!negated?)
177
+ @components.each do |component|
178
+ denormalized.add_component(component.negate)
179
+ end
180
+ denormalized
181
+ end
182
+ end
183
+
184
+ #
185
+ # Conjunctions combine their components with an AND operator.
186
+ #
187
+ class Conjunction < Abstract
188
+ class <<self
189
+ def inverse
190
+ Disjunction
191
+ end
192
+ end
193
+
194
+ def add_conjunction
195
+ self
196
+ end
197
+
198
+ private
199
+
200
+ def connector
201
+ 'AND'
202
+ end
203
+ end
204
+ end
205
+ end
206
+ end
@@ -0,0 +1,14 @@
1
+ module Sunspot
2
+ module Query
3
+ class DateFieldFacet < AbstractFieldFacet
4
+ def to_params
5
+ params = super
6
+ params[:"facet.date"] = [@field.indexed_name]
7
+ params[qualified_param('date.start')] = @field.to_indexed(@options[:time_range].first)
8
+ params[qualified_param('date.end')] = @field.to_indexed(@options[:time_range].last)
9
+ params[qualified_param('date.gap')] = "+#{@options[:time_interval] || 86400}SECONDS"
10
+ params
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,132 @@
1
+ module Sunspot
2
+ module Query
3
+
4
+ #
5
+ # Solr full-text queries use Solr's DisMaxRequestHandler, a search handler
6
+ # designed to process user-entered phrases, and search for individual
7
+ # words across a union of several fields.
8
+ #
9
+ class Dismax
10
+ attr_writer :minimum_match, :phrase_slop, :query_phrase_slop, :tie
11
+
12
+ def initialize(keywords)
13
+ @keywords = keywords
14
+ @fulltext_fields = {}
15
+ @boost_queries = []
16
+ @boost_functions = []
17
+ @highlights = []
18
+ end
19
+
20
+ #
21
+ # The query as Solr parameters
22
+ #
23
+ def to_params
24
+ params = { :q => @keywords }
25
+ params[:fl] = '* score'
26
+ params[:qf] = @fulltext_fields.values.map { |field| field.to_boosted_field }.join(' ')
27
+ params[:defType] = 'dismax'
28
+ if @phrase_fields
29
+ params[:pf] = @phrase_fields.map { |field| field.to_boosted_field }.join(' ')
30
+ end
31
+ unless @boost_queries.empty?
32
+ params[:bq] = @boost_queries.map do |boost_query|
33
+ boost_query.to_boolean_phrase
34
+ end
35
+ end
36
+ unless @boost_functions.empty?
37
+ params[:bf] = @boost_functions.map do |boost_function|
38
+ boost_function.to_s
39
+ end
40
+ end
41
+ if @minimum_match
42
+ params[:mm] = @minimum_match
43
+ end
44
+ if @phrase_slop
45
+ params[:ps] = @phrase_slop
46
+ end
47
+ if @query_phrase_slop
48
+ params[:qs] = @query_phrase_slop
49
+ end
50
+ if @tie
51
+ params[:tie] = @tie
52
+ end
53
+ @highlights.each do |highlight|
54
+ Sunspot::Util.deep_merge!(params, highlight.to_params)
55
+ end
56
+ params
57
+ end
58
+
59
+ #
60
+ # Serialize the query as a Solr nested subquery.
61
+ #
62
+ def to_subquery
63
+ params = self.to_params
64
+ params.delete :defType
65
+ params.delete :fl
66
+ keywords = params.delete(:q)
67
+ options = params.map { |key, value| escape_param(key, value) }.join(' ')
68
+ "_query_:\"{!dismax #{options}}#{escape_quotes(keywords)}\""
69
+ end
70
+
71
+ #
72
+ # Assign a new boost query and return it.
73
+ #
74
+ def create_boost_query(factor)
75
+ @boost_queries << boost_query = BoostQuery.new(factor)
76
+ boost_query
77
+ end
78
+
79
+ #
80
+ # Add a boost function
81
+ #
82
+ def add_boost_function(function_query)
83
+ @boost_functions << function_query
84
+ end
85
+
86
+ #
87
+ # Add a fulltext field to be searched, with optional boost.
88
+ #
89
+ def add_fulltext_field(field, boost = nil)
90
+ @fulltext_fields[field.indexed_name] = TextFieldBoost.new(field, boost)
91
+ end
92
+
93
+ #
94
+ # Add a phrase field for extra boost.
95
+ #
96
+ def add_phrase_field(field, boost = nil)
97
+ @phrase_fields ||= []
98
+ @phrase_fields << TextFieldBoost.new(field, boost)
99
+ end
100
+
101
+ #
102
+ # Set highlighting options for the query. If fields is empty, the
103
+ # Highlighting object won't pass field names at all, which means
104
+ # the dismax's :qf parameter will be used by Solr.
105
+ #
106
+ def add_highlight(fields=[], options={})
107
+ @highlights << Highlighting.new(fields, options)
108
+ end
109
+
110
+ #
111
+ # Determine if a given field is being searched. Used by DSL to avoid
112
+ # overwriting boost parameters when injecting defaults.
113
+ #
114
+ def has_fulltext_field?(field)
115
+ @fulltext_fields.has_key?(field.indexed_name)
116
+ end
117
+
118
+
119
+ private
120
+
121
+ def escape_param(key, value)
122
+ "#{key}='#{escape_quotes(Array(value).join(" "))}'"
123
+ end
124
+
125
+ def escape_quotes(value)
126
+ return value unless value.is_a? String
127
+ value.gsub(/(['"])/, '\\\\\1')
128
+ end
129
+
130
+ end
131
+ end
132
+ end
@@ -0,0 +1,41 @@
1
+ module Sunspot
2
+ module Query
3
+ class FieldFacet < AbstractFieldFacet
4
+ def initialize(field, options)
5
+ if exclude_filters = options[:exclude]
6
+ @exclude_tag = Util.Array(exclude_filters).map do |filter|
7
+ filter.tag
8
+ end.join(',')
9
+ end
10
+ super
11
+ end
12
+
13
+ def to_params
14
+ super.merge(:"facet.field" => [field_name_with_local_params])
15
+ end
16
+
17
+ private
18
+
19
+ def local_params
20
+ @local_params ||=
21
+ begin
22
+ local_params = {}
23
+ local_params[:ex] = @exclude_tag if @exclude_tag
24
+ local_params[:key] = @options[:name] if @options[:name]
25
+ local_params
26
+ end
27
+ end
28
+
29
+ def field_name_with_local_params
30
+ if local_params.empty?
31
+ @field.indexed_name
32
+ else
33
+ pairs = local_params.map do |key, value|
34
+ "#{key}=#{value}"
35
+ end
36
+ "{!#{pairs.join(' ')}}#{@field.indexed_name}"
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,36 @@
1
+ module Sunspot
2
+ module Query
3
+ #
4
+ # A FieldGroup groups by the unique values of a given field.
5
+ #
6
+ class FieldGroup
7
+ attr_accessor :limit, :truncate
8
+
9
+ def initialize(field)
10
+ if field.multiple?
11
+ raise(ArgumentError, "#{field.name} cannot be used for grouping because it is a multiple-value field")
12
+ end
13
+ @field = field
14
+
15
+ @sort = SortComposite.new
16
+ end
17
+
18
+ def add_sort(sort)
19
+ @sort << sort
20
+ end
21
+
22
+ def to_params
23
+ params = {
24
+ :group => "true",
25
+ :"group.field" => @field.indexed_name,
26
+ }
27
+
28
+ params.merge!(@sort.to_params("group."))
29
+ params[:"group.limit"] = @limit if @limit
30
+ params[:"group.truncate"] = @truncate if @truncate
31
+
32
+ params
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,38 @@
1
+ module Sunspot
2
+ module Query
3
+ module Filter
4
+
5
+ #
6
+ # Express this filter as an :fq parameter; i.e., the boolean phrase,
7
+ # maybe prefixed by local params.
8
+ #
9
+ def to_filter_query
10
+ if tagged? then "{!tag=#{tag}}#{to_boolean_phrase}"
11
+ else to_boolean_phrase
12
+ end
13
+ end
14
+
15
+ #
16
+ # Generate and return a tag that can be attached to this restriction,
17
+ # for use with multiselect faceting. This needs to be unique, but doesn't
18
+ # really need to be human-readable, so just generate a string based on the
19
+ # hash of the boolean phrase.
20
+ #
21
+ def tag
22
+ @tag ||= to_boolean_phrase.hash.abs.to_s(36)
23
+ end
24
+
25
+ private
26
+
27
+ #
28
+ # True if a tag has been generated for this filter (e.g., if it's been
29
+ # excluded from a given facet). If a tag has not been generated at the
30
+ # time that the filter query param is requested, then it is not necessary
31
+ # to include a tag in the local params.
32
+ #
33
+ def tagged?
34
+ !!@tag
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,52 @@
1
+ module Sunspot
2
+ module Query
3
+ #
4
+ # Abstract class for function queries.
5
+ #
6
+ class FunctionQuery
7
+ include RSolr::Char
8
+ end
9
+
10
+ #
11
+ # Function query which represents a constant.
12
+ #
13
+ class ConstantFunctionQuery < FunctionQuery
14
+ def initialize(constant)
15
+ @constant = constant
16
+ end
17
+
18
+ def to_s
19
+ Type.to_literal(@constant)
20
+ end
21
+ end
22
+
23
+ #
24
+ # Function query which represents a field.
25
+ #
26
+ class FieldFunctionQuery < FunctionQuery
27
+ def initialize(field)
28
+ @field = field
29
+ end
30
+
31
+ def to_s
32
+ "#{escape(@field.indexed_name)}"
33
+ end
34
+ end
35
+
36
+ #
37
+ # Function query which represents an actual function invocation.
38
+ # Takes a function name and arguments as parameters.
39
+ # Arguments are in turn FunctionQuery objects.
40
+ #
41
+ class FunctionalFunctionQuery < FunctionQuery
42
+ def initialize(function_name, function_args)
43
+ @function_name, @function_args = function_name, function_args
44
+ end
45
+
46
+ def to_s
47
+ params = @function_args.map { |arg| arg.to_s }.join(",")
48
+ "#{@function_name}(#{params})"
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,53 @@
1
+ begin
2
+ require 'geohash'
3
+ rescue LoadError => e
4
+ require 'pr_geohash'
5
+ end
6
+
7
+ module Sunspot
8
+ module Query
9
+ class Geo
10
+ MAX_PRECISION = 12
11
+ DEFAULT_PRECISION = 7
12
+ DEFAULT_PRECISION_FACTOR = 16.0
13
+
14
+ def initialize(field, lat, lng, options)
15
+ @field, @options = field, options
16
+ @geohash = GeoHash.encode(lat.to_f, lng.to_f, MAX_PRECISION)
17
+ end
18
+
19
+ def to_params
20
+ { :q => to_boolean_query }
21
+ end
22
+
23
+ def to_subquery
24
+ "(#{to_boolean_query})"
25
+ end
26
+
27
+ private
28
+
29
+ def to_boolean_query
30
+ queries = []
31
+ MAX_PRECISION.downto(precision) do |i|
32
+ star = i == MAX_PRECISION ? '' : '*'
33
+ precision_boost = Util.format_float(
34
+ boost * precision_factor ** (i-MAX_PRECISION).to_f, 3)
35
+ queries << "#{@field.indexed_name}:#{@geohash[0, i]}#{star}^#{precision_boost}"
36
+ end
37
+ queries.join(' OR ')
38
+ end
39
+
40
+ def precision
41
+ @options[:precision] || DEFAULT_PRECISION
42
+ end
43
+
44
+ def precision_factor
45
+ @options[:precision_factor] || DEFAULT_PRECISION_FACTOR
46
+ end
47
+
48
+ def boost
49
+ @options[:boost] || 1.0
50
+ end
51
+ end
52
+ end
53
+ end