sunspot 1.1.0 → 1.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (66) hide show
  1. data/Gemfile +10 -0
  2. data/Gemfile.lock +32 -0
  3. data/History.txt +24 -0
  4. data/README.rdoc +18 -5
  5. data/lib/sunspot.rb +40 -0
  6. data/lib/sunspot/dsl.rb +2 -2
  7. data/lib/sunspot/dsl/field_query.rb +2 -2
  8. data/lib/sunspot/dsl/fields.rb +0 -10
  9. data/lib/sunspot/dsl/restriction.rb +4 -4
  10. data/lib/sunspot/dsl/restriction_with_near.rb +121 -0
  11. data/lib/sunspot/dsl/scope.rb +55 -67
  12. data/lib/sunspot/dsl/standard_query.rb +11 -15
  13. data/lib/sunspot/field.rb +30 -29
  14. data/lib/sunspot/field_factory.rb +0 -18
  15. data/lib/sunspot/installer/solrconfig_updater.rb +0 -30
  16. data/lib/sunspot/query.rb +4 -3
  17. data/lib/sunspot/query/common_query.rb +2 -2
  18. data/lib/sunspot/query/composite_fulltext.rb +7 -2
  19. data/lib/sunspot/query/connective.rb +21 -6
  20. data/lib/sunspot/query/dismax.rb +1 -0
  21. data/lib/sunspot/query/geo.rb +53 -0
  22. data/lib/sunspot/query/more_like_this.rb +1 -0
  23. data/lib/sunspot/query/restriction.rb +5 -5
  24. data/lib/sunspot/query/standard_query.rb +0 -4
  25. data/lib/sunspot/search/abstract_search.rb +1 -7
  26. data/lib/sunspot/search/hit.rb +10 -10
  27. data/lib/sunspot/search/query_facet.rb +8 -3
  28. data/lib/sunspot/session.rb +10 -2
  29. data/lib/sunspot/session_proxy.rb +16 -0
  30. data/lib/sunspot/session_proxy/master_slave_session_proxy.rb +1 -1
  31. data/lib/sunspot/session_proxy/sharding_session_proxy.rb +7 -0
  32. data/lib/sunspot/session_proxy/silent_fail_session_proxy.rb +42 -0
  33. data/lib/sunspot/session_proxy/thread_local_session_proxy.rb +1 -1
  34. data/lib/sunspot/setup.rb +1 -17
  35. data/lib/sunspot/type.rb +38 -6
  36. data/lib/sunspot/util.rb +21 -31
  37. data/lib/sunspot/version.rb +1 -1
  38. data/solr/solr/conf/solrconfig.xml +0 -4
  39. data/spec/api/binding_spec.rb +12 -0
  40. data/spec/api/indexer/attributes_spec.rb +22 -22
  41. data/spec/api/query/connectives_examples.rb +14 -1
  42. data/spec/api/query/fulltext_examples.rb +3 -3
  43. data/spec/api/query/geo_examples.rb +69 -0
  44. data/spec/api/query/scope_examples.rb +32 -13
  45. data/spec/api/query/standard_spec.rb +1 -1
  46. data/spec/api/search/faceting_spec.rb +5 -1
  47. data/spec/api/search/hits_spec.rb +14 -12
  48. data/spec/api/session_proxy/class_sharding_session_proxy_spec.rb +1 -1
  49. data/spec/api/session_proxy/sharding_session_proxy_spec.rb +1 -1
  50. data/spec/api/session_proxy/silent_fail_session_proxy_spec.rb +24 -0
  51. data/spec/api/session_spec.rb +22 -0
  52. data/spec/integration/local_search_spec.rb +42 -69
  53. data/spec/integration/scoped_search_spec.rb +30 -0
  54. data/spec/mocks/connection.rb +6 -2
  55. data/spec/mocks/photo.rb +0 -1
  56. data/spec/mocks/post.rb +11 -2
  57. data/spec/mocks/user.rb +6 -1
  58. data/spec/spec_helper.rb +2 -12
  59. metadata +209 -177
  60. data/lib/sunspot/query/local.rb +0 -26
  61. data/solr/solr/lib/lucene-spatial-2.9.1.jar +0 -0
  62. data/solr/solr/lib/solr-spatial-light-0.0.6.jar +0 -0
  63. data/spec/api/query/local_examples.rb +0 -38
  64. data/tasks/gemspec.rake +0 -33
  65. data/tasks/rcov.rake +0 -28
  66. data/tasks/spec.rake +0 -24
@@ -81,7 +81,7 @@ module Sunspot
81
81
  begin
82
82
  hits = if solr_response && solr_response['docs']
83
83
  solr_response['docs'].map do |doc|
84
- Hit.new(doc, highlights_for(doc), distance_for(doc), self)
84
+ Hit.new(doc, highlights_for(doc), self)
85
85
  end
86
86
  end
87
87
  maybe_will_paginate(hits || [])
@@ -268,12 +268,6 @@ module Sunspot
268
268
  end
269
269
  end
270
270
 
271
- def distance_for(doc)
272
- if @solr_result['distances']
273
- @solr_result['distances'][doc['id']]
274
- end
275
- end
276
-
277
271
  def verified_hits
278
272
  @verified_hits ||= maybe_will_paginate(hits.select { |hit| hit.instance })
279
273
  end
@@ -3,8 +3,8 @@ module Sunspot
3
3
  #
4
4
  # Hit objects represent the raw information returned by Solr for a single
5
5
  # document. As well as the primary key and class name, hit objects give
6
- # access to stored field values, keyword relevance score, and geographical
7
- # distance (for geographical search).
6
+ # access to stored field values, keyword relevance score, and keyword
7
+ # highlighting.
8
8
  #
9
9
  class Hit
10
10
  SPECIAL_KEYS = Set.new(%w(id type score)) #:nodoc:
@@ -23,17 +23,12 @@ module Sunspot
23
23
  #
24
24
  attr_reader :score
25
25
  #
26
- # For geographical searches, this is the distance between the search
27
- # centerpoint and the document's location. Otherwise, it's nil.
28
- #
29
- attr_reader :distance
30
26
 
31
27
  attr_writer :result #:nodoc:
32
28
 
33
- def initialize(raw_hit, highlights, distance, search) #:nodoc:
29
+ def initialize(raw_hit, highlights, search) #:nodoc:
34
30
  @class_name, @primary_key = *raw_hit['id'].match(/([^ ]+) (.+)/)[1..2]
35
31
  @score = raw_hit['score']
36
- @distance = distance
37
32
  @search = search
38
33
  @stored_values = raw_hit
39
34
  @stored_cache = {}
@@ -104,7 +99,7 @@ module Sunspot
104
99
  private
105
100
 
106
101
  def setup
107
- @setup ||= Sunspot::Setup.for(@class_name)
102
+ @setup ||= Sunspot::Setup.for(Util.full_const_get(@class_name))
108
103
  end
109
104
 
110
105
  def highlights_cache
@@ -126,7 +121,12 @@ module Sunspot
126
121
  def stored_value(field_name, dynamic_field_name)
127
122
  setup.stored_fields(field_name, dynamic_field_name).each do |field|
128
123
  if value = @stored_values[field.indexed_name]
129
- return field.cast(value)
124
+ case value
125
+ when Array
126
+ return value.map { |item| field.cast(item) }
127
+ else
128
+ return field.cast(value)
129
+ end
130
130
  end
131
131
  end
132
132
  nil
@@ -38,7 +38,7 @@ module Sunspot
38
38
  private
39
39
 
40
40
  def sort_rows!(rows)
41
- case @options[:sort] || (:count if @options[:limit])
41
+ case @options[:sort] || (:count if limit)
42
42
  when :count
43
43
  rows.sort! { |lrow, rrow| rrow.count <=> lrow.count }
44
44
  when :index
@@ -52,11 +52,16 @@ module Sunspot
52
52
  end
53
53
  end
54
54
  end
55
- if @options[:limit]
56
- rows.replace(rows.first(@options[:limit]))
55
+ if limit
56
+ rows.replace(rows.first(limit))
57
57
  end
58
58
  rows
59
59
  end
60
+
61
+ def limit
62
+ return @limit if defined?(@limit)
63
+ @limit = (@options[:limit].to_i if @options[:limit].to_i > 0)
64
+ end
60
65
  end
61
66
  end
62
67
  end
@@ -107,6 +107,14 @@ module Sunspot
107
107
  connection.commit
108
108
  end
109
109
 
110
+ #
111
+ # See Sunspot.optimize
112
+ #
113
+ def optimize
114
+ @adds = @deletes = 0
115
+ connection.optimize
116
+ end
117
+
110
118
  #
111
119
  # See Sunspot.remove
112
120
  #
@@ -115,9 +123,9 @@ module Sunspot
115
123
  types = objects
116
124
  conjunction = Query::Connective::Conjunction.new
117
125
  if types.length == 1
118
- conjunction.add_restriction(TypeField.instance, Query::Restriction::EqualTo, types.first)
126
+ conjunction.add_positive_restriction(TypeField.instance, Query::Restriction::EqualTo, types.first)
119
127
  else
120
- conjunction.add_restriction(TypeField.instance, Query::Restriction::AnyOf, types)
128
+ conjunction.add_positive_restriction(TypeField.instance, Query::Restriction::AnyOf, types)
121
129
  end
122
130
  dsl = DSL::Scope.new(conjunction, setup_for_types(types))
123
131
  Util.instance_eval_or_call(dsl, &block)
@@ -27,6 +27,14 @@ module Sunspot
27
27
  module SessionProxy
28
28
  NotSupportedError = Class.new(StandardError)
29
29
 
30
+ autoload(
31
+ :AbstractSessionProxy,
32
+ File.join(
33
+ File.dirname(__FILE__),
34
+ 'session_proxy',
35
+ 'abstract_session_proxy'
36
+ )
37
+ )
30
38
  autoload(
31
39
  :ThreadLocalSessionProxy,
32
40
  File.join(
@@ -67,5 +75,13 @@ module Sunspot
67
75
  'id_sharding_session_proxy'
68
76
  )
69
77
  )
78
+ autoload(
79
+ :SilentFailSessionProxy,
80
+ File.join(
81
+ File.dirname(__FILE__),
82
+ 'session_proxy',
83
+ 'silent_fail_session_proxy'
84
+ )
85
+ )
70
86
  end
71
87
  end
@@ -18,7 +18,7 @@ module Sunspot
18
18
  attr_reader :slave_session
19
19
 
20
20
  delegate :batch, :commit, :commit_if_delete_dirty, :commit_if_dirty,
21
- :config, :delete_dirty?, :dirty?, :index, :index!, :remove,
21
+ :config, :delete_dirty?, :dirty?, :index, :index!, :optimize, :remove,
22
22
  :remove!, :remove_all, :remove_all!, :remove_by_id,
23
23
  :remove_by_id!, :to => :master_session
24
24
  delegate :new_search, :search, :new_more_like_this, :more_like_this, :to => :slave_session
@@ -119,6 +119,13 @@ module Sunspot
119
119
  all_sessions.each { |session| session.commit }
120
120
  end
121
121
 
122
+ #
123
+ # Optimize all shards. See Sunspot.optimize
124
+ #
125
+ def optimize
126
+ all_sessions.each { |session| session.optimize }
127
+ end
128
+
122
129
  #
123
130
  # Commit all dirty sessions. Only dirty sessions will be committed.
124
131
  #
@@ -0,0 +1,42 @@
1
+ require File.join(File.dirname(__FILE__), 'abstract_session_proxy')
2
+
3
+ module Sunspot
4
+ module SessionProxy
5
+ class SilentFailSessionProxy < AbstractSessionProxy
6
+
7
+ attr_reader :search_session
8
+
9
+ delegate :new_search, :search, :config,
10
+ :new_more_like_this, :more_like_this,
11
+ :delete_dirty, :delete_dirty?,
12
+ :to => :search_session
13
+
14
+ def initialize(search_session = Sunspot.session)
15
+ @search_session = search_session
16
+ end
17
+
18
+ def rescued_exception(method, e)
19
+ $stderr.puts("Exception in #{method}: #{e.message}")
20
+ end
21
+
22
+ SUPPORTED_METHODS = [
23
+ :batch, :commit, :commit_if_dirty, :commit_if_delete_dirty, :dirty?,
24
+ :index!, :index, :optimize, :remove!, :remove, :remove_all!, :remove_all,
25
+ :remove_by_id!, :remove_by_id
26
+ ]
27
+
28
+ SUPPORTED_METHODS.each do |method|
29
+ module_eval(<<-RUBY)
30
+ def #{method}(*args, &block)
31
+ begin
32
+ search_session.#{method}(*args, &block)
33
+ rescue => e
34
+ self.rescued_exception(:#{method}, e)
35
+ end
36
+ end
37
+ RUBY
38
+ end
39
+
40
+ end
41
+ end
42
+ end
@@ -17,7 +17,7 @@ module Sunspot
17
17
  attr_reader :config
18
18
  @@next_id = 0
19
19
 
20
- delegate :batch, :commit, :commit_if_delete_dirty, :commit_if_dirty, :delete_dirty?, :dirty?, :index, :index!, :new_search, :remove, :remove!, :remove_all, :remove_all!, :remove_by_id, :remove_by_id!, :search, :more_like_this, :new_more_like_this, :to => :session
20
+ delegate :batch, :commit, :commit_if_delete_dirty, :commit_if_dirty, :delete_dirty?, :dirty?, :index, :index!, :new_search, :optimize, :remove, :remove!, :remove_all, :remove_all!, :remove_by_id, :remove_by_id!, :search, :more_like_this, :new_more_like_this, :to => :session
21
21
 
22
22
  #
23
23
  # Optionally pass an existing Sunspot::Configuration object. If none is
data/lib/sunspot/setup.rb CHANGED
@@ -77,15 +77,6 @@ module Sunspot
77
77
  end
78
78
  end
79
79
 
80
- #
81
- # The coordinates field factory is used for populating the coordinate fields
82
- # of documents during index, but does not actually generate fields (since
83
- # the field names used in search are static).
84
- #
85
- def set_coordinates_field(name = nil, &block)
86
- @coordinates_field_factory = FieldFactory::Coordinates.new(name, &block)
87
- end
88
-
89
80
  #
90
81
  # Add a document boost to documents at index time. Document boost can be
91
82
  # static (the same for all documents of this class), or extracted on a per-
@@ -234,7 +225,6 @@ module Sunspot
234
225
  def all_field_factories
235
226
  all_field_factories = []
236
227
  all_field_factories.concat(field_factories).concat(text_field_factories).concat(dynamic_field_factories)
237
- all_field_factories << @coordinates_field_factory if @coordinates_field_factory
238
228
  all_field_factories
239
229
  end
240
230
 
@@ -319,13 +309,7 @@ module Sunspot
319
309
  # Setup instance associated with the given class or its nearest ancestor
320
310
  #
321
311
  def for(clazz) #:nodoc:
322
- class_name =
323
- if clazz.respond_to?(:name)
324
- clazz.name
325
- else
326
- clazz
327
- end
328
- setups[class_name.to_sym] || self.for(clazz.superclass) if clazz
312
+ setups[clazz.name.to_sym] || self.for(clazz.superclass) if clazz
329
313
  end
330
314
 
331
315
  protected
data/lib/sunspot/type.rb CHANGED
@@ -1,3 +1,10 @@
1
+ require 'singleton'
2
+ begin
3
+ require 'geohash'
4
+ rescue LoadError => e
5
+ require 'pr_geohash'
6
+ end
7
+
1
8
  module Sunspot
2
9
  #
3
10
  # This module contains singleton objects that represent the types that can be
@@ -58,12 +65,7 @@ module Sunspot
58
65
  end
59
66
 
60
67
  class AbstractType #:nodoc:
61
- class <<self
62
- def instance
63
- @instance ||= new
64
- end
65
- private :new
66
- end
68
+ include Singleton
67
69
 
68
70
  def accepts_dynamic?
69
71
  true
@@ -317,11 +319,41 @@ module Sunspot
317
319
  true
318
320
  when 'false'
319
321
  false
322
+ when true, false
323
+ string
320
324
  end
321
325
  end
322
326
  end
323
327
  register BooleanType, TrueClass, FalseClass
324
328
 
329
+ #
330
+ # The Location type encodes geographical coordinates as a GeoHash.
331
+ # The data for this type must respond to the `lat` and `lng` methods; you
332
+ # can use Sunspot::Util::Coordinates as a wrapper if your source data does
333
+ # not follow this API.
334
+ #
335
+ # Location fields are most usefully searched using the
336
+ # Sunspot::DSL::RestrictionWithType#near method; see that method for more
337
+ # information on geographical search.
338
+ #
339
+ # ==== Example
340
+ #
341
+ # Sunspot.setup(Post) do
342
+ # location :coordinates do
343
+ # Sunspot::Util::Coordinates.new(coordinates[0], coordinates[1])
344
+ # end
345
+ # end
346
+ #
347
+ class LocationType < AbstractType
348
+ def indexed_name(name)
349
+ "#{name}_s"
350
+ end
351
+
352
+ def to_indexed(value)
353
+ GeoHash.encode(value.lat.to_f, value.lng.to_f, 12)
354
+ end
355
+ end
356
+
325
357
  class ClassType < AbstractType
326
358
  def indexed_name(name) #:nodoc:
327
359
  'class_name'
data/lib/sunspot/util.rb CHANGED
@@ -108,6 +108,20 @@ module Sunspot
108
108
  end
109
109
  end
110
110
 
111
+ #
112
+ # When generating boosts, Solr requires that the values be in standard
113
+ # (not scientific) notation. We would like to ensure a minimum number of
114
+ # significant digits (i.e., digits that are not prefix zeros) for small
115
+ # float values.
116
+ #
117
+ def format_float(f, digits)
118
+ if f < 1
119
+ sprintf('%.*f', digits - Math.log10(f), f)
120
+ else
121
+ f.to_s
122
+ end
123
+ end
124
+
111
125
  #
112
126
  # Perform a deep merge of hashes, returning the result as a new hash.
113
127
  # See #deep_merge_into for rules used to merge the hashes
@@ -182,35 +196,7 @@ module Sunspot
182
196
  end
183
197
  end
184
198
 
185
- class Coordinates #:nodoc:
186
- def initialize(coords)
187
- @coords = coords
188
- end
189
-
190
- def lat
191
- if @coords.respond_to?(:first)
192
- @coords.first
193
- elsif @coords.respond_to?(:lat)
194
- @coords.lat
195
- else
196
- @coords.latitude
197
- end.to_f
198
- end
199
-
200
- def lng
201
- if @coords.respond_to?(:last)
202
- @coords.last
203
- elsif @coords.respond_to?(:lng)
204
- @coords.lng
205
- elsif @coords.respond_to?(:lon)
206
- @coords.lon
207
- elsif @coords.respond_to?(:long)
208
- @coords.long
209
- elsif @coords.respond_to?(:longitude)
210
- @coords.longitude
211
- end.to_f
212
- end
213
- end
199
+ Coordinates = Struct.new(:lat, :lng)
214
200
 
215
201
  class ContextBoundDelegate
216
202
  class <<self
@@ -237,12 +223,16 @@ module Sunspot
237
223
  @__receiver__, @__calling_context__ = receiver, calling_context
238
224
  end
239
225
 
226
+ def id
227
+ @__calling_context__.__send__(:id)
228
+ end
229
+
240
230
  def method_missing(method, *args, &block)
241
231
  begin
242
- @__receiver__.send(method.to_sym, *args, &block)
232
+ @__receiver__.__send__(method.to_sym, *args, &block)
243
233
  rescue ::NoMethodError => e
244
234
  begin
245
- @__calling_context__.send(method.to_sym, *args, &block)
235
+ @__calling_context__.__send__(method.to_sym, *args, &block)
246
236
  rescue ::NoMethodError
247
237
  raise(e)
248
238
  end
@@ -1,3 +1,3 @@
1
1
  module Sunspot
2
- VERSION = '1.1.0'
2
+ VERSION = '1.2.0'
3
3
  end
@@ -442,9 +442,6 @@
442
442
  <str name="version">2.1</str>
443
443
  -->
444
444
  </lst>
445
- <arr name="last-components">
446
- <str>spatial</str>
447
- </arr>
448
445
  </requestHandler>
449
446
  <!-- Please refer to http://wiki.apache.org/solr/SolrReplication for details on configuring replication -->
450
447
  <!-- remove the <lst name="master"> section if this is just a slave -->
@@ -928,7 +925,6 @@
928
925
  <healthcheck type="file">server-enabled</healthcheck>
929
926
  -->
930
927
  </admin>
931
- <searchComponent name="spatial" class="me.outofti.solrspatiallight.SpatialQueryComponent"/>
932
928
  <requestHandler class="solr.MoreLikeThisHandler" name="/mlt">
933
929
  <lst name="defaults">
934
930
  <str name="mlt.mintf">1</str>
@@ -9,6 +9,14 @@ describe "DSL bindings" do
9
9
  value.should == 'value'
10
10
  end
11
11
 
12
+ it 'should give access to calling context\'s id method in search DSL' do
13
+ value = nil
14
+ session.search(Post) do
15
+ value = id
16
+ end
17
+ value.should == 16
18
+ end
19
+
12
20
  it 'should give access to calling context\'s methods in nested DSL block' do
13
21
  value = nil
14
22
  session.search(Post) do
@@ -35,4 +43,8 @@ describe "DSL bindings" do
35
43
  def test_method
36
44
  'value'
37
45
  end
46
+
47
+ def id
48
+ 16
49
+ end
38
50
  end