sunspot 2.0.0 → 2.5.0

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