sunspot 2.2.0 → 2.2.1

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 (39) hide show
  1. checksums.yaml +7 -0
  2. data/lib/sunspot/dsl/field_query.rb +11 -9
  3. data/lib/sunspot/dsl/fulltext.rb +9 -1
  4. data/lib/sunspot/dsl/group.rb +108 -0
  5. data/lib/sunspot/dsl/search.rb +1 -1
  6. data/lib/sunspot/dsl.rb +1 -1
  7. data/lib/sunspot/field_factory.rb +28 -15
  8. data/lib/sunspot/indexer.rb +63 -17
  9. data/lib/sunspot/query/abstract_fulltext.rb +9 -2
  10. data/lib/sunspot/query/dismax.rb +11 -4
  11. data/lib/sunspot/query/{field_group.rb → group.rb} +16 -6
  12. data/lib/sunspot/query/group_query.rb +17 -0
  13. data/lib/sunspot/query/restriction.rb +45 -1
  14. data/lib/sunspot/query.rb +1 -1
  15. data/lib/sunspot/schema.rb +2 -1
  16. data/lib/sunspot/search/abstract_search.rb +7 -3
  17. data/lib/sunspot/search/query_group.rb +74 -0
  18. data/lib/sunspot/search/standard_search.rb +1 -1
  19. data/lib/sunspot/search.rb +1 -1
  20. data/lib/sunspot/session.rb +16 -0
  21. data/lib/sunspot/session_proxy/master_slave_session_proxy.rb +2 -2
  22. data/lib/sunspot/session_proxy/retry_5xx_session_proxy.rb +1 -1
  23. data/lib/sunspot/session_proxy/sharding_session_proxy.rb +4 -2
  24. data/lib/sunspot/session_proxy/silent_fail_session_proxy.rb +1 -1
  25. data/lib/sunspot/session_proxy/thread_local_session_proxy.rb +3 -1
  26. data/lib/sunspot/type.rb +20 -0
  27. data/lib/sunspot/version.rb +1 -1
  28. data/lib/sunspot.rb +36 -2
  29. data/spec/api/indexer/attributes_spec.rb +5 -0
  30. data/spec/api/query/function_spec.rb +103 -0
  31. data/spec/api/query/group_spec.rb +23 -1
  32. data/spec/api/session_proxy/sharding_session_proxy_spec.rb +1 -1
  33. data/spec/helpers/integration_helper.rb +9 -0
  34. data/spec/integration/atomic_updates_spec.rb +44 -0
  35. data/spec/integration/field_grouping_spec.rb +13 -0
  36. data/spec/integration/scoped_search_spec.rb +51 -0
  37. data/spec/mocks/post.rb +3 -1
  38. metadata +11 -15
  39. data/lib/sunspot/dsl/field_group.rb +0 -57
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 4a3b915b7cc088b4c26ba0eb4ae3e541666da526
4
+ data.tar.gz: a3021876e0d940137daf4527c0d3ac354f93119c
5
+ SHA512:
6
+ metadata.gz: 62193e81b7834636ad0b2a03110e0054778700fe93a7cd6d6f5a52366708703bf3ce3a0ef24cf83d78a3d3532e79c274d893019aab1e26c289932e5b501bf005
7
+ data.tar.gz: 3a69aa3883a08bdab4c31ece01218cc3ac2a5bde250704448b9ca5e8dda070a1c7aaf401f7658621e2616f15596a84acc34319173dab91cb71a25b56a199db09
@@ -68,20 +68,22 @@ module Sunspot
68
68
  #
69
69
  # ==== Parameters
70
70
  #
71
- # field_name<Symbol>:: the field to use for grouping
71
+ # field_names...<Symbol>:: the fields to use for grouping
72
72
  def group(*field_names, &block)
73
+ group = Sunspot::Query::Group.new()
74
+
73
75
  field_names.each do |field_name|
74
76
  field = @setup.field(field_name)
75
- group = @query.add_group(Sunspot::Query::FieldGroup.new(field))
76
- @search.add_field_group(field)
77
+ group.add_field(field)
78
+ end
77
79
 
78
- if block
79
- Sunspot::Util.instance_eval_or_call(
80
- FieldGroup.new(@setup, group),
81
- &block
82
- )
83
- end
80
+ if block
81
+ dsl = Group.new(@setup, group)
82
+ Sunspot::Util.instance_eval_or_call(dsl, &block)
84
83
  end
84
+
85
+ @query.add_group(group)
86
+ @search.add_group(group)
85
87
  end
86
88
 
87
89
  #
@@ -168,8 +168,12 @@ module Sunspot
168
168
  # will be boosted by (average_rating + popularity * 10).
169
169
  #
170
170
  def boost(factor_or_function, &block)
171
+ additive_boost(factor_or_function, &block)
172
+ end
173
+
174
+ def additive_boost(factor_or_function, &block)
171
175
  if factor_or_function.is_a?(Sunspot::Query::FunctionQuery)
172
- @query.add_boost_function(factor_or_function)
176
+ @query.add_additive_boost_function(factor_or_function)
173
177
  else
174
178
  Sunspot::Util.instance_eval_or_call(
175
179
  Scope.new(@query.create_boost_query(factor_or_function), @setup),
@@ -178,6 +182,10 @@ module Sunspot
178
182
  end
179
183
  end
180
184
 
185
+ def multiplicative_boost(factor_or_function)
186
+ @query.add_multiplicative_boost_function(factor_or_function)
187
+ end
188
+
181
189
  #
182
190
  # Add boost to certain fields, without restricting which fields are
183
191
  # searched.
@@ -0,0 +1,108 @@
1
+ module Sunspot
2
+ module DSL
3
+ class Group
4
+ def initialize(setup, group)
5
+ @setup, @group = setup, group
6
+ end
7
+
8
+ # Specify one or more fields for result grouping.
9
+ #
10
+ # ==== Parameters
11
+ #
12
+ # field_names...<Symbol>:: the fields to use for grouping
13
+ #
14
+ def field(*field_names, &block)
15
+ field_names.each do |field_name|
16
+ field = @setup.field(field_name)
17
+ @group.add_field(field)
18
+ end
19
+ end
20
+
21
+ # Specify a query to group results by.
22
+ #
23
+ # ==== Parameters
24
+ #
25
+ # label<Object>:: a label for this group; when #value is called on this
26
+ # group's results, this label will be returned.
27
+ #
28
+ def query(label, &block)
29
+ group_query = Sunspot::Query::GroupQuery.new(label)
30
+ Sunspot::Util.instance_eval_or_call(Scope.new(group_query, @setup), &block)
31
+ @group.add_query(group_query)
32
+ end
33
+
34
+ #
35
+ # Sets the number of results (documents) to return for each group.
36
+ # Defaults to 1.
37
+ #
38
+ def limit(num)
39
+ @group.limit = num
40
+ end
41
+
42
+ #
43
+ # If set, facet counts are based on the most relevant document of
44
+ # each group matching the query.
45
+ #
46
+ # Supported in Solr 3.4 and above.
47
+ #
48
+ # ==== Example
49
+ #
50
+ # Sunspot.search(Post) do
51
+ # group :title do
52
+ # truncate
53
+ # end
54
+ #
55
+ # facet :title, :extra => :any
56
+ # end
57
+ #
58
+ def truncate
59
+ @group.truncate = true
60
+ end
61
+
62
+ # Specify the order that results should be returned in. This method can
63
+ # be called multiple times; precedence will be in the order given.
64
+ #
65
+ # ==== Parameters
66
+ #
67
+ # field_name<Symbol>:: the field to use for ordering
68
+ # direction<Symbol>:: :asc or :desc (default :asc)
69
+ #
70
+ def order_by(field_name, direction = nil)
71
+ sort =
72
+ if special = Sunspot::Query::Sort.special(field_name)
73
+ special.new(direction)
74
+ else
75
+ Sunspot::Query::Sort::FieldSort.new(
76
+ @setup.field(field_name), direction
77
+ )
78
+ end
79
+ @group.add_sort(sort)
80
+ end
81
+
82
+ #
83
+ # Specify that results should be ordered based on a
84
+ # FunctionQuery - http://wiki.apache.org/solr/FunctionQuery
85
+ # Solr 3.1 and up
86
+ #
87
+ # For example, to order by field1 + (field2*field3):
88
+ #
89
+ # order_by_function :sum, :field1, [:product, :field2, :field3], :desc
90
+ #
91
+ # ==== Parameters
92
+ # function_name<Symbol>::
93
+ # the function to run
94
+ # arguments::
95
+ # the arguments for this function.
96
+ # - Symbol for a field or function name
97
+ # - Array for a nested function
98
+ # - String for a literal constant
99
+ # direction<Symbol>::
100
+ # :asc or :desc
101
+ def order_by_function(*args)
102
+ @group.add_sort(
103
+ Sunspot::Query::Sort::FunctionSort.new(@setup,args)
104
+ )
105
+ end
106
+ end
107
+ end
108
+ end
@@ -19,7 +19,7 @@ module Sunspot
19
19
  # ==== Example
20
20
  #
21
21
  # Sunspot.search Post do
22
- # data_acccessor_for(Post).includes = [:blog, :comments]
22
+ # data_accessor_for(Post).include = [:blog, :comments]
23
23
  # end
24
24
  #
25
25
  def data_accessor_for(clazz)
data/lib/sunspot/dsl.rb CHANGED
@@ -1,6 +1,6 @@
1
1
  %w(spellcheckable fields scope paginatable adjustable field_query
2
2
  standard_query query_facet functional fulltext restriction
3
3
  restriction_with_near search more_like_this_query function
4
- field_group field_stats).each do |file|
4
+ group field_stats).each do |file|
5
5
  require File.join(File.dirname(__FILE__), 'dsl', file)
6
6
  end
@@ -22,6 +22,23 @@ module Sunspot
22
22
  DataExtractor::AttributeExtractor.new(options.delete(:using) || name)
23
23
  end
24
24
  end
25
+
26
+ #
27
+ # Extract the encapsulated field's data from the given model and add it
28
+ # into the Solr document for indexing. (noop here for joins)
29
+ #
30
+ def populate_document(document, model, options = {}) #:nodoc:
31
+ end
32
+
33
+ protected
34
+
35
+ def extract_value(model, options = {})
36
+ if options.has_key?(:value)
37
+ options.delete(:value)
38
+ else
39
+ @data_extractor.value_for(model)
40
+ end
41
+ end
25
42
  end
26
43
 
27
44
  #
@@ -54,15 +71,16 @@ module Sunspot
54
71
  # Extract the encapsulated field's data from the given model and add it
55
72
  # into the Solr document for indexing.
56
73
  #
57
- def populate_document(document, model) #:nodoc:
58
- unless (value = @data_extractor.value_for(model)).nil?
74
+ def populate_document(document, model, options = {}) #:nodoc:
75
+ value = extract_value(model, options)
76
+ unless value.nil?
59
77
  Util.Array(@field.to_indexed(value)).each do |scalar_value|
60
- options = {}
61
- options[:boost] = @field.boost if @field.boost
78
+ field_options = {}
79
+ field_options[:boost] = @field.boost if @field.boost
62
80
  document.add_field(
63
81
  @field.indexed_name.to_sym,
64
82
  scalar_value,
65
- options
83
+ field_options.merge(options)
66
84
  )
67
85
  end
68
86
  end
@@ -92,14 +110,7 @@ module Sunspot
92
110
  @field
93
111
  end
94
112
 
95
- #
96
- # Extract the encapsulated field's data from the given model and add it
97
- # into the Solr document for indexing. (noop here for joins)
98
113
  #
99
- def populate_document(document, model) #:nodoc:
100
- end
101
-
102
- #
103
114
  # A unique signature identifying this field by name and type.
104
115
  #
105
116
  def signature
@@ -135,14 +146,16 @@ module Sunspot
135
146
  # Generate dynamic fields based on hash returned by data accessor and
136
147
  # add the field data to the document.
137
148
  #
138
- def populate_document(document, model)
139
- if values = @data_extractor.value_for(model)
149
+ def populate_document(document, model, options = {})
150
+ values = extract_value(model, options)
151
+ if values
140
152
  values.each_pair do |dynamic_name, value|
141
153
  field_instance = build(dynamic_name)
142
154
  Util.Array(field_instance.to_indexed(value)).each do |scalar_value|
143
155
  document.add_field(
144
156
  field_instance.indexed_name.to_sym,
145
- scalar_value
157
+ scalar_value,
158
+ options
146
159
  )
147
160
  end
148
161
  end
@@ -22,12 +22,23 @@ module Sunspot
22
22
  # model<Object>:: the model to index
23
23
  #
24
24
  def add(model)
25
- documents = Util.Array(model).map { |m| prepare(m) }
26
- if batcher.batching?
27
- batcher.concat(documents)
28
- else
29
- add_documents(documents)
30
- end
25
+ documents = Util.Array(model).map { |m| prepare_full_update(m) }
26
+ add_batch_documents(documents)
27
+ end
28
+
29
+ #
30
+ # Construct a representation of the given class instances for atomic properties update
31
+ # and send it to the connection for indexing
32
+ #
33
+ # ==== Parameters
34
+ #
35
+ # clazz<Class>:: the class of the models to be updated
36
+ # updates<Hash>:: hash of updates where keys are model ids
37
+ # and values are hash with property name/values to be updated
38
+ #
39
+ def add_atomic_update(clazz, updates={})
40
+ documents = updates.map { |id, m| prepare_atomic_update(clazz, id, m) }
41
+ add_batch_documents(documents)
31
42
  end
32
43
 
33
44
  #
@@ -49,7 +60,7 @@ module Sunspot
49
60
  )
50
61
  end
51
62
 
52
- #
63
+ #
53
64
  # Delete all documents of the class indexed by this indexer from Solr.
54
65
  #
55
66
  def remove_all(clazz = nil)
@@ -90,9 +101,9 @@ module Sunspot
90
101
  #
91
102
  # Convert documents into hash of indexed properties
92
103
  #
93
- def prepare(model)
94
- document = document_for(model)
95
- setup = setup_for(model)
104
+ def prepare_full_update(model)
105
+ document = document_for(model.class, model.id)
106
+ setup = setup_for_object(model)
96
107
  if boost = setup.document_boost_for(model)
97
108
  document.attrs[:boost] = boost
98
109
  end
@@ -102,20 +113,40 @@ module Sunspot
102
113
  document
103
114
  end
104
115
 
116
+ def prepare_atomic_update(clazz, id, updates = {})
117
+ document = document_for(clazz, id)
118
+ setup_for_class(clazz).all_field_factories.each do |field_factory|
119
+ if updates.has_key?(field_factory.name)
120
+ field_factory.populate_document(document, nil, value: updates[field_factory.name], update: :set)
121
+ end
122
+ end
123
+ document
124
+ end
125
+
105
126
  def add_documents(documents)
106
127
  @connection.add(documents)
107
128
  end
108
129
 
130
+ def add_batch_documents(documents)
131
+ if batcher.batching?
132
+ batcher.concat(documents)
133
+ else
134
+ add_documents(documents)
135
+ end
136
+ end
137
+
109
138
  #
110
139
  # All indexed documents index and store the +id+ and +type+ fields.
111
140
  # This method constructs the document hash containing those key-value
112
141
  # pairs.
113
142
  #
114
- def document_for(model)
115
- RSolr::Xml::Document.new(
116
- :id => Adapters::InstanceAdapter.adapt(model).index_id,
117
- :type => Util.superclasses_for(model.class).map { |clazz| clazz.name }
118
- )
143
+ def document_for(clazz, id)
144
+ if Adapters::InstanceAdapter.for(clazz)
145
+ RSolr::Xml::Document.new(
146
+ id: Adapters::InstanceAdapter.index_id_for(clazz.name, id),
147
+ type: Util.superclasses_for(clazz).map(&:name)
148
+ )
149
+ end
119
150
  end
120
151
 
121
152
  #
@@ -129,8 +160,23 @@ module Sunspot
129
160
  #
130
161
  # Sunspot::Setup:: The setup for the object's class
131
162
  #
132
- def setup_for(object)
133
- Setup.for(object.class) || raise(NoSetupError, "Sunspot is not configured for #{object.class.inspect}")
163
+ def setup_for_object(object)
164
+ setup_for_class(object.class)
165
+ end
166
+
167
+ #
168
+ # Get the Setup object for the given class.
169
+ #
170
+ # ==== Parameters
171
+ #
172
+ # clazz<Class>:: The class whose setup is to be retrieved
173
+ #
174
+ # ==== Returns
175
+ #
176
+ # Sunspot::Setup:: The setup for the class
177
+ #
178
+ def setup_for_class(clazz)
179
+ Setup.for(clazz) || raise(NoSetupError, "Sunspot is not configured for #{clazz.inspect}")
134
180
  end
135
181
  end
136
182
  end
@@ -18,8 +18,15 @@ module Sunspot
18
18
  #
19
19
  # Add a boost function
20
20
  #
21
- def add_boost_function(function_query)
22
- @boost_functions << function_query
21
+ def add_additive_boost_function(function_query)
22
+ @additive_boost_functions << function_query
23
+ end
24
+
25
+ #
26
+ # Add a multiplicative boost function
27
+ #
28
+ def add_multiplicative_boost_function(function_query)
29
+ @multiplicative_boost_functions << function_query
23
30
  end
24
31
 
25
32
  #
@@ -13,7 +13,8 @@ module Sunspot
13
13
  @keywords = keywords
14
14
  @fulltext_fields = {}
15
15
  @boost_queries = []
16
- @boost_functions = []
16
+ @additive_boost_functions = []
17
+ @multiplicative_boost_functions = []
17
18
  @highlights = []
18
19
 
19
20
  @minimum_match = nil
@@ -46,9 +47,15 @@ module Sunspot
46
47
  end
47
48
  end
48
49
 
49
- unless @boost_functions.empty?
50
- params[:bf] = @boost_functions.map do |boost_function|
51
- boost_function.to_s
50
+ unless @additive_boost_functions.empty?
51
+ params[:bf] = @additive_boost_functions.map do |additive_boost_function|
52
+ additive_boost_function.to_s
53
+ end
54
+ end
55
+
56
+ unless @multiplicative_boost_functions.empty?
57
+ params[:boost] = @multiplicative_boost_functions.map do |multiplicative_boost_function|
58
+ multiplicative_boost_function.to_s
52
59
  end
53
60
  end
54
61
 
@@ -1,18 +1,27 @@
1
1
  module Sunspot
2
2
  module Query
3
3
  #
4
- # A FieldGroup groups by the unique values of a given field.
4
+ # A Group groups by the unique values of a given field, or by given queries.
5
5
  #
6
- class FieldGroup
6
+ class Group
7
7
  attr_accessor :limit, :truncate
8
+ attr_reader :fields, :queries
8
9
 
9
- def initialize(field)
10
+ def initialize
11
+ @sort = SortComposite.new
12
+ @fields = []
13
+ @queries = []
14
+ end
15
+
16
+ def add_field(field)
10
17
  if field.multiple?
11
18
  raise(ArgumentError, "#{field.name} cannot be used for grouping because it is a multiple-value field")
12
19
  end
13
- @field = field
20
+ @fields << field
21
+ end
14
22
 
15
- @sort = SortComposite.new
23
+ def add_query(query)
24
+ @queries << query
16
25
  end
17
26
 
18
27
  def add_sort(sort)
@@ -23,10 +32,11 @@ module Sunspot
23
32
  params = {
24
33
  :group => "true",
25
34
  :"group.ngroups" => "true",
26
- :"group.field" => @field.indexed_name
27
35
  }
28
36
 
29
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?
30
40
  params[:"group.limit"] = @limit if @limit
31
41
  params[:"group.truncate"] = @truncate if @truncate
32
42
 
@@ -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
@@ -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
  #
@@ -330,6 +341,39 @@ module Sunspot
330
341
  "#{solr_value(@value)}*"
331
342
  end
332
343
  end
344
+
345
+ class AbstractRange < Between
346
+ private
347
+
348
+ def operation
349
+ @operation || self.class.name.split('::').last
350
+ end
351
+
352
+ def solr_value(value = @value)
353
+ @field.to_indexed(value)
354
+ end
355
+
356
+ def to_positive_boolean_phrase
357
+ "_query_:\"{!field f=#{@field.indexed_name} op=#{operation}}#{solr_value}\""
358
+ end
359
+ end
360
+
361
+ class Containing < AbstractRange
362
+ def initialize(negated, field, value)
363
+ @operation = 'Contains'
364
+ super
365
+ end
366
+ end
367
+
368
+ class Intersecting < AbstractRange
369
+ def initialize(negated, field, value)
370
+ @operation = 'Intersects'
371
+ super
372
+ end
373
+ end
374
+
375
+ class Within < AbstractRange
376
+ end
333
377
  end
334
378
  end
335
379
  end
data/lib/sunspot/query.rb CHANGED
@@ -2,7 +2,7 @@
2
2
  field_facet highlighting pagination restriction common_query spellcheck
3
3
  standard_query more_like_this more_like_this_query geo geofilt bbox query_facet
4
4
  scope sort sort_composite text_field_boost function_query field_stats
5
- composite_fulltext field_group).each do |file|
5
+ composite_fulltext group group_query).each do |file|
6
6
  require(File.join(File.dirname(__FILE__), 'query', file))
7
7
  end
8
8
  module Sunspot
@@ -23,7 +23,8 @@ module Sunspot
23
23
  FieldType.new('slong', 'SortableLong', 'l'),
24
24
  FieldType.new('tint', 'TrieInteger', 'it'),
25
25
  FieldType.new('tfloat', 'TrieFloat', 'ft'),
26
- FieldType.new('tdate', 'TrieInt', 'dt')
26
+ FieldType.new('tdate', 'TrieInt', 'dt'),
27
+ FieldType.new('daterange', 'DateRange', 'dr')
27
28
 
28
29
  ]
29
30
 
@@ -222,8 +222,12 @@ module Sunspot
222
222
  "<Sunspot::Search:#{query.to_params.inspect}>"
223
223
  end
224
224
 
225
- def add_field_group(field) #:nodoc:
226
- add_group(field.name, FieldGroup.new(field, self))
225
+ def add_group(group) #:nodoc:
226
+ group.fields.each do |field|
227
+ add_subgroup(field.name, FieldGroup.new(field, self))
228
+ end
229
+
230
+ add_subgroup(:queries, QueryGroup.new(group.queries, self)) if group.queries.any?
227
231
  end
228
232
 
229
233
  def add_field_facet(field, options = {}) #:nodoc:
@@ -303,7 +307,7 @@ module Sunspot
303
307
  @stats_by_name[name.to_sym] = stats
304
308
  end
305
309
 
306
- def add_group(name, group)
310
+ def add_subgroup(name, group)
307
311
  @groups << group
308
312
  @groups_by_name[name.to_sym] = group
309
313
  end