sunspot 2.0.0 → 2.1.0

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