sunspot 2.0.0 → 2.5.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 (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
@@ -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)
@@ -0,0 +1,14 @@
1
+ module Sunspot
2
+ module DSL #:nodoc:
3
+ module Spellcheckable #:nodoc
4
+ # Ask Solr to suggest alternative spellings for the query
5
+ #
6
+ # ==== Options
7
+ #
8
+ # The list of options can be found here: http://wiki.apache.org/solr/SpellCheckComponent
9
+ def spellcheck(options = {})
10
+ @query.add_spellcheck(options)
11
+ end
12
+ end
13
+ end
14
+ end
@@ -9,7 +9,7 @@ module Sunspot
9
9
  # See Sunspot.search for usage examples
10
10
  #
11
11
  class StandardQuery < FieldQuery
12
- include Paginatable, Adjustable
12
+ include Paginatable, Adjustable, Spellcheckable
13
13
 
14
14
  # Specify a phrase that should be searched as fulltext. Only +text+
15
15
  # fields are searched - see DSL::Fields.text
@@ -56,67 +56,95 @@ module Sunspot
56
56
  # a pizza" will not. Default behavior is a query phrase slop of zero.
57
57
  #
58
58
  def fulltext(keywords, options = {}, &block)
59
- if keywords && !(keywords.to_s =~ /^\s*$/)
60
- fulltext_query = @query.add_fulltext(keywords)
61
- if field_names = options.delete(:fields)
62
- Util.Array(field_names).each do |field_name|
63
- @setup.text_fields(field_name).each do |field|
64
- fulltext_query.add_fulltext_field(field, field.default_boost)
65
- end
66
- end
67
- end
68
- if minimum_match = options.delete(:minimum_match)
69
- fulltext_query.minimum_match = minimum_match.to_i
70
- end
71
- if tie = options.delete(:tie)
72
- fulltext_query.tie = tie.to_f
73
- end
74
- if query_phrase_slop = options.delete(:query_phrase_slop)
75
- fulltext_query.query_phrase_slop = query_phrase_slop.to_i
76
- end
59
+ return if not keywords or keywords.to_s =~ /^\s*$/
60
+
61
+ field_names = Util.Array(options.delete(:fields)).compact
62
+
63
+ add_fulltext(keywords, field_names) do |query, fields|
64
+ query.minimum_match = options.delete(:minimum_match).to_i if options.key?(:minimum_match)
65
+ query.tie = options.delete(:tie).to_f if options.key?(:tie)
66
+ query.query_phrase_slop = options.delete(:query_phrase_slop).to_i if options.key?(:query_phrase_slop)
67
+
77
68
  if highlight_field_names = options.delete(:highlight)
78
69
  if highlight_field_names == true
79
- fulltext_query.add_highlight
70
+ query.add_highlight
80
71
  else
81
72
  highlight_fields = []
82
73
  Util.Array(highlight_field_names).each do |field_name|
83
74
  highlight_fields.concat(@setup.text_fields(field_name))
84
75
  end
85
- fulltext_query.add_highlight(highlight_fields)
76
+ query.add_highlight(highlight_fields)
86
77
  end
87
78
  end
88
- if block && fulltext_query
89
- fulltext_dsl = Fulltext.new(fulltext_query, @setup)
90
- Util.instance_eval_or_call(
91
- fulltext_dsl,
92
- &block
93
- )
79
+
80
+ if block && query
81
+ fulltext_dsl = Fulltext.new(query, @setup)
82
+ Util.instance_eval_or_call(fulltext_dsl, &block)
83
+ else
84
+ fulltext_dsl = nil
94
85
  end
95
- if !field_names && (!fulltext_dsl || !fulltext_dsl.fields_added?)
86
+
87
+ if fields.empty? && (!fulltext_dsl || !fulltext_dsl.fields_added?)
96
88
  @setup.all_text_fields.each do |field|
97
- unless fulltext_query.has_fulltext_field?(field)
89
+ unless query.has_fulltext_field?(field)
98
90
  unless fulltext_dsl && fulltext_dsl.exclude_fields.include?(field.name)
99
- fulltext_query.add_fulltext_field(field, field.default_boost)
91
+ query.add_fulltext_field(field, field.default_boost)
100
92
  end
101
93
  end
102
94
  end
103
95
  end
104
96
  end
105
97
  end
98
+
106
99
  alias_method :keywords, :fulltext
107
100
 
108
101
  def with(*args)
109
102
  case args.first
110
- when String, Symbol
111
- if args.length == 1 # NONE
112
- field = @setup.field(args[0].to_sym)
113
- return DSL::RestrictionWithNear.new(field, @scope, @query, false)
114
- end
103
+ when String, Symbol
104
+ if args.length == 1 # NONE
105
+ field = @setup.field(args[0].to_sym)
106
+ return DSL::RestrictionWithNear.new(field, @scope, @query, false)
107
+ end
115
108
  end
116
109
 
117
110
  # else
118
111
  super
119
112
  end
113
+
114
+ def any(&block)
115
+ @query.disjunction do
116
+ Util.instance_eval_or_call(self, &block)
117
+ end
118
+ end
119
+
120
+ def all(&block)
121
+ @query.conjunction do
122
+ Util.instance_eval_or_call(self, &block)
123
+ end
124
+ end
125
+
126
+ private
127
+
128
+ def add_fulltext(keywords, field_names)
129
+ return yield(@query.add_fulltext(keywords), []) unless field_names.any?
130
+
131
+ all_fields = field_names.map { |name| @setup.text_fields(name) }.flatten
132
+ all_fields -= join_fields = all_fields.find_all(&:joined?)
133
+
134
+ if all_fields.any?
135
+ fulltext_query = @query.add_fulltext(keywords)
136
+ all_fields.each { |field| fulltext_query.add_fulltext_field(field, field.default_boost) }
137
+ yield(fulltext_query, all_fields)
138
+ end
139
+
140
+ if join_fields.any?
141
+ join_fields.group_by { |field| [field.target, field.from, field.to] }.each_pair do |(target, from, to), fields|
142
+ join_query = @query.add_join(keywords, target, from, to)
143
+ fields.each { |field| join_query.add_fulltext_field(field, field.default_boost) }
144
+ yield(join_query, fields)
145
+ end
146
+ end
147
+ end
120
148
  end
121
149
  end
122
150
  end
data/lib/sunspot/field.rb CHANGED
@@ -82,17 +82,46 @@ module Sunspot
82
82
  !!@more_like_this
83
83
  end
84
84
 
85
+ #
86
+ # Whether the field was joined from another model.
87
+ #
88
+ # ==== Returns
89
+ #
90
+ # Boolean:: True if this field was joined from another model
91
+ #
92
+ def joined?
93
+ !!@joined
94
+ end
95
+
96
+ #
97
+ # Whether the field is stored or not.
98
+ #
99
+ # ==== Returns
100
+ #
101
+ # Boolean:: True if this field is a stored field
102
+ #
103
+ def stored?
104
+ !!@stored
105
+ end
106
+
85
107
  def hash
86
108
  indexed_name.hash
87
109
  end
88
110
 
89
111
  def eql?(field)
90
- indexed_name == field.indexed_name
112
+ field.is_a?(self.class) && indexed_name == field.indexed_name
91
113
  end
92
114
  alias_method :==, :eql?
93
115
 
94
116
  private
95
117
 
118
+ #
119
+ # Raise if an unknown option passed
120
+ #
121
+ def check_options(options)
122
+ raise ArgumentError, "Unknown field option #{options.keys.first.inspect} provided for field #{name.inspect}" unless options.empty?
123
+ end
124
+
96
125
  #
97
126
  # Determine the indexed name. If the :as option is given use that, otherwise
98
127
  # create the value based on the indexed_name of the type with additional
@@ -107,7 +136,8 @@ module Sunspot
107
136
  if options[:as]
108
137
  options.delete(:as).to_s
109
138
  else
110
- "#{@type.indexed_name(@name).to_s}#{'m' if multiple? }#{'s' if @stored}#{'v' if more_like_this?}"
139
+ name = options[:prefix] ? @name.to_s.sub(/^#{options[:prefix]}_/, '') : @name
140
+ "#{@type.indexed_name(name)}#{'m' if multiple? }#{'s' if @stored}#{'v' if more_like_this?}"
111
141
  end
112
142
  end
113
143
 
@@ -129,7 +159,8 @@ module Sunspot
129
159
  @multiple = true
130
160
  @boost = options.delete(:boost)
131
161
  @default_boost = options.delete(:default_boost)
132
- raise ArgumentError, "Unknown field option #{options.keys.first.inspect} provided for field #{name.inspect}" unless options.empty?
162
+
163
+ check_options(options)
133
164
  end
134
165
 
135
166
  def indexed_name
@@ -154,9 +185,50 @@ module Sunspot
154
185
  elsif reference.respond_to?(:to_sym)
155
186
  reference.to_sym
156
187
  end
157
- raise ArgumentError, "Unknown field option #{options.keys.first.inspect} provided for field #{name.inspect}" unless options.empty?
188
+
189
+ check_options(options)
158
190
  end
191
+ end
192
+
193
+ #
194
+ # JoinField encapsulates attributes from referenced models.
195
+ # Could be of any type
196
+ #
197
+ class JoinField < Field #:nodoc:
198
+ attr_reader :default_boost, :target
199
+
200
+ def initialize(name, type, options = {})
201
+ @multiple = !!options.delete(:multiple)
202
+
203
+ super(name, type, options)
159
204
 
205
+ @prefix = options.delete(:prefix)
206
+ @join = options.delete(:join)
207
+ @clazz = options.delete(:clazz)
208
+ @target = options.delete(:target)
209
+ @default_boost = options.delete(:default_boost)
210
+ @joined = true
211
+
212
+ check_options(options)
213
+ end
214
+
215
+ def from
216
+ Sunspot::Setup.for(@target).field(@join[:from]).indexed_name
217
+ end
218
+
219
+ def to
220
+ Sunspot::Setup.for(@clazz).field(@join[:to]).indexed_name
221
+ end
222
+
223
+ def local_params
224
+ "{!join from=#{from} to=#{to}}"
225
+ end
226
+
227
+ def eql?(field)
228
+ super && target == field.target && from == field.from && to == field.to
229
+ end
230
+
231
+ alias_method :==, :eql?
160
232
  end
161
233
 
162
234
  class TypeField #:nodoc:
@@ -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,20 @@ 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?
59
- Util.Array(@field.to_indexed(value)).each do |scalar_value|
60
- options = {}
61
- options[:boost] = @field.boost if @field.boost
74
+ def populate_document(document, model, options = {}) #:nodoc:
75
+ atomic_operation = options[:update] == :set
76
+ value = extract_value(model, options)
77
+ if value != nil || atomic_operation
78
+ indexed_values = Util.Array(@field.to_indexed(value))
79
+ indexed_values = [nil] if indexed_values.empty? && atomic_operation
80
+ indexed_values.each do |scalar_value|
81
+ field_options = {}
82
+ field_options[:boost] = @field.boost if @field.boost
83
+ field_options[:null] = true if scalar_value.nil? && atomic_operation
62
84
  document.add_field(
63
85
  @field.indexed_name.to_sym,
64
86
  scalar_value,
65
- options
87
+ field_options.merge(options)
66
88
  )
67
89
  end
68
90
  end
@@ -76,23 +98,48 @@ module Sunspot
76
98
  end
77
99
  end
78
100
 
101
+ class Join < Abstract
102
+ def initialize(name, type, options = {}, &block)
103
+ super(options[:prefix] ? "#{options[:prefix]}_#{name}" : name, options, &block)
104
+ unless name.to_s =~ /^\w+$/
105
+ raise ArgumentError, "Invalid field name #{name}: only letters, numbers, and underscores are allowed."
106
+ end
107
+ @field = JoinField.new(self.name, type, options)
108
+ end
109
+
110
+ #
111
+ # Return the field instance built by this factory
112
+ #
113
+ def build
114
+ @field
115
+ end
116
+
117
+ #
118
+ # A unique signature identifying this field by name and type.
119
+ #
120
+ def signature
121
+ ['join', @field.name, @field.type]
122
+ end
123
+ end
124
+
79
125
  #
80
126
  # DynamicFieldFactories create dynamic field instances based on dynamic
81
127
  # configuration.
82
128
  #
83
129
  class Dynamic < Abstract
84
- attr_accessor :name, :type
130
+ attr_accessor :name, :type, :separator
85
131
 
86
132
  def initialize(name, type, options = {}, &block)
87
133
  super(name, options, &block)
88
134
  @type, @options = type, options
135
+ @separator = @options.delete(:separator) || ':'
89
136
  end
90
137
 
91
138
  #
92
139
  # Build a field based on the dynamic name given.
93
140
  #
94
141
  def build(dynamic_name)
95
- AttributeField.new("#{@name}:#{dynamic_name}", @type, @options.dup)
142
+ AttributeField.new([@name, dynamic_name].join(separator), @type, @options.dup)
96
143
  end
97
144
  #
98
145
  # This alias allows a DynamicFieldFactory to be used in place of a Setup
@@ -104,14 +151,16 @@ module Sunspot
104
151
  # Generate dynamic fields based on hash returned by data accessor and
105
152
  # add the field data to the document.
106
153
  #
107
- def populate_document(document, model)
108
- if values = @data_extractor.value_for(model)
154
+ def populate_document(document, model, options = {})
155
+ values = extract_value(model, options)
156
+ if values
109
157
  values.each_pair do |dynamic_name, value|
110
158
  field_instance = build(dynamic_name)
111
159
  Util.Array(field_instance.to_indexed(value)).each do |scalar_value|
112
160
  document.add_field(
113
161
  field_instance.indexed_name.to_sym,
114
- scalar_value
162
+ scalar_value,
163
+ options
115
164
  )
116
165
  end
117
166
  end
@@ -8,7 +8,6 @@ module Sunspot
8
8
  # subclasses).
9
9
  #
10
10
  class Indexer #:nodoc:
11
- include RSolr::Char
12
11
 
13
12
  def initialize(connection)
14
13
  @connection = connection
@@ -23,12 +22,23 @@ module Sunspot
23
22
  # model<Object>:: the model to index
24
23
  #
25
24
  def add(model)
26
- documents = Util.Array(model).map { |m| prepare(m) }
27
- if batcher.batching?
28
- batcher.concat(documents)
29
- else
30
- add_documents(documents)
31
- 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)
32
42
  end
33
43
 
34
44
  #
@@ -44,17 +54,18 @@ module Sunspot
44
54
  # Remove the model from the Solr index by specifying the class and ID
45
55
  #
46
56
  def remove_by_id(class_name, *ids)
57
+ ids.flatten!
47
58
  @connection.delete_by_id(
48
59
  ids.map { |id| Adapters::InstanceAdapter.index_id_for(class_name, id) }
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)
56
67
  if clazz
57
- @connection.delete_by_query("type:#{escape(clazz.name)}")
68
+ @connection.delete_by_query("type:#{Util.escape(clazz.name)}")
58
69
  else
59
70
  @connection.delete_by_query("*:*")
60
71
  end
@@ -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_full_update(model)
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,22 +113,48 @@ module Sunspot
102
113
  document
103
114
  end
104
115
 
116
+ def prepare_atomic_update(clazz, id, updates = {})
117
+ document = document_for_atomic_update(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
- # This method constructs the document hash containing those key-value
140
+ # These methods construct the document hash containing those key-value
112
141
  # pairs.
113
142
  #
114
- def document_for(model)
143
+ def document_for_full_update(model)
115
144
  RSolr::Xml::Document.new(
116
- :id => Adapters::InstanceAdapter.adapt(model).index_id,
117
- :type => Util.superclasses_for(model.class).map { |clazz| clazz.name }
145
+ id: Adapters::InstanceAdapter.adapt(model).index_id,
146
+ type: Util.superclasses_for(model.class).map(&:name)
118
147
  )
119
148
  end
120
149
 
150
+ def document_for_atomic_update(clazz, id)
151
+ if Adapters::InstanceAdapter.for(clazz)
152
+ RSolr::Xml::Document.new(
153
+ id: Adapters::InstanceAdapter.index_id_for(clazz.name, id),
154
+ type: Util.superclasses_for(clazz).map(&:name)
155
+ )
156
+ end
157
+ end
121
158
  #
122
159
  # Get the Setup object for the given object's class.
123
160
  #
@@ -129,8 +166,23 @@ module Sunspot
129
166
  #
130
167
  # Sunspot::Setup:: The setup for the object's class
131
168
  #
132
- def setup_for(object)
133
- Setup.for(object.class) || raise(NoSetupError, "Sunspot is not configured for #{object.class.inspect}")
169
+ def setup_for_object(object)
170
+ setup_for_class(object.class)
171
+ end
172
+
173
+ #
174
+ # Get the Setup object for the given class.
175
+ #
176
+ # ==== Parameters
177
+ #
178
+ # clazz<Class>:: The class whose setup is to be retrieved
179
+ #
180
+ # ==== Returns
181
+ #
182
+ # Sunspot::Setup:: The setup for the class
183
+ #
184
+ def setup_for_class(clazz)
185
+ Setup.for(clazz) || raise(NoSetupError, "Sunspot is not configured for #{clazz.inspect}")
134
186
  end
135
187
  end
136
188
  end