sunspot 2.0.0 → 2.5.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (165) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +1 -0
  3. data/.rspec +2 -0
  4. data/Appraisals +7 -0
  5. data/Gemfile +0 -2
  6. data/History.txt +10 -0
  7. data/lib/sunspot.rb +55 -17
  8. data/lib/sunspot/adapters.rb +68 -18
  9. data/lib/sunspot/batcher.rb +1 -1
  10. data/lib/sunspot/configuration.rb +4 -2
  11. data/lib/sunspot/data_extractor.rb +36 -6
  12. data/lib/sunspot/dsl.rb +4 -3
  13. data/lib/sunspot/dsl/adjustable.rb +2 -2
  14. data/lib/sunspot/dsl/field_query.rb +69 -16
  15. data/lib/sunspot/dsl/field_stats.rb +25 -0
  16. data/lib/sunspot/dsl/fields.rb +28 -8
  17. data/lib/sunspot/dsl/fulltext.rb +9 -1
  18. data/lib/sunspot/dsl/group.rb +118 -0
  19. data/lib/sunspot/dsl/paginatable.rb +4 -1
  20. data/lib/sunspot/dsl/scope.rb +19 -10
  21. data/lib/sunspot/dsl/search.rb +1 -1
  22. data/lib/sunspot/dsl/spellcheckable.rb +14 -0
  23. data/lib/sunspot/dsl/standard_query.rb +63 -35
  24. data/lib/sunspot/field.rb +76 -4
  25. data/lib/sunspot/field_factory.rb +60 -11
  26. data/lib/sunspot/indexer.rb +70 -18
  27. data/lib/sunspot/query.rb +5 -4
  28. data/lib/sunspot/query/abstract_field_facet.rb +0 -2
  29. data/lib/sunspot/query/abstract_fulltext.rb +76 -0
  30. data/lib/sunspot/query/abstract_json_field_facet.rb +70 -0
  31. data/lib/sunspot/query/bbox.rb +5 -1
  32. data/lib/sunspot/query/common_query.rb +31 -6
  33. data/lib/sunspot/query/composite_fulltext.rb +58 -8
  34. data/lib/sunspot/query/date_field_json_facet.rb +25 -0
  35. data/lib/sunspot/query/dismax.rb +25 -71
  36. data/lib/sunspot/query/field_json_facet.rb +19 -0
  37. data/lib/sunspot/query/field_list.rb +15 -0
  38. data/lib/sunspot/query/field_stats.rb +61 -0
  39. data/lib/sunspot/query/function_query.rb +1 -2
  40. data/lib/sunspot/query/geo.rb +1 -1
  41. data/lib/sunspot/query/geofilt.rb +8 -3
  42. data/lib/sunspot/query/group.rb +46 -0
  43. data/lib/sunspot/query/group_query.rb +17 -0
  44. data/lib/sunspot/query/join.rb +88 -0
  45. data/lib/sunspot/query/more_like_this.rb +1 -1
  46. data/lib/sunspot/query/pagination.rb +12 -4
  47. data/lib/sunspot/query/range_json_facet.rb +28 -0
  48. data/lib/sunspot/query/restriction.rb +99 -13
  49. data/lib/sunspot/query/sort.rb +41 -0
  50. data/lib/sunspot/query/sort_composite.rb +7 -0
  51. data/lib/sunspot/query/spellcheck.rb +19 -0
  52. data/lib/sunspot/query/standard_query.rb +24 -2
  53. data/lib/sunspot/query/text_field_boost.rb +1 -3
  54. data/lib/sunspot/schema.rb +12 -3
  55. data/lib/sunspot/search.rb +4 -2
  56. data/lib/sunspot/search/abstract_search.rb +93 -43
  57. data/lib/sunspot/search/cursor_paginated_collection.rb +32 -0
  58. data/lib/sunspot/search/field_facet.rb +4 -4
  59. data/lib/sunspot/search/field_json_facet.rb +33 -0
  60. data/lib/sunspot/search/field_stats.rb +21 -0
  61. data/lib/sunspot/search/hit.rb +6 -1
  62. data/lib/sunspot/search/hit_enumerable.rb +4 -1
  63. data/lib/sunspot/search/json_facet_row.rb +40 -0
  64. data/lib/sunspot/search/json_facet_stats.rb +23 -0
  65. data/lib/sunspot/search/paginated_collection.rb +1 -0
  66. data/lib/sunspot/search/query_group.rb +74 -0
  67. data/lib/sunspot/search/standard_search.rb +70 -3
  68. data/lib/sunspot/search/stats_facet.rb +25 -0
  69. data/lib/sunspot/search/stats_json_row.rb +82 -0
  70. data/lib/sunspot/search/stats_row.rb +68 -0
  71. data/lib/sunspot/session.rb +62 -37
  72. data/lib/sunspot/session_proxy/class_sharding_session_proxy.rb +6 -4
  73. data/lib/sunspot/session_proxy/id_sharding_session_proxy.rb +16 -8
  74. data/lib/sunspot/session_proxy/master_slave_session_proxy.rb +2 -2
  75. data/lib/sunspot/session_proxy/retry_5xx_session_proxy.rb +1 -1
  76. data/lib/sunspot/session_proxy/sharding_session_proxy.rb +4 -2
  77. data/lib/sunspot/session_proxy/silent_fail_session_proxy.rb +1 -1
  78. data/lib/sunspot/session_proxy/thread_local_session_proxy.rb +6 -4
  79. data/lib/sunspot/setup.rb +42 -0
  80. data/lib/sunspot/type.rb +20 -0
  81. data/lib/sunspot/util.rb +78 -14
  82. data/lib/sunspot/version.rb +1 -1
  83. data/spec/api/adapters_spec.rb +40 -15
  84. data/spec/api/batcher_spec.rb +15 -15
  85. data/spec/api/binding_spec.rb +3 -3
  86. data/spec/api/class_set_spec.rb +6 -6
  87. data/spec/api/data_extractor_spec.rb +39 -0
  88. data/spec/api/hit_enumerable_spec.rb +32 -9
  89. data/spec/api/indexer/attributes_spec.rb +35 -30
  90. data/spec/api/indexer/batch_spec.rb +8 -7
  91. data/spec/api/indexer/dynamic_fields_spec.rb +8 -8
  92. data/spec/api/indexer/fixed_fields_spec.rb +16 -11
  93. data/spec/api/indexer/fulltext_spec.rb +8 -8
  94. data/spec/api/indexer/removal_spec.rb +24 -14
  95. data/spec/api/indexer_spec.rb +2 -2
  96. data/spec/api/query/advanced_manipulation_examples.rb +3 -3
  97. data/spec/api/query/connectives_examples.rb +26 -14
  98. data/spec/api/query/dsl_spec.rb +24 -6
  99. data/spec/api/query/dynamic_fields_examples.rb +18 -18
  100. data/spec/api/query/faceting_examples.rb +80 -61
  101. data/spec/api/query/fulltext_examples.rb +194 -40
  102. data/spec/api/query/function_spec.rb +116 -13
  103. data/spec/api/query/geo_examples.rb +8 -12
  104. data/spec/api/query/group_spec.rb +27 -5
  105. data/spec/api/query/highlighting_examples.rb +26 -26
  106. data/spec/api/query/join_spec.rb +19 -0
  107. data/spec/api/query/more_like_this_spec.rb +40 -27
  108. data/spec/api/query/ordering_pagination_examples.rb +37 -23
  109. data/spec/api/query/scope_examples.rb +39 -39
  110. data/spec/api/query/spatial_examples.rb +3 -3
  111. data/spec/api/query/spellcheck_examples.rb +20 -0
  112. data/spec/api/query/standard_spec.rb +3 -1
  113. data/spec/api/query/stats_examples.rb +66 -0
  114. data/spec/api/query/text_field_scoping_examples.rb +5 -5
  115. data/spec/api/query/types_spec.rb +4 -4
  116. data/spec/api/search/cursor_paginated_collection_spec.rb +35 -0
  117. data/spec/api/search/dynamic_fields_spec.rb +4 -4
  118. data/spec/api/search/faceting_spec.rb +55 -52
  119. data/spec/api/search/highlighting_spec.rb +7 -7
  120. data/spec/api/search/hits_spec.rb +43 -29
  121. data/spec/api/search/paginated_collection_spec.rb +19 -18
  122. data/spec/api/search/results_spec.rb +13 -13
  123. data/spec/api/search/search_spec.rb +3 -3
  124. data/spec/api/search/stats_spec.rb +94 -0
  125. data/spec/api/session_proxy/class_sharding_session_proxy_spec.rb +23 -16
  126. data/spec/api/session_proxy/id_sharding_session_proxy_spec.rb +16 -4
  127. data/spec/api/session_proxy/master_slave_session_proxy_spec.rb +10 -6
  128. data/spec/api/session_proxy/retry_5xx_session_proxy_spec.rb +11 -11
  129. data/spec/api/session_proxy/sharding_session_proxy_spec.rb +15 -14
  130. data/spec/api/session_proxy/silent_fail_session_proxy_spec.rb +3 -3
  131. data/spec/api/session_proxy/spec_helper.rb +1 -1
  132. data/spec/api/session_proxy/thread_local_session_proxy_spec.rb +40 -26
  133. data/spec/api/session_spec.rb +78 -38
  134. data/spec/api/sunspot_spec.rb +7 -4
  135. data/spec/helpers/integration_helper.rb +11 -1
  136. data/spec/helpers/query_helper.rb +1 -1
  137. data/spec/helpers/search_helper.rb +30 -0
  138. data/spec/integration/atomic_updates_spec.rb +58 -0
  139. data/spec/integration/dynamic_fields_spec.rb +31 -20
  140. data/spec/integration/faceting_spec.rb +252 -39
  141. data/spec/integration/field_grouping_spec.rb +47 -15
  142. data/spec/integration/field_lists_spec.rb +57 -0
  143. data/spec/integration/geospatial_spec.rb +34 -8
  144. data/spec/integration/highlighting_spec.rb +8 -8
  145. data/spec/integration/indexing_spec.rb +7 -6
  146. data/spec/integration/join_spec.rb +45 -0
  147. data/spec/integration/keyword_search_spec.rb +68 -38
  148. data/spec/integration/local_search_spec.rb +4 -4
  149. data/spec/integration/more_like_this_spec.rb +7 -7
  150. data/spec/integration/scoped_search_spec.rb +193 -74
  151. data/spec/integration/spellcheck_spec.rb +119 -0
  152. data/spec/integration/stats_spec.rb +88 -0
  153. data/spec/integration/stored_fields_spec.rb +1 -1
  154. data/spec/integration/test_pagination.rb +4 -4
  155. data/spec/integration/unicode_spec.rb +1 -1
  156. data/spec/mocks/adapters.rb +36 -0
  157. data/spec/mocks/connection.rb +5 -3
  158. data/spec/mocks/photo.rb +32 -1
  159. data/spec/mocks/post.rb +18 -3
  160. data/spec/spec_helper.rb +13 -8
  161. data/sunspot.gemspec +6 -4
  162. data/tasks/rdoc.rake +22 -14
  163. metadata +101 -44
  164. data/lib/sunspot/dsl/field_group.rb +0 -57
  165. data/lib/sunspot/query/field_group.rb +0 -37
@@ -0,0 +1,19 @@
1
+ module Sunspot
2
+ module Query
3
+ class FieldJsonFacet < AbstractJsonFieldFacet
4
+
5
+ def initialize(field, options, setup)
6
+ super
7
+ end
8
+
9
+ def field_name_with_local_params
10
+ {
11
+ @field.name => {
12
+ type: 'terms',
13
+ field: @field.indexed_name,
14
+ }.merge!(init_params)
15
+ }
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,15 @@
1
+ module Sunspot
2
+ module Query #:nodoc:
3
+
4
+ # This DSL represents only fields that would come out of the results of the search type.
5
+ class FieldList
6
+ def initialize(list)
7
+ @list = list
8
+ end
9
+
10
+ def to_params
11
+ { :fl => @list }
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,61 @@
1
+ module Sunspot
2
+ module Query
3
+ class FieldStats
4
+ def initialize(field, options)
5
+ @field, @options = field, options
6
+ @facets = []
7
+ end
8
+
9
+ def add_facet field
10
+ @facets << field
11
+ end
12
+
13
+ def add_json_facet(json_facet)
14
+ @json_facet = json_facet
15
+ end
16
+
17
+ def to_params
18
+ params = {}
19
+ if !@json_facet.nil?
20
+ params['json.facet'] = recursive_add_stats(@json_facet.get_params).to_json
21
+ else
22
+ params.merge!(:stats => true, :"stats.field" => [@field.indexed_name])
23
+ params[facet_key] = @facets.map(&:indexed_name) unless @facets.empty?
24
+ end
25
+ params
26
+ end
27
+
28
+ STATS_FUNCTIONS = [:min, :max, :sum, :avg, :sumsq]
29
+
30
+ def recursive_add_stats(query)
31
+ query.keys.each do |k|
32
+ if !query[k][:facet].nil?
33
+ query[k][:facet] = recursive_add_stats(query[k][:facet])
34
+ end
35
+ query[k][:facet] ||= {}
36
+ query[k][:sort] = { @options[:sort] => @options[:sort_type]||'desc' } unless @options[:sort].nil?
37
+ query[k][:facet].merge!(json_stats_params)
38
+ end
39
+ query
40
+ end
41
+
42
+ def json_stats_params
43
+ params = {}
44
+ STATS_FUNCTIONS.each { |s| params[s] = "#{s.to_s}(#{@field.indexed_name})" }
45
+ unless @options[:stats].nil?
46
+ to_remove = STATS_FUNCTIONS - @options[:stats]
47
+ to_remove.map { |s| params.delete(s)}
48
+ end
49
+ params
50
+ end
51
+
52
+ def facet_key
53
+ qualified_param 'facet'
54
+ end
55
+
56
+ def qualified_param name
57
+ :"f.#{@field.indexed_name}.stats.#{name}"
58
+ end
59
+ end
60
+ end
61
+ end
@@ -4,7 +4,6 @@ module Sunspot
4
4
  # Abstract class for function queries.
5
5
  #
6
6
  class FunctionQuery
7
- include RSolr::Char
8
7
 
9
8
  def ^(y)
10
9
  @boost_amount = y
@@ -34,7 +33,7 @@ module Sunspot
34
33
  end
35
34
 
36
35
  def to_s
37
- "#{escape(@field.indexed_name)}" << (@boost_amount ? "^#{@boost_amount}" : "")
36
+ "#{Util.escape(@field.indexed_name)}" << (@boost_amount ? "^#{@boost_amount}" : "")
38
37
  end
39
38
  end
40
39
 
@@ -21,7 +21,7 @@ module Sunspot
21
21
  end
22
22
 
23
23
  def to_subquery
24
- "(#{to_boolean_query})"
24
+ { :q => "(#{to_boolean_query})" }
25
25
  end
26
26
 
27
27
  private
@@ -1,15 +1,20 @@
1
1
  module Sunspot
2
2
  module Query
3
3
  class Geofilt
4
+ include Filter
5
+ attr_reader :field
6
+
4
7
  def initialize(field, lat, lon, radius, options = {})
5
8
  @field, @lat, @lon, @radius, @options = field, lat, lon, radius, options
6
9
  end
7
10
 
8
- def to_params
11
+ def to_boolean_phrase
9
12
  func = @options[:bbox] ? "bbox" : "geofilt"
13
+ "{!#{func} sfield=#{@field.indexed_name} pt=#{@lat},#{@lon} d=#{@radius}}"
14
+ end
10
15
 
11
- filter = "{!#{func} sfield=#{@field.indexed_name} pt=#{@lat},#{@lon} d=#{@radius}}"
12
- {:fq => filter}
16
+ def to_params
17
+ {:fq => to_filter_query}
13
18
  end
14
19
  end
15
20
  end
@@ -0,0 +1,46 @@
1
+ module Sunspot
2
+ module Query
3
+ #
4
+ # A Group groups by the unique values of a given field, or by given queries.
5
+ #
6
+ class Group
7
+ attr_accessor :limit, :truncate, :ngroups
8
+ attr_reader :fields, :queries
9
+
10
+ def initialize
11
+ @sort = SortComposite.new
12
+ @fields = []
13
+ @queries = []
14
+ end
15
+
16
+ def add_field(field)
17
+ if field.multiple?
18
+ raise(ArgumentError, "#{field.name} cannot be used for grouping because it is a multiple-value field")
19
+ end
20
+ @fields << field
21
+ end
22
+
23
+ def add_query(query)
24
+ @queries << query
25
+ end
26
+
27
+ def add_sort(sort)
28
+ @sort << sort
29
+ end
30
+
31
+ def to_params
32
+ params = {
33
+ :group => 'true',
34
+ :"group.ngroups" => @ngroups.nil? ? 'true' : @ngroups.to_s
35
+ }
36
+
37
+ params.merge!(@sort.to_params('group.'))
38
+ params[:"group.field"] = @fields.map(&:indexed_name) if @fields.any?
39
+ params[:"group.query"] = @queries.map(&:to_boolean_phrase) if @queries.any?
40
+ params[:"group.limit"] = @limit if @limit
41
+ params[:"group.truncate"] = @truncate if @truncate
42
+ params
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,17 @@
1
+ module Sunspot
2
+ module Query
3
+ #
4
+ # Representation of a GroupQuery, which allows the searcher to specify a
5
+ # query to group matching documents. This is essentially a conjunction,
6
+ # with an extra instance variable containing the label for the group.
7
+ #
8
+ class GroupQuery < Connective::Conjunction #:nodoc:
9
+ attr_reader :label
10
+
11
+ def initialize(label, negated = false)
12
+ super(negated)
13
+ @label = label
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,88 @@
1
+ module Sunspot
2
+ module Query
3
+
4
+ #
5
+ # Solr full-text queries use Solr's JoinRequestHandler.
6
+ #
7
+ class Join < AbstractFulltext
8
+ attr_writer :minimum_match, :phrase_slop, :query_phrase_slop, :tie
9
+
10
+ def initialize(keywords, target, from, to)
11
+ @keywords = keywords
12
+ @target = target
13
+ @from = from
14
+ @to = to
15
+
16
+ @fulltext_fields = {}
17
+
18
+ @minimum_match = nil
19
+ end
20
+
21
+ #
22
+ # The query as Solr parameters
23
+ #
24
+ def to_params
25
+ params = { :q => @keywords }
26
+ params[:fl] = '* score'
27
+ params[:qf] = @fulltext_fields.values.map { |field| field.to_boosted_field }.join(' ')
28
+ params[:defType] = 'join'
29
+ params[:mm] = @minimum_match if @minimum_match
30
+
31
+ params
32
+ end
33
+
34
+ #
35
+ # Serialize the query as a Solr nested subquery.
36
+ #
37
+ def to_subquery
38
+ params = self.to_params
39
+ params.delete :defType
40
+ params.delete :fl
41
+
42
+ keywords = escape_quotes(params.delete(:q))
43
+ options = params.map { |key, value| escape_param(key, value) }.join(' ')
44
+ q_name = "q#{@target.name}#{self.object_id}"
45
+
46
+ {
47
+ :q => "_query_:\"{!join from=#{@from} to=#{@to} v=$#{q_name}}\"",
48
+ q_name => "_query_:\"{!field f=type}#{@target.name}\"+_query_:\"{!edismax #{options}}#{keywords}\""
49
+ }
50
+ end
51
+
52
+ #
53
+ # Assign a new boost query and return it.
54
+ #
55
+ def create_boost_query(factor)
56
+ end
57
+
58
+ #
59
+ # Add a boost function
60
+ #
61
+ def add_boost_function(function_query)
62
+ end
63
+
64
+ #
65
+ # Add a fulltext field to be searched, with optional boost.
66
+ #
67
+ def add_fulltext_field(field, boost = nil)
68
+ super if field.is_a?(Sunspot::JoinField) &&
69
+ field.target == @target && field.from == @from && field.to == @to
70
+ end
71
+
72
+ #
73
+ # Add a phrase field for extra boost.
74
+ #
75
+ def add_phrase_field(field, boost = nil)
76
+ end
77
+
78
+ #
79
+ # Set highlighting options for the query. If fields is empty, the
80
+ # Highlighting object won't pass field names at all, which means
81
+ # the dismax's :qf parameter will be used by Solr.
82
+ #
83
+ def add_highlight(fields=[], options={})
84
+ end
85
+
86
+ end
87
+ end
88
+ end
@@ -50,7 +50,7 @@ module Sunspot
50
50
  params[:"mlt.fl"] = @fields.keys.join(",")
51
51
  boosted_fields = @fields.values.select { |field| field.boost }
52
52
  unless boosted_fields.empty?
53
- params[:qf] = boosted_fields.map do |field|
53
+ params[:"mlt.qf"] = boosted_fields.map do |field|
54
54
  field.to_boosted_field
55
55
  end.join(' ')
56
56
  end
@@ -6,14 +6,18 @@ module Sunspot
6
6
  # reference to it and updates it if pagination is changed.
7
7
  #
8
8
  class Pagination #:nodoc:
9
- attr_reader :page, :per_page, :offset
9
+ attr_reader :page, :per_page, :offset, :cursor
10
10
 
11
- def initialize(page = nil, per_page = nil, offset = nil)
12
- self.offset, self.page, self.per_page = offset, page, per_page
11
+ def initialize(page = nil, per_page = nil, offset = nil, cursor = nil)
12
+ self.offset, self.page, self.per_page, self.cursor = offset, page, per_page, cursor
13
13
  end
14
14
 
15
15
  def to_params
16
- { :start => start, :rows => rows }
16
+ if @cursor
17
+ { :cursorMark => @cursor, :rows => rows }
18
+ else
19
+ { :start => start, :rows => rows }
20
+ end
17
21
  end
18
22
 
19
23
  def page=(page)
@@ -28,6 +32,10 @@ module Sunspot
28
32
  @offset = offset.to_i
29
33
  end
30
34
 
35
+ def cursor=(cursor)
36
+ @cursor = cursor if cursor
37
+ end
38
+
31
39
  private
32
40
 
33
41
  def start
@@ -0,0 +1,28 @@
1
+ module Sunspot
2
+ module Query
3
+ class RangeJsonFacet < AbstractJsonFieldFacet
4
+
5
+ SECONDS_IN_DAY = 86400
6
+
7
+ def initialize(field, options, setup)
8
+ raise Exception.new("Need to specify a range") if options[:range].nil?
9
+ @start = options[:range].first
10
+ @end = options[:range].last
11
+ @gap = options[:gap] || SECONDS_IN_DAY
12
+ super
13
+ end
14
+
15
+ def field_name_with_local_params
16
+ {
17
+ @field.name => {
18
+ type: 'range',
19
+ field: @field.indexed_name,
20
+ start: @field.to_indexed(@start),
21
+ end: @field.to_indexed(@end),
22
+ gap: @gap
23
+ }.merge!(init_params)
24
+ }
25
+ end
26
+ end
27
+ end
28
+ end
@@ -11,7 +11,7 @@ module Sunspot
11
11
  # Array:: Collection of restriction class names
12
12
  #
13
13
  def names
14
- constants - %w(Base) #XXX this seems ugly
14
+ constants - abstract_constants
15
15
  end
16
16
 
17
17
  #
@@ -22,6 +22,17 @@ module Sunspot
22
22
  @types ||= {}
23
23
  @types[restriction_name.to_sym] ||= const_get(Sunspot::Util.camel_case(restriction_name.to_s))
24
24
  end
25
+
26
+ private
27
+
28
+ #
29
+ # Return the names of all abstract restriction classes that should not
30
+ # be made available to the DSL. Considers abstract classes are any class
31
+ # ending with '::Base' or containing a namespace prefixed with 'Abstract'
32
+ #
33
+ def abstract_constants
34
+ constants.grep(/(^|::)(Base$|Abstract)/)
35
+ end
25
36
  end
26
37
 
27
38
  #
@@ -38,7 +49,6 @@ module Sunspot
38
49
  #
39
50
  class Base #:nodoc:
40
51
  include Filter
41
- include RSolr::Char
42
52
 
43
53
  RESERVED_WORDS = Set['AND', 'OR', 'NOT']
44
54
 
@@ -68,11 +78,14 @@ module Sunspot
68
78
  # on whether this restriction is negated.
69
79
  #
70
80
  def to_boolean_phrase
81
+ phrase = []
82
+ phrase << @field.local_params if @field.respond_to? :local_params
71
83
  unless negated?
72
- to_positive_boolean_phrase
84
+ phrase << to_positive_boolean_phrase
73
85
  else
74
- to_negated_boolean_phrase
86
+ phrase << to_negated_boolean_phrase
75
87
  end
88
+ phrase.join
76
89
  end
77
90
 
78
91
  #
@@ -89,7 +102,7 @@ module Sunspot
89
102
  # String:: Boolean phrase for restriction in the positive
90
103
  #
91
104
  def to_positive_boolean_phrase
92
- "#{escape(@field.indexed_name)}:#{to_solr_conditional}"
105
+ "#{Util.escape(@field.indexed_name)}:#{to_solr_conditional}"
93
106
  end
94
107
 
95
108
  #
@@ -122,7 +135,7 @@ module Sunspot
122
135
 
123
136
  protected
124
137
 
125
- #
138
+ #
126
139
  # Return escaped Solr API representation of given value
127
140
  #
128
141
  # ==== Parameters
@@ -135,7 +148,7 @@ module Sunspot
135
148
  # String:: Solr API representation of given value
136
149
  #
137
150
  def solr_value(value = @value)
138
- solr_value = escape(@field.to_indexed(value))
151
+ solr_value = Util.escape(@field.to_indexed(value))
139
152
  if RESERVED_WORDS.include?(solr_value)
140
153
  %Q("#{solr_value}")
141
154
  else
@@ -145,9 +158,13 @@ module Sunspot
145
158
  end
146
159
 
147
160
  class InRadius < Base
148
- def initialize(negated, field, lat, lon, radius)
149
- @lat, @lon, @radius = lat, lon, radius
150
- super negated, field, [lat, lon, radius]
161
+ def initialize(negated, field, *value)
162
+ @lat, @lon, @radius = value
163
+ super negated, field, value
164
+ end
165
+
166
+ def negate
167
+ self.class.new(!@negated, @field, *@value)
151
168
  end
152
169
 
153
170
  private
@@ -156,6 +173,17 @@ module Sunspot
156
173
  end
157
174
  end
158
175
 
176
+ class InBoundingBox < Base
177
+ def initialize(negated, field, first_corner, second_corner)
178
+ @bbox = Sunspot::Query::Bbox.new(field, first_corner, second_corner)
179
+ super negated, field, [first_corner, second_corner]
180
+ end
181
+
182
+ def to_solr_conditional
183
+ @bbox.to_solr_conditional
184
+ end
185
+ end
186
+
159
187
  #
160
188
  # Results must have field with value equal to given value. If the value
161
189
  # is nil, results must have no value for the given field.
@@ -165,7 +193,7 @@ module Sunspot
165
193
  unless @value.nil?
166
194
  super
167
195
  else
168
- "#{escape(@field.indexed_name)}:[* TO *]"
196
+ "#{Util.escape(@field.indexed_name)}:[* TO *]"
169
197
  end
170
198
  end
171
199
 
@@ -273,10 +301,23 @@ module Sunspot
273
301
  # Results must have field with value included in given collection
274
302
  #
275
303
  class AnyOf < Base
304
+
305
+ def negated?
306
+ if @value.empty?
307
+ false
308
+ else
309
+ super
310
+ end
311
+ end
312
+
276
313
  private
277
314
 
278
315
  def to_solr_conditional
279
- "(#{@value.map { |v| solr_value v } * ' OR '})"
316
+ if @value.empty?
317
+ "[* TO *]"
318
+ else
319
+ "(#{@value.map { |v| solr_value v } * ' OR '})"
320
+ end
280
321
  end
281
322
  end
282
323
 
@@ -285,10 +326,22 @@ module Sunspot
285
326
  # collection (only makes sense for fields with multiple values)
286
327
  #
287
328
  class AllOf < Base
329
+ def negated?
330
+ if @value.empty?
331
+ false
332
+ else
333
+ super
334
+ end
335
+ end
336
+
288
337
  private
289
338
 
290
339
  def to_solr_conditional
291
- "(#{@value.map { |v| solr_value v } * ' AND '})"
340
+ if @value.empty?
341
+ "[* TO *]"
342
+ else
343
+ "(#{@value.map { |v| solr_value v } * ' AND '})"
344
+ end
292
345
  end
293
346
  end
294
347
 
@@ -303,6 +356,39 @@ module Sunspot
303
356
  "#{solr_value(@value)}*"
304
357
  end
305
358
  end
359
+
360
+ class AbstractRange < Between
361
+ private
362
+
363
+ def operation
364
+ @operation || self.class.name.split('::').last
365
+ end
366
+
367
+ def solr_value(value = @value)
368
+ @field.to_indexed(value)
369
+ end
370
+
371
+ def to_positive_boolean_phrase
372
+ "_query_:\"{!field f=#{@field.indexed_name} op=#{operation}}#{solr_value}\""
373
+ end
374
+ end
375
+
376
+ class Containing < AbstractRange
377
+ def initialize(negated, field, value)
378
+ @operation = 'Contains'
379
+ super
380
+ end
381
+ end
382
+
383
+ class Intersecting < AbstractRange
384
+ def initialize(negated, field, value)
385
+ @operation = 'Intersects'
386
+ super
387
+ end
388
+ end
389
+
390
+ class Within < AbstractRange
391
+ end
306
392
  end
307
393
  end
308
394
  end