sunspot 2.0.0.pre.111215 → 2.0.0.pre.120415

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.
data/History.txt CHANGED
@@ -5,6 +5,13 @@
5
5
  * Adds #query_time method to retrieve the Solr query time in
6
6
  milliseconds (Jason Weathered)
7
7
  * Fixes syntax of highlighting when used with nested dismax queries (Marco Crepaldi)
8
+ * Adds ability to nest `Sunspot.batch` calls (Thorbjørn Hermansen)
9
+ * Adds `open_timeout` and `read_timeout` configuration options (Rob Di
10
+ Marco)
11
+ * Adds `offset` options to facets (Federico Gonzalez)
12
+ * Adds `Retry5xxSessionProxy` to retry requests when an internal server error
13
+ occurs (Nick Zadrozny)
14
+ * Adds support for range queries (Jan Ulrich)
8
15
 
9
16
  == 1.3.0 2011-11-26
10
17
  * Requests to Solr use HTTP POST verb by default to avoid issues when the query string grows too large for GET (Johan Van Ryseghem)
@@ -0,0 +1,62 @@
1
+ module Sunspot
2
+ #
3
+ # Keeps a stack of batches and helps out when Indexer is asked to batch documents.
4
+ #
5
+ # If the client does something like
6
+ #
7
+ # Sunspot.batch do
8
+ # some_code_here
9
+ # which_triggers_some_other_code
10
+ # which_again_calls
11
+ # Sunspot.batch { ... }
12
+ # end
13
+ #
14
+ # it is the Batcher's job to keep track of these nestings. The inner will
15
+ # be sent of to be indexed first.
16
+ #
17
+ class Batcher
18
+ include Enumerable
19
+
20
+ # Raised if you ask to end current, but no current exists
21
+ class NoCurrentBatchError < StandardError; end
22
+
23
+ def initialize
24
+ @stack = []
25
+ end
26
+
27
+ def current
28
+ @stack.last or start_new
29
+ end
30
+
31
+ def start_new
32
+ (@stack << []).last
33
+ end
34
+
35
+ def end_current
36
+ fail NoCurrentBatchError if @stack.empty?
37
+
38
+ @stack.pop
39
+ end
40
+
41
+ def depth
42
+ @stack.length
43
+ end
44
+
45
+ def batching?
46
+ depth > 0
47
+ end
48
+
49
+ def each(&block)
50
+ current.each(&block)
51
+ end
52
+
53
+ def push(value)
54
+ current << value
55
+ end
56
+ alias << push
57
+
58
+ def concat(values)
59
+ current.concat values
60
+ end
61
+ end
62
+ end
@@ -23,6 +23,8 @@ module Sunspot
23
23
  LightConfig.build do
24
24
  solr do
25
25
  url 'http://127.0.0.1:8983/solr'
26
+ read_timeout nil
27
+ open_timeout nil
26
28
  end
27
29
  master_solr do
28
30
  url nil
@@ -13,6 +13,26 @@ module Sunspot
13
13
  @group.limit = num
14
14
  end
15
15
 
16
+ #
17
+ # If set, facet counts are based on the most relevant document of
18
+ # each group matching the query.
19
+ #
20
+ # Supported in Solr 3.4 and above.
21
+ #
22
+ # ==== Example
23
+ #
24
+ # Sunspot.search(Post) do
25
+ # group :title do
26
+ # truncate
27
+ # end
28
+ #
29
+ # facet :title, :extra => :any
30
+ # end
31
+ #
32
+ def truncate
33
+ @group.truncate = true
34
+ end
35
+
16
36
  # Specify the order that results should be returned in. This method can
17
37
  # be called multiple times; precedence will be in the order given.
18
38
  #
@@ -185,6 +185,19 @@ module Sunspot
185
185
  # semantic meaning is attached to them. The label for +facet+ should be
186
186
  # a symbol; the label for +row+ can be whatever you'd like.
187
187
  #
188
+ # ==== Range Facets
189
+ #
190
+ # One can use the Range Faceting feature on any date field or any numeric
191
+ # field that supports range queries. This is particularly useful for the
192
+ # cases in the past where one might stitch together a series of range
193
+ # queries (as facet by query) for things like prices, etc.
194
+ #
195
+ # For example faceting over average ratings can be done as follows:
196
+ #
197
+ # Sunspot.search(Post) do
198
+ # facet :average_rating, :range => 1..5, :range_interval => 1
199
+ # end
200
+ #
188
201
  # ==== Parameters
189
202
  #
190
203
  # field_names...<Symbol>:: fields for which to return field facets
@@ -195,6 +208,8 @@ module Sunspot
195
208
  # Either :count (values matching the most terms first) or :index (lexical)
196
209
  # :limit<Integer>::
197
210
  # The maximum number of facet rows to return
211
+ # :offset<Integer>::
212
+ # The offset from which to start returning facet rows
198
213
  # :minimum_count<Integer>::
199
214
  # The minimum count a facet row must have to be returned
200
215
  # :zeros<Boolean>::
@@ -276,6 +291,15 @@ module Sunspot
276
291
  end
277
292
  search_facet = @search.add_date_facet(field, options)
278
293
  Sunspot::Query::DateFieldFacet.new(field, options)
294
+ elsif options[:range]
295
+ unless [Sunspot::Type::TimeType, Sunspot::Type::FloatType, Sunspot::Type::IntegerType ].inject(false){|res,type| res || field.type.is_a?(type)}
296
+ raise(
297
+ ArgumentError,
298
+ ':range can only be specified for date or numeric fields'
299
+ )
300
+ end
301
+ search_facet = @search.add_range_facet(field, options)
302
+ Sunspot::Query::RangeFacet.new(field, options)
279
303
  else
280
304
  search_facet = @search.add_field_facet(field, options)
281
305
  Sunspot::Query::FieldFacet.new(field, options)
@@ -130,8 +130,30 @@ module Sunspot
130
130
  # :radius<Numeric>::
131
131
  # Radius (in kilometers)
132
132
  #
133
- def in_radius(lat, lon, radius)
134
- @query.add_geo(Sunspot::Query::Geofilt.new(@field, lat, lon, radius))
133
+ # ==== Options
134
+ #
135
+ # <dt><code>:bbox</code></dt>
136
+ # <dd>If `true`, performs the search using `bbox`. `bbox` is
137
+ # more performant, but also more inexact (guaranteed to encompass
138
+ # all of the points of interest, but may also include other points
139
+ # that are slightly outside of the required distance).</dd>
140
+ #
141
+ def in_radius(lat, lon, radius, options = {})
142
+ @query.add_geo(Sunspot::Query::Geofilt.new(@field, lat, lon, radius, options))
143
+ end
144
+
145
+ #
146
+ # Performs a query that is filtered by a bounding box
147
+ #
148
+ # ==== Parameters
149
+ #
150
+ # :first_corner<Array>::
151
+ # First corner (expressed as an array `[latitude, longitude]`)
152
+ # :second_corner<Array>::
153
+ # Second corner (expressed as an array `[latitude, longitude]`)
154
+ #
155
+ def in_bounding_box(first_corner, second_corner)
156
+ @query.add_geo(Sunspot::Query::Bbox.new(@field, first_corner, second_corner))
135
157
  end
136
158
  end
137
159
  end
@@ -1,3 +1,5 @@
1
+ require 'sunspot/batcher'
2
+
1
3
  module Sunspot
2
4
  #
3
5
  # This class presents a service for adding, updating, and removing data
@@ -22,10 +24,10 @@ module Sunspot
22
24
  #
23
25
  def add(model)
24
26
  documents = Util.Array(model).map { |m| prepare(m) }
25
- if @batch.nil?
26
- add_documents(documents)
27
+ if batcher.batching?
28
+ batcher.concat(documents)
27
29
  else
28
- @batch.concat(documents)
30
+ add_documents(documents)
29
31
  end
30
32
  end
31
33
 
@@ -69,19 +71,22 @@ module Sunspot
69
71
  # Start batch processing
70
72
  #
71
73
  def start_batch
72
- @batch = []
74
+ batcher.start_new
73
75
  end
74
76
 
75
77
  #
76
78
  # Write batch out to Solr and clear it
77
79
  #
78
80
  def flush_batch
79
- add_documents(@batch)
80
- @batch = nil
81
+ add_documents(batcher.end_current)
81
82
  end
82
83
 
83
84
  private
84
85
 
86
+ def batcher
87
+ @batcher ||= Batcher.new
88
+ end
89
+
85
90
  #
86
91
  # Convert documents into hash of indexed properties
87
92
  #
data/lib/sunspot/query.rb CHANGED
@@ -1,7 +1,7 @@
1
- %w(filter abstract_field_facet connective boost_query date_field_facet dismax
1
+ %w(filter abstract_field_facet connective boost_query date_field_facet range_facet dismax
2
2
  field_facet highlighting pagination restriction common_query
3
- standard_query more_like_this more_like_this_query geo geofilt query_facet scope
4
- sort sort_composite text_field_boost function_query
3
+ standard_query more_like_this more_like_this_query geo geofilt bbox query_facet
4
+ scope sort sort_composite text_field_boost function_query
5
5
  composite_fulltext field_group).each do |file|
6
6
  require(File.join(File.dirname(__FILE__), 'query', file))
7
7
  end
@@ -26,6 +26,9 @@ module Sunspot
26
26
  if @options[:limit]
27
27
  params[qualified_param('limit')] = @options[:limit].to_i
28
28
  end
29
+ if @options[:offset]
30
+ params[qualified_param('offset')] = @options[:offset].to_i
31
+ end
29
32
  if @options[:prefix]
30
33
  params[qualified_param('prefix')] = escape(@options[:prefix].to_s)
31
34
  end
@@ -0,0 +1,15 @@
1
+ module Sunspot
2
+ module Query
3
+ class Bbox
4
+ def initialize(field, first_corner, second_corner)
5
+ @field, @first_corner, @second_corner = field, first_corner, second_corner
6
+ end
7
+
8
+ def to_params
9
+ filter = "#{@field.indexed_name}:[#{@first_corner.join(",")} TO #{@second_corner.join(",")}]"
10
+
11
+ {:fq => filter}
12
+ end
13
+ end
14
+ end
15
+ end
@@ -4,7 +4,7 @@ module Sunspot
4
4
  # A FieldGroup groups by the unique values of a given field.
5
5
  #
6
6
  class FieldGroup
7
- attr_accessor :limit
7
+ attr_accessor :limit, :truncate
8
8
 
9
9
  def initialize(field)
10
10
  if field.multiple?
@@ -21,12 +21,14 @@ module Sunspot
21
21
 
22
22
  def to_params
23
23
  params = {
24
- :group => "true",
25
- :"group.field" => @field.indexed_name,
24
+ :group => "true",
25
+ :"group.ngroups" => "true",
26
+ :"group.field" => @field.indexed_name
26
27
  }
27
28
 
28
29
  params.merge!(@sort.to_params("group."))
29
30
  params[:"group.limit"] = @limit if @limit
31
+ params[:"group.truncate"] = @truncate if @truncate
30
32
 
31
33
  params
32
34
  end
@@ -1,13 +1,14 @@
1
1
  module Sunspot
2
2
  module Query
3
3
  class Geofilt
4
- def initialize(field, lat, lon, radius)
5
- @field, @lat, @lon, @radius = field, lat, lon, radius
4
+ def initialize(field, lat, lon, radius, options = {})
5
+ @field, @lat, @lon, @radius, @options = field, lat, lon, radius, options
6
6
  end
7
7
 
8
8
  def to_params
9
- filter = "{!geofilt sfield=#{@field.indexed_name} pt=#{@lat},#{@lon} d=#{@radius}}"
9
+ func = @options[:bbox] ? "bbox" : "geofilt"
10
10
 
11
+ filter = "{!#{func} sfield=#{@field.indexed_name} pt=#{@lat},#{@lon} d=#{@radius}}"
11
12
  {:fq => filter}
12
13
  end
13
14
  end
@@ -0,0 +1,14 @@
1
+ module Sunspot
2
+ module Query
3
+ class RangeFacet < AbstractFieldFacet
4
+ def to_params
5
+ params = super
6
+ params[:"facet.range"] = [@field.indexed_name]
7
+ params[qualified_param('range.start')] = @field.to_indexed(@options[:range].first)
8
+ params[qualified_param('range.end')] = @field.to_indexed(@options[:range].last)
9
+ params[qualified_param('range.gap')] = "#{@options[:range_interval] || 10}"
10
+ params
11
+ end
12
+ end
13
+ end
14
+ end
@@ -1,5 +1,5 @@
1
1
  %w(abstract_search standard_search more_like_this_search query_facet field_facet
2
- date_facet facet_row hit highlight field_group group hit_enumerable).each do |file|
2
+ date_facet range_facet facet_row hit highlight field_group group hit_enumerable).each do |file|
3
3
  require File.join(File.dirname(__FILE__), 'search', file)
4
4
  end
5
5
 
@@ -226,6 +226,11 @@ module Sunspot
226
226
  add_facet(name, DateFacet.new(field, self, options))
227
227
  end
228
228
 
229
+ def add_range_facet(field, options) #:nodoc:
230
+ name = (options[:name] || field.name)
231
+ add_facet(name, RangeFacet.new(field, self, options))
232
+ end
233
+
229
234
  def highlights_for(doc) #:nodoc:
230
235
  if @solr_result['highlighting']
231
236
  @solr_result['highlighting'][doc['id']]
@@ -22,6 +22,12 @@ module Sunspot
22
22
  end
23
23
  end
24
24
 
25
+ def total
26
+ if solr_response
27
+ solr_response['ngroups'].to_i
28
+ end
29
+ end
30
+
25
31
  private
26
32
 
27
33
  def solr_response
@@ -23,6 +23,10 @@ module Sunspot
23
23
  @verified_hits ||= super
24
24
  end
25
25
 
26
+ def results
27
+ @results ||= verified_hits.map { |hit| hit.instance }
28
+ end
29
+
26
30
  def highlights_for(doc)
27
31
  @search.highlights_for(doc)
28
32
  end
@@ -30,6 +34,21 @@ module Sunspot
30
34
  def solr_docs
31
35
  @doclist['docs']
32
36
  end
37
+
38
+ def data_accessor_for(clazz)
39
+ @search.data_accessor_for(clazz)
40
+ end
41
+
42
+ #
43
+ # The total number of documents matching the query for this group
44
+ #
45
+ # ==== Returns
46
+ #
47
+ # Integer:: Total matching documents
48
+ #
49
+ def total
50
+ @doclist['numFound']
51
+ end
33
52
  end
34
53
  end
35
54
  end
@@ -0,0 +1,37 @@
1
+ module Sunspot
2
+ module Search
3
+ class RangeFacet
4
+ def initialize(field, search, options)
5
+ @field, @search, @options = field, search, options
6
+ end
7
+
8
+ def field_name
9
+ @field.name
10
+ end
11
+
12
+ def rows
13
+ @rows ||=
14
+ begin
15
+ data = @search.facet_response['facet_ranges'][@field.indexed_name]
16
+ gap = (@options[:range_interval] || 10).to_i
17
+ rows = []
18
+
19
+ if data['counts']
20
+ Hash[*data['counts']].each_pair do |start_str, count|
21
+ start = start_str.to_f
22
+ finish = start + gap
23
+ rows << FacetRow.new(start..finish, count, self)
24
+ end
25
+ end
26
+
27
+ if @options[:sort] == :count
28
+ rows.sort! { |lrow, rrow| rrow.count <=> lrow.count }
29
+ else
30
+ rows.sort! { |lrow, rrow| lrow.value.first <=> rrow.value.first }
31
+ end
32
+ rows
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -239,7 +239,9 @@ module Sunspot
239
239
  #
240
240
  def connection
241
241
  @connection ||=
242
- self.class.connection_class.connect(:url => config.solr.url)
242
+ self.class.connection_class.connect(:url => config.solr.url,
243
+ :read_timeout => config.solr.read_timeout,
244
+ :open_timeout => config.solr.open_timeout)
243
245
  end
244
246
 
245
247
  def indexer
@@ -83,5 +83,13 @@ module Sunspot
83
83
  'silent_fail_session_proxy'
84
84
  )
85
85
  )
86
+ autoload(
87
+ :Retry5xxSessionProxy,
88
+ File.join(
89
+ File.dirname(__FILE__),
90
+ 'session_proxy',
91
+ 'retry_5xx_session_proxy'
92
+ )
93
+ )
86
94
  end
87
95
  end
@@ -0,0 +1,67 @@
1
+ require File.join(File.dirname(__FILE__), 'abstract_session_proxy')
2
+
3
+ module Sunspot
4
+ module SessionProxy
5
+ class Retry5xxSessionProxy < AbstractSessionProxy
6
+
7
+ class RetryHandler
8
+ attr_reader :search_session
9
+
10
+ def initialize(search_session)
11
+ @search_session = search_session
12
+ end
13
+
14
+ def method_missing(m, *args, &block)
15
+ retry_count = 1
16
+ begin
17
+ search_session.send(m, *args, &block)
18
+ rescue Errno::ECONNRESET => e
19
+ if retry_count > 0
20
+ $stderr.puts "Error - #{e.message[/^.*$/]} - retrying..."
21
+ retry_count -= 1
22
+ retry
23
+ else
24
+ $stderr.puts "Error - #{e.message[/^.*$/]} - ignoring..."
25
+ end
26
+ rescue RSolr::Error::Http => e
27
+ if (500..599).include?(e.response[:status].to_i)
28
+ if retry_count > 0
29
+ $stderr.puts "Error - #{e.message[/^.*$/]} - retrying..."
30
+ retry_count -= 1
31
+ retry
32
+ else
33
+ $stderr.puts "Error - #{e.message[/^.*$/]} - ignoring..."
34
+ e.response
35
+ end
36
+ else
37
+ raise e
38
+ end
39
+ end
40
+ end
41
+ end
42
+
43
+ attr_reader :search_session
44
+ attr_reader :retry_handler
45
+
46
+ delegate :new_search, :search, :config,
47
+ :new_more_like_this, :more_like_this,
48
+ :delete_dirty, :delete_dirty?,
49
+ :to => :search_session
50
+
51
+ def initialize(search_session = Sunspot.session)
52
+ @search_session = search_session
53
+ @retry_handler = RetryHandler.new(search_session)
54
+ end
55
+
56
+ def rescued_exception(method, e)
57
+ $stderr.puts("Exception in #{method}: #{e.message}")
58
+ end
59
+
60
+ delegate :batch, :commit, :commit_if_dirty, :commit_if_delete_dirty,
61
+ :dirty?, :index!, :index, :optimize, :remove!, :remove, :remove_all!,
62
+ :remove_all, :remove_by_id!, :remove_by_id,
63
+ :to => :retry_handler
64
+
65
+ end
66
+ end
67
+ end
@@ -1,3 +1,3 @@
1
1
  module Sunspot
2
- VERSION = '2.0.0.pre.111215'
2
+ VERSION = '2.0.0.pre.120415'
3
3
  end
@@ -0,0 +1,112 @@
1
+ require File.expand_path('spec_helper', File.dirname(__FILE__))
2
+
3
+ describe Sunspot::Batcher do
4
+ it "includes Enumerable" do
5
+ described_class.should include Enumerable
6
+ end
7
+
8
+ describe "#each" do
9
+ let(:current) { [:foo, :bar] }
10
+ before { subject.stub(:current).and_return current }
11
+
12
+ it "iterates over current" do
13
+ yielded_values = []
14
+
15
+ subject.each do |value|
16
+ yielded_values << value
17
+ end
18
+
19
+ yielded_values.should eq current
20
+ end
21
+ end
22
+
23
+ describe "adding to current batch" do
24
+ it "#push pushes to current" do
25
+ subject.push :foo
26
+ subject.current.should include :foo
27
+ end
28
+
29
+ it "#<< pushes to current" do
30
+ subject.push :foo
31
+ subject.current.should include :foo
32
+ end
33
+
34
+ it "#concat concatinates on current batch" do
35
+ subject << :foo
36
+ subject.concat [:bar, :mix]
37
+ should include :foo, :bar, :mix
38
+ end
39
+ end
40
+
41
+
42
+ describe "#current" do
43
+ context "no current" do
44
+ it "starts a new" do
45
+ expect { subject.current }.to change(subject, :depth).by 1
46
+ end
47
+
48
+ it "is empty by default" do
49
+ subject.current.should be_empty
50
+ end
51
+ end
52
+
53
+ context "with a current" do
54
+ before { subject.start_new }
55
+
56
+ it "does not start a new" do
57
+ expect { subject.current }.to_not change(subject, :depth)
58
+ end
59
+
60
+ it "returns the same as last time" do
61
+ subject.current.should eq subject.current
62
+ end
63
+ end
64
+ end
65
+
66
+ describe "#start_new" do
67
+ it "creates a new batches" do
68
+ expect { 2.times { subject.start_new } }.to change(subject, :depth).by 2
69
+ end
70
+
71
+ it "changes current" do
72
+ subject << :foo
73
+ subject.start_new
74
+ should_not include :foo
75
+ end
76
+ end
77
+
78
+ describe "#end_current" do
79
+ context "no current batch" do
80
+ it "fails" do
81
+ expect { subject.end_current }.to raise_error Sunspot::Batcher::NoCurrentBatchError
82
+ end
83
+ end
84
+
85
+ context "with current batch" do
86
+ before { subject.start_new }
87
+
88
+ it "changes current" do
89
+ subject << :foo
90
+ subject.end_current
91
+ should_not include :foo
92
+ end
93
+
94
+ it "returns current" do
95
+ subject << :foo
96
+ subject.end_current.should include :foo
97
+ end
98
+ end
99
+ end
100
+
101
+ describe "#batching?" do
102
+ it "is false when depth is 0" do
103
+ subject.should_receive(:depth).and_return 0
104
+ should_not be_batching
105
+ end
106
+
107
+ it "is true when depth is more than 0" do
108
+ subject.should_receive(:depth).and_return 1
109
+ should be_batching
110
+ end
111
+ end
112
+ end
@@ -1,8 +1,9 @@
1
1
  require File.expand_path('spec_helper', File.dirname(__FILE__))
2
2
 
3
3
  describe 'batch indexing', :type => :indexer do
4
+ let(:posts) { Array.new(2) { |index| Post.new :title => "Post number #{index}!" } }
5
+
4
6
  it 'should send all batched adds in a single request' do
5
- posts = Array.new(2) { Post.new }
6
7
  session.batch do
7
8
  for post in posts
8
9
  session.index(post)
@@ -12,7 +13,6 @@ describe 'batch indexing', :type => :indexer do
12
13
  end
13
14
 
14
15
  it 'should add all batched adds' do
15
- posts = Array.new(2) { Post.new }
16
16
  session.batch do
17
17
  for post in posts
18
18
  session.index(post)
@@ -36,11 +36,37 @@ describe 'batch indexing', :type => :indexer do
36
36
  pending 'batching all operations'
37
37
  connection.should_not_receive(:add)
38
38
  connection.should_not_receive(:remove)
39
- posts = Array.new(2) { Post.new }
40
39
  session.batch do
41
40
  session.index(posts[0])
42
41
  session.remove(posts[1])
43
42
  end
44
43
  connection.adds
45
44
  end
45
+
46
+ describe "nesting of batches" do
47
+ let(:a_nested_batch) do
48
+ session.batch do
49
+ session.index posts[0]
50
+
51
+ session.batch do
52
+ session.index posts[1]
53
+ end
54
+ end
55
+ end
56
+
57
+ it "behaves like two sets of batches, does the inner first, then outer" do
58
+ session.batch { session.index posts[1] }
59
+ session.batch { session.index posts[0] }
60
+
61
+ two_sets_of_batches_adds = connection.adds.dup
62
+ connection.adds.clear
63
+
64
+ a_nested_batch
65
+ nested_batches_adds = connection.adds
66
+
67
+ nested_batches_adds.first.first.field_by_name(:title_ss).value.should eq(
68
+ two_sets_of_batches_adds.first.first.field_by_name(:title_ss).value
69
+ )
70
+ end
71
+ end
46
72
  end
@@ -54,6 +54,13 @@ shared_examples_for "facetable query" do
54
54
  end
55
55
  connection.should have_last_search_with(:"f.category_ids_im.facet.limit" => 10)
56
56
  end
57
+
58
+ it 'sets the facet offset' do
59
+ search do
60
+ facet :category_ids, :offset => 10
61
+ end
62
+ connection.should have_last_search_with(:"f.category_ids_im.facet.offset" => 10)
63
+ end
57
64
 
58
65
  it 'sets the facet minimum count' do
59
66
  search do
@@ -255,6 +262,58 @@ shared_examples_for "facetable query" do
255
262
  end
256
263
  end
257
264
 
265
+ describe 'on range facets' do
266
+ before :each do
267
+ @range = 2..4
268
+ end
269
+
270
+ it 'does not send range facet parameters if integer range is not specified' do
271
+ search do |query|
272
+ query.facet :average_rating
273
+ end
274
+ connection.should_not have_last_search_with(:"facet.range")
275
+ end
276
+
277
+ it 'sets the facet to a range facet if the range is specified' do
278
+ search do |query|
279
+ query.facet :average_rating, :range => @range
280
+ end
281
+ connection.should have_last_search_with(:"facet.range" => ['average_rating_ft'])
282
+ end
283
+
284
+ it 'sets the facet start and end' do
285
+ search do |query|
286
+ query.facet :average_rating, :range => @range
287
+ end
288
+ connection.should have_last_search_with(
289
+ :"f.average_rating_ft.facet.range.start" => '2.0',
290
+ :"f.average_rating_ft.facet.range.end" => '4.0'
291
+ )
292
+ end
293
+
294
+ it 'defaults the range interval to 10' do
295
+ search do |query|
296
+ query.facet :average_rating, :range => @range
297
+ end
298
+ connection.should have_last_search_with(:"f.average_rating_ft.facet.range.gap" => "10")
299
+ end
300
+
301
+ it 'uses custom range interval' do
302
+ search do |query|
303
+ query.facet :average_rating, :range => @range, :range_interval => 1
304
+ end
305
+ connection.should have_last_search_with(:"f.average_rating_ft.facet.range.gap" => "1")
306
+ end
307
+
308
+ it 'does not allow date faceting on a non-continuous field' do
309
+ lambda do
310
+ search do |query|
311
+ query.facet :title, :range => @range
312
+ end
313
+ end.should raise_error(ArgumentError)
314
+ end
315
+ end
316
+
258
317
  describe 'using queries' do
259
318
  it 'turns faceting on' do
260
319
  search do
@@ -8,4 +8,20 @@ shared_examples_for "spatial query" do
8
8
 
9
9
  connection.should have_last_search_including(:fq, "{!geofilt sfield=coordinates_new_ll pt=23,-46 d=100}")
10
10
  end
11
+
12
+ it 'filters by radius via bbox (inexact)' do
13
+ search do
14
+ with(:coordinates_new).in_radius(23, -46, 100, :bbox => true)
15
+ end
16
+
17
+ connection.should have_last_search_including(:fq, "{!bbox sfield=coordinates_new_ll pt=23,-46 d=100}")
18
+ end
19
+
20
+ it 'filters by bounding box' do
21
+ search do
22
+ with(:coordinates_new).in_bounding_box([45, -94], [46, -93])
23
+ end
24
+
25
+ connection.should have_last_search_including(:fq, "coordinates_new_ll:[45,-94 TO 46,-93]")
26
+ end
11
27
  end
@@ -0,0 +1,73 @@
1
+ require File.expand_path('spec_helper', File.dirname(__FILE__))
2
+
3
+ describe Sunspot::SessionProxy::Retry5xxSessionProxy do
4
+
5
+ before :each do
6
+ Sunspot::Session.connection_class = Mock::ConnectionFactory.new
7
+ @sunspot_session = Sunspot.session
8
+ @proxy = Sunspot::SessionProxy::Retry5xxSessionProxy.new(@sunspot_session)
9
+ Sunspot.session = @proxy
10
+ end
11
+
12
+ class FakeRSolrErrorHttp < RSolr::Error::Http
13
+ def backtrace
14
+ []
15
+ end
16
+ end
17
+
18
+ let :fake_rsolr_request do
19
+ {:uri => 'http://solr.test/uri'}
20
+ end
21
+
22
+ def fake_rsolr_response(status)
23
+ {:status => status.to_s}
24
+ end
25
+
26
+ let :post do
27
+ Post.new(:title => 'test')
28
+ end
29
+
30
+ it "should behave normally without a stubbed exception" do
31
+ @sunspot_session.should_receive(:index).and_return(mock)
32
+ Sunspot.index(post)
33
+ end
34
+
35
+ it "should be successful with a single exception followed by a sucess" do
36
+ e = FakeRSolrErrorHttp.new(fake_rsolr_request, fake_rsolr_response(503))
37
+ @sunspot_session.should_receive(:index).and_return do
38
+ @sunspot_session.should_receive(:index).and_return(mock)
39
+ raise e
40
+ end
41
+ Sunspot.index(post)
42
+ end
43
+
44
+ it "should return the error response after two exceptions" do
45
+ fake_response = fake_rsolr_response(503)
46
+ e = FakeRSolrErrorHttp.new(fake_rsolr_request, fake_response)
47
+ fake_success = mock('success')
48
+
49
+ @sunspot_session.should_receive(:index).and_return do
50
+ @sunspot_session.should_receive(:index).and_return do
51
+ @sunspot_session.stub!(:index).and_return(fake_success)
52
+ raise e
53
+ end
54
+ raise e
55
+ end
56
+
57
+ response = Sunspot.index(post)
58
+ response.should_not == fake_success
59
+ response.should == fake_response
60
+ end
61
+
62
+ it "should not retry a 4xx" do
63
+ e = FakeRSolrErrorHttp.new(fake_rsolr_request, fake_rsolr_response(400))
64
+ @sunspot_session.should_receive(:index).and_raise(e)
65
+ lambda { Sunspot.index(post) }.should raise_error
66
+ end
67
+
68
+ # TODO: try against more than just Sunspot.index? but that's just testing the
69
+ # invocation of delegate, so probably not important. -nz 11Apr12
70
+
71
+ it_should_behave_like 'session proxy'
72
+
73
+ end
@@ -85,6 +85,18 @@ describe 'Session' do
85
85
  Sunspot.commit
86
86
  connection.opts[:url].should == 'http://127.0.0.1:8981/solr'
87
87
  end
88
+
89
+ it 'should open a connection with custom read timeout' do
90
+ Sunspot.config.solr.read_timeout = 0.5
91
+ Sunspot.commit
92
+ connection.opts[:read_timeout].should == 0.5
93
+ end
94
+
95
+ it 'should open a connection with custom open timeout' do
96
+ Sunspot.config.solr.open_timeout = 0.5
97
+ Sunspot.commit
98
+ connection.opts[:open_timeout].should == 0.5
99
+ end
88
100
  end
89
101
 
90
102
  context 'custom session' do
@@ -84,6 +84,13 @@ describe 'search faceting' do
84
84
  end
85
85
  search.facet(:title).rows.map { |row| row.value }.should include('zero')
86
86
  end
87
+
88
+ it 'should return facet rows from an offset' do
89
+ search = Sunspot.search(Post) do
90
+ facet :title, :offset => 3
91
+ end
92
+ search.facet(:title).rows.map { |row| row.value }.should == %w(one zero)
93
+ end
87
94
 
88
95
  it 'should return a specified minimum count' do
89
96
  search = Sunspot.search(Post) do
@@ -134,6 +141,19 @@ describe 'search faceting' do
134
141
  search.facet(:title).rows.first.value.should == :none
135
142
  search.facet(:title).rows.first.count.should == 1
136
143
  end
144
+
145
+ it 'gives correct facet count when group == true and truncate == true' do
146
+ search = Sunspot.search(Post) do
147
+ group :title do
148
+ truncate
149
+ end
150
+
151
+ facet :title, :extra => :any
152
+ end
153
+
154
+ # Should be 5 instead of 11
155
+ search.facet(:title).rows.first.count.should == 5
156
+ end
137
157
  end
138
158
 
139
159
  context 'multiselect faceting' do
@@ -1,4 +1,5 @@
1
1
  require File.expand_path("../spec_helper", File.dirname(__FILE__))
2
+ require File.expand_path("../helpers/search_helper", File.dirname(__FILE__))
2
3
 
3
4
  describe "field grouping" do
4
5
  before :each do
@@ -22,6 +23,14 @@ describe "field grouping" do
22
23
  search.group(:title).groups.should include { |g| g.value == "Title2" }
23
24
  end
24
25
 
26
+ it "returns the number of matches unique groups" do
27
+ search = Sunspot.search(Post) do
28
+ group :title
29
+ end
30
+
31
+ search.group(:title).total.should == 2
32
+ end
33
+
25
34
  it "provides access to the number of matches before grouping" do
26
35
  search = Sunspot.search(Post) do
27
36
  group :title
@@ -10,7 +10,7 @@ describe "geospatial search" do
10
10
  Sunspot.index!(@post)
11
11
  end
12
12
 
13
- it "matches posts with the radius" do
13
+ it "matches posts within the radius" do
14
14
  results = Sunspot.search(Post) {
15
15
  with(:coordinates_new).in_radius(32, -68, 1)
16
16
  }.results
@@ -27,6 +27,32 @@ describe "geospatial search" do
27
27
  end
28
28
  end
29
29
 
30
+ describe "filtering by bounding box" do
31
+ before :all do
32
+ Sunspot.remove_all
33
+
34
+ @post = Post.new(:title => "Howdy",
35
+ :coordinates => Sunspot::Util::Coordinates.new(32, -68))
36
+ Sunspot.index!(@post)
37
+ end
38
+
39
+ it "matches post within the bounding box" do
40
+ results = Sunspot.search(Post) {
41
+ with(:coordinates_new).in_bounding_box [31, -69], [33, -67]
42
+ }.results
43
+
44
+ results.should include(@post)
45
+ end
46
+
47
+ it "filters out posts not in the bounding box" do
48
+ results = Sunspot.search(Post) {
49
+ with(:coordinates_new).in_bounding_box [20, -70], [21, -69]
50
+ }.results
51
+
52
+ results.should_not include(@post)
53
+ end
54
+ end
55
+
30
56
  describe "ordering by geodist" do
31
57
  before :all do
32
58
  Sunspot.remove_all
@@ -30,4 +30,26 @@ describe 'indexing' do
30
30
  end
31
31
  Sunspot.search(Post).should have(2).results
32
32
  end
33
+
34
+
35
+ describe "in batches" do
36
+ let(:post_1) { Post.new :title => 'A tittle' }
37
+ let(:post_2) { Post.new :title => 'Another title' }
38
+
39
+ describe "nested" do
40
+ let(:a_nested_batch) do
41
+ Sunspot.batch do
42
+ Sunspot.index post_1
43
+
44
+ Sunspot.batch do
45
+ Sunspot.index post_2
46
+ end
47
+ end
48
+ end
49
+
50
+ it "does not fail" do
51
+ expect { a_nested_batch }.to_not raise_error
52
+ end
53
+ end
54
+ end
33
55
  end
data/sunspot.gemspec CHANGED
@@ -25,8 +25,7 @@ Gem::Specification.new do |s|
25
25
  s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
26
26
  s.require_paths = ["lib"]
27
27
 
28
- s.add_dependency 'rsolr', '~>1.0.6'
29
- s.add_dependency 'escape', '~>0.0.4'
28
+ s.add_dependency 'rsolr', '~>1.0.7'
30
29
  s.add_dependency 'pr_geohash', '~>1.0'
31
30
 
32
31
  s.add_development_dependency 'rspec', '~>2.6.0'
metadata CHANGED
@@ -1,14 +1,15 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: sunspot
3
3
  version: !ruby/object:Gem::Version
4
- prerelease: true
4
+ hash: -2204637468
5
+ prerelease: 6
5
6
  segments:
6
7
  - 2
7
8
  - 0
8
9
  - 0
9
10
  - pre
10
- - 111215
11
- version: 2.0.0.pre.111215
11
+ - 120415
12
+ version: 2.0.0.pre.120415
12
13
  platform: ruby
13
14
  authors:
14
15
  - Mat Brown
@@ -34,76 +35,69 @@ autorequire:
34
35
  bindir: bin
35
36
  cert_chain: []
36
37
 
37
- date: 2011-12-15 00:00:00 -07:00
38
- default_executable:
38
+ date: 2012-04-15 00:00:00 Z
39
39
  dependencies:
40
40
  - !ruby/object:Gem::Dependency
41
41
  name: rsolr
42
42
  prerelease: false
43
43
  requirement: &id001 !ruby/object:Gem::Requirement
44
+ none: false
44
45
  requirements:
45
46
  - - ~>
46
47
  - !ruby/object:Gem::Version
48
+ hash: 25
47
49
  segments:
48
50
  - 1
49
51
  - 0
50
- - 6
51
- version: 1.0.6
52
+ - 7
53
+ version: 1.0.7
52
54
  type: :runtime
53
55
  version_requirements: *id001
54
- - !ruby/object:Gem::Dependency
55
- name: escape
56
- prerelease: false
57
- requirement: &id002 !ruby/object:Gem::Requirement
58
- requirements:
59
- - - ~>
60
- - !ruby/object:Gem::Version
61
- segments:
62
- - 0
63
- - 0
64
- - 4
65
- version: 0.0.4
66
- type: :runtime
67
- version_requirements: *id002
68
56
  - !ruby/object:Gem::Dependency
69
57
  name: pr_geohash
70
58
  prerelease: false
71
- requirement: &id003 !ruby/object:Gem::Requirement
59
+ requirement: &id002 !ruby/object:Gem::Requirement
60
+ none: false
72
61
  requirements:
73
62
  - - ~>
74
63
  - !ruby/object:Gem::Version
64
+ hash: 15
75
65
  segments:
76
66
  - 1
77
67
  - 0
78
68
  version: "1.0"
79
69
  type: :runtime
80
- version_requirements: *id003
70
+ version_requirements: *id002
81
71
  - !ruby/object:Gem::Dependency
82
72
  name: rspec
83
73
  prerelease: false
84
- requirement: &id004 !ruby/object:Gem::Requirement
74
+ requirement: &id003 !ruby/object:Gem::Requirement
75
+ none: false
85
76
  requirements:
86
77
  - - ~>
87
78
  - !ruby/object:Gem::Version
79
+ hash: 23
88
80
  segments:
89
81
  - 2
90
82
  - 6
91
83
  - 0
92
84
  version: 2.6.0
93
85
  type: :development
94
- version_requirements: *id004
86
+ version_requirements: *id003
95
87
  - !ruby/object:Gem::Dependency
96
88
  name: hanna
97
89
  prerelease: false
98
- requirement: &id005 !ruby/object:Gem::Requirement
90
+ requirement: &id004 !ruby/object:Gem::Requirement
91
+ none: false
99
92
  requirements:
100
93
  - - ">="
101
94
  - !ruby/object:Gem::Version
95
+ hash: 3
102
96
  segments:
103
97
  - 0
104
98
  version: "0"
105
99
  type: :development
106
- version_requirements: *id005
100
+ version_requirements: *id004
107
101
  description: " Sunspot is a library providing a powerful, all-ruby API for the Solr search engine. Sunspot manages the configuration of persistent\n Ruby classes for search and indexing and exposes Solr's most powerful features through a collection of DSLs. Complex search operations\n can be performed without hand-writing any boolean queries or building Solr parameters by hand.\n"
108
102
  email:
109
103
  - mat@patch.com
@@ -123,6 +117,7 @@ files:
123
117
  - lib/light_config.rb
124
118
  - lib/sunspot.rb
125
119
  - lib/sunspot/adapters.rb
120
+ - lib/sunspot/batcher.rb
126
121
  - lib/sunspot/class_set.rb
127
122
  - lib/sunspot/composite_setup.rb
128
123
  - lib/sunspot/configuration.rb
@@ -148,6 +143,7 @@ files:
148
143
  - lib/sunspot/indexer.rb
149
144
  - lib/sunspot/query.rb
150
145
  - lib/sunspot/query/abstract_field_facet.rb
146
+ - lib/sunspot/query/bbox.rb
151
147
  - lib/sunspot/query/boost_query.rb
152
148
  - lib/sunspot/query/common_query.rb
153
149
  - lib/sunspot/query/composite_fulltext.rb
@@ -165,6 +161,7 @@ files:
165
161
  - lib/sunspot/query/more_like_this_query.rb
166
162
  - lib/sunspot/query/pagination.rb
167
163
  - lib/sunspot/query/query_facet.rb
164
+ - lib/sunspot/query/range_facet.rb
168
165
  - lib/sunspot/query/restriction.rb
169
166
  - lib/sunspot/query/scope.rb
170
167
  - lib/sunspot/query/sort.rb
@@ -185,6 +182,7 @@ files:
185
182
  - lib/sunspot/search/more_like_this_search.rb
186
183
  - lib/sunspot/search/paginated_collection.rb
187
184
  - lib/sunspot/search/query_facet.rb
185
+ - lib/sunspot/search/range_facet.rb
188
186
  - lib/sunspot/search/standard_search.rb
189
187
  - lib/sunspot/session.rb
190
188
  - lib/sunspot/session_proxy.rb
@@ -192,6 +190,7 @@ files:
192
190
  - lib/sunspot/session_proxy/class_sharding_session_proxy.rb
193
191
  - lib/sunspot/session_proxy/id_sharding_session_proxy.rb
194
192
  - lib/sunspot/session_proxy/master_slave_session_proxy.rb
193
+ - lib/sunspot/session_proxy/retry_5xx_session_proxy.rb
195
194
  - lib/sunspot/session_proxy/sharding_session_proxy.rb
196
195
  - lib/sunspot/session_proxy/silent_fail_session_proxy.rb
197
196
  - lib/sunspot/session_proxy/thread_local_session_proxy.rb
@@ -204,6 +203,7 @@ files:
204
203
  - pkg/.gitignore
205
204
  - script/console
206
205
  - spec/api/adapters_spec.rb
206
+ - spec/api/batcher_spec.rb
207
207
  - spec/api/binding_spec.rb
208
208
  - spec/api/class_set_spec.rb
209
209
  - spec/api/hit_enumerable_spec.rb
@@ -244,6 +244,7 @@ files:
244
244
  - spec/api/session_proxy/class_sharding_session_proxy_spec.rb
245
245
  - spec/api/session_proxy/id_sharding_session_proxy_spec.rb
246
246
  - spec/api/session_proxy/master_slave_session_proxy_spec.rb
247
+ - spec/api/session_proxy/retry_5xx_session_proxy_spec.rb
247
248
  - spec/api/session_proxy/sharding_session_proxy_spec.rb
248
249
  - spec/api/session_proxy/silent_fail_session_proxy_spec.rb
249
250
  - spec/api/session_proxy/spec_helper.rb
@@ -287,7 +288,6 @@ files:
287
288
  - tasks/rdoc.rake
288
289
  - tasks/schema.rake
289
290
  - tasks/todo.rake
290
- has_rdoc: true
291
291
  homepage: http://outoftime.github.com/sunspot
292
292
  licenses: []
293
293
 
@@ -301,16 +301,20 @@ rdoc_options:
301
301
  require_paths:
302
302
  - lib
303
303
  required_ruby_version: !ruby/object:Gem::Requirement
304
+ none: false
304
305
  requirements:
305
306
  - - ">="
306
307
  - !ruby/object:Gem::Version
308
+ hash: 3
307
309
  segments:
308
310
  - 0
309
311
  version: "0"
310
312
  required_rubygems_version: !ruby/object:Gem::Requirement
313
+ none: false
311
314
  requirements:
312
315
  - - ">"
313
316
  - !ruby/object:Gem::Version
317
+ hash: 25
314
318
  segments:
315
319
  - 1
316
320
  - 3
@@ -319,12 +323,13 @@ required_rubygems_version: !ruby/object:Gem::Requirement
319
323
  requirements: []
320
324
 
321
325
  rubyforge_project: sunspot
322
- rubygems_version: 1.3.6
326
+ rubygems_version: 1.8.15
323
327
  signing_key:
324
328
  specification_version: 3
325
329
  summary: Library for expressive, powerful interaction with the Solr search engine
326
330
  test_files:
327
331
  - spec/api/adapters_spec.rb
332
+ - spec/api/batcher_spec.rb
328
333
  - spec/api/binding_spec.rb
329
334
  - spec/api/class_set_spec.rb
330
335
  - spec/api/hit_enumerable_spec.rb
@@ -365,6 +370,7 @@ test_files:
365
370
  - spec/api/session_proxy/class_sharding_session_proxy_spec.rb
366
371
  - spec/api/session_proxy/id_sharding_session_proxy_spec.rb
367
372
  - spec/api/session_proxy/master_slave_session_proxy_spec.rb
373
+ - spec/api/session_proxy/retry_5xx_session_proxy_spec.rb
368
374
  - spec/api/session_proxy/sharding_session_proxy_spec.rb
369
375
  - spec/api/session_proxy/silent_fail_session_proxy_spec.rb
370
376
  - spec/api/session_proxy/spec_helper.rb