sunspot 1.1.0 → 1.2.0

Sign up to get free protection for your applications and to get access to all the features.
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