benjaminkrause-sunspot 0.9.7

Sign up to get free protection for your applications and to get access to all the features.
Files changed (138) hide show
  1. data/History.txt +107 -0
  2. data/LICENSE +18 -0
  3. data/README.rdoc +159 -0
  4. data/Rakefile +9 -0
  5. data/TODO +11 -0
  6. data/VERSION.yml +4 -0
  7. data/bin/sunspot-configure-solr +46 -0
  8. data/bin/sunspot-solr +86 -0
  9. data/lib/light_config.rb +40 -0
  10. data/lib/sunspot/adapters.rb +265 -0
  11. data/lib/sunspot/composite_setup.rb +184 -0
  12. data/lib/sunspot/configuration.rb +49 -0
  13. data/lib/sunspot/data_extractor.rb +50 -0
  14. data/lib/sunspot/dsl/field_query.rb +77 -0
  15. data/lib/sunspot/dsl/fields.rb +95 -0
  16. data/lib/sunspot/dsl/fulltext.rb +106 -0
  17. data/lib/sunspot/dsl/query.rb +107 -0
  18. data/lib/sunspot/dsl/query_facet.rb +31 -0
  19. data/lib/sunspot/dsl/restriction.rb +25 -0
  20. data/lib/sunspot/dsl/scope.rb +193 -0
  21. data/lib/sunspot/dsl/search.rb +30 -0
  22. data/lib/sunspot/dsl.rb +4 -0
  23. data/lib/sunspot/facet.rb +24 -0
  24. data/lib/sunspot/facet_data.rb +152 -0
  25. data/lib/sunspot/facet_row.rb +12 -0
  26. data/lib/sunspot/field.rb +148 -0
  27. data/lib/sunspot/field_factory.rb +141 -0
  28. data/lib/sunspot/indexer.rb +129 -0
  29. data/lib/sunspot/instantiated_facet.rb +45 -0
  30. data/lib/sunspot/instantiated_facet_row.rb +27 -0
  31. data/lib/sunspot/query/base_query.rb +55 -0
  32. data/lib/sunspot/query/boost_query.rb +20 -0
  33. data/lib/sunspot/query/connective.rb +148 -0
  34. data/lib/sunspot/query/dynamic_query.rb +61 -0
  35. data/lib/sunspot/query/field_facet.rb +129 -0
  36. data/lib/sunspot/query/field_query.rb +69 -0
  37. data/lib/sunspot/query/fulltext_base_query.rb +86 -0
  38. data/lib/sunspot/query/highlighting.rb +36 -0
  39. data/lib/sunspot/query/local.rb +24 -0
  40. data/lib/sunspot/query/pagination.rb +39 -0
  41. data/lib/sunspot/query/query_facet.rb +78 -0
  42. data/lib/sunspot/query/query_facet_row.rb +19 -0
  43. data/lib/sunspot/query/query_field_facet.rb +20 -0
  44. data/lib/sunspot/query/restriction.rb +272 -0
  45. data/lib/sunspot/query/scope.rb +185 -0
  46. data/lib/sunspot/query/sort.rb +105 -0
  47. data/lib/sunspot/query/sort_composite.rb +33 -0
  48. data/lib/sunspot/query/text_field_boost.rb +15 -0
  49. data/lib/sunspot/query.rb +108 -0
  50. data/lib/sunspot/schema.rb +147 -0
  51. data/lib/sunspot/search/highlight.rb +38 -0
  52. data/lib/sunspot/search/hit.rb +113 -0
  53. data/lib/sunspot/search.rb +240 -0
  54. data/lib/sunspot/session.rb +206 -0
  55. data/lib/sunspot/setup.rb +312 -0
  56. data/lib/sunspot/text_field_setup.rb +29 -0
  57. data/lib/sunspot/type.rb +200 -0
  58. data/lib/sunspot/util.rb +190 -0
  59. data/lib/sunspot.rb +459 -0
  60. data/solr/etc/jetty.xml +212 -0
  61. data/solr/etc/webdefault.xml +379 -0
  62. data/solr/lib/jetty-6.1.3.jar +0 -0
  63. data/solr/lib/jetty-util-6.1.3.jar +0 -0
  64. data/solr/lib/jsp-2.1/ant-1.6.5.jar +0 -0
  65. data/solr/lib/jsp-2.1/core-3.1.1.jar +0 -0
  66. data/solr/lib/jsp-2.1/jsp-2.1.jar +0 -0
  67. data/solr/lib/jsp-2.1/jsp-api-2.1.jar +0 -0
  68. data/solr/lib/servlet-api-2.5-6.1.3.jar +0 -0
  69. data/solr/solr/conf/elevate.xml +36 -0
  70. data/solr/solr/conf/protwords.txt +21 -0
  71. data/solr/solr/conf/schema.xml +64 -0
  72. data/solr/solr/conf/solrconfig.xml +726 -0
  73. data/solr/solr/conf/stopwords.txt +57 -0
  74. data/solr/solr/conf/synonyms.txt +31 -0
  75. data/solr/start.jar +0 -0
  76. data/solr/webapps/solr.war +0 -0
  77. data/spec/api/adapters_spec.rb +33 -0
  78. data/spec/api/indexer/attributes_spec.rb +100 -0
  79. data/spec/api/indexer/batch_spec.rb +46 -0
  80. data/spec/api/indexer/dynamic_fields_spec.rb +33 -0
  81. data/spec/api/indexer/fixed_fields_spec.rb +57 -0
  82. data/spec/api/indexer/fulltext_spec.rb +43 -0
  83. data/spec/api/indexer/removal_spec.rb +46 -0
  84. data/spec/api/indexer/spec_helper.rb +1 -0
  85. data/spec/api/indexer_spec.rb +4 -0
  86. data/spec/api/query/connectives_spec.rb +161 -0
  87. data/spec/api/query/dsl_spec.rb +12 -0
  88. data/spec/api/query/dynamic_fields_spec.rb +148 -0
  89. data/spec/api/query/faceting_spec.rb +272 -0
  90. data/spec/api/query/fulltext_spec.rb +152 -0
  91. data/spec/api/query/highlighting_spec.rb +82 -0
  92. data/spec/api/query/local_spec.rb +37 -0
  93. data/spec/api/query/ordering_pagination_spec.rb +95 -0
  94. data/spec/api/query/scope_spec.rb +253 -0
  95. data/spec/api/query/spec_helper.rb +1 -0
  96. data/spec/api/query/text_field_scoping_spec.rb +30 -0
  97. data/spec/api/query/types_spec.rb +20 -0
  98. data/spec/api/search/dynamic_fields_spec.rb +27 -0
  99. data/spec/api/search/faceting_spec.rb +206 -0
  100. data/spec/api/search/highlighting_spec.rb +65 -0
  101. data/spec/api/search/hits_spec.rb +62 -0
  102. data/spec/api/search/results_spec.rb +52 -0
  103. data/spec/api/search/search_spec.rb +11 -0
  104. data/spec/api/search/spec_helper.rb +1 -0
  105. data/spec/api/session_spec.rb +157 -0
  106. data/spec/api/spec_helper.rb +1 -0
  107. data/spec/api/sunspot_spec.rb +18 -0
  108. data/spec/helpers/indexer_helper.rb +29 -0
  109. data/spec/helpers/query_helper.rb +13 -0
  110. data/spec/helpers/search_helper.rb +78 -0
  111. data/spec/integration/dynamic_fields_spec.rb +55 -0
  112. data/spec/integration/faceting_spec.rb +169 -0
  113. data/spec/integration/highlighting_spec.rb +22 -0
  114. data/spec/integration/keyword_search_spec.rb +148 -0
  115. data/spec/integration/local_search_spec.rb +47 -0
  116. data/spec/integration/scoped_search_spec.rb +303 -0
  117. data/spec/integration/spec_helper.rb +1 -0
  118. data/spec/integration/stored_fields_spec.rb +10 -0
  119. data/spec/integration/test_pagination.rb +32 -0
  120. data/spec/mocks/adapters.rb +32 -0
  121. data/spec/mocks/blog.rb +3 -0
  122. data/spec/mocks/comment.rb +19 -0
  123. data/spec/mocks/connection.rb +84 -0
  124. data/spec/mocks/mock_adapter.rb +30 -0
  125. data/spec/mocks/mock_record.rb +48 -0
  126. data/spec/mocks/photo.rb +8 -0
  127. data/spec/mocks/post.rb +75 -0
  128. data/spec/mocks/super_class.rb +2 -0
  129. data/spec/mocks/user.rb +8 -0
  130. data/spec/spec_helper.rb +60 -0
  131. data/tasks/gemspec.rake +35 -0
  132. data/tasks/rcov.rake +28 -0
  133. data/tasks/rdoc.rake +22 -0
  134. data/tasks/schema.rake +19 -0
  135. data/tasks/spec.rake +24 -0
  136. data/tasks/todo.rake +4 -0
  137. data/templates/schema.xml.erb +36 -0
  138. metadata +312 -0
@@ -0,0 +1,148 @@
1
+ require File.join(File.dirname(__FILE__), 'spec_helper')
2
+
3
+ describe 'keyword search' do
4
+ describe 'generally' do
5
+ before :all do
6
+ Sunspot.remove_all
7
+ @posts = []
8
+ @posts << Post.new(:title => 'The toast elects the insufficient spirit',
9
+ :body => 'Does the wind write?')
10
+ @posts << Post.new(:title => 'A nail abbreviates the recovering insight outside the moron',
11
+ :body => 'The interpreted strain scans the buffer around the upper temper')
12
+ @posts << Post.new(:title => 'The toast abbreviates the recovering spirit',
13
+ :body => 'Does the wind interpret the buffer, moron?')
14
+ Sunspot.index!(*@posts)
15
+ @comment = Namespaced::Comment.new(:body => 'Hey there where ya goin, not exactly knowin, who says you have to call just one place toast.')
16
+ Sunspot.index!(@comment)
17
+ end
18
+
19
+ it 'matches a single keyword out of a single field' do
20
+ results = Sunspot.search(Post) { keywords 'toast' }.results
21
+ [0, 2].each { |i| results.should include(@posts[i]) }
22
+ [1].each { |i| results.should_not include(@posts[i]) }
23
+ end
24
+
25
+ it 'matches multiple words out of a single field' do
26
+ results = Sunspot.search(Post) { keywords 'elects toast' }.results
27
+ results.should == [@posts[0]]
28
+ end
29
+
30
+ it 'matches multiple words in multiple fields' do
31
+ results = Sunspot.search(Post) { keywords 'toast wind' }.results
32
+ [0, 2].each { |i| results.should include(@posts[i]) }
33
+ [1].each { |i| results.should_not include(@posts[i]) }
34
+ end
35
+
36
+ it 'matches multiple types' do
37
+ results = Sunspot.search(Post, Namespaced::Comment) do
38
+ keywords 'toast'
39
+ end.results
40
+ [@posts[0], @posts[2], @comment].each { |obj| results.should include(obj) }
41
+ results.should_not include(@posts[1])
42
+ end
43
+
44
+ it 'matches keywords from only the fields specified' do
45
+ results = Sunspot.search(Post) do
46
+ keywords 'moron', :fields => [:title]
47
+ end.results
48
+ results.should == [@posts[1]]
49
+ end
50
+ end
51
+
52
+ describe 'with field boost' do
53
+ before :all do
54
+ Sunspot.remove_all
55
+ @posts = [:title, :body].map { |field| Post.new(field => 'rhinoceros') }
56
+ Sunspot.index!(*@posts)
57
+ end
58
+
59
+ it 'should assign a higher score to the result matching the higher-boosted field' do
60
+ search = Sunspot.search(Post) { keywords 'rhinoceros' }
61
+ search.hits.map { |hit| hit.primary_key }.should ==
62
+ @posts.map { |post| post.id.to_s }
63
+ search.hits.first.score.should > search.hits.last.score
64
+ end
65
+ end
66
+
67
+ describe 'with document boost' do
68
+ before :all do
69
+ Sunspot.remove_all
70
+ @posts = [4.0, 2.0].map do |rating|
71
+ Post.new(:title => 'Test', :ratings_average => rating)
72
+ end
73
+ Sunspot.index!(*@posts)
74
+ end
75
+
76
+ it 'should assign a higher score to the higher-boosted document' do
77
+ search = Sunspot.search(Post) { keywords 'test' }
78
+ search.hits.map { |hit| hit.primary_key }.should ==
79
+ @posts.map { |post| post.id.to_s }
80
+ search.hits.first.score.should > search.hits.last.score
81
+ end
82
+ end
83
+
84
+ describe 'with search-time boost' do
85
+ before :each do
86
+ Sunspot.remove_all
87
+ @comments = [
88
+ Namespaced::Comment.new(:body => 'test text'),
89
+ Namespaced::Comment.new(:author_name => 'test text')
90
+ ]
91
+ Sunspot.index!(@comments)
92
+ end
93
+
94
+ it 'assigns a higher score to documents in which all words appear in the phrase field' do
95
+ hits = Sunspot.search(Namespaced::Comment) do
96
+ keywords 'test text' do
97
+ phrase_fields :body
98
+ end
99
+ end.hits
100
+ hits.first.instance.should == @comments.first
101
+ hits.first.score.should > hits.last.score
102
+ end
103
+
104
+ it 'assigns a higher score to documents in which the search terms appear in a boosted field' do
105
+ hits = Sunspot.search(Namespaced::Comment) do
106
+ keywords 'test' do
107
+ fields :body => 2.0, :author_name => 0.75
108
+ end
109
+ end.hits
110
+ hits.first.instance.should == @comments.first
111
+ hits.first.score.should > hits.last.score
112
+ end
113
+
114
+ it 'assigns a higher score to documents in which the search terms appear in a higher boosted phrase field' do
115
+ hits = Sunspot.search(Namespaced::Comment) do
116
+ keywords 'test text' do
117
+ phrase_fields :body => 2.0, :author_name => 0.75
118
+ end
119
+ end.hits
120
+ hits.first.instance.should == @comments.first
121
+ hits.first.score.should > hits.last.score
122
+ end
123
+ end
124
+
125
+ describe 'boost query' do
126
+ before :all do
127
+ Sunspot.remove_all
128
+ Sunspot.index!(
129
+ @posts = [
130
+ Post.new(:title => 'Rhino', :featured => true),
131
+ Post.new(:title => 'Rhino', :featured => false)
132
+ ]
133
+ )
134
+ end
135
+
136
+ it 'should assign a higher boost to the document matching the boost query' do
137
+ search = Sunspot.search(Post) do
138
+ keywords('rhino') do
139
+ boost(2.0) do
140
+ with(:featured, true)
141
+ end
142
+ end
143
+ end
144
+ search.results.should == @posts
145
+ search.hits[0].score.should > search.hits[1].score
146
+ end
147
+ end
148
+ end
@@ -0,0 +1,47 @@
1
+ require File.join(File.dirname(__FILE__), 'spec_helper')
2
+
3
+ describe 'local search' do
4
+ ORIGIN = [40.6749113, -73.9648859]
5
+ before :each do
6
+ Sunspot.remove_all
7
+ @posts = [
8
+ Post.new(:coordinates => ORIGIN),
9
+ Post.new(:coordinates => [40.725304, -73.997211], :title => 'teacup'),
10
+ Post.new(:coordinates => [40.800069, -73.962283]),
11
+ Post.new(:coordinates => [43.706488, -72.292233]),
12
+ Post.new(:coordinates => [38.920303, -77.110934], :title => 'teacup'),
13
+ Post.new(:coordinates => [47.661557, -122.349938])
14
+ ]
15
+ @posts.each_with_index { |post, i| post.blog_id = @posts.length - i }
16
+ Sunspot.index!(@posts)
17
+ end
18
+
19
+ it 'should find all the posts within a given radius' do
20
+ search = Sunspot.search(Post) { |query| query.near(ORIGIN, 20) }
21
+ search.results.to_set.should == @posts[0..2].to_set
22
+ end
23
+
24
+ it 'should perform a radial search with fulltext matching' do
25
+ search = Sunspot.search(Post) do |query|
26
+ query.keywords 'teacup'
27
+ query.near(ORIGIN, 20)
28
+ end
29
+ search.results.should == [@posts[1]]
30
+ end
31
+
32
+ it 'should order by arbitrary field' do
33
+ search = Sunspot.search(Post) do |query|
34
+ query.near(ORIGIN, 20)
35
+ query.order_by(:blog_id)
36
+ end
37
+ search.results.should == @posts[0..2].reverse
38
+ end
39
+
40
+ it 'should order by geo distance' do
41
+ search = Sunspot.search(Post) do |query|
42
+ query.near(ORIGIN, 20)
43
+ query.order_by(:distance)
44
+ end
45
+ search.results.should == @posts[0..2]
46
+ end
47
+ end
@@ -0,0 +1,303 @@
1
+ require File.join(File.dirname(__FILE__), 'spec_helper')
2
+
3
+ describe 'scoped_search' do
4
+ def self.test_field_type(name, attribute, field, *values)
5
+ raise(ArgumentError, 'Please supply five values') unless values.length == 5
6
+
7
+ context "with field of type #{name}" do
8
+ before :all do
9
+ Sunspot.remove_all
10
+ @posts = values.map do |value|
11
+ post = Post.new(attribute => value)
12
+ Sunspot.index(post)
13
+ post
14
+ end
15
+ Sunspot.commit
16
+ end
17
+
18
+ it 'should filter by exact match' do
19
+ Sunspot.search(Post) { with(field, values[2]) }.results.should == [@posts[2]]
20
+ end
21
+
22
+ it 'should reject by inexact match' do
23
+ results = Sunspot.search(Post) { without(field, values[2]) }.results
24
+ [0, 1, 3, 4].each { |i| results.should include(@posts[i]) }
25
+ results.should_not include(@posts[2])
26
+ end
27
+
28
+ it 'should filter by less than' do
29
+ results = Sunspot.search(Post) { with(field).less_than values[2] }.results
30
+ (0..2).each { |i| results.should include(@posts[i]) }
31
+ (3..4).each { |i| results.should_not include(@posts[i]) }
32
+ end
33
+
34
+ it 'should reject by less than' do
35
+ results = Sunspot.search(Post) { without(field).less_than values[2] }.results
36
+ (0..2).each { |i| results.should_not include(@posts[i]) }
37
+ (3..4).each { |i| results.should include(@posts[i]) }
38
+ end
39
+
40
+ it 'should filter by greater than' do
41
+ results = Sunspot.search(Post) { with(field).greater_than values[2] }.results
42
+ (2..4).each { |i| results.should include(@posts[i]) }
43
+ (0..1).each { |i| results.should_not include(@posts[i]) }
44
+ end
45
+
46
+ it 'should reject by greater than' do
47
+ results = Sunspot.search(Post) { without(field).greater_than values[2] }.results
48
+ (2..4).each { |i| results.should_not include(@posts[i]) }
49
+ (0..1).each { |i| results.should include(@posts[i]) }
50
+ end
51
+
52
+ it 'should filter by between' do
53
+ results = Sunspot.search(Post) { with(field).between(values[1]..values[3]) }.results
54
+ (1..3).each { |i| results.should include(@posts[i]) }
55
+ [0, 4].each { |i| results.should_not include(@posts[i]) }
56
+ end
57
+
58
+ it 'should reject by between' do
59
+ results = Sunspot.search(Post) { without(field).between(values[1]..values[3]) }.results
60
+ (1..3).each { |i| results.should_not include(@posts[i]) }
61
+ [0, 4].each { |i| results.should include(@posts[i]) }
62
+ end
63
+
64
+ it 'should filter by any of' do
65
+ results = Sunspot.search(Post) { with(field).any_of(values.values_at(1, 3)) }.results
66
+ [1, 3].each { |i| results.should include(@posts[i]) }
67
+ [0, 2, 4].each { |i| results.should_not include(@posts[i]) }
68
+ end
69
+
70
+ it 'should reject by any of' do
71
+ results = Sunspot.search(Post) { without(field).any_of(values.values_at(1, 3)) }.results
72
+ [1, 3].each { |i| results.should_not include(@posts[i]) }
73
+ [0, 2, 4].each { |i| results.should include(@posts[i]) }
74
+ end
75
+
76
+ it 'should order by field ascending' do
77
+ results = Sunspot.search(Post) { order_by field, :asc }.results
78
+ results.should == @posts
79
+ end
80
+
81
+ it 'should order by field descending' do
82
+ results = Sunspot.search(Post) { order_by field, :desc }.results
83
+ results.should == @posts.reverse
84
+ end
85
+ end
86
+ end
87
+
88
+ test_field_type 'String', :title, :title, 'apple pie', 'banana split', 'cherry tart', 'date pastry', 'eggplant a la mode'
89
+ test_field_type 'Integer', :blog_id, :blog_id, -2, 0, 3, 12, 20
90
+ test_field_type 'Float', :ratings_average, :average_rating, -2.5, 0.0, 3.2, 3.5, 16.0
91
+ test_field_type 'Time', :published_at, :published_at, *(['1970-01-01 00:00:00 UTC', '1983-07-08 04:00:00 UTC', '1983-07-08 02:00:00 -0500',
92
+ '2005-11-05 10:00:00 UTC', Time.now.to_s].map { |t| Time.parse(t) })
93
+
94
+ describe 'Boolean field type' do
95
+ before :all do
96
+ Sunspot.remove_all
97
+ @posts = [Post.new(:featured => true), Post.new(:featured => false), Post.new]
98
+ Sunspot.index!(@posts)
99
+ end
100
+
101
+ it 'should filter by exact match for true' do
102
+ Sunspot.search(Post) { with(:featured, true) }.results.should == [@posts[0]]
103
+ end
104
+
105
+ it 'should filter for exact match for false' do
106
+ Sunspot.search(Post) { with(:featured, false) }.results.should == [@posts[1]]
107
+ end
108
+ end
109
+
110
+ describe 'passing nil value to equal' do
111
+ before :all do
112
+ Sunspot.remove_all
113
+ @posts = [Post.new(:title => 'apple'), Post.new]
114
+ Sunspot.index!(@posts)
115
+ end
116
+
117
+ it 'should filter results without value for field' do
118
+ Sunspot.search(Post) { with(:title, nil) }.results.should == [@posts[1]]
119
+ end
120
+
121
+ it 'should exclude results without value for field' do
122
+ Sunspot.search(Post) { without(:title, nil) }.results.should == [@posts[0]]
123
+ end
124
+ end
125
+
126
+ describe 'prefix searching' do
127
+ before :each do
128
+ Sunspot.remove_all
129
+ @posts = ['test', 'test post', 'some test', 'bogus'].map do |title|
130
+ Post.new(:title => title)
131
+ end
132
+ Sunspot.index!(@posts)
133
+ end
134
+
135
+ it 'should return results whose prefix matches' do
136
+ Sunspot.search(Post) { with(:title).starting_with('test') }.results.should == @posts[0..1]
137
+ end
138
+ end
139
+
140
+ describe 'exclusion by identity' do
141
+ before do
142
+ @posts = (1..5).map do |i|
143
+ post = Post.new
144
+ Sunspot.index(post)
145
+ post
146
+ end
147
+ Sunspot.commit
148
+ end
149
+
150
+ it 'should not return excluded object' do
151
+ excluded_post = @posts.shift
152
+ Sunspot.search(Post) { without(excluded_post) }.results.should_not include(excluded_post)
153
+ end
154
+
155
+ it 'should return objects not excluded' do
156
+ excluded_post = @posts.shift
157
+ for included_post in @posts
158
+ Sunspot.search(Post) { without(excluded_post) }.results.should include(included_post)
159
+ end
160
+ end
161
+
162
+ it 'should not return excluded objects' do
163
+ excluded_posts = [@posts.shift, @posts.shift]
164
+ for excluded_post in excluded_posts
165
+ Sunspot.search(Post) { without(excluded_posts) }.results.should_not include(excluded_post)
166
+ end
167
+ end
168
+ end
169
+
170
+ describe 'connectives' do
171
+ before :each do
172
+ Sunspot.remove_all
173
+ end
174
+
175
+ it 'should return results that match any restriction in a disjunction' do
176
+ posts = (1..3).map { |i| Post.new(:blog_id => i)}
177
+ Sunspot.index!(posts)
178
+ Sunspot.search(Post) do
179
+ any_of do
180
+ with(:blog_id, 1)
181
+ with(:blog_id, 2)
182
+ end
183
+ end.results.should == posts[0..1]
184
+ end
185
+
186
+ it 'should return results that match a nested conjunction in a disjunction' do
187
+ posts = [
188
+ Post.new(:title => 'No', :blog_id => 1),
189
+ Post.new(:title => 'Yes', :blog_id => 2),
190
+ Post.new(:title => 'Yes', :blog_id => 3),
191
+ Post.new(:title => 'No', :blog_id => 2)
192
+ ]
193
+ Sunspot.index!(posts)
194
+ Sunspot.search(Post) do
195
+ any_of do
196
+ with(:blog_id, 1)
197
+ all_of do
198
+ with(:blog_id, 2)
199
+ with(:title, 'Yes')
200
+ end
201
+ end
202
+ end.results.should == posts[0..1]
203
+ end
204
+
205
+ it 'should return results that match a conjunction with a negated restriction' do
206
+ posts = [
207
+ Post.new(:title => 'No', :blog_id => 1),
208
+ Post.new(:title => 'Yes', :blog_id => 2),
209
+ Post.new(:title => 'No', :blog_id => 2)
210
+ ]
211
+ Sunspot.index!(posts)
212
+ search = Sunspot.search(Post) do
213
+ any_of do
214
+ with(:blog_id, 1)
215
+ without(:title, 'No')
216
+ end
217
+ end
218
+ search.results.should == posts[0..1]
219
+ end
220
+
221
+ it 'should return results that match a conjunction with a disjunction with a conjunction with a negated restriction' do
222
+ posts = [
223
+ Post.new(:title => 'Yes', :ratings_average => 2.0),
224
+ Post.new(:blog_id => 1, :category_ids => [4], :ratings_average => 2.0),
225
+ Post.new(:blog_id => 1),
226
+ Post.new(:blog_id => 2),
227
+ Post.new(:blog_id => 1, :ratings_average => 2.0)
228
+ ]
229
+ Sunspot.index!(posts)
230
+ search = Sunspot.search(Post) do
231
+ any_of do
232
+ with(:title, 'Yes')
233
+ all_of do
234
+ with(:blog_id, 1)
235
+ any_of do
236
+ with(:category_ids, 4)
237
+ without(:average_rating, 2.0)
238
+ end
239
+ end
240
+ end
241
+ end
242
+ search.results.should == posts[0..2]
243
+ end
244
+
245
+ it 'should return results that match a disjunction with a negated restriction and a nested disjunction in a conjunction with a negated restriction' do
246
+ posts = [
247
+ Post.new,
248
+ Post.new(:title => 'Yes', :blog_id => 1, :category_ids => [4], :ratings_average => 2.0),
249
+ Post.new(:title => 'Yes', :blog_id => 1),
250
+ Post.new(:title => 'Yes'),
251
+ Post.new(:title => 'Yes', :category_ids => [4], :ratings_average => 2.0),
252
+ Post.new(:title => 'Yes', :blog_id => 1, :ratings_average => 2.0)
253
+ ]
254
+ Sunspot.index!(posts)
255
+ search = Sunspot.search(Post) do
256
+ any_of do
257
+ without(:title, 'Yes')
258
+ all_of do
259
+ with(:blog_id, 1)
260
+ any_of do
261
+ with(:category_ids, 4)
262
+ without(:average_rating, 2.0)
263
+ end
264
+ end
265
+ end
266
+ end
267
+ search.results.should == posts[0..2]
268
+ end
269
+ end
270
+
271
+ describe 'multiple column ordering' do
272
+ before do
273
+ Sunspot.remove_all
274
+ @posts = [
275
+ Post.new(:ratings_average => 2, :title => 'banana'),
276
+ Post.new(:ratings_average => 2, :title => 'eggplant'),
277
+ Post.new(:ratings_average => 1, :title => 'apple')
278
+ ].each { |post| Sunspot.index(post) }
279
+ Sunspot.commit
280
+ end
281
+
282
+ it 'should order with precedence given' do
283
+ search = Sunspot.search(Post) do
284
+ order_by :average_rating, :desc
285
+ order_by :sort_title, :asc
286
+ end
287
+ search.results.should == @posts
288
+ end
289
+ end
290
+
291
+ describe 'ordering by random' do
292
+ it 'should order randomly (run this test again if it fails)' do
293
+ Sunspot.remove_all
294
+ Sunspot.index!(Array.new(100) { Post.new })
295
+ result_sets = Array.new(2) do
296
+ Sunspot.search(Post) { order_by_random }.results.map do |result|
297
+ result.id
298
+ end
299
+ end
300
+ result_sets[0].should_not == result_sets[1]
301
+ end
302
+ end
303
+ end
@@ -0,0 +1 @@
1
+ require File.expand_path(File.join(File.dirname(__FILE__), '..', 'spec_helper'))
@@ -0,0 +1,10 @@
1
+ describe 'stored fields' do
2
+ before :all do
3
+ Sunspot.remove_all
4
+ Sunspot.index!(Post.new(:title => 'A Title'))
5
+ end
6
+
7
+ it 'should return stored fields' do
8
+ Sunspot.search(Post).hits.first.stored(:title).should == 'A Title'
9
+ end
10
+ end
@@ -0,0 +1,32 @@
1
+ require File.join(File.dirname(__FILE__), 'spec_helper')
2
+
3
+ describe 'pagination' do
4
+ before :all do
5
+ Sunspot.remove_all
6
+ @posts = (0..19).map do |i|
7
+ Post.new(:blog_id => i)
8
+ end
9
+ Sunspot.index(*@posts)
10
+ end
11
+
12
+ it 'should return all by default' do
13
+ results = Sunspot.search(Post) { order_by :blog_id }.results
14
+ results.should == @posts
15
+ end
16
+
17
+ it 'should return first page of 10' do
18
+ results = Sunspot.search(Post) do
19
+ order_by :blog_id
20
+ paginate :page => 1, :per_page => 10
21
+ end.results
22
+ results.should == @posts[0,10]
23
+ end
24
+
25
+ it 'should return second page of 10' do
26
+ results = Sunspot.search(Post) do
27
+ order_by :blog_id
28
+ paginate :page => 2, :per_page => 10
29
+ end.results
30
+ results.should == @posts[10,10]
31
+ end
32
+ end
@@ -0,0 +1,32 @@
1
+ class AbstractModel
2
+ end
3
+
4
+ class Model < AbstractModel
5
+ end
6
+
7
+ class AbstractModelInstanceAdapter < Sunspot::Adapters::InstanceAdapter
8
+ end
9
+
10
+ class AbstractModelDataAccessor < Sunspot::Adapters::DataAccessor
11
+ end
12
+
13
+ Sunspot::Adapters::InstanceAdapter.register(AbstractModelInstanceAdapter, AbstractModel)
14
+ Sunspot::Adapters::DataAccessor.register(AbstractModelDataAccessor, AbstractModel)
15
+
16
+
17
+ module MixInModel
18
+ end
19
+
20
+ class MixModel
21
+ include MixInModel
22
+ end
23
+
24
+ class MixInModelInstanceAdapter < Sunspot::Adapters::InstanceAdapter
25
+ end
26
+
27
+ class MixInModelDataAccessor < Sunspot::Adapters::DataAccessor
28
+ end
29
+
30
+ Sunspot::Adapters::InstanceAdapter.register(MixInModelInstanceAdapter, MixInModel)
31
+ Sunspot::Adapters::DataAccessor.register(MixInModelDataAccessor, MixInModel)
32
+
@@ -0,0 +1,3 @@
1
+ class Blog < MockRecord
2
+ attr_accessor :name
3
+ end
@@ -0,0 +1,19 @@
1
+ module Namespaced
2
+ class Comment < MockRecord
3
+ attr_reader :id
4
+ attr_accessor :author_name, :published_at, :body, :average_rating, :boost
5
+
6
+ def custom_string
7
+ @custom_string ||= {}
8
+ end
9
+ end
10
+ end
11
+
12
+ Sunspot.setup(Namespaced::Comment) do
13
+ text :body, :author_name
14
+ string :author_name
15
+ time :published_at
16
+ integer :average_rating
17
+ dynamic_string :custom_string
18
+ boost :boost
19
+ end
@@ -0,0 +1,84 @@
1
+ module Mock
2
+ class ConnectionFactory
3
+ def new(adapter = nil, opts = nil)
4
+ if @instance
5
+ raise('Factory can only create an instance once!')
6
+ else
7
+ @instance = Connection.new(adapter, opts)
8
+ end
9
+ end
10
+
11
+ def instance
12
+ @instance ||= Connection.new
13
+ end
14
+ end
15
+
16
+ class Connection
17
+ attr_reader :adds, :commits, :searches
18
+ attr_accessor :adapter, :opts
19
+
20
+ def initialize(adapter = nil, opts = nil)
21
+ @adapter, @opts = adapter, opts
22
+ @adds, @deletes, @deletes_by_query, @commits, @searches = Array.new(5) { [] }
23
+ end
24
+
25
+ def add(documents)
26
+ @adds << Array(documents)
27
+ end
28
+
29
+ def delete_by_id(*ids)
30
+ @deletes << ids
31
+ end
32
+
33
+ def delete_by_query(query)
34
+ @deletes_by_query << query
35
+ end
36
+
37
+ def commit
38
+ @commits << Time.now
39
+ end
40
+
41
+ def select(params)
42
+ @searches << @last_search = params
43
+ end
44
+
45
+ def has_add_with?(*documents)
46
+ @adds.any? do |add|
47
+ documents.all? do |document|
48
+ add.any? do |added|
49
+ if document.is_a?(Hash)
50
+ document.all? do |field, value|
51
+ added.fields_by_name(field).map do |field|
52
+ field.value
53
+ end == Array(value)
54
+ end
55
+ else
56
+ !added.fields_by_name(document).empty?
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
62
+
63
+ def has_delete?(*ids)
64
+ @deletes.any? do |delete|
65
+ delete & ids == ids
66
+ end
67
+ end
68
+
69
+ def has_delete_by_query?(query)
70
+ @deletes_by_query.include?(query)
71
+ end
72
+
73
+ def has_last_search_with?(params)
74
+ return unless @last_search
75
+ if params.respond_to?(:all?)
76
+ params.all? do |key, value|
77
+ @last_search.has_key?(key) && @last_search[key] == value
78
+ end
79
+ else
80
+ @last_search.has_key?(params)
81
+ end
82
+ end
83
+ end
84
+ end