sunspot 2.0.0 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,3 +1,5 @@
1
+ require 'forwardable'
2
+
1
3
  module Sunspot
2
4
  #
3
5
  # Sunspot works by saving references to the primary key (or natural ID) of
@@ -46,7 +48,7 @@ module Sunspot
46
48
  # end
47
49
  #
48
50
  # # then in your initializer
49
- # Sunspot::Adapters::InstanceAdapter.register(MyAdapter, File)
51
+ # Sunspot::Adapters::InstanceAdapter.register(FileAdapter, File)
50
52
  #
51
53
  class InstanceAdapter
52
54
  def initialize(instance) #:nodoc:
@@ -119,16 +121,28 @@ module Sunspot
119
121
  #
120
122
  # Sunspot::NoAdapterError:: If no adapter is registered for this class
121
123
  #
122
- def for(clazz) #:nodoc:
123
- original_class_name = clazz.name
124
- clazz.ancestors.each do |ancestor_class|
125
- next if ancestor_class.name.nil? || ancestor_class.name.empty?
126
- class_name = ancestor_class.name.to_sym
127
- return instance_adapters[class_name] if instance_adapters[class_name]
128
- end
129
-
124
+ def for(clazz)
125
+ adapter = registered_adapter_for(clazz) || registered_adapter_for_ancestors_of(clazz)
126
+ return adapter if adapter
130
127
  raise(Sunspot::NoAdapterError,
131
- "No adapter is configured for #{original_class_name} or its superclasses. See the documentation for Sunspot::Adapters")
128
+ "No adapter is configured for #{clazz.name} or its superclasses. See the documentation for Sunspot::Adapters")
129
+ end
130
+
131
+ # Returns the directly-registered adapter for the specified class,
132
+ # if one exists, without searching the class's ancestors.
133
+ #
134
+ # === Parameters
135
+ #
136
+ # clazz<Class>:: The model class to be checked for the registered
137
+ # adapter
138
+ #
139
+ # === Returns
140
+ #
141
+ # Class:: Subclass of InstanceAdapter, or nil if none found
142
+ #
143
+ def registered_adapter_for(clazz)
144
+ return nil if clazz.name.nil? || clazz.name.empty?
145
+ instance_adapters[clazz.name.to_sym]
132
146
  end
133
147
 
134
148
  def index_id_for(class_name, id) #:nodoc:
@@ -146,6 +160,16 @@ module Sunspot
146
160
  def instance_adapters #:nodoc:
147
161
  @instance_adapters ||= {}
148
162
  end
163
+
164
+ def registered_adapter_for_ancestors_of(clazz) # :nodoc:
165
+ clazz.ancestors.each do |ancestor_class|
166
+ if adapter = registered_adapter_for(ancestor_class)
167
+ register(adapter, clazz)
168
+ return adapter
169
+ end
170
+ end
171
+ nil
172
+ end
149
173
  end
150
174
  end
151
175
 
@@ -247,14 +271,27 @@ module Sunspot
247
271
  # Sunspot::NoAdapterError:: If no data accessor exists for the given class
248
272
  #
249
273
  def for(clazz) #:nodoc:
250
- original_class_name = clazz.name
251
- clazz.ancestors.each do |ancestor_class|
252
- next if ancestor_class.name.nil? || ancestor_class.name.empty?
253
- class_name = ancestor_class.name.to_sym
254
- return data_accessors[class_name] if data_accessors[class_name]
255
- end
274
+ accessor = registered_accessor_for(clazz) || registered_accessor_for_ancestors_of(clazz)
275
+ return accessor if accessor
256
276
  raise(Sunspot::NoAdapterError,
257
- "No data accessor is configured for #{original_class_name} or its superclasses. See the documentation for Sunspot::Adapters")
277
+ "No data accessor is configured for #{clazz.name} or its superclasses. See the documentation for Sunspot::Adapters")
278
+ end
279
+
280
+ # Returns the directly-registered accessor for the specified class, if
281
+ # one exists, without searching the class's ancestors.
282
+ #
283
+ # === Parameters
284
+ #
285
+ # clazz<Class>:: The model class to be checked for the registered
286
+ # data accessor
287
+ #
288
+ # === Returns
289
+ #
290
+ # Class:: Subclass of DataAccessor, or nil if none found
291
+ #
292
+ def registered_accessor_for(clazz)
293
+ return nil if clazz.name.nil? || clazz.name.empty?
294
+ data_accessors[clazz.name.to_sym]
258
295
  end
259
296
 
260
297
  protected
@@ -268,6 +305,16 @@ module Sunspot
268
305
  def data_accessors #:nodoc:
269
306
  @adapters ||= {}
270
307
  end
308
+
309
+ def registered_accessor_for_ancestors_of(clazz) # :nodoc:
310
+ clazz.ancestors.each do |ancestor_class|
311
+ if accessor = registered_accessor_for(ancestor_class)
312
+ register(accessor, clazz)
313
+ return accessor
314
+ end
315
+ end
316
+ nil
317
+ end
271
318
  end
272
319
  end
273
320
 
@@ -22,7 +22,7 @@ module Sunspot
22
22
  def build #:nodoc:
23
23
  LightConfig.build do
24
24
  solr do
25
- url 'http://127.0.0.1:8983/solr'
25
+ url 'http://127.0.0.1:8983/solr/default'
26
26
  read_timeout nil
27
27
  open_timeout nil
28
28
  end
@@ -30,7 +30,7 @@ module Sunspot
30
30
  params = { :q => @keywords }
31
31
  params[:fl] = '* score'
32
32
  params[:qf] = @fulltext_fields.values.map { |field| field.to_boosted_field }.join(' ')
33
- params[:defType] = 'dismax'
33
+ params[:defType] = 'edismax'
34
34
  if @phrase_fields
35
35
  params[:pf] = @phrase_fields.map { |field| field.to_boosted_field }.join(' ')
36
36
  end
@@ -71,7 +71,7 @@ module Sunspot
71
71
  params.delete :fl
72
72
  keywords = params.delete(:q)
73
73
  options = params.map { |key, value| escape_param(key, value) }.join(' ')
74
- "_query_:\"{!dismax #{options}}#{escape_quotes(keywords)}\""
74
+ "_query_:\"{!edismax #{options}}#{escape_quotes(keywords)}\""
75
75
  end
76
76
 
77
77
  #
@@ -82,7 +82,7 @@ module Sunspot
82
82
  boost_query
83
83
  end
84
84
 
85
- #
85
+ #
86
86
  # Add a boost function
87
87
  #
88
88
  def add_boost_function(function_query)
@@ -123,7 +123,7 @@ module Sunspot
123
123
 
124
124
 
125
125
  private
126
-
126
+
127
127
  def escape_param(key, value)
128
128
  "#{key}='#{escape_quotes(Array(value).join(" "))}'"
129
129
  end
@@ -273,10 +273,23 @@ module Sunspot
273
273
  # Results must have field with value included in given collection
274
274
  #
275
275
  class AnyOf < Base
276
+
277
+ def negated?
278
+ if @value.empty?
279
+ false
280
+ else
281
+ super
282
+ end
283
+ end
284
+
276
285
  private
277
286
 
278
287
  def to_solr_conditional
279
- "(#{@value.map { |v| solr_value v } * ' OR '})"
288
+ if @value.empty?
289
+ "[* TO *]"
290
+ else
291
+ "(#{@value.map { |v| solr_value v } * ' OR '})"
292
+ end
280
293
  end
281
294
  end
282
295
 
@@ -285,10 +298,22 @@ module Sunspot
285
298
  # collection (only makes sense for fields with multiple values)
286
299
  #
287
300
  class AllOf < Base
301
+ def negated?
302
+ if @value.empty?
303
+ false
304
+ else
305
+ super
306
+ end
307
+ end
308
+
288
309
  private
289
310
 
290
311
  def to_solr_conditional
291
- "(#{@value.map { |v| solr_value v } * ' AND '})"
312
+ if @value.empty?
313
+ "[* TO *]"
314
+ else
315
+ "(#{@value.map { |v| solr_value v } * ' AND '})"
316
+ end
292
317
  end
293
318
  end
294
319
 
@@ -1,3 +1,3 @@
1
1
  module Sunspot
2
- VERSION = '2.0.0'
2
+ VERSION = '2.1.0'
3
3
  end
@@ -14,6 +14,12 @@ describe Sunspot::Adapters::InstanceAdapter do
14
14
  Sunspot::Adapters::InstanceAdapter::for(Module.new)
15
15
  end.should raise_error(Sunspot::NoAdapterError)
16
16
  end
17
+
18
+ it "registers adapters found by ancestor lookup with the descendant class" do
19
+ Sunspot::Adapters::InstanceAdapter::registered_adapter_for(UnseenModel).should be(nil)
20
+ Sunspot::Adapters::InstanceAdapter::for(UnseenModel)
21
+ Sunspot::Adapters::InstanceAdapter::registered_adapter_for(UnseenModel).should be(AbstractModelInstanceAdapter)
22
+ end
17
23
  end
18
24
 
19
25
  describe Sunspot::Adapters::DataAccessor do
@@ -30,6 +36,12 @@ describe Sunspot::Adapters::DataAccessor do
30
36
  Sunspot::Adapters::DataAccessor::for(Module.new)
31
37
  end.should raise_error(Sunspot::NoAdapterError)
32
38
  end
39
+
40
+ it "registers adapters found by ancestor lookup with the descendant class" do
41
+ Sunspot::Adapters::DataAccessor::registered_accessor_for(UnseenModel).should be(nil)
42
+ Sunspot::Adapters::DataAccessor::for(UnseenModel)
43
+ Sunspot::Adapters::DataAccessor::registered_accessor_for(UnseenModel).should be(AbstractModelDataAccessor)
44
+ end
33
45
  end
34
46
 
35
47
  describe Sunspot::Adapters::Registry do
@@ -10,21 +10,21 @@ shared_examples_for 'fulltext query' do
10
10
  search do
11
11
  keywords ''
12
12
  end
13
- connection.should_not have_last_search_with(:defType => 'dismax')
13
+ connection.should_not have_last_search_with(:defType => 'edismax')
14
14
  end
15
15
 
16
16
  it 'ignores keywords if nil' do
17
17
  search do
18
18
  keywords nil
19
19
  end
20
- connection.should_not have_last_search_with(:defType => 'dismax')
20
+ connection.should_not have_last_search_with(:defType => 'edismax')
21
21
  end
22
22
 
23
23
  it 'ignores keywords with only whitespace' do
24
24
  search do
25
25
  keywords " \t"
26
26
  end
27
- connection.should_not have_last_search_with(:defType => 'dismax')
27
+ connection.should_not have_last_search_with(:defType => 'edismax')
28
28
  end
29
29
 
30
30
  it 'gracefully ignores keywords block if keywords ignored' do
@@ -37,7 +37,7 @@ shared_examples_for 'fulltext query' do
37
37
  search do
38
38
  keywords 'keyword search'
39
39
  end
40
- connection.should have_last_search_with(:defType => 'dismax')
40
+ connection.should have_last_search_with(:defType => 'edismax')
41
41
  end
42
42
 
43
43
  it 'searches types in filter query if keywords used' do
@@ -41,11 +41,11 @@ shared_examples_for 'geohash query' do
41
41
  fulltext 'pizza', :fields => :title
42
42
  with(:coordinates).near(40.7, -73.5)
43
43
  end
44
- expected =
45
- "{!dismax fl='* score' qf='title_text'}pizza (#{build_geo_query})"
44
+ expected =
45
+ "{!edismax fl='* score' qf='title_text'}pizza (#{build_geo_query})"
46
46
  connection.should have_last_search_including(
47
47
  :q,
48
- %Q(_query_:"{!dismax qf='title_text'}pizza" (#{build_geo_query}))
48
+ %Q(_query_:"{!edismax qf='title_text'}pizza" (#{build_geo_query}))
49
49
  )
50
50
  end
51
51
 
@@ -57,7 +57,7 @@ shared_examples_for 'geohash query' do
57
57
  boost = options[:boost] || 1.0
58
58
  hash = 'dr5xx3nytvgs'
59
59
  (precision..12).map do |i|
60
- phrase =
60
+ phrase =
61
61
  if i == 12 then hash
62
62
  else "#{hash[0, i]}*"
63
63
  end
@@ -68,7 +68,7 @@ describe Sunspot::SessionProxy::ClassShardingSessionProxy do
68
68
 
69
69
  [:dirty, :delete_dirty].each do |method|
70
70
  it "should be dirty if any of the sessions are dirty" do
71
- @proxy.post_session.stub!(:"#{method}?").and_return(true)
71
+ @proxy.post_session.stub(:"#{method}?").and_return(true)
72
72
  @proxy.should send("be_#{method}")
73
73
  end
74
74
 
@@ -53,7 +53,7 @@ describe Sunspot::SessionProxy::Retry5xxSessionProxy do
53
53
 
54
54
  @sunspot_session.should_receive(:index).and_return do
55
55
  @sunspot_session.should_receive(:index).and_return do
56
- @sunspot_session.stub!(:index).and_return(fake_success)
56
+ @sunspot_session.stub(:index).and_return(fake_success)
57
57
  raise e
58
58
  end
59
59
  raise e
@@ -60,7 +60,7 @@ describe Sunspot::SessionProxy::ShardingSessionProxy do
60
60
 
61
61
  [:dirty, :delete_dirty].each do |method|
62
62
  it "should be dirty if any of the sessions are dirty" do
63
- @proxy.sessions[0].stub!(:"#{method}?").and_return(true)
63
+ @proxy.sessions[0].stub(:"#{method}?").and_return(true)
64
64
  @proxy.should send("be_#{method}")
65
65
  end
66
66
 
@@ -13,7 +13,7 @@ describe Sunspot::SessionProxy::ShardingSessionProxy do
13
13
  it "should call rescued_exception when an exception is caught" do
14
14
  SUPPORTED_METHODS.each do |method|
15
15
  e = FakeException.new(method)
16
- @search_session.stub!(method).and_raise(e)
16
+ @search_session.stub(method).and_raise(e)
17
17
  @proxy.should_receive(:rescued_exception).with(method, e)
18
18
  @proxy.send(method)
19
19
  end
@@ -77,7 +77,7 @@ describe 'Session' do
77
77
 
78
78
  it 'should open connection with defaults if nothing specified' do
79
79
  Sunspot.commit
80
- connection.opts[:url].should == 'http://127.0.0.1:8983/solr'
80
+ connection.opts[:url].should == 'http://127.0.0.1:8983/solr/default'
81
81
  end
82
82
 
83
83
  it 'should open a connection with custom host' do
@@ -217,7 +217,7 @@ describe 'Session' do
217
217
 
218
218
  context 'session proxy' do
219
219
  it 'should send messages to manually assigned session proxy' do
220
- stub_session = stub!('session')
220
+ stub_session = stub('session')
221
221
  Sunspot.session = stub_session
222
222
  post = Post.new
223
223
  stub_session.should_receive(:index).with(post)
@@ -1,7 +1,7 @@
1
1
  module IntegrationHelper
2
2
  def self.included(base)
3
3
  base.before(:all) do
4
- Sunspot.config.solr.url = ENV['SOLR_URL'] || 'http://localhost:8983/solr'
4
+ Sunspot.config.solr.url = ENV['SOLR_URL'] || 'http://localhost:8983/solr/default'
5
5
  Sunspot.reset!(true)
6
6
  end
7
7
  end
@@ -11,7 +11,7 @@ module QueryHelper
11
11
  def subqueries(param)
12
12
  q = connection.searches.last[:q]
13
13
  subqueries = []
14
- subqueries = q.scan(%r(_query_:"\{!dismax (.*?)\}(.*?)"))
14
+ subqueries = q.scan(%r(_query_:"\{!edismax (.*?)\}(.*?)"))
15
15
  subqueries.map do |subquery|
16
16
  params = {}
17
17
  subquery[0].scan(%r((\S+?)='(.+?)')) do |key, value|
@@ -16,6 +16,34 @@ describe 'keyword search' do
16
16
  Sunspot.index!(@comment)
17
17
  end
18
18
 
19
+ context 'edismax' do
20
+ it 'matches with wildcards' do
21
+ results = Sunspot.search(Post) { keywords '*oas*' }.results
22
+ [0,2].each { |i| results.should include(@posts[i])}
23
+ [1].each { |i| results.should_not include(@posts[i])}
24
+ end
25
+
26
+ it 'matches multiple keywords on different fields with wildcards using subqueries' do
27
+ results = Sunspot.search(Post) do
28
+ keywords 'insuffic*',:fields=>[:title]
29
+ keywords 'win*',:fields=>[:body]
30
+ end.results
31
+ [0].each {|i| results.should include(@posts[i])}
32
+ [1,2].each {|i| results.should_not include(@posts[i])}
33
+ end
34
+
35
+ it 'matches with proximity' do
36
+ results = Sunspot.search(Post) { keywords '"wind buffer"~4' }.results
37
+ [0,1].each {|i| results.should_not include(@posts[i])}
38
+ [2].each {|i| results.should include(@posts[i])}
39
+ end
40
+
41
+ it 'does not match if not within proximity' do
42
+ results = Sunspot.search(Post) { keywords '"wind buffer"~1' }.results
43
+ results.should == []
44
+ end
45
+ end
46
+
19
47
  it 'matches a single keyword out of a single field' do
20
48
  results = Sunspot.search(Post) { keywords 'toast' }.results
21
49
  [0, 2].each { |i| results.should include(@posts[i]) }
@@ -266,6 +266,44 @@ describe 'scoped_search' do
266
266
  end.results.should == posts[0..1]
267
267
  end
268
268
 
269
+ it 'should return results, ignoring any restriction in a disjunction that has been passed an empty array' do
270
+ posts = (1..3).map { |i| Post.new(:blog_id => i)}
271
+ Sunspot.index!(posts)
272
+ Sunspot.search(Post) do
273
+ with(:blog_id, [])
274
+ end.results.should == posts
275
+ end
276
+
277
+ it 'should return results, ignoring any restriction in a negative disjunction that has been passed an empty array' do
278
+ posts = (1..3).map { |i| Post.new(:blog_id => i)}
279
+ Sunspot.index!(posts)
280
+ Sunspot.search(Post) do
281
+ without(:blog_id, [])
282
+ end.results.should == posts
283
+ end
284
+
285
+ it 'should return results, ignoring any restriction in a conjunction that has been passed an empty array' do
286
+ posts = (1..3).map { |i| Post.new(:blog_id => i)}
287
+ Sunspot.index!(posts)
288
+ Sunspot.search(Post) do
289
+ all_of do
290
+ with(:blog_id, 1)
291
+ with(:blog_id, [])
292
+ end
293
+ end.results.should == posts[0..0]
294
+ end
295
+
296
+ it 'should return results, ignoring any restriction in a negative conjunction that has been passed an empty array' do
297
+ posts = (1..3).map { |i| Post.new(:blog_id => i)}
298
+ Sunspot.index!(posts)
299
+ Sunspot.search(Post) do
300
+ all_of do
301
+ with(:blog_id, 1)
302
+ without(:blog_id, [])
303
+ end
304
+ end.results.should == posts[0..0]
305
+ end
306
+
269
307
  it 'should return results that match a nested conjunction in a disjunction' do
270
308
  posts = [
271
309
  Post.new(:title => 'No', :blog_id => 1),
@@ -379,7 +417,7 @@ describe 'scoped_search' do
379
417
 
380
418
  it 'should order randomly (run this test again if it fails)' do
381
419
  result_sets = Array.new(2) do
382
- Sunspot.search(Post) { order_by_random }.results.map do |result|
420
+ Sunspot.search(Post) { order_by(:random) }.results.map do |result|
383
421
  result.id
384
422
  end
385
423
  end
@@ -4,6 +4,9 @@ end
4
4
  class Model < AbstractModel
5
5
  end
6
6
 
7
+ class UnseenModel < AbstractModel
8
+ end
9
+
7
10
  class AbstractModelInstanceAdapter < Sunspot::Adapters::InstanceAdapter
8
11
  end
9
12
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: sunspot
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.0.0
4
+ version: 2.1.0
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -27,7 +27,7 @@ authors:
27
27
  autorequire:
28
28
  bindir: bin
29
29
  cert_chain: []
30
- date: 2013-02-26 00:00:00.000000000 Z
30
+ date: 2013-10-25 00:00:00.000000000 Z
31
31
  dependencies:
32
32
  - !ruby/object:Gem::Dependency
33
33
  name: rsolr
@@ -82,7 +82,7 @@ dependencies:
82
82
  requirement: !ruby/object:Gem::Requirement
83
83
  none: false
84
84
  requirements:
85
- - - ! '>='
85
+ - - '>='
86
86
  - !ruby/object:Gem::Version
87
87
  version: '0'
88
88
  type: :development
@@ -90,14 +90,13 @@ dependencies:
90
90
  version_requirements: !ruby/object:Gem::Requirement
91
91
  none: false
92
92
  requirements:
93
- - - ! '>='
93
+ - - '>='
94
94
  - !ruby/object:Gem::Version
95
95
  version: '0'
96
- description: ! " Sunspot is a library providing a powerful, all-ruby API for the
97
- Solr search engine. Sunspot manages the configuration of persistent\n Ruby classes
98
- for search and indexing and exposes Solr's most powerful features through a collection
99
- of DSLs. Complex search operations\n can be performed without hand-writing any
100
- boolean queries or building Solr parameters by hand.\n"
96
+ description: |2
97
+ Sunspot is a library providing a powerful, all-ruby API for the Solr search engine. Sunspot manages the configuration of persistent
98
+ Ruby classes for search and indexing and exposes Solr's most powerful features through a collection of DSLs. Complex search operations
99
+ can be performed without hand-writing any boolean queries or building Solr parameters by hand.
101
100
  email:
102
101
  - mat@patch.com
103
102
  executables: []
@@ -299,18 +298,18 @@ require_paths:
299
298
  required_ruby_version: !ruby/object:Gem::Requirement
300
299
  none: false
301
300
  requirements:
302
- - - ! '>='
301
+ - - '>='
303
302
  - !ruby/object:Gem::Version
304
303
  version: '0'
305
304
  required_rubygems_version: !ruby/object:Gem::Requirement
306
305
  none: false
307
306
  requirements:
308
- - - ! '>='
307
+ - - '>='
309
308
  - !ruby/object:Gem::Version
310
309
  version: '0'
311
310
  requirements: []
312
311
  rubyforge_project: sunspot
313
- rubygems_version: 1.8.23
312
+ rubygems_version: 1.8.25
314
313
  signing_key:
315
314
  specification_version: 3
316
315
  summary: Library for expressive, powerful interaction with the Solr search engine