sunspot 0.9.8 → 0.10.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (115) hide show
  1. data/History.txt +32 -0
  2. data/README.rdoc +40 -3
  3. data/TODO +10 -8
  4. data/VERSION.yml +2 -2
  5. data/bin/sunspot-configure-solr +22 -28
  6. data/bin/sunspot-solr +50 -29
  7. data/lib/sunspot/adapters.rb +1 -1
  8. data/lib/sunspot/composite_setup.rb +13 -15
  9. data/lib/sunspot/configuration.rb +14 -0
  10. data/lib/sunspot/data_extractor.rb +3 -0
  11. data/lib/sunspot/dsl/field_query.rb +33 -6
  12. data/lib/sunspot/dsl/fields.rb +14 -1
  13. data/lib/sunspot/dsl/fulltext.rb +168 -0
  14. data/lib/sunspot/dsl/query.rb +82 -5
  15. data/lib/sunspot/dsl/query_facet.rb +3 -3
  16. data/lib/sunspot/dsl/restriction.rb +7 -7
  17. data/lib/sunspot/dsl/scope.rb +17 -10
  18. data/lib/sunspot/dsl/search.rb +2 -2
  19. data/lib/sunspot/dsl.rb +2 -1
  20. data/lib/sunspot/facet.rb +9 -1
  21. data/lib/sunspot/facet_data.rb +56 -7
  22. data/lib/sunspot/facet_row.rb +2 -0
  23. data/lib/sunspot/field.rb +50 -26
  24. data/lib/sunspot/field_factory.rb +15 -0
  25. data/lib/sunspot/indexer.rb +6 -0
  26. data/lib/sunspot/instantiated_facet.rb +6 -9
  27. data/lib/sunspot/instantiated_facet_row.rb +7 -2
  28. data/lib/sunspot/query/boost_query.rb +20 -0
  29. data/lib/sunspot/query/connective.rb +98 -35
  30. data/lib/sunspot/query/dismax.rb +69 -0
  31. data/lib/sunspot/query/field_facet.rb +1 -22
  32. data/lib/sunspot/query/fulltext_base_query.rb +47 -0
  33. data/lib/sunspot/query/highlighting.rb +43 -0
  34. data/lib/sunspot/query/local.rb +24 -0
  35. data/lib/sunspot/query/pagination.rb +3 -4
  36. data/lib/sunspot/query/query.rb +93 -0
  37. data/lib/sunspot/query/query_facet.rb +14 -9
  38. data/lib/sunspot/query/query_facet_row.rb +3 -3
  39. data/lib/sunspot/query/query_field_facet.rb +10 -3
  40. data/lib/sunspot/query/restriction.rb +36 -15
  41. data/lib/sunspot/query/scope.rb +3 -159
  42. data/lib/sunspot/query/sort.rb +84 -15
  43. data/lib/sunspot/query/text_field_boost.rb +15 -0
  44. data/lib/sunspot/query.rb +2 -188
  45. data/lib/sunspot/schema.rb +7 -25
  46. data/lib/sunspot/search/highlight.rb +38 -0
  47. data/lib/sunspot/search/hit.rb +50 -3
  48. data/lib/sunspot/search.rb +51 -32
  49. data/lib/sunspot/session.rb +32 -12
  50. data/lib/sunspot/setup.rb +47 -10
  51. data/lib/sunspot/text_field_setup.rb +29 -0
  52. data/lib/sunspot/type.rb +4 -4
  53. data/lib/sunspot/util.rb +27 -1
  54. data/lib/sunspot.rb +8 -17
  55. data/solr/solr/conf/schema.xml +54 -40
  56. data/solr/solr/conf/solrconfig.xml +30 -0
  57. data/solr/solr/lib/geoapi-nogenerics-2.1-M2.jar +0 -0
  58. data/solr/solr/lib/gt2-referencing-2.3.1.jar +0 -0
  59. data/solr/solr/lib/jsr108-0.01.jar +0 -0
  60. data/solr/solr/lib/locallucene.jar +0 -0
  61. data/solr/solr/lib/localsolr.jar +0 -0
  62. data/spec/api/indexer/attributes_spec.rb +100 -0
  63. data/spec/api/indexer/batch_spec.rb +46 -0
  64. data/spec/api/indexer/dynamic_fields_spec.rb +33 -0
  65. data/spec/api/indexer/fixed_fields_spec.rb +57 -0
  66. data/spec/api/indexer/fulltext_spec.rb +43 -0
  67. data/spec/api/indexer/removal_spec.rb +46 -0
  68. data/spec/api/indexer/spec_helper.rb +1 -0
  69. data/spec/api/indexer_spec.rb +1 -308
  70. data/spec/api/query/connectives_spec.rb +162 -0
  71. data/spec/api/query/dsl_spec.rb +12 -0
  72. data/spec/api/query/dynamic_fields_spec.rb +149 -0
  73. data/spec/api/query/faceting_spec.rb +272 -0
  74. data/spec/api/query/fulltext_spec.rb +193 -0
  75. data/spec/api/query/highlighting_spec.rb +138 -0
  76. data/spec/api/query/local_spec.rb +54 -0
  77. data/spec/api/query/ordering_pagination_spec.rb +95 -0
  78. data/spec/api/query/scope_spec.rb +266 -0
  79. data/spec/api/query/spec_helper.rb +1 -0
  80. data/spec/api/query/text_field_scoping_spec.rb +30 -0
  81. data/spec/api/query/types_spec.rb +20 -0
  82. data/spec/api/search/dynamic_fields_spec.rb +27 -0
  83. data/spec/api/search/faceting_spec.rb +206 -0
  84. data/spec/api/search/highlighting_spec.rb +65 -0
  85. data/spec/api/search/hits_spec.rb +62 -0
  86. data/spec/api/search/results_spec.rb +52 -0
  87. data/spec/api/search/search_spec.rb +23 -0
  88. data/spec/api/search/spec_helper.rb +1 -0
  89. data/spec/api/spec_helper.rb +1 -1
  90. data/spec/helpers/indexer_helper.rb +29 -0
  91. data/spec/helpers/query_helper.rb +13 -0
  92. data/spec/helpers/search_helper.rb +78 -0
  93. data/spec/integration/faceting_spec.rb +1 -1
  94. data/spec/integration/highlighting_spec.rb +22 -0
  95. data/spec/integration/keyword_search_spec.rb +65 -0
  96. data/spec/integration/local_search_spec.rb +56 -0
  97. data/spec/integration/scoped_search_spec.rb +15 -1
  98. data/spec/integration/spec_helper.rb +3 -3
  99. data/spec/mocks/connection.rb +14 -1
  100. data/spec/mocks/photo.rb +1 -1
  101. data/spec/mocks/post.rb +5 -3
  102. data/spec/mocks/super_class.rb +2 -0
  103. data/spec/spec_helper.rb +13 -0
  104. data/tasks/gemspec.rake +18 -7
  105. data/tasks/schema.rake +1 -1
  106. data/tasks/spec.rake +1 -1
  107. data/templates/schema.xml.erb +36 -0
  108. metadata +117 -48
  109. data/lib/sunspot/query/base_query.rb +0 -90
  110. data/lib/sunspot/query/dynamic_query.rb +0 -69
  111. data/lib/sunspot/query/field_query.rb +0 -63
  112. data/spec/api/build_search_spec.rb +0 -1017
  113. data/spec/api/query_spec.rb +0 -153
  114. data/spec/api/search_retrieval_spec.rb +0 -362
  115. data/templates/schema.xml.haml +0 -24
@@ -0,0 +1,206 @@
1
+ require File.join(File.dirname(__FILE__), 'spec_helper')
2
+
3
+ describe 'faceting', :type => :search do
4
+ it 'returns field name for facet' do
5
+ stub_facet(:title_ss, {})
6
+ result = session.search Post do
7
+ facet :title
8
+ end
9
+ result.facet(:title).field_name.should == :title
10
+ end
11
+
12
+ it 'returns string facet' do
13
+ stub_facet(:title_ss, 'Author 1' => 2, 'Author 2' => 1)
14
+ result = session.search Post do
15
+ facet :title
16
+ end
17
+ facet_values(result, :title).should == ['Author 1', 'Author 2']
18
+ end
19
+
20
+ it 'returns counts for facet' do
21
+ stub_facet(:title_ss, 'Author 1' => 2, 'Author 2' => 1)
22
+ result = session.search Post do
23
+ facet :title
24
+ end
25
+ facet_counts(result, :title).should == [2, 1]
26
+ end
27
+
28
+ it 'returns integer facet' do
29
+ stub_facet(:blog_id_i, '3' => 2, '1' => 1)
30
+ result = session.search Post do
31
+ facet :blog_id
32
+ end
33
+ facet_values(result, :blog_id).should == [3, 1]
34
+ end
35
+
36
+ it 'returns float facet' do
37
+ stub_facet(:average_rating_f, '9.3' => 2, '1.1' => 1)
38
+ result = session.search Post do
39
+ facet :average_rating
40
+ end
41
+ facet_values(result, :average_rating).should == [9.3, 1.1]
42
+ end
43
+
44
+ it 'returns time facet' do
45
+ stub_facet(
46
+ :published_at_d,
47
+ '2009-04-07T20:25:23Z' => 3,
48
+ '2009-04-07T20:26:19Z' => 1
49
+ )
50
+ result = session.search Post do
51
+ facet :published_at
52
+ end
53
+ facet_values(result, :published_at).should ==
54
+ [Time.gm(2009, 04, 07, 20, 25, 23),
55
+ Time.gm(2009, 04, 07, 20, 26, 19)]
56
+ end
57
+
58
+ it 'should return date facet' do
59
+ stub_facet(
60
+ :expire_date_d,
61
+ '2009-07-13T00:00:00Z' => 3,
62
+ '2009-04-01T00:00:00Z' => 1
63
+ )
64
+ result = session.search(Post) do
65
+ facet :expire_date
66
+ end
67
+ facet_values(result, :expire_date).should ==
68
+ [Date.new(2009, 07, 13),
69
+ Date.new(2009, 04, 01)]
70
+ end
71
+
72
+ it 'should return boolean facet' do
73
+ stub_facet(:featured_b, 'true' => 3, 'false' => 1)
74
+ result = session.search(Post) { facet(:featured) }
75
+ facet_values(result, :featured).should == [true, false]
76
+ end
77
+
78
+ it 'should return class facet' do
79
+ stub_facet(:class_name, 'Post' => 3, 'Namespaced::Comment' => 1)
80
+ result = session.search(Post) { facet(:class) }
81
+ facet_values(result, :class).should == [Post, Namespaced::Comment]
82
+ end
83
+
84
+ it 'should return date range facet' do
85
+ stub_date_facet(:published_at_d, 60*60*24, '2009-07-08T04:00:00Z' => 2, '2009-07-07T04:00:00Z' => 1)
86
+ start_time = Time.utc(2009, 7, 7, 4)
87
+ end_time = start_time + 2*24*60*60
88
+ result = session.search(Post) { facet(:published_at, :time_range => start_time..end_time) }
89
+ facet = result.facet(:published_at)
90
+ facet.rows.first.value.should == (start_time..(start_time+24*60*60))
91
+ facet.rows.last.value.should == ((start_time+24*60*60)..end_time)
92
+ end
93
+
94
+ it 'returns query facet' do
95
+ stub_query_facet(
96
+ 'average_rating_f:[3\.0 TO 5\.0]' => 3,
97
+ 'average_rating_f:[1\.0 TO 3\.0]' => 1
98
+ )
99
+ search = session.search(Post) do
100
+ facet :average_rating do
101
+ row 3.0..5.0 do
102
+ with :average_rating, 3.0..5.0
103
+ end
104
+ row 1.0..3.0 do
105
+ with :average_rating, 1.0..3.0
106
+ end
107
+ end
108
+ end
109
+ facet = search.facet(:average_rating)
110
+ facet.rows.first.value.should == (3.0..5.0)
111
+ facet.rows.first.count.should == 3
112
+ facet.rows.last.value.should == (1.0..3.0)
113
+ facet.rows.last.count.should == 1
114
+ end
115
+
116
+ describe 'query facet option handling' do
117
+ def facet_values_from_options(options = {})
118
+ session.search(Post) do
119
+ facet :average_rating, options do
120
+ row(1) { with(:average_rating, 1.0..2.0) }
121
+ row(2) { with(:average_rating, 2.0..3.0) }
122
+ row(3) { with(:average_rating, 3.0..4.0) }
123
+ end
124
+ end.facet(:average_rating).rows.map { |row| row.value }
125
+ end
126
+
127
+ before :each do
128
+ stub_query_facet(
129
+ 'average_rating_f:[1\.0 TO 2\.0]' => 1,
130
+ 'average_rating_f:[2\.0 TO 3\.0]' => 2,
131
+ 'average_rating_f:[3\.0 TO 4\.0]' => 0
132
+ )
133
+ end
134
+
135
+ it 'sorts lexically by default if no limit is given' do
136
+ facet_values_from_options.should == [1, 2]
137
+ end
138
+
139
+ it 'sorts by count by default if limit is given' do
140
+ facet_values_from_options(:limit => 2).should == [2, 1]
141
+ end
142
+
143
+ it 'sorts by count if count option is specified' do
144
+ facet_values_from_options(:sort => :count).should == [2, 1]
145
+ end
146
+
147
+ it 'sorts lexically if lexical option is specified even if limit is given' do
148
+ facet_values_from_options(:sort => :index, :limit => 2).should == [1, 2]
149
+ end
150
+
151
+ it 'limits facets if limit option is given' do
152
+ facet_values_from_options(:limit => 1).should == [2]
153
+ end
154
+
155
+ it 'allows zero count if specified' do
156
+ facet_values_from_options(:zeros => true).should == [1, 2, 3]
157
+ end
158
+
159
+ it 'sets minimum count' do
160
+ facet_values_from_options(:minimum_count => 2).should == [2]
161
+ end
162
+ end
163
+
164
+ it 'returns limited field facet' do
165
+ stub_query_facet(
166
+ 'category_ids_im:1' => 3,
167
+ 'category_ids_im:3' => 1
168
+ )
169
+ search = session.search(Post) do
170
+ facet :category_ids, :only => [1, 3, 5]
171
+ end
172
+ facet = search.facet(:category_ids)
173
+ facet.rows.first.value.should == 1
174
+ facet.rows.first.count.should == 3
175
+ facet.rows.last.value.should == 3
176
+ facet.rows.last.count.should == 1
177
+ end
178
+
179
+ it 'returns instantiated facet values' do
180
+ blogs = Array.new(2) { Blog.new }
181
+ stub_facet(:blog_id_i, blogs[0].id.to_s => 2, blogs[1].id.to_s => 1)
182
+ search = session.search(Post) { facet(:blog_id) }
183
+ search.facet(:blog_id).rows.map { |row| row.instance }.should == blogs
184
+ end
185
+
186
+ it 'returns instantiated facet values for limited field facet' do
187
+ blogs = Array.new(2) { Blog.new }
188
+ stub_query_facet(
189
+ "blog_id_i:#{blogs[0].id}" => 3,
190
+ "blog_id_i:#{blogs[1].id}" => 1
191
+ )
192
+ search = session.search(Post) do
193
+ facet(:blog_id, :only => blogs.map { |blog| blog.id })
194
+ end
195
+ search.facet(:blog_id).rows.map { |row| row.instance }.should == blogs
196
+ end
197
+
198
+ it 'only queries the persistent store once for an instantiated facet' do
199
+ query_count = Blog.query_count
200
+ blogs = Array.new(2) { Blog.new }
201
+ stub_facet(:blog_id_i, blogs[0].id.to_s => 2, blogs[1].id.to_s => 1)
202
+ result = session.search(Post) { facet(:blog_id) }
203
+ result.facet(:blog_id).rows.each { |row| row.instance }
204
+ (Blog.query_count - query_count).should == 1
205
+ end
206
+ end
@@ -0,0 +1,65 @@
1
+ require File.join(File.dirname(__FILE__), 'spec_helper')
2
+
3
+ describe 'search with highlighting results', :type => :search do
4
+ before :each do
5
+ @posts = Array.new(2) { Post.new }
6
+ stub_results_with_highlighting(
7
+ @posts[0],
8
+ { 'title_text' => ['one @@@hl@@@two@@@endhl@@@ three'] },
9
+ @posts[1],
10
+ { 'title_text' => ['three four @@@hl@@@five@@@endhl@@@'],
11
+ 'body_text' => ['@@@hl@@@five@@@ six seven', '@@@hl@@@eight@@@endhl@@@ nine @@@hl@@@ten@@@endhl@@@'] }
12
+ )
13
+ @search = session.search(Post)
14
+ end
15
+
16
+ it 'returns all highlights' do
17
+ @search.hits.last.should have(3).highlights
18
+ end
19
+
20
+ it 'returns all highlights for a specified field' do
21
+ @search.hits.last.should have(2).highlights(:body)
22
+ end
23
+
24
+ it 'returns nil if a given field does not have a highlight' do
25
+ @search.hits.first.highlights(:body).should be_nil
26
+ end
27
+
28
+ it 'should format hits with <em> by default' do
29
+ highlight = @search.hits.first.highlights(:title).first.formatted
30
+ highlight.should == 'one <em>two</em> three'
31
+ end
32
+
33
+ it 'should format hits with provided block' do
34
+ highlight = @search.hits.first.highlights(:title).first.format do |word|
35
+ "<i>#{word}</i>"
36
+ end
37
+ highlight.should == 'one <i>two</i> three'
38
+ end
39
+
40
+ it 'should handle multiple highlighted words' do
41
+ highlight = @search.hits.last.highlights(:body).last.format do |word|
42
+ "<b>#{word}</b>"
43
+ end
44
+ highlight.should == '<b>eight</b> nine <b>ten</b>'
45
+ end
46
+
47
+ private
48
+
49
+ def stub_results_with_highlighting(*instances_and_highlights)
50
+ docs, highlights = [], []
51
+ instances_and_highlights.each_slice(2) do |doc, highlight|
52
+ docs << doc
53
+ highlights << highlight
54
+ end
55
+ response = stub_full_results(*docs.map { |doc| { 'instance' => doc }})
56
+ highlighting = response['highlighting'] = {}
57
+ highlights.each_with_index do |highlight, i|
58
+ if highlight
59
+ instance = docs[i]
60
+ highlighting["#{instance.class.name} #{instance.id}"] = highlight
61
+ end
62
+ end
63
+ response
64
+ end
65
+ end
@@ -0,0 +1,62 @@
1
+ require File.join(File.dirname(__FILE__), 'spec_helper')
2
+
3
+ describe 'hits', :type => :search do
4
+ it 'should return hits without loading instances' do
5
+ post_1, post_2 = Array.new(2) { Post.new }
6
+ stub_results(post_1, post_2)
7
+ %w(load load_all).each do |message|
8
+ MockAdapter::DataAccessor.should_not_receive(message)
9
+ end
10
+ session.search(Post).hits.map do |hit|
11
+ [hit.class_name, hit.primary_key]
12
+ end.should == [['Post', post_1.id.to_s], ['Post', post_2.id.to_s]]
13
+ end
14
+
15
+ it 'should return instance from hit' do
16
+ posts = Array.new(2) { Post.new }
17
+ stub_results(*posts)
18
+ session.search(Post).hits.first.instance.should == posts.first
19
+ end
20
+
21
+ it 'should hydrate all hits when an instance is requested from a hit' do
22
+ posts = Array.new(2) { Post.new }
23
+ stub_results(*posts)
24
+ search = session.search(Post)
25
+ search.hits.first.instance
26
+ %w(load load_all).each do |message|
27
+ MockAdapter::DataAccessor.should_not_receive(message)
28
+ end
29
+ search.hits.last.instance.should == posts.last
30
+ end
31
+
32
+ it 'should attach score to hits' do
33
+ stub_full_results('instance' => Post.new, 'score' => 1.23)
34
+ session.search(Post).hits.first.score.should == 1.23
35
+ end
36
+
37
+ it 'should return stored field values in hits' do
38
+ stub_full_results('instance' => Post.new, 'title_ss' => 'Title')
39
+ session.search(Post).hits.first.stored(:title).should == 'Title'
40
+ end
41
+
42
+ it 'should return stored field values for searches against multiple types' do
43
+ stub_full_results('instance' => Post.new, 'title_ss' => 'Title')
44
+ session.search(Post, Namespaced::Comment).hits.first.stored(:title).should == 'Title'
45
+ end
46
+
47
+ it 'should typecast stored field values in hits' do
48
+ time = Time.utc(2008, 7, 8, 2, 45)
49
+ stub_full_results('instance' => Post.new, 'last_indexed_at_ds' => time.xmlschema)
50
+ session.search(Post).hits.first.stored(:last_indexed_at).should == time
51
+ end
52
+
53
+ it 'should return geo distance' do
54
+ stub_full_results('instance' => Post.new, 'geo_distance' => '1.23')
55
+ session.search(Post).hits.first.distance.should == 1.23
56
+ end
57
+
58
+ it 'should return nil if no geo distance' do
59
+ stub_results(Post.new)
60
+ session.search(Post).hits.first.distance.should be_nil
61
+ end
62
+ end
@@ -0,0 +1,52 @@
1
+ require File.join(File.dirname(__FILE__), 'spec_helper')
2
+
3
+ describe 'search results', :type => :search do
4
+ it 'loads single result' do
5
+ post = Post.new
6
+ stub_results(post)
7
+ session.search(Post).results.should == [post]
8
+ end
9
+
10
+ it 'loads multiple results in order' do
11
+ post_1, post_2 = Post.new, Post.new
12
+ stub_results(post_1, post_2)
13
+ session.search(Post).results.should == [post_1, post_2]
14
+ stub_results(post_2, post_1)
15
+ session.search(Post).results.should == [post_2, post_1]
16
+ end
17
+
18
+ # This is a reduction of a crazy bug I found in production where some hits
19
+ # were inexplicably not being populated.
20
+ it 'properly loads results of multiple classes that have the same primary key' do
21
+ Post.reset!
22
+ Namespaced::Comment.reset!
23
+ results = [Post.new, Namespaced::Comment.new]
24
+ stub_results(*results)
25
+ session.search(Post, Namespaced::Comment).results.should == results
26
+ end
27
+
28
+ if ENV['USE_WILL_PAGINATE']
29
+
30
+ it 'returns search total as attribute of results' do
31
+ stub_results(Post.new, 4)
32
+ session.search(Post) do
33
+ paginate(:page => 1)
34
+ end.results.total_entries.should == 4
35
+ end
36
+
37
+ else
38
+
39
+ it 'returns vanilla array if WillPaginate is not available' do
40
+ stub_results(Post.new)
41
+ session.search(Post) do
42
+ paginate(:page => 1)
43
+ end.results.should_not respond_to(:total_entries)
44
+ end
45
+
46
+ end
47
+
48
+ it 'should return total' do
49
+ stub_results(Post.new, Post.new, 4)
50
+ session.search(Post) { paginate(:page => 1) }.total.should == 4
51
+ end
52
+ end
@@ -0,0 +1,23 @@
1
+ require File.join(File.dirname(__FILE__), 'spec_helper')
2
+
3
+ describe Sunspot::Search do
4
+ it 'should allow access to the data accessor' do
5
+ stub_results(posts = Post.new)
6
+ search = session.search Post do
7
+ data_accessor_for(Post).custom_title = 'custom title'
8
+ end
9
+ search.results.first.title.should == 'custom title'
10
+ end
11
+
12
+ it 'should re-execute search' do
13
+ post_1, post_2 = Post.new, Post.new
14
+
15
+ stub_results(post_1)
16
+ search = session.search Post
17
+ search.results.should == [post_1]
18
+
19
+ stub_results(post_2)
20
+ search.execute!
21
+ search.results.should == [post_2]
22
+ end
23
+ end
@@ -0,0 +1 @@
1
+ require File.expand_path(File.join(File.dirname(__FILE__), '..', 'spec_helper'))
@@ -1 +1 @@
1
- require File.join(File.dirname(__FILE__), '..', 'spec_helper')
1
+ require File.expand_path(File.join(File.dirname(__FILE__), '..', 'spec_helper'))
@@ -0,0 +1,29 @@
1
+ module IndexerHelper
2
+ def config
3
+ Sunspot::Configuration.build
4
+ end
5
+
6
+ def connection
7
+ @connection ||= Mock::Connection.new
8
+ end
9
+
10
+ def session
11
+ @session ||= Sunspot::Session.new(config, connection)
12
+ end
13
+
14
+ def post(attrs = {})
15
+ @post ||= Post.new(attrs)
16
+ end
17
+
18
+ def last_add
19
+ @connection.adds.last
20
+ end
21
+
22
+ def value_in_last_document_for(field_name)
23
+ @connection.adds.last.last.field_by_name(field_name).value
24
+ end
25
+
26
+ def values_in_last_document_for(field_name)
27
+ @connection.adds.last.last.fields_by_name(field_name).map { |field| field.value }
28
+ end
29
+ end
@@ -0,0 +1,13 @@
1
+ module QueryHelper
2
+ def config
3
+ @config ||= Sunspot::Configuration.build
4
+ end
5
+
6
+ def connection
7
+ @connection ||= Mock::Connection.new
8
+ end
9
+
10
+ def session
11
+ @session ||= Sunspot::Session.new(config, connection)
12
+ end
13
+ end
@@ -0,0 +1,78 @@
1
+ module SearchHelper
2
+ def stub_full_results(*results)
3
+ count =
4
+ if results.last.is_a?(Integer) then results.pop
5
+ else results.length
6
+ end
7
+ docs = results.map do |result|
8
+ instance = result.delete('instance')
9
+ result.merge('id' => "#{instance.class.name} #{instance.id}")
10
+ end
11
+ response = {
12
+ 'response' => {
13
+ 'docs' => docs,
14
+ 'numFound' => count
15
+ }
16
+ }
17
+ connection.stub!(:select).and_return(response)
18
+ response
19
+ end
20
+
21
+ def stub_results(*results)
22
+ stub_full_results(
23
+ *results.map do |result|
24
+ if result.is_a?(Integer)
25
+ result
26
+ else
27
+ { 'instance' => result }
28
+ end
29
+ end
30
+ )
31
+ end
32
+
33
+ def stub_facet(name, values)
34
+ connection.stub!(:select).and_return(
35
+ 'facet_counts' => {
36
+ 'facet_fields' => {
37
+ name.to_s => values.to_a.sort_by { |value, count| -count }.flatten
38
+ }
39
+ }
40
+ )
41
+ end
42
+
43
+ def stub_date_facet(name, gap, values)
44
+ connection.stub!(:select).and_return(
45
+ 'facet_counts' => {
46
+ 'facet_dates' => {
47
+ name.to_s => { 'gap' => "+#{gap}SECONDS" }.merge(values)
48
+ }
49
+ }
50
+ )
51
+ end
52
+
53
+ def stub_query_facet(values)
54
+ connection.stub!(:select).and_return(
55
+ 'facet_counts' => { 'facet_queries' => values }
56
+ )
57
+ end
58
+
59
+ def facet_values(result, field_name)
60
+ result.facet(field_name).rows.map { |row| row.value }
61
+ end
62
+
63
+ def facet_counts(result, field_name)
64
+ result.facet(field_name).rows.map { |row| row.count }
65
+ end
66
+
67
+ def config
68
+ @config ||= Sunspot::Configuration.build
69
+ end
70
+
71
+ def connection
72
+ @connection ||= Mock::Connection.new
73
+ end
74
+
75
+ def session
76
+ @session ||= Sunspot::Session.new(config, connection)
77
+ end
78
+ end
@@ -148,7 +148,7 @@ describe 'search faceting' do
148
148
 
149
149
  it 'should return specified facets' do
150
150
  search = Sunspot.search(Post) do
151
- facet :rating_range do
151
+ facet :rating_range, :sort => :count do
152
152
  for rating in [1.0, 2.0, 3.0, 4.0]
153
153
  range = rating..(rating + 1.0)
154
154
  row range do
@@ -0,0 +1,22 @@
1
+ describe 'keyword highlighting' do
2
+ before :all do
3
+ @posts = []
4
+ @posts << Post.new(:body => 'And the fox laughed')
5
+ @posts << Post.new(:body => 'Lorem ipsum dolor sit amet, consectetur adipiscing elit', :blog_id => 1)
6
+ Sunspot.index!(*@posts)
7
+ @search_result = Sunspot.search(Post) { keywords 'fox', :highlight => true }
8
+ end
9
+
10
+ it 'should include highlights in the results' do
11
+ @search_result.hits.first.highlights.length.should == 1
12
+ end
13
+
14
+ it 'should return formatted highlight fragments' do
15
+ @search_result.hits.first.highlights(:body).first.format.should == 'And the <em>fox</em> laughed'
16
+ end
17
+
18
+ it 'should be empty for non-keyword searches' do
19
+ search_result = Sunspot.search(Post){ with :blog_id, 1 }
20
+ search_result.hits.first.highlights.should be_empty
21
+ end
22
+ end
@@ -80,4 +80,69 @@ describe 'keyword search' do
80
80
  search.hits.first.score.should > search.hits.last.score
81
81
  end
82
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 => 2.0
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
83
148
  end