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

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