sunspot 2.2.0 → 2.2.1

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