sunspot 1.1.0 → 1.2.0

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 (66) hide show
  1. data/Gemfile +10 -0
  2. data/Gemfile.lock +32 -0
  3. data/History.txt +24 -0
  4. data/README.rdoc +18 -5
  5. data/lib/sunspot.rb +40 -0
  6. data/lib/sunspot/dsl.rb +2 -2
  7. data/lib/sunspot/dsl/field_query.rb +2 -2
  8. data/lib/sunspot/dsl/fields.rb +0 -10
  9. data/lib/sunspot/dsl/restriction.rb +4 -4
  10. data/lib/sunspot/dsl/restriction_with_near.rb +121 -0
  11. data/lib/sunspot/dsl/scope.rb +55 -67
  12. data/lib/sunspot/dsl/standard_query.rb +11 -15
  13. data/lib/sunspot/field.rb +30 -29
  14. data/lib/sunspot/field_factory.rb +0 -18
  15. data/lib/sunspot/installer/solrconfig_updater.rb +0 -30
  16. data/lib/sunspot/query.rb +4 -3
  17. data/lib/sunspot/query/common_query.rb +2 -2
  18. data/lib/sunspot/query/composite_fulltext.rb +7 -2
  19. data/lib/sunspot/query/connective.rb +21 -6
  20. data/lib/sunspot/query/dismax.rb +1 -0
  21. data/lib/sunspot/query/geo.rb +53 -0
  22. data/lib/sunspot/query/more_like_this.rb +1 -0
  23. data/lib/sunspot/query/restriction.rb +5 -5
  24. data/lib/sunspot/query/standard_query.rb +0 -4
  25. data/lib/sunspot/search/abstract_search.rb +1 -7
  26. data/lib/sunspot/search/hit.rb +10 -10
  27. data/lib/sunspot/search/query_facet.rb +8 -3
  28. data/lib/sunspot/session.rb +10 -2
  29. data/lib/sunspot/session_proxy.rb +16 -0
  30. data/lib/sunspot/session_proxy/master_slave_session_proxy.rb +1 -1
  31. data/lib/sunspot/session_proxy/sharding_session_proxy.rb +7 -0
  32. data/lib/sunspot/session_proxy/silent_fail_session_proxy.rb +42 -0
  33. data/lib/sunspot/session_proxy/thread_local_session_proxy.rb +1 -1
  34. data/lib/sunspot/setup.rb +1 -17
  35. data/lib/sunspot/type.rb +38 -6
  36. data/lib/sunspot/util.rb +21 -31
  37. data/lib/sunspot/version.rb +1 -1
  38. data/solr/solr/conf/solrconfig.xml +0 -4
  39. data/spec/api/binding_spec.rb +12 -0
  40. data/spec/api/indexer/attributes_spec.rb +22 -22
  41. data/spec/api/query/connectives_examples.rb +14 -1
  42. data/spec/api/query/fulltext_examples.rb +3 -3
  43. data/spec/api/query/geo_examples.rb +69 -0
  44. data/spec/api/query/scope_examples.rb +32 -13
  45. data/spec/api/query/standard_spec.rb +1 -1
  46. data/spec/api/search/faceting_spec.rb +5 -1
  47. data/spec/api/search/hits_spec.rb +14 -12
  48. data/spec/api/session_proxy/class_sharding_session_proxy_spec.rb +1 -1
  49. data/spec/api/session_proxy/sharding_session_proxy_spec.rb +1 -1
  50. data/spec/api/session_proxy/silent_fail_session_proxy_spec.rb +24 -0
  51. data/spec/api/session_spec.rb +22 -0
  52. data/spec/integration/local_search_spec.rb +42 -69
  53. data/spec/integration/scoped_search_spec.rb +30 -0
  54. data/spec/mocks/connection.rb +6 -2
  55. data/spec/mocks/photo.rb +0 -1
  56. data/spec/mocks/post.rb +11 -2
  57. data/spec/mocks/user.rb +6 -1
  58. data/spec/spec_helper.rb +2 -12
  59. metadata +209 -177
  60. data/lib/sunspot/query/local.rb +0 -26
  61. data/solr/solr/lib/lucene-spatial-2.9.1.jar +0 -0
  62. data/solr/solr/lib/solr-spatial-light-0.0.6.jar +0 -0
  63. data/spec/api/query/local_examples.rb +0 -38
  64. data/tasks/gemspec.rake +0 -33
  65. data/tasks/rcov.rake +0 -28
  66. data/tasks/spec.rake +0 -24
@@ -103,22 +103,18 @@ module Sunspot
103
103
  end
104
104
  alias_method :keywords, :fulltext
105
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 }
106
+ def with(*args)
107
+ case args.first
108
+ when String, Symbol
109
+ field_name = args[0]
110
+ value = args.length > 1 ? args[1] : Scope::NONE
111
+ if value == Scope::NONE
112
+ return DSL::RestrictionWithNear.new(@setup.field(field_name.to_sym), @scope, @query, false)
113
+ end
120
114
  end
121
- @query.add_location_restriction(coordinates, options)
115
+
116
+ # else
117
+ super
122
118
  end
123
119
  end
124
120
  end
data/lib/sunspot/field.rb CHANGED
@@ -4,13 +4,15 @@ module Sunspot
4
4
  attr_accessor :type # The Type of the field
5
5
  attr_accessor :reference # Model class that the value of this field refers to
6
6
  attr_reader :boost
7
+ attr_reader :indexed_name # Name with which this field is indexed internally. Based on public name and type or the +:as+ option.
7
8
 
8
- #
9
+ #
9
10
  #
10
11
  def initialize(name, type, options = {}) #:nodoc
11
12
  @name, @type = name.to_sym, type
12
13
  @stored = !!options.delete(:stored)
13
14
  @more_like_this = !!options.delete(:more_like_this)
15
+ set_indexed_name(options)
14
16
  raise ArgumentError, "Field of type #{type} cannot be used for more_like_this" unless type.accepts_more_like_this? or !@more_like_this
15
17
  end
16
18
 
@@ -56,19 +58,7 @@ module Sunspot
56
58
  @type.cast(value)
57
59
  end
58
60
 
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
61
  #
67
- def indexed_name
68
- @type.indexed_name(@name)
69
- end
70
-
71
- #
72
62
  # Whether this field accepts multiple values.
73
63
  #
74
64
  # ==== Returns
@@ -79,7 +69,7 @@ module Sunspot
79
69
  !!@multiple
80
70
  end
81
71
 
82
- #
72
+ #
83
73
  # Whether this field can be used for more_like_this queries.
84
74
  # If true, the field is configured to store termVectors.
85
75
  #
@@ -99,9 +89,30 @@ module Sunspot
99
89
  indexed_name == field.indexed_name
100
90
  end
101
91
  alias_method :==, :eql?
92
+
93
+ private
94
+
95
+ #
96
+ # Determine the indexed name. If the :as option is given use that, otherwise
97
+ # create the value based on the indexed_name of the type with additional
98
+ # suffixes for multiple, stored, and more_like_this.
99
+ #
100
+ # ==== Returns
101
+ #
102
+ # String: The field's indexed name
103
+ #
104
+ def set_indexed_name(options)
105
+ @indexed_name =
106
+ if options[:as]
107
+ options.delete(:as)
108
+ else
109
+ "#{@type.indexed_name(@name).to_s}#{'m' if @multiple }#{'s' if @stored}#{'v' if more_like_this?}"
110
+ end
111
+ end
112
+
102
113
  end
103
114
 
104
- #
115
+ #
105
116
  # FulltextField instances represent fields that are indexed as fulltext.
106
117
  # These fields are tokenized in the index, and can have boost applied to
107
118
  # them. They also always allow multiple values (since the only downside of
@@ -121,11 +132,11 @@ module Sunspot
121
132
  end
122
133
 
123
134
  def indexed_name
124
- "#{super}#{'s' if @stored}#{'v' if more_like_this?}"
135
+ "#{super}"
125
136
  end
126
137
  end
127
138
 
128
- #
139
+ #
129
140
  # AttributeField instances encapsulate non-tokenized attribute data.
130
141
  # AttributeFields can have any type except TextType, and can also have
131
142
  # a reference (for instantiated facets), optionally allow multiple values
@@ -134,8 +145,8 @@ module Sunspot
134
145
  #
135
146
  class AttributeField < Field #:nodoc:
136
147
  def initialize(name, type, options = {})
137
- super(name, type, options)
138
148
  @multiple = !!options.delete(:multiple)
149
+ super(name, type, options)
139
150
  @reference =
140
151
  if (reference = options.delete(:references)).respond_to?(:name)
141
152
  reference.name
@@ -145,17 +156,6 @@ module Sunspot
145
156
  raise ArgumentError, "Unknown field option #{options.keys.first.inspect} provided for field #{name.inspect}" unless options.empty?
146
157
  end
147
158
 
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
159
  end
160
160
 
161
161
  class TypeField #:nodoc:
@@ -190,3 +190,4 @@ module Sunspot
190
190
  end
191
191
  end
192
192
  end
193
+
@@ -125,23 +125,5 @@ module Sunspot
125
125
  [@name, @type]
126
126
  end
127
127
  end
128
-
129
- class Coordinates
130
- def initialize(name = nil, &block)
131
- if block
132
- @data_extractor = DataExtractor::BlockExtractor.new(&block)
133
- else
134
- @data_extractor = DataExtractor::AttributeExtractor.new(name)
135
- end
136
- end
137
-
138
- def populate_document(document, model)
139
- if coordinates = @data_extractor.value_for(model)
140
- coordinates = Util::Coordinates.new(coordinates)
141
- document.add_field(:lat, coordinates.lat)
142
- document.add_field(:lng, coordinates.lng)
143
- end
144
- end
145
- end
146
128
  end
147
129
  end
@@ -44,8 +44,6 @@ module Sunspot
44
44
  )
45
45
  end
46
46
  @root = @document.root
47
- maybe_create_spatial_component
48
- maybe_add_spatial_component_to_standard_handler
49
47
  maybe_add_more_like_this_handler
50
48
  original_path = "#{@solrconfig_path}.orig"
51
49
  FileUtils.cp(@solrconfig_path, original_path)
@@ -62,34 +60,6 @@ module Sunspot
62
60
 
63
61
  private
64
62
 
65
- def maybe_create_spatial_component
66
- if @root.xpath('searchComponent[@name="spatial"]').any?
67
- say('Spatial search component already defined')
68
- else
69
- say('Defining spatial search component')
70
- search_component_node =
71
- Nokogiri::XML::Node.new('searchComponent', @document)
72
- search_component_node['name'] = 'spatial'
73
- search_component_node['class'] =
74
- 'me.outofti.solrspatiallight.SpatialQueryComponent'
75
- @root << search_component_node
76
- end
77
- end
78
-
79
- def maybe_add_spatial_component_to_standard_handler
80
- standard_handler_node =
81
- @root.xpath('requestHandler[@name="standard"]').first
82
- last_components_node =
83
- standard_handler_node.xpath('arr[@name="last-components"]').first ||
84
- add_element(standard_handler_node, 'arr', 'name' => 'last-components')
85
- if last_components_node.xpath('str[normalize-space()="spatial"]').any?
86
- say('Spatial search component already in standard search handler')
87
- else
88
- say('Adding spatial search component into standard search handler')
89
- add_element(last_components_node, 'str').content = 'spatial'
90
- end
91
- end
92
-
93
63
  def maybe_add_more_like_this_handler
94
64
  unless @root.xpath('requestHandler[@name="/mlt"]').first
95
65
  mlt_node = add_element(
data/lib/sunspot/query.rb CHANGED
@@ -1,7 +1,8 @@
1
1
  %w(filter abstract_field_facet connective boost_query date_field_facet dismax
2
- field_facet highlighting local pagination restriction common_query
3
- standard_query more_like_this more_like_this_query query_facet scope sort
4
- sort_composite text_field_boost function_query composite_fulltext).each do |file|
2
+ field_facet highlighting pagination restriction common_query
3
+ standard_query more_like_this more_like_this_query geo query_facet scope
4
+ sort sort_composite text_field_boost function_query
5
+ composite_fulltext).each do |file|
5
6
  require(File.join(File.dirname(__FILE__), 'query', file))
6
7
  end
7
8
  module Sunspot
@@ -6,9 +6,9 @@ module Sunspot
6
6
  @sort = SortComposite.new
7
7
  @components = [@scope, @sort]
8
8
  if types.length == 1
9
- @scope.add_restriction(TypeField.instance, Restriction::EqualTo, types.first)
9
+ @scope.add_positive_restriction(TypeField.instance, Restriction::EqualTo, types.first)
10
10
  else
11
- @scope.add_restriction(TypeField.instance, Restriction::AnyOf, types)
11
+ @scope.add_positive_restriction(TypeField.instance, Restriction::AnyOf, types)
12
12
  end
13
13
  end
14
14
 
@@ -9,15 +9,20 @@ module Sunspot
9
9
  @components << dismax = Dismax.new(keywords)
10
10
  dismax
11
11
  end
12
+
13
+ def add_location(field, lat, lng, options)
14
+ @components << location = Geo.new(field, lat, lng, options)
15
+ location
16
+ end
12
17
 
13
18
  def to_params
14
19
  case @components.length
15
20
  when 0
16
21
  {}
17
22
  when 1
18
- @components.first.to_params
23
+ @components.first.to_params.merge(:fl => '* score')
19
24
  else
20
- to_subqueries
25
+ to_subqueries.merge(:fl => '* score')
21
26
  end
22
27
  end
23
28
 
@@ -15,22 +15,37 @@ module Sunspot
15
15
  #
16
16
  # Add a restriction to the connective.
17
17
  #
18
- def add_restriction(field, restriction_type, value, negated = false)
19
- add_component(restriction_type.new(field, value, negated))
18
+ def add_restriction(negated, field, restriction_type, *value)
19
+ add_component(restriction_type.new(negated, field, *value))
20
20
  end
21
21
 
22
22
  #
23
23
  # Add a shorthand restriction; the restriction type is determined by
24
24
  # the value.
25
25
  #
26
- def add_shorthand_restriction(field, value, negated = false)
26
+ def add_shorthand_restriction(negated, field, value)
27
27
  restriction_type =
28
28
  case value
29
29
  when Array then Restriction::AnyOf
30
30
  when Range then Restriction::Between
31
31
  else Restriction::EqualTo
32
32
  end
33
- add_restriction(field, restriction_type, value, negated)
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)
34
49
  end
35
50
 
36
51
  #
@@ -38,14 +53,14 @@ module Sunspot
38
53
  # documents who do not match the terms of the restriction.
39
54
  #
40
55
  def add_negated_restriction(field, restriction_type, value)
41
- add_restriction(field, restriction_type, value, true)
56
+ add_restriction(true, field, restriction_type, value)
42
57
  end
43
58
 
44
59
  #
45
60
  # Add a negated shorthand restriction (see add_shorthand_restriction)
46
61
  #
47
62
  def add_negated_shorthand_restriction(field, value)
48
- add_shorthand_restriction(field, value, true)
63
+ add_shorthand_restriction(true, field, value)
49
64
  end
50
65
 
51
66
  #
@@ -62,6 +62,7 @@ module Sunspot
62
62
  def to_subquery
63
63
  params = self.to_params
64
64
  params.delete :defType
65
+ params.delete :fl
65
66
  keywords = params.delete(:q)
66
67
  options = params.map { |key, value| "#{key}='#{escape_quotes(value)}'"}.join(' ')
67
68
  "_query_:\"{!dismax #{options}}#{escape_quotes(keywords)}\""
@@ -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
@@ -5,6 +5,7 @@ module Sunspot
5
5
 
6
6
  def initialize(document)
7
7
  @document_scope = Restriction::EqualTo.new(
8
+ false,
8
9
  IdField.instance,
9
10
  Adapters::InstanceAdapter.adapt(document).index_id
10
11
  )
@@ -42,8 +42,9 @@ module Sunspot
42
42
 
43
43
  RESERVED_WORDS = Set['AND', 'OR', 'NOT']
44
44
 
45
- def initialize(field, value, negated = false)
46
- @field, @value, @negated = field, value, negated
45
+ def initialize(negated, field, value)
46
+ raise ArgumentError.new("RFCTR") unless [true, false].include?(negated)
47
+ @negated, @field, @value = negated, field, value
47
48
  end
48
49
 
49
50
  #
@@ -116,7 +117,7 @@ module Sunspot
116
117
  # is used by disjunction denormalization.
117
118
  #
118
119
  def negate
119
- self.class.new(@field, @value, !@negated)
120
+ self.class.new(!@negated, @field, @value)
120
121
  end
121
122
 
122
123
  protected
@@ -218,8 +219,7 @@ module Sunspot
218
219
  end
219
220
 
220
221
  def to_solr_conditional
221
- first, last = [@value.first, @value.last].sort
222
- "[#{solr_value(first)} TO #{solr_value(last)}]"
222
+ "[#{solr_value(@value.first)} TO #{solr_value(@value.last)}]"
223
223
  end
224
224
  end
225
225
 
@@ -11,10 +11,6 @@ module Sunspot
11
11
  def add_fulltext(keywords)
12
12
  @fulltext.add(keywords)
13
13
  end
14
-
15
- def add_location_restriction(coordinates, radius)
16
- @components << @local = Local.new(coordinates, radius)
17
- end
18
14
  end
19
15
  end
20
16
  end